فهرست منبع

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.

    The AMS-HT card needed separate work, because its temperature and
    humidity readings sit beside the slot rather than under it. That single
    slot was the only growable item on its row, so it took every spare pixel
    and pushed the readings hard against the card's edge; it is now capped
    at roughly two ordinary slots, which keeps them clear at any card width.
    The card itself is capped at one full AMS card's width, so a unit that
    wraps onto a line of its own no longer stretches that slot across the
    whole card.

    The AMS slot minimums are deliberately NOT scaled. Raising them was
    tried and reverted: those cards already grow to fill their row, so
    3.5rem is a floor they sit well above, and raising it only cost a unit
    its place on the row -- which is what pushed the AMS-HT onto a line by
    itself and exposed the stretching above. A test pins them at 3.5rem at
    XL so this reads as a decision rather than a missed spot.
maziggy 3 هفته پیش
والد
کامیت
b0aafb8d26

+ 130 - 1
frontend/src/__tests__/pages/PrintersPageCardScale.test.tsx

@@ -32,6 +32,23 @@ const mockPrinter = {
   updated_at: '2024-01-01T00:00:00Z',
   updated_at: '2024-01-01T00:00:00Z',
 };
 };
 
 
+const baseTray = {
+  tray_color: 'FF0000FF',
+  tray_type: 'PLA',
+  tray_sub_brands: 'PLA Basic',
+  tray_info_idx: 'GFA00',
+  remain: 80,
+  k: 0.02,
+  cali_idx: null,
+  tag_uid: null,
+  tray_uuid: null,
+  nozzle_temp_min: 190,
+  nozzle_temp_max: 230,
+  drying_temp: null,
+  drying_time: null,
+  state: 3,
+};
+
 const STATUS = {
 const STATUS = {
   connected: true,
   connected: true,
   state: 'IDLE',
   state: 'IDLE',
@@ -43,7 +60,38 @@ const STATUS = {
   filename: null,
   filename: null,
   wifi_signal: -29,
   wifi_signal: -29,
   speed_level: 2,
   speed_level: 2,
-  ams: [],
+  ams: [
+    {
+      id: 0,
+      humidity: 30,
+      temp: 28.2,
+      is_ams_ht: false,
+      serial_number: 'AMS00',
+      sw_ver: '03.00.21.29',
+      dry_time: 0,
+      dry_status: 0,
+      dry_sub_status: 0,
+      dry_sf_reason: [],
+      module_type: 'n3f',
+      tray: [0, 1, 2, 3].map((id) => ({ id, ...baseTray })),
+    },
+    {
+      // AMS-HT: a single tray, with its temperature and humidity readings
+      // beside the slot rather than under it.
+      id: 128,
+      humidity: 52,
+      temp: 28.6,
+      is_ams_ht: true,
+      serial_number: 'HT00',
+      sw_ver: '03.00.21.29',
+      dry_time: 0,
+      dry_status: 0,
+      dry_sub_status: 0,
+      dry_sf_reason: [],
+      module_type: 'n3s',
+      tray: [{ id: 0, ...baseTray }],
+    },
+  ],
   vt_tray: [],
   vt_tray: [],
 };
 };
 
 
@@ -61,6 +109,39 @@ async function cardStyleAt(cardSize: string) {
   return card.style;
   return card.style;
 }
 }
 
 
