mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-19 07:33:18 +02:00
fix(label-picker): pack templates into a 2x2 grid so all 4 plus Cancel fit on tight viewports (issue #1230)
The earlier `min-h-0` fix on the spool list (61314cf2) made the
shrinkable child shrinkable, but on @elit3ge's 838px viewport the
four stacked templates (~310px) plus footer still blew past
max-h-[90vh] once Brave's browser chrome ate into vh, and
overflow-hidden on the modal clipped Avery 5160 mid-row with the
Cancel button entirely below the clipped bottom edge — no scroll
path. The screenshot showed the spool list at ~5 visible rows with
its own scrollbar still active, confirming the templates section's
natural height was the dominant problem, not the spool list.
Templates now render as a responsive grid (grid-cols-1
sm:grid-cols-2 gap-2) so the four buttons pack into a 2x2 grid
above the sm breakpoint, trimming ~150px of vertical. Per-cell
padding tightens to p-2.5, labels/hints get text-sm + truncate,
and the full strings are reachable via title="<label> — <hint>"
on each button. Footer drops py-3 to py-2 for a few extra pixels.
The min-h-0 on the spool list is kept as a belt-and-braces shrink
for any viewport tighter still. Mobile (<sm) keeps the stacked
layout — no regression there.
This commit is contained in:
@@ -275,13 +275,15 @@ describe('LabelTemplatePickerModal', () => {
|
||||
expect(screen.getByText(/No spools match/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('lets the spool list shrink (min-h-0) so all 4 templates and Cancel stay visible on short viewports (#1230)', () => {
|
||||
// Regression for #1230: on viewports where 90vh is tight (Windows 11
|
||||
// browser-chrome or DPI scaling), an explicit min-h on the spool list
|
||||
// pinned it taller than the modal could give back to templates + footer,
|
||||
// and `overflow-hidden` on the outer modal clipped the 4th template
|
||||
// (Avery 5160) and the Cancel button. The fix is `min-h-0` so the
|
||||
// flex-1 spool list can yield space when needed.
|
||||
it('packs templates into a 2x2 grid so all 4 plus Cancel fit on short viewports (#1230)', () => {
|
||||
// Regression for #1230: with 4 templates stacked vertically (~310px) plus
|
||||
// header/search/action bar/footer, the modal blew past max-h-[90vh] on
|
||||
// Windows-11 + Brave-style viewports where browser chrome eats into 90vh.
|
||||
// overflow-hidden on the modal then clipped Avery 5160 and the Cancel
|
||||
// footer with no scroll path. The fix uses sm:grid-cols-2 so the 4
|
||||
// templates render as a 2x2 grid (~155px), trimming ~150px of vertical
|
||||
// and leaving room for the footer. The earlier min-h-0 on the spool list
|
||||
// is kept so it still yields any remaining slack.
|
||||
const { container } = render(
|
||||
<LabelTemplatePickerModal
|
||||
isOpen={true}
|
||||
@@ -299,9 +301,15 @@ describe('LabelTemplatePickerModal', () => {
|
||||
expect(screen.getByText(/Avery 5160/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Cancel/i })).toBeInTheDocument();
|
||||
|
||||
// Structural guard against the regression: the scrollable spool list
|
||||
// must have `min-h-0` so flex shrinking actually works, and must NOT
|
||||
// pin a fixed minimum height that prevents it.
|
||||
// Templates section must be a responsive grid (single column on mobile,
|
||||
// two columns from sm: up) — a future refactor that drops the grid and
|
||||
// reintroduces stacked rows fails CI.
|
||||
const templatesSection = container.querySelector('div.grid.sm\\:grid-cols-2');
|
||||
expect(templatesSection).not.toBeNull();
|
||||
expect(templatesSection!.className).toContain('grid-cols-1');
|
||||
expect(templatesSection!.querySelectorAll('button').length).toBe(4);
|
||||
|
||||
// Spool list still uses min-h-0 so it can yield further on very tight viewports.
|
||||
const spoolListScroller = container.querySelector('div.flex-1.overflow-y-auto');
|
||||
expect(spoolListScroller).not.toBeNull();
|
||||
expect(spoolListScroller!.className).toContain('min-h-0');
|
||||
|
||||
@@ -351,32 +351,33 @@ export function LabelTemplatePickerModal({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Templates */}
|
||||
<div className="px-3 pb-3 pt-3 space-y-2 border-t border-bambu-dark-tertiary">
|
||||
{/* Templates — 2x2 grid on >= sm so all 4 plus the Cancel footer fit
|
||||
inside max-h-[90vh] even when browser chrome eats into the viewport
|
||||
(#1230). Stacked single column on mobile widths. */}
|
||||
<div className="px-3 pt-2 pb-2 grid grid-cols-1 sm:grid-cols-2 gap-2 border-t border-bambu-dark-tertiary">
|
||||
{TEMPLATE_OPTIONS.map((opt) => {
|
||||
const isPending = pending === opt.value;
|
||||
const label = t(`inventory.labels.templates.${opt.i18nKey}.label`, opt.fallbackLabel);
|
||||
const hint = t(`inventory.labels.templates.${opt.i18nKey}.hint`, opt.fallbackHint);
|
||||
return (
|
||||
<button
|
||||
key={opt.value}
|
||||
disabled={noSelection || pending !== null}
|
||||
onClick={() => handlePick(opt.value)}
|
||||
className="w-full text-left p-3 rounded-lg border border-bambu-dark-tertiary bg-bambu-dark hover:border-bambu-green hover:bg-bambu-green/10 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:border-bambu-dark-tertiary disabled:hover:bg-bambu-dark transition flex items-center gap-3"
|
||||
title={`${label} — ${hint}`}
|
||||
className="w-full text-left p-2.5 rounded-lg border border-bambu-dark-tertiary bg-bambu-dark hover:border-bambu-green hover:bg-bambu-green/10 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:border-bambu-dark-tertiary disabled:hover:bg-bambu-dark transition flex items-center gap-3"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-white">
|
||||
{t(`inventory.labels.templates.${opt.i18nKey}.label`, opt.fallbackLabel)}
|
||||
</div>
|
||||
<div className="text-xs text-bambu-gray mt-0.5">
|
||||
{t(`inventory.labels.templates.${opt.i18nKey}.hint`, opt.fallbackHint)}
|
||||
</div>
|
||||
<div className="font-medium text-white text-sm truncate">{label}</div>
|
||||
<div className="text-xs text-bambu-gray mt-0.5 truncate">{hint}</div>
|
||||
</div>
|
||||
{isPending && <Loader2 className="w-4 h-4 animate-spin text-bambu-green" />}
|
||||
{isPending && <Loader2 className="w-4 h-4 animate-spin text-bambu-green shrink-0" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 px-5 py-3 border-t border-bambu-dark-tertiary">
|
||||
<div className="flex justify-end gap-2 px-5 py-2 border-t border-bambu-dark-tertiary">
|
||||
<Button variant="secondary" onClick={onClose} disabled={pending !== null}>
|
||||
{t('common.cancel', 'Cancel')}
|
||||
</Button>
|
||||
|
||||
@@ -6743,6 +6743,7 @@ export function PrintersPage() {
|
||||
setCompactToolbar(prev => (prev === shouldCompact ? prev : shouldCompact));
|
||||
}, []);
|
||||
|
||||
const smartPlugCount = Object.keys(smartPlugByPrinter).length;
|
||||
useLayoutEffect(() => {
|
||||
measureToolbar();
|
||||
|
||||
@@ -6767,7 +6768,7 @@ export function PrintersPage() {
|
||||
printers?.length,
|
||||
availableLocations.length,
|
||||
hideDisconnected,
|
||||
Object.keys(smartPlugByPrinter).length,
|
||||
smartPlugCount,
|
||||
]);
|
||||
|
||||
const renderFilterControls = (inMenu = false) => (
|
||||
|
||||
Reference in New Issue
Block a user