Size the H2C nozzle rack card to its contents and number its slots

The rack card shared a row with the nozzle, bed and chamber readings but
    was set to flex: 2 1 190px -- a 190px floor plus twice their growth share
    -- to draw six fixed 28px chips. On anything wider than a compact card it
    claimed several hundred pixels and left most of them empty, and the width
    came out of the cards that needed it: the combined dual-nozzle reading was
    wrapping "220 / 220" onto two lines beside a mostly blank rack. It is now
    flex: 0 1 auto, so it takes its content width and gives the remainder
    back. Shrink stays enabled so it still gives way on a narrow card instead
    of overflowing.

    Each slot also carries its physical rack position 1-6 below the chip, so a
    nozzle can be named rather than counted along. The numbering is positional
    -- an empty slot keeps its number -- so "the nozzle in slot 4" means the
    same thing however many of the six are occupied.

    The #943 regression test read every span in the slot row, which the new
    number spans would have interleaved with the diameters; it now matches the
    diameter spans specifically. A second test pins the 1-6 labelling across a
    rack with empty positions.
This commit is contained in:
maziggy
2026-08-15 14:58:09 +02:00
6 changed files with 58 additions and 18 deletions
@@ -812,10 +812,34 @@ describe('PrintersPage', () => {
const rackLabel = screen.getAllByText('Nozzle Rack')[0];
const rackCard = rackLabel.parentElement!;
const slotRow = rackCard.querySelectorAll('div.flex')[0];
const slotTexts = Array.from(slotRow.querySelectorAll('span')).map(s => s.textContent);
// Match the diameter spans specifically — each chip also carries a slot
// number span, and a bare `span` sweep would interleave the two.
const slotTexts = Array.from(slotRow.querySelectorAll('span[data-rack-diameter]')).map(s => s.textContent);
expect(slotTexts).toEqual(['—', '0.2', '0.6', '0.8', '1.0', '1.2']);
});
it('labels every rack position 1..6 regardless of which nozzles are present', async () => {
// The numbering is positional, not a count of what is loaded: the empty
// position keeps its number so "the nozzle in slot 4" means the same
// thing whether or not slots 1..3 are occupied.
server.use(
http.get('/api/v1/printers/:id/status', () => {
return HttpResponse.json(h2cStatus);
})
);
render(<PrintersPage />);
await waitFor(() => {
expect(screen.getAllByText('Nozzle Rack').length).toBeGreaterThan(0);
});
const rackLabel = screen.getAllByText('Nozzle Rack')[0];
const slotRow = rackLabel.parentElement!.querySelectorAll('div.flex')[0];
const numbers = Array.from(slotRow.querySelectorAll('span:not([data-rack-diameter])')).map(s => s.textContent);
expect(numbers).toEqual(['1', '2', '3', '4', '5', '6']);
});
it('hides nozzle rack when only L/R nozzles present (H2D)', async () => {
const h2dStatus = {
...mockPrinterStatus,
+28 -12
View File
@@ -555,7 +555,12 @@ function NozzleRackCard({ slots, filamentInfo }: { slots: import('../api/client'
);
return (
<div className="text-center px-2.5 py-1.5 bg-bambu-dark rounded-lg flex-[2_1_190px] flex flex-col justify-center">
// Sized to its contents rather than growing: the six chips are a fixed
// 28px each, so any extra width is dead space taken from the temperature
// cards beside it — which are flex-1 with a 0 basis and wrap their values
// ("220° / 220°") as soon as they lose it. Shrink stays enabled so the
// card still gives way on a narrow printer card instead of overflowing.
<div className="text-center px-2.5 py-1.5 bg-bambu-dark rounded-lg flex-[0_1_auto] flex flex-col justify-center">
<p className="text-[length:var(--pc-t9,9px)] text-bambu-gray mb-1">{t('printers.nozzleRack')}</p>
<div className="flex gap-[3px] justify-center">
{rackSlots.map((slot, i) => {
@@ -565,18 +570,29 @@ function NozzleRackCard({ slots, filamentInfo }: { slots: import('../api/client'
return (
<NozzleSlotHoverCard key={slot.id >= 0 ? slot.id : `empty-${i}`} slot={slot} index={i} filamentName={slot.filament_id ? filamentInfo?.[slot.filament_id]?.name : undefined}>
<div
className={`w-7 h-7 rounded flex items-center justify-center cursor-default transition-colors border-b-2 ${
isEmpty
? 'bg-bambu-dark-tertiary/20 border-bambu-dark-tertiary/20'
: 'bg-bambu-dark-tertiary/40 border-bambu-dark-tertiary/40'
}`}
style={filamentBg ? { backgroundColor: filamentBg } : undefined}
>
<span className={`text-[length:var(--pc-t10,10px)] font-semibold ${isEmpty ? 'text-bambu-gray/30' : lightBg ? 'text-black/80' : 'text-white'}`}
style={filamentBg && !lightBg ? { textShadow: '0 1px 3px rgba(0,0,0,0.9)' } : undefined}
<div className="flex flex-col items-center gap-0.5">
<div
className={`w-7 h-7 rounded flex items-center justify-center cursor-default transition-colors border-b-2 ${
isEmpty
? 'bg-bambu-dark-tertiary/20 border-bambu-dark-tertiary/20'
: 'bg-bambu-dark-tertiary/40 border-bambu-dark-tertiary/40'
}`}
style={filamentBg ? { backgroundColor: filamentBg } : undefined}
>
{isEmpty ? '—' : (slot.nozzle_diameter || '?')}
<span
data-rack-diameter
className={`text-[length:var(--pc-t10,10px)] font-semibold ${isEmpty ? 'text-bambu-gray/30' : lightBg ? 'text-black/80' : 'text-white'}`}
style={filamentBg && !lightBg ? { textShadow: '0 1px 3px rgba(0,0,0,0.9)' } : undefined}
>
{isEmpty ? '—' : (slot.nozzle_diameter || '?')}
</span>
</div>
{/* Physical rack position, so "swap the nozzle in slot 4" can be
acted on without counting chips. Sits below the chip rather
than inside it: the chip is 28px and already carries the
diameter over a filament-coloured background. */}
<span className="text-[length:var(--pc-t8,8px)] leading-none tabular-nums text-bambu-gray/70">
{i + 1}
</span>
</div>
</NozzleSlotHoverCard>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -26,8 +26,8 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-D686R9Ft.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BVhCaZnn.css">
<script type="module" crossorigin src="/assets/index-CiO0qpbC.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B3n6Lnj1.css">
</head>
<body>
<div id="root"></div>