+/** The AMS slot grid's track sizing, once the status has populated the card. */
+async function slotGridColumns(): Promise<string> {
+  return waitFor(() => {
+    const grid = document.querySelector<HTMLElement>('#printer-card-1 [style*="minmax"]');
+    if (!grid) throw new Error('AMS slot grid not rendered');
+    return grid.style.gridTemplateColumns;
+  });
+}
+
+/** The AMS-HT card's own sizing — it is the unit whose readings sit beside the slot. */
+async function htCardStyle(): Promise<CSSStyleDeclaration> {
+  return waitFor(() => {
+    const el = [...document.querySelectorAll<HTMLElement>('#printer-card-1 [class*="rounded-[10px]"]')]
+      .find((d) => /^HT-/.test((d.textContent || '').trim()));
+    if (!el) throw new Error('AMS-HT card not rendered');
+    return el.style;
+  });
+}
+
+/**
+ * The single filament slot inside the AMS-HT card. Scoped through the card
+ * itself, since the card carries a max-width of its own.
+ */
+async function htSlotStyle(): Promise<CSSStyleDeclaration> {
+  return waitFor(() => {
+    const card = [...document.querySelectorAll<HTMLElement>('#printer-card-1 [class*="rounded-[10px]"]')]
+      .find((d) => /^HT-/.test((d.textContent || '').trim()));
+    const el = card?.querySelector<HTMLElement>('[style*="max-width"]');
+    if (!el) throw new Error('AMS-HT slot not rendered');
+    return el.style;
+  });
+}
+
 describe('PrintersPage — printer card body scale (#1848)', () => {
 describe('PrintersPage — printer card body scale (#1848)', () => {
   beforeEach(() => {
   beforeEach(() => {
     store = {};
     store = {};
@@ -119,6 +200,54 @@ describe('PrintersPage — printer card body scale (#1848)', () => {
     }
     }
   });
   });
 
 
+  // Scaling these along with the type was tried and reverted. The AMS cards
+  // already grow to fill their row, so 3.5rem is a floor they sit well above;
+  // raising it only costs a unit its place on the row, and a wrapped AMS-HT is
+  // then alone on its line where flex-grow stretches its single slot across the
+  // whole card.
+  it('leaves the AMS slot columns at their fixed floor, at every size', async () => {
+    await cardStyleAt('2');
+    expect(await slotGridColumns()).toContain('3.5rem');
+  });
+
+  it('still leaves them alone at XL, where the type is largest', async () => {
+    await cardStyleAt('4');
+    expect(await slotGridColumns()).toContain('3.5rem');
+    expect(await slotGridColumns()).not.toContain('4.9rem');
+  });
+
+  // The AMS-HT does need its width scaled: its readings sit beside the slot,
+  // so bigger type eats the room they occupy.
+  it('widens the AMS-HT card with the type, and caps how wide it can get', async () => {
+    await cardStyleAt('3');
+    const ht = await htCardStyle();
+
+    expect(ht.minWidth).toBe('13.2rem'); // 11rem * 1.2
+    expect(ht.flex).toContain('13.2rem');
+    // Without a ceiling, an AMS-HT that wraps onto a line of its own is the
+    // only flex item there and grow stretches its single slot across the
+    // entire card, stranding the readings at the far edge.
+    expect(ht.maxWidth).toBe('calc(4 * 3.5rem + 3 * 0.25rem + 1rem)');
+  });
+
+  it('leaves the AMS-HT card as it was at M', async () => {
+    await cardStyleAt('2');
+    expect((await htCardStyle()).minWidth).toBe('11rem');
+  });
+
+  // The AMS-HT's single slot is the only growable item on its row, so it took
+  // every spare pixel and pushed the readings beside it hard against the card
+  // edge. Capping it is what keeps them clear.
+  it('caps the AMS-HT slot so it cannot swallow the row', async () => {
+    await cardStyleAt('2');
+    expect((await htSlotStyle()).maxWidth).toBe('7.25rem');
+  });
+
+  it('scales that cap with the type, since the slot label scales too', async () => {
+    await cardStyleAt('4');
+    expect((await htSlotStyle()).maxWidth).toBe('10.15rem'); // 7.25rem * 1.4
+  });
+
   it('drives real elements, not just the root variables', async () => {
   it('drives real elements, not just the root variables', async () => {
     await cardStyleAt('3');
     await cardStyleAt('3');
 
 

+ 44 - 8
frontend/src/pages/PrintersPage.tsx

@@ -4733,7 +4733,31 @@ function PrinterCard({
               const canHideExternalSpool = amsData.length > 0 && status.vt_tray.length > 0;
               const canHideExternalSpool = amsData.length > 0 && status.vt_tray.length > 0;
               const showExternalSpool = !(canHideExternalSpool && externalSpoolHidden);
               const showExternalSpool = !(canHideExternalSpool && externalSpoolHidden);
               const isDualNozzle = printer.nozzle_count === 2 || status?.temperatures?.nozzle_2 !== undefined;
               const isDualNozzle = printer.nozzle_count === 2 || status?.temperatures?.nozzle_2 !== undefined;
-              const filamentSlotClass = 'min-w-14';
+              const bodyScale = CARD_BODY_SCALE[cardSize] ?? 1;
+              // Rounded because 11 * 1.2 is 13.200000000000001 in binary
+              // floating point, and that lands verbatim in the DOM.
+              const scaledRem = (base: number) => `${Math.round(base * bodyScale * 1000) / 1000}rem`;
+              // Deliberately NOT scaled with the body (#1848). These AMS cards
+              // already grow to fill the row, so the 3.5rem is a floor they sit
+              // well above in practice -- raising it just costs a unit its place
+              // on the row. Losing one matters: a wrapped AMS-HT is then alone on
+              // its line, and flex-grow stretches its single slot across the whole
+              // card. The AMS-HT's own width does scale, below, because its
+              // readings sit beside the slot rather than under it.
+              const slotMinWidth = '3.5rem';
+              const filamentSlotStyle: React.CSSProperties = { minWidth: slotMinWidth };
+              // The AMS-HT holds a single spool, and its slot is the only thing
+              // on that row able to grow -- so it swallowed every spare pixel and
+              // shoved the temperature and humidity hard against the card's edge.
+              // Capping it at roughly two ordinary slots keeps the readings clear
+              // of the edge; the cap scales because the text inside the slot does.
+              const htSlotStyle: React.CSSProperties = {
+                minWidth: slotMinWidth,
+                maxWidth: scaledRem(7.25),
+              };
+              const slotGridStyle = (columns: number): React.CSSProperties => ({
+                gridTemplateColumns: `repeat(${columns}, minmax(${slotMinWidth}, 1fr))`,
+              });
               // #1762 (comment 2): while a print is running/paused, overlay a small
               // #1762 (comment 2): while a print is running/paused, overlay a small
               // "P1 / P2 / P3" pill on each slot referenced by the active print's
               // "P1 / P2 / P3" pill on each slot referenced by the active print's
               // mapping. Catches the reporter's scenario — "any X1C" queue job
               // mapping. Catches the reporter's scenario — "any X1C" queue job
@@ -4746,7 +4770,7 @@ function PrinterCard({
               const getAmsCardStyle = (slotCount: number): React.CSSProperties => {
               const getAmsCardStyle = (slotCount: number): React.CSSProperties => {
                 const boundedSlotCount = Math.max(1, slotCount);
                 const boundedSlotCount = Math.max(1, slotCount);
                 const gapCount = Math.max(0, boundedSlotCount - 1);
                 const gapCount = Math.max(0, boundedSlotCount - 1);
-                const minWidth = `calc(${boundedSlotCount} * 3.5rem + ${gapCount} * 0.25rem + 1rem)`;
+                const minWidth = `calc(${boundedSlotCount} * ${slotMinWidth} + ${gapCount} * 0.25rem + 1rem)`;
                 return {
                 return {
                   flex: `1 1 ${minWidth}`,
                   flex: `1 1 ${minWidth}`,
                   minWidth,
                   minWidth,
@@ -4914,7 +4938,7 @@ function PrinterCard({
                               </div>
                               </div>
                             )}
                             )}
                             {/* Slots grid: 4 columns - always render 4 slots */}
                             {/* Slots grid: 4 columns - always render 4 slots */}
-                            <div className="grid w-full grid-cols-[repeat(4,minmax(3.5rem,1fr))] gap-1">
+                            <div className="grid w-full gap-1" style={slotGridStyle(4)}>
                               {[0, 1, 2, 3].map((slotIdx) => {
                               {[0, 1, 2, 3].map((slotIdx) => {
                                 // Find tray data for this slot (may be undefined if data incomplete)
                                 // Find tray data for this slot (may be undefined if data incomplete)
                                 // Use array index if available, as tray.id may not always be set
                                 // Use array index if available, as tray.id may not always be set
@@ -5057,7 +5081,7 @@ function PrinterCard({
 
 
                                 // Wrapper with menu button, dropdown, and loading overlay (outside hover card)
                                 // Wrapper with menu button, dropdown, and loading overlay (outside hover card)
                                 return (
                                 return (
-                                  <div key={slotIdx} className={`relative group w-full ${filamentSlotClass}`}>
+                                  <div key={slotIdx} className="relative group w-full" style={filamentSlotStyle}>
                                     {/* Loading overlay during RFID re-read */}
                                     {/* Loading overlay during RFID re-read */}
                                     {isRefreshing && (
                                     {isRefreshing && (
                                       <div className="absolute inset-0 bg-bambu-dark-tertiary/80 rounded flex items-center justify-center z-20">
                                       <div className="absolute inset-0 bg-bambu-dark-tertiary/80 rounded flex items-center justify-center z-20">
@@ -5351,7 +5375,19 @@ function PrinterCard({
                         // like regular AMS), so they need more horizontal room than a 1-slot basis.
                         // like regular AMS), so they need more horizontal room than a 1-slot basis.
                         // Without this override, the L view squishes HT into a sliver next to the
                         // Without this override, the L view squishes HT into a sliver next to the
                         // 4-slot AMS neighbors.
                         // 4-slot AMS neighbors.
-                        const htCardStyle: React.CSSProperties = { flex: '1 1 11rem', minWidth: '11rem' };
+                        // 11rem holds one slot plus the temperature/humidity
+                        // column beside it; both grow with the body scale.
+                        const htCardWidth = scaledRem(11);
+                        const htCardStyle: React.CSSProperties = {
+                          flex: `1 1 ${htCardWidth}`,
+                          minWidth: htCardWidth,
+                          // An AMS-HT is never wider than a full AMS. Without a
+                          // ceiling, a unit that wraps onto a line of its own is
+                          // the only flex item there and grow stretches its single
+                          // slot across the entire card, leaving the readings
+                          // stranded at the far edge.
+                          maxWidth: `calc(4 * ${slotMinWidth} + 3 * 0.25rem + 1rem)`,
+                        };
                         return (
                         return (
                           <div key={ams.id} style={htCardStyle} className="min-w-0 p-2 bg-bambu-dark rounded-[10px] space-y-1">
                           <div key={ams.id} style={htCardStyle} className="min-w-0 p-2 bg-bambu-dark rounded-[10px] space-y-1">
                             {/* Row 1: Label + Nozzle + Drying */}
                             {/* Row 1: Label + Nozzle + Drying */}
@@ -5442,7 +5478,7 @@ function PrinterCard({
                             {/* Row 2: Slot (left) + Stats (right stacked) */}
                             {/* Row 2: Slot (left) + Stats (right stacked) */}
                             <div className="flex gap-1.5 max-[550px]:flex-col max-[550px]:items-start">
                             <div className="flex gap-1.5 max-[550px]:flex-col max-[550px]:items-start">
                               {/* Slot wrapper with loading overlay */}
                               {/* Slot wrapper with loading overlay */}
-                              <div className="relative group min-w-14 flex-1">
+                              <div className="relative group flex-1" style={htSlotStyle}>
                                 {/* Loading overlay during RFID re-read */}
                                 {/* Loading overlay during RFID re-read */}
                                 {isHtRefreshing && (
                                 {isHtRefreshing && (
                                   <div className="absolute inset-0 bg-bambu-dark-tertiary/80 rounded flex items-center justify-center z-20">
                                   <div className="absolute inset-0 bg-bambu-dark-tertiary/80 rounded flex items-center justify-center z-20">
@@ -5629,7 +5665,7 @@ function PrinterCard({
                           <div className="flex w-full min-h-7 items-center gap-1.5 rounded-lg bg-bambu-dark-secondary px-2 py-1">
                           <div className="flex w-full min-h-7 items-center gap-1.5 rounded-lg bg-bambu-dark-secondary px-2 py-1">
                             <span className="block min-w-0 flex-1 truncate text-[length:var(--pc-t10,10px)] text-white font-medium">{t('printers.external')}</span>
                             <span className="block min-w-0 flex-1 truncate text-[length:var(--pc-t10,10px)] text-white font-medium">{t('printers.external')}</span>
                           </div>
                           </div>
-                          <div className={`grid w-full ${status.vt_tray.length > 1 ? 'grid-cols-[repeat(2,minmax(3.5rem,1fr))]' : 'grid-cols-[minmax(3.5rem,1fr)]'} gap-1`}>
+                          <div className="grid w-full gap-1" style={slotGridStyle(status.vt_tray.length > 1 ? 2 : 1)}>
                             {[...status.vt_tray].sort((a, b) => (a.id ?? 254) - (b.id ?? 254)).map((extTray) => {
                             {[...status.vt_tray].sort((a, b) => (a.id ?? 254) - (b.id ?? 254)).map((extTray) => {
                               const extTrayId = extTray.id ?? 254;
                               const extTrayId = extTray.id ?? 254;
                               // On dual-nozzle (H2C/H2D), tray_now=254 means "external spool"
                               // On dual-nozzle (H2C/H2D), tray_now=254 means "external spool"
@@ -5721,7 +5757,7 @@ function PrinterCard({
                               );
                               );
 
 
                               return (
                               return (
-                                <div key={extTrayId} className={`relative group w-full ${filamentSlotClass}`}>
+                                <div key={extTrayId} className="relative group w-full" style={filamentSlotStyle}>
                                   {!isEmpty ? (
                                   {!isEmpty ? (
                                     <FilamentHoverCard
                                     <FilamentHoverCard
                                       data={extFilamentData}
                                       data={extFilamentData}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 0
static/assets/index-B3jj6-fz.css


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/index-B67xFyee.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 1
static/assets/index-CJAv6Q5F.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-Dl70tNWe.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-CJAv6Q5F.css">
+    <script type="module" crossorigin src="/assets/index-B67xFyee.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-B3jj6-fz.css">
   </head>
   </head>
   <body>
   <body>
     <div id="root"></div>
     <div id="root"></div>

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است