Browse Source

Scale the printer card's body text and icons with its size (#1848)

Switching a card from M to XL made it wider, enlarged the printer name
and the thumbnail, and left everything else where it was. The AMS slot
labels, temperatures, filament names, status text and every small button
stayed pinned between 8 and 11 pixels -- under the smallest size used
anywhere else in the app -- so a full-width card carried the same tiny
text as the compact one. Browser zoom does not answer this: it enlarges
the whole page and so preserves the very disparity being reported.

The card root now carries ten custom properties derived from cardSize,
and the 200 fixed sizes in its subtree reference them: text-[10px]
becomes text-[length:var(--pc-t10,10px)], w-3 h-3 becomes
w-[var(--pc-i3,0.75rem)]. L draws the body 20% larger and XL 40%,
icons included, so the controls grow with the text instead of staying
fiddly to hit.

Custom properties rather than an em-based root font-size. Setting
font-size on the card would silently reshape any text that declares no
size of its own, and would break for portalled content. Each converted
class names its old fixed value as the fallback, so anything rendering
outside a card root is untouched -- which is what leaves the portalled
temperature popover exactly as it is. Its four sites stay fixed on
purpose, as does the page chrome; the conversion was scoped from the
function declarations rather than line numbers, and afterwards only
those four intended sites still hold a literal px value.

S and M stay at 1.0. S is the dense fleet view where density is the
point and M is the default, so an existing install looks identical until
the user reaches for a size that is already asking for more room -- the
same control the request asked this to follow.

Wiki notes the scaling in the card-size table and why it differs from
browser zoom. Tests pin the variable values at every size, including
that S and M still emit the pre-change sizes.
maziggy 1 month ago
parent
commit
45b678692c

+ 2 - 0
CHANGELOG.md

@@ -9,6 +9,8 @@ All notable changes to Bambuddy will be documented in this file.
 - **The external spool can be hidden from the printer card (#1782, reporter @Arn0uDz)** — An external spool holder that never gets used still occupies a full card's width in the **Filaments** row, next to the AMS units that are actually being used. An eye icon at the right-hand end of that row's header now hides it, and clicking it again brings it back, so nothing is lost behind a settings page you would have to remember. The choice is remembered per printer and stored in the browser, like the card size and the offline-printer filter — one machine in a fleet can be tidied up without touching the others, and nothing changes for anyone else using the same Bambuddy. The icon is deliberately absent on a printer with no AMS: there the external spool is the entire filament section, and hiding it would leave an empty row. That guard also covers the case of an AMS being unplugged from a printer whose external spool was hidden earlier — the spool reappears rather than leaving a blank row behind. On the H2D and H2S both external positions share one card and so hide together. Translated in all locales, wiki updated, covered by frontend tests.
 - **The external spool can be hidden from the printer card (#1782, reporter @Arn0uDz)** — An external spool holder that never gets used still occupies a full card's width in the **Filaments** row, next to the AMS units that are actually being used. An eye icon at the right-hand end of that row's header now hides it, and clicking it again brings it back, so nothing is lost behind a settings page you would have to remember. The choice is remembered per printer and stored in the browser, like the card size and the offline-printer filter — one machine in a fleet can be tidied up without touching the others, and nothing changes for anyone else using the same Bambuddy. The icon is deliberately absent on a printer with no AMS: there the external spool is the entire filament section, and hiding it would leave an empty row. That guard also covers the case of an AMS being unplugged from a printer whose external spool was hidden earlier — the spool reappears rather than leaving a blank row behind. On the H2D and H2S both external positions share one card and so hide together. Translated in all locales, wiki updated, covered by frontend tests.
 
 
 ### Changed
 ### Changed
+- **The L and XL printer cards now scale their text and icons, not just their width (#1848, reporter @misterff1)** — Switching a card from M to XL made it wider, enlarged the printer name and the thumbnail, and left everything else exactly as it was: the AMS slot labels, temperatures, filament names, status text and every small button stayed pinned between 8 and 11 pixels, well under the smallest size used anywhere else in Bambuddy. The result was a full-width card carrying the same tiny text as the compact one, which is precisely the opposite of what someone reaching for a bigger card is asking for. Browser zoom is not an answer to this, since it enlarges the entire page and so preserves the very disparity being complained about. The card body now scales along with the card: L draws it 20% larger and XL 40% larger, icons included, so the controls grow with the text rather than staying fiddly to hit. S and M are deliberately untouched — S is the dense fleet view where density is the point, and M is the default, so an existing install looks identical until you reach for a size that is already asking for more room. Wiki updated. Covered by frontend tests.
+
 - **Error and warning toasts now stay up twice as long** — Every pop-up notification disappeared after three seconds regardless of what it said. That is about right for "Settings saved", which confirms something you just did and is skimmed rather than read, but errors and warnings are a different kind of message: they carry a reason, often one relayed from the printer or the backend, and they run to a couple of lines. Three seconds was not long enough to finish reading one, and a missed error message is gone for good — there is no notification history to go back to. Errors and warnings now hold for six seconds. Success and informational toasts keep the three-second default, so the common case of clicking something and seeing it confirmed is unchanged, and the close button and the manual dismiss work exactly as before on all of them. The background print-dispatch toast is unaffected: it stays up while it has work in progress and clears itself shortly after the last job settles. Covered by frontend tests.
 - **Error and warning toasts now stay up twice as long** — Every pop-up notification disappeared after three seconds regardless of what it said. That is about right for "Settings saved", which confirms something you just did and is skimmed rather than read, but errors and warnings are a different kind of message: they carry a reason, often one relayed from the printer or the backend, and they run to a couple of lines. Three seconds was not long enough to finish reading one, and a missed error message is gone for good — there is no notification history to go back to. Errors and warnings now hold for six seconds. Success and informational toasts keep the three-second default, so the common case of clicking something and seeing it confirmed is unchanged, and the close button and the manual dismiss work exactly as before on all of them. The background print-dispatch toast is unaffected: it stays up while it has work in progress and clears itself shortly after the last job settles. Covered by frontend tests.
 
 
 ### Fixed
 ### Fixed

+ 133 - 0
frontend/src/__tests__/pages/PrintersPageCardScale.test.tsx

@@ -0,0 +1,133 @@
+/**
+ * Printer-card body scale (#1848, reporter @misterff1).
+ *
+ * S/M/L/XL already scaled the card's width, thumbnail and printer name, but
+ * every label in the body was pinned at 8-11px, so an XL card carried the same
+ * tiny text as an S one. The body now scales too, driven by custom properties
+ * on the card root.
+ *
+ * S and M stay at 1.0 on purpose: an existing install must look identical
+ * until the user reaches for a size that is already asking for more space.
+ */
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
+import { screen, waitFor } from '@testing-library/react';
+import { render } from '../utils';
+import { PrintersPage } from '../../pages/PrintersPage';
+import { http, HttpResponse } from 'msw';
+import { server } from '../mocks/server';
+
+const mockPrinter = {
+  id: 1,
+  name: 'X1C',
+  ip_address: '192.168.1.100',
+  serial_number: '01P00A000000001',
+  access_code: '12345678',
+  model: 'X1C',
+  enabled: true,
+  nozzle_diameter: 0.4,
+  nozzle_type: 'stainless_steel',
+  location: 'Workshop',
+  auto_archive: true,
+  created_at: '2024-01-01T00:00:00Z',
+  updated_at: '2024-01-01T00:00:00Z',
+};
+
+const STATUS = {
+  connected: true,
+  state: 'IDLE',
+  progress: 0,
+  layer_num: 0,
+  total_layers: 0,
+  temperatures: { nozzle: 25, bed: 25, chamber: 25 },
+  remaining_time: 0,
+  filename: null,
+  wifi_signal: -29,
+  speed_level: 2,
+  ams: [],
+  vt_tray: [],
+};
+
+let store: Record<string, string>;
+
+/** Render at a given card size and hand back the card root's inline style. */
+async function cardStyleAt(cardSize: string) {
+  store['printerCardSize'] = cardSize;
+  render(<PrintersPage />);
+  const card = await waitFor(() => {
+    const el = document.getElementById('printer-card-1');
+    if (!el) throw new Error('card not rendered');
+    return el as HTMLElement;
+  });
+  return card.style;
+}
+
+describe('PrintersPage — printer card body scale (#1848)', () => {
+  beforeEach(() => {
+    store = {};
+    vi.mocked(localStorage.getItem).mockImplementation((key: string) => store[key] ?? null);
+    vi.mocked(localStorage.setItem).mockImplementation((key: string, value: string) => {
+      store[key] = String(value);
+    });
+    server.use(
+      http.get('/api/v1/printers/', () => HttpResponse.json([mockPrinter])),
+      http.get('/api/v1/printers/:id/status', () => HttpResponse.json(STATUS)),
+      http.get('/api/v1/queue/', () => HttpResponse.json([])),
+    );
+  });
+
+  afterEach(() => {
+    vi.mocked(localStorage.getItem).mockReset();
+    vi.mocked(localStorage.setItem).mockReset();
+  });
+
+  it('leaves M — the default — at the sizes shipped before this change', async () => {
+    const style = await cardStyleAt('2');
+
+    expect(style.getPropertyValue('--pc-t10')).toBe('10px');
+    expect(style.getPropertyValue('--pc-t8')).toBe('8px');
+    expect(style.getPropertyValue('--pc-i3')).toBe('12px');
+    expect(style.getPropertyValue('--pc-i4')).toBe('16px');
+  });
+
+  it('leaves S at the same sizes — the dense fleet view wants density', async () => {
+    const style = await cardStyleAt('1');
+
+    expect(style.getPropertyValue('--pc-t10')).toBe('10px');
+    expect(style.getPropertyValue('--pc-i3')).toBe('12px');
+  });
+
+  it('scales the body type and icons at L', async () => {
+    const style = await cardStyleAt('3');
+
+    expect(style.getPropertyValue('--pc-t10')).toBe('12px');
+    expect(style.getPropertyValue('--pc-t8')).toBe('9.6px');
+    expect(style.getPropertyValue('--pc-t11')).toBe('13.2px');
+    expect(style.getPropertyValue('--pc-i3')).toBe('14.4px');
+    expect(style.getPropertyValue('--pc-i4')).toBe('19.2px');
+  });
+
+  it('scales further at XL, where the card is full width', async () => {
+    const style = await cardStyleAt('4');
+
+    expect(style.getPropertyValue('--pc-t10')).toBe('14px');
+    expect(style.getPropertyValue('--pc-i4')).toBe('22.4px');
+    // Every property is set at every size, so a converted class can never
+    // fall through to its fallback while sitting inside a card.
+    for (const name of ['--pc-t8', '--pc-t9', '--pc-t10', '--pc-t11',
+      '--pc-i2', '--pc-i25', '--pc-i3', '--pc-i35', '--pc-i4', '--pc-i5']) {
+      expect(style.getPropertyValue(name)).not.toBe('');
+    }
+  });
+
+  it('drives real elements, not just the root variables', async () => {
+    await cardStyleAt('3');
+
+    // The printer name already scaled before this change and still does.
+    const heading = await screen.findByRole('heading', { name: 'X1C' });
+    expect(heading.className).toContain('text-xl');
+
+    // Body labels now reference the scaled property rather than a fixed px.
+    const scaled = document.querySelectorAll('#printer-card-1 [class*="--pc-t"]');
+    expect(scaled.length).toBeGreaterThan(0);
+  });
+});

File diff suppressed because it is too large
+ 175 - 134
frontend/src/pages/PrintersPage.tsx


File diff suppressed because it is too large
+ 1 - 0
static/assets/index-CJAv6Q5F.css


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-Dl70tNWe.js


File diff suppressed because it is too large
+ 0 - 1
static/assets/index-GBTQ2eaA.css


+ 2 - 2
static/index.html

@@ -26,8 +26,8 @@
 
 
     <!-- Splash screens for iOS -->
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-COuw8Kkt.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-GBTQ2eaA.css">
+    <script type="module" crossorigin src="/assets/index-Dl70tNWe.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-CJAv6Q5F.css">
   </head>
   </head>
   <body>
   <body>
     <div id="root"></div>
     <div id="root"></div>

Some files were not shown because too many files changed in this diff