Преглед изворни кода

fix(print-modal): the slot picker can be read on a phone held upright (#3294)

- The open slot list took the width of its button, which on a portrait
  phone is a swatch and a chevron (about 55px on a nozzle-rack H2C). It
  is now as wide as its rows want, at least the button's width and at
  most the screen's less a margin, and moves left rather than running
  off the right edge. The screen width leaves out a desktop scrollbar.
- Below sm the mapping row, the model-mode override row and the
  per-printer custom slot mapping give the picker a line of its own
  under the required filament. From sm up the columns are unchanged.
maziggy пре 13 часа
родитељ
комит
a857cd1804

+ 2 - 0
CHANGELOG.md

@@ -174,6 +174,8 @@ All notable changes to Bambuddy will be documented in this file.
 - **The frontend build no longer warns about `path` and `crypto` being externalized for the STEP previewer (#2976)** — `occt-import-js`, the Emscripten build behind STEP previews, requires both modules, but only inside its `ENVIRONMENT_IS_NODE` branches; in the browser it loads its `.wasm` from the URL the preview worker passes and draws randomness from `crypto.getRandomValues`. Vite still externalized both and printed two warnings on every build. `vite.config.ts` now drops exactly those two warnings for that one package through `build.rolldownOptions.onLog`, so an externalization anywhere else, or of any other module, still shows.
 
 ### Fixed
+- **The print dialog's slot picker can be read on a phone held upright (#3294, reported by @apizz)** — In the Print and Schedule dialogs, the picker that chooses a printer slot for each filament was squeezed to a color dot and an arrow on a portrait phone. On printers with a nozzle rack (H2C) it had about 55 pixels. Its open list was only as wide as the picker, so the slot names couldn't be read until the phone was turned sideways.
+  - **Now:** on screens narrower than 640 pixels the picker gets a line of its own under the filament the file asks for. The open list is as wide as the slot names need, up to the width of the screen, and moves left instead of running off the right edge. The same applies to the filament override picker used when printing to any printer of a model, and to the custom slot mapping shown for each printer when printing to several. On wider screens the rows are laid out as before, and a long slot name in the open list is shown in full instead of cut off.
 - **Stitching a layer timelapse no longer needs gigabytes of memory (#3292, reported by @fma965)** — ffmpeg's x264 encoder picks its number of threads from the host's cores, not the container's CPU limit, so stitching 300 frames of 1080p peaked at 1.2 GiB on a 30-core host and 120 frames of 4K at 4.6 GiB. ffmpeg runs inside Bambuddy's container and counts against its memory limit, so a pod with a 2 GiB limit was killed the moment a long print finished, and its timelapse was lost.
   - **Now:** the stitch uses two encoder threads and a shorter lookahead, with the same quality settings. Frames larger than 1080p are scaled down to it, keeping their shape. Measured: 1080p 1.2 GiB → 300 MiB, 4K 4.6 GiB → 270 MiB. On a host with many cores the stitch takes longer. Saving an edit in the timelapse editor encodes with the same limits.
   - **No ffmpeg left behind:** the camera snapshots, the printer camera capture, the last frame taken from a timelapse and the stitch now stop their ffmpeg however they end. Before, only running out of time did, and an error or a cancelled capture left ffmpeg running and holding memory. A stitch that stops part-way no longer leaves a partial video behind.

+ 246 - 0
frontend/src/__tests__/components/PrintModalNarrowScreen.test.tsx

@@ -0,0 +1,246 @@
+/**
+ * The print dialog's slot pickers can be read on a phone held upright (#3294).
+ *
+ * Two things hid them. The open list took the width of its button and nothing
+ * more, and the button had almost no width: on a nozzle-rack printer the
+ * required-filament cell has a 210px floor, which on a ~400px portrait screen
+ * left the slot button about 55px -- a swatch and a chevron. Only turning the
+ * phone sideways made the slot names readable.
+ *
+ * Now the list is as wide as its rows want, never narrower than its button and
+ * never wider than the screen, and it moves left rather than running off the
+ * right edge. Below the `sm` breakpoint the picker gets a line of its own
+ * under the required filament. jsdom does no layout, so these pin the numbers
+ * and classes that make it fit.
+ */
+
+import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest';
+import { screen, fireEvent, cleanup } from '@testing-library/react';
+import { http, HttpResponse } from 'msw';
+import { render } from '../utils';
+import { server } from '../mocks/server';
+import { SlotPicker } from '../../components/PrintModal/SlotPicker';
+import { FilamentMapping } from '../../components/PrintModal/FilamentMapping';
+import { FilamentOverride } from '../../components/PrintModal/FilamentOverride';
+import { PrinterSelector } from '../../components/PrintModal/PrinterSelector';
+import type { PrinterMappingResult } from '../../hooks/useMultiPrinterFilamentMapping';
+import type { Printer, PrinterStatus } from '../../api/client';
+
+const SCREEN = 412;
+const LIST_WIDTH = 380;
+
+function rect(left: number, width: number): DOMRect {
+  return { left, width, top: 300, bottom: 324, right: left + width, height: 24, x: left, y: 300, toJSON: () => ({}) } as DOMRect;
+}
+
+describe('SlotPicker list on a narrow screen', () => {
+  let natural = LIST_WIDTH;
+
+  beforeEach(() => {
+    natural = LIST_WIDTH;
+    // A screen SCREEN wide; a page scrollbar would make innerWidth larger.
+    vi.spyOn(document.documentElement, 'clientWidth', 'get').mockReturnValue(SCREEN);
+    // The list is as wide as its rows want, within the limits written on it --
+    // what a browser's layout does with width: max-content.
+    vi.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockImplementation(function (this: HTMLElement) {
+      if (this.getAttribute('role') !== 'listbox') return 0;
+      const min = parseFloat(this.style.minWidth) || 0;
+      const max = parseFloat(this.style.maxWidth) || Infinity;
+      return Math.min(Math.max(natural, min), max);
+    });
+  });
+
+  afterEach(() => {
+    vi.restoreAllMocks();
+    cleanup();
+  });
+
+  function openAt(triggerRect: DOMRect) {
+    render(
+      <SlotPicker
+        value=""
+        options={[{ value: '1', label: 'A2: Polymaker PLA Matte (Charcoal Black)', meta: ' - 602g left', hex: '#2F2E30' }]}
+        placeholder="-- Select slot --"
+        ariaLabel="Slot for PLA"
+        onChange={() => {}}
+      />,
+    );
+    const trigger = screen.getByRole('combobox');
+    vi.spyOn(trigger, 'getBoundingClientRect').mockReturnValue(triggerRect);
+    fireEvent.click(trigger);
+    return screen.getByRole('listbox');
+  }
+
+  it('is as wide as its rows want, not as narrow as its button', () => {
+    const list = openAt(rect(320, 55));
+    expect(list.style.width).toBe('max-content');
+    expect(list.style.minWidth).toBe('55px');
+  });
+
+  it('is never wider than the screen, less a margin', () => {
+    const list = openAt(rect(320, 55));
+    expect(list.style.maxWidth).toBe(`${SCREEN - 16}px`);
+  });
+
+  it('moves left rather than running off the right edge', () => {
+    const list = openAt(rect(320, 55));
+    expect(list.style.left).toBe(`${SCREEN - 8 - LIST_WIDTH}px`);
+  });
+
+  it('stays under its button when it fits', () => {
+    const list = openAt(rect(16, 300));
+    expect(list.style.left).toBe('16px');
+    expect(list.style.minWidth).toBe('300px');
+  });
+
+  it('is kept on screen at the width of its button, when that is wider than its rows', () => {
+    natural = 200;
+    const list = openAt(rect(150, 300));
+    expect(list.style.left).toBe(`${SCREEN - 8 - 300}px`);
+  });
+
+  it('is kept inside the margin on both sides when its rows want more than the screen', () => {
+    natural = 1000;
+    const list = openAt(rect(320, 55));
+    // It starts at the left margin rather than off the left edge.
+    expect(list.style.left).toBe('8px');
+  });
+
+  it('leaves out the scrollbar', () => {
+    Object.defineProperty(window, 'innerWidth', { configurable: true, value: SCREEN + 15 });
+    try {
+      const list = openAt(rect(320, 55));
+      expect(list.style.maxWidth).toBe(`${SCREEN - 16}px`);
+      expect(list.style.left).toBe(`${SCREEN - 8 - LIST_WIDTH}px`);
+    } finally {
+      Object.defineProperty(window, 'innerWidth', { configurable: true, value: 1024 });
+    }
+  });
+});
+
+/** The grid row that holds a slot picker. */
+function rowOf(picker: HTMLElement): HTMLElement {
+  return picker.parentElement as HTMLElement;
+}
+
+function expectStackedBelowSm(row: HTMLElement, wideColumns: string) {
+  // One column for the swatch / arrow, the picker, one for the status icon.
+  expect(row.className).toMatch(/(^|\s)grid-cols-\[16px_minmax\(0,1fr\)_\d+px\](\s|$)/);
+  expect(row.className).toContain(`sm:grid-cols-[${wideColumns}]`);
+  // The required filament takes the whole first line, so the arrow, the
+  // picker and the status icon wrap onto the second.
+  const required = row.children[1] as HTMLElement;
+  expect(required.className).toMatch(/(^|\s)col-span-2(\s|$)/);
+  expect(required.className).toContain('sm:col-span-1');
+}
+
+describe('mapping row on a narrow screen', () => {
+  const tray = (id: number, color: string) => ({ id, tray_type: 'PLA', tray_color: color, tray_info_idx: 'GFA01' });
+
+  function mount(status: Record<string, unknown>, filament: Record<string, unknown> = {}) {
+    server.use(
+      http.get('/api/v1/printers/:id/status', () =>
+        HttpResponse.json({
+          id: 1,
+          name: 'H2C-1',
+          connected: true,
+          state: 'IDLE',
+          ams: [{ id: 0, tray: [tray(0, 'DE4343'), tray(1, '2F2E30')] }],
+          vt_tray: [],
+          ams_extruder_map: {},
+          ...status,
+        } as unknown as PrinterStatus),
+      ),
+      http.get('/api/v1/printers/:id/spool-assignments', () => HttpResponse.json([])),
+    );
+    render(
+      <FilamentMapping
+        printerId={1}
+        filamentReqs={{
+          filaments: [{ slot_id: 1, type: 'PLA', color: '#DE4343', used_grams: 297.6, used_meters: 90, ...filament }],
+        }}
+        manualMappings={{}}
+        onManualMappingChange={() => {}}
+        currencySymbol="$"
+        defaultCostPerKg={20}
+        defaultExpanded
+      />,
+    );
+  }
+
+  afterEach(() => cleanup());
+
+  it('puts the picker on its own line, and keeps the wide layout from sm up', async () => {
+    mount({});
+    const picker = await screen.findByRole('combobox', { name: /printer slot for/i });
+    expectStackedBelowSm(rowOf(picker), '16px_minmax(70px,1fr)_auto_2fr_16px');
+  });
+
+  it('does the same on a nozzle-rack printer, where the squeeze was worst', async () => {
+    const rackSlot = (id: number) => ({ id, nozzle_type: 'HH01', nozzle_diameter: '0.4', filament_color: '', filament_id: '', filament_type: '' });
+    mount(
+      { nozzle_rack: [rackSlot(16), rackSlot(17), rackSlot(1)] },
+      { group_id: 1, group: { on_rack: true, nozzle_diameter: '0.40', volume_type: 'Standard', filament_color: '#DE4343' } },
+    );
+    await screen.findByLabelText('Rack position');
+    expectStackedBelowSm(rowOf(screen.getByRole('combobox', { name: /printer slot for/i })), '16px_minmax(210px,1.4fr)_auto_2fr_16px');
+  });
+});
+
+describe('override row on a narrow screen', () => {
+  afterEach(() => cleanup());
+
+  it('puts the picker on its own line, and keeps the wide layout from sm up', () => {
+    render(
+      <FilamentOverride
+        filamentReqs={{ filaments: [{ slot_id: 1, type: 'PLA', color: '#FF0000', used_grams: 25, used_meters: 8.5 }] }}
+        availableFilaments={[
+          { type: 'PLA', color: '#00FF00', tray_info_idx: 'GFA01', tray_sub_brands: 'PLA Basic', extruder_id: null },
+        ]}
+        overrides={{}}
+        onChange={() => {}}
+      />,
+    );
+    expectStackedBelowSm(rowOf(screen.getByRole('combobox')), '16px_minmax(70px,1fr)_auto_2fr_20px');
+  });
+});
+
+describe('custom slot mapping of one of several printers, on a narrow screen', () => {
+  afterEach(() => cleanup());
+
+  it('puts the select on its own line, and keeps the wide layout from sm up', async () => {
+    const printer = (id: number) => ({ id, name: `H2C-${id}`, model: 'H2C', ip_address: `192.0.2.${id}`, is_active: true }) as unknown as Printer;
+    const result = (id: number): PrinterMappingResult => ({
+      printerId: id,
+      printerName: `H2C-${id}`,
+      status: undefined,
+      isLoading: false,
+      loadedFilaments: [],
+      autoMapping: undefined,
+      finalMapping: undefined,
+      matchStatus: 'missing',
+      exactMatches: 0,
+      typeOnlyMatches: 0,
+      missingTypes: 1,
+      totalSlots: 1,
+      config: { useDefault: false, manualMappings: {}, autoConfigured: false },
+    });
+    render(
+      <PrinterSelector
+        printers={[printer(1), printer(2)]}
+        selectedPrinterIds={[1, 2]}
+        onMultiSelect={() => {}}
+        allowMultiple
+        printerMappingResults={[result(1), result(2)]}
+        filamentReqs={{ filaments: [{ slot_id: 1, type: 'PLA', color: '#FF0000', used_grams: 25, used_meters: 8.5 }] }}
+        onAutoConfigurePrinter={() => {}}
+        onUpdatePrinterConfig={() => {}}
+      />,
+    );
+    const selects = await screen.findAllByRole('combobox');
+    expect(selects.length).toBe(2);
+    for (const select of selects) {
+      expectStackedBelowSm(rowOf(select), '16px_minmax(70px,1fr)_auto_2fr_16px');
+    }
+  });
+});

+ 18 - 13
frontend/src/components/PrintModal/FilamentMapping.tsx

@@ -414,19 +414,24 @@ export function FilamentMapping({
             );
             return (
             <div key={idx} className="space-y-1">
+              {/*
+                The rack picker sits inside the required-filament cell, so on a
+                rack machine that cell has to carry the name *and* an ~85px
+                dropdown. Raising only the floor (not the fraction) keeps every
+                other printer's layout exactly as it was, and keeps the AMS
+                dropdown — which names type, colour and remaining weight — the
+                widest column.
+
+                Below `sm` the slot picker gets a line of its own under the
+                required filament (#3294): on a phone held upright, the 210px
+                floor left it about 55px, a swatch and a chevron.
+              */}
               <div
-                className="grid items-center gap-2 text-xs"
-                style={{
-                  // The rack picker sits inside the required-filament cell, so
-                  // on a rack machine that cell has to carry the name *and* an
-                  // ~85px dropdown. Raising only the floor (not the fraction)
-                  // keeps every other printer's layout exactly as it was, and
-                  // keeps the AMS dropdown — which names type, colour and
-                  // remaining weight — the widest column.
-                  gridTemplateColumns: hasRack
-                    ? '16px minmax(210px, 1.4fr) auto 2fr 16px'
-                    : '16px minmax(70px, 1fr) auto 2fr 16px',
-                }}
+                className={`grid items-center gap-2 text-xs grid-cols-[16px_minmax(0,1fr)_16px] ${
+                  hasRack
+                    ? 'sm:grid-cols-[16px_minmax(210px,1.4fr)_auto_2fr_16px]'
+                    : 'sm:grid-cols-[16px_minmax(70px,1fr)_auto_2fr_16px]'
+                }`}
               >
                 {/* Required color */}
                 <span title={t('printModal.requiredTooltip', { name: resolvedName, color: requiredColorLabel })}>
@@ -435,7 +440,7 @@ export function FilamentMapping({
                 {/* Required type + grams + nozzle badge. Only the name
                     truncates; the gram usage is pinned (shrink-0) so it never
                     clips on narrow/mobile widths (#2669). */}
-                <span className="text-white flex items-center gap-1 min-w-0">
+                <span className="text-white flex items-center gap-1 min-w-0 col-span-2 sm:col-span-1">
                   {hasRack && item.group_id != null && item.group ? (
                     item.group.on_rack ? (
                       <select

+ 4 - 5
frontend/src/components/PrintModal/FilamentOverride.tsx

@@ -111,16 +111,15 @@ export function FilamentOverride({
 
           return (
             <div key={req.slot_id} className="space-y-1">
-              <div
-                className="grid items-center gap-2 text-xs"
-                style={{ gridTemplateColumns: '16px minmax(70px, 1fr) auto 2fr 20px' }}
-              >
+              {/* Below `sm` the picker gets a line of its own, as on the
+                  mapping row (#3294). */}
+              <div className="grid items-center gap-2 text-xs grid-cols-[16px_minmax(0,1fr)_20px] sm:grid-cols-[16px_minmax(70px,1fr)_auto_2fr_20px]">
                 {/* Original color swatch */}
                 <span title={`${t('printModal.originalFilament')}: ${resolvedName} - ${colorLabel}`}>
                   <Circle className="w-3 h-3" fill={req.color} stroke={req.color} />
                 </span>
                 {/* Original type + grams */}
-                <span className="text-white truncate">
+                <span className="text-white truncate col-span-2 sm:col-span-1">
                   {resolvedName} <span className="text-bambu-gray">({req.used_grams}g)</span>
                 </span>
                 {/* Arrow */}

+ 5 - 4
frontend/src/components/PrintModal/PrinterSelector.tsx

@@ -155,17 +155,18 @@ function InlineMappingEditor({
       </div>
 
       {slotAssignments.map(({ req, loaded, status, isManual }, idx) => (
+        // Below `sm` the slot select gets a line of its own, as on the
+        // single-printer mapping row (#3294).
         <div
           key={idx}
-          className="grid items-center gap-2 text-xs"
-          style={{ gridTemplateColumns: '16px minmax(70px, 1fr) auto 2fr 16px' }}
+          className="grid items-center gap-2 text-xs grid-cols-[16px_minmax(0,1fr)_16px] sm:grid-cols-[16px_minmax(70px,1fr)_auto_2fr_16px]"
         >
           <span title={`Required: ${req.type} - ${getColorName(req.color)}`}>
             <Circle className="w-3 h-3" fill={req.color} stroke={req.color} />
           </span>
           {/* Only the name truncates; the gram usage is pinned (shrink-0) so
               it never clips on narrow/mobile widths (#2669). */}
-          <span className="text-white flex items-center gap-1 min-w-0">
+          <span className="text-white flex items-center gap-1 min-w-0 col-span-2 sm:col-span-1">
             <span className="truncate min-w-0" title={req.type}>{req.type}</span>
             <span className="text-bambu-gray shrink-0 whitespace-nowrap">({req.used_grams}g)</span>
           </span>
@@ -173,7 +174,7 @@ function InlineMappingEditor({
           <select
             value={loaded?.globalTrayId ?? ''}
             onChange={(e) => handleSlotChange(req.slot_id || 0, e.target.value)}
-            className={`flex-1 px-2 py-1 rounded border text-xs bg-bambu-dark-secondary focus:outline-none focus:ring-1 focus:ring-bambu-green ${
+            className={`flex-1 min-w-0 px-2 py-1 rounded border text-xs bg-bambu-dark-secondary focus:outline-none focus:ring-1 focus:ring-bambu-green ${
               status === 'match'
                 ? 'border-bambu-green/50 text-bambu-green'
                 : status === 'type_only'

+ 25 - 4
frontend/src/components/PrintModal/SlotPicker.tsx

@@ -74,6 +74,8 @@ interface SlotPickerProps {
 }
 
 const SWATCH_CLASS = 'w-3 h-3';
+/** Gap kept between the open list and the screen's left and right edges. */
+const EDGE = 8;
 
 export function SlotPicker({
   value,
@@ -89,7 +91,7 @@ export function SlotPicker({
 }: SlotPickerProps) {
   const [open, setOpen] = useState(false);
   const [activeIndex, setActiveIndex] = useState(0);
-  const [coords, setCoords] = useState<{ top: number; left: number; width: number } | null>(null);
+  const [coords, setCoords] = useState<{ top: number; left: number; minWidth: number; maxWidth: number } | null>(null);
   const triggerRef = useRef<HTMLButtonElement>(null);
   const listRef = useRef<HTMLDivElement>(null);
 
@@ -116,10 +118,27 @@ export function SlotPicker({
     const below = window.innerHeight - rect.bottom;
     const height = Math.min(listRef.current?.scrollHeight ?? 0, 240);
     const flip = below < height + 8 && rect.top > below;
+    // As wide as the rows want, never narrower than the trigger and never
+    // wider than the screen. Taking the trigger's width alone made the list
+    // unreadable on a phone held upright, where the trigger is a swatch and a
+    // chevron (#3294). clientWidth leaves out a desktop's vertical scrollbar,
+    // which innerWidth counts; jsdom reports 0 for it.
+    const screenWidth = document.documentElement.clientWidth || window.innerWidth;
+    const maxWidth = screenWidth - 2 * EDGE;
+    const minWidth = Math.min(rect.width, maxWidth);
+    // Measured with this trigger's minimum, not the one left from the last
+    // open, so the width used to keep it on screen is the one it will have.
+    // The maximum needs no such care: a list that wants more than the screen
+    // starts at the left margin either way.
+    const list = listRef.current;
+    if (list) list.style.minWidth = `${minWidth}px`;
+    const width = list?.offsetWidth ?? minWidth;
     setCoords({
       top: flip ? Math.max(8, rect.top - height - 4) : rect.bottom + 4,
-      left: rect.left,
-      width: rect.width,
+      // Under the trigger, moved left when that would run off the right edge.
+      left: Math.max(EDGE, Math.min(rect.left, screenWidth - EDGE - width)),
+      minWidth,
+      maxWidth,
     });
   }, []);
 
@@ -251,7 +270,9 @@ export function SlotPicker({
             style={{
               top: coords?.top ?? -9999,
               left: coords?.left ?? -9999,
-              width: coords?.width,
+              width: 'max-content',
+              minWidth: coords?.minWidth,
+              maxWidth: coords?.maxWidth,
               // Hide until placed, so there is no flash at (-9999,-9999) —
               // same trick FilamentHoverCard uses.
               visibility: coords ? 'visible' : 'hidden',

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
static/assets/ArchivesPage-Cb7AMX0z.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
static/assets/CameraPage-fR-X9Rcg.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
static/assets/ConfirmOutcomeDialog-DkgbavIS.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
static/assets/FileManagerPage-Df66VFIs.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
static/assets/InventoryPage-C3TLQLa5.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
static/assets/Layout-C9f49H5A.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
static/assets/ModelSourcesPage-HCViktbw.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
static/assets/PhotoGalleryModal-7qNB2zdA.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
static/assets/PrintModal-BtM1dElr.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
static/assets/PrintModal-CJu7nKOF.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
static/assets/PrintersPage-DORKejGO.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
static/assets/ProjectDetailPage-B6zD7j31.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
static/assets/QueuePage-BWrK8xh_.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
static/assets/RichTextEditor-ZpwKCUZV.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
static/assets/SettingsPage-Vrpnp9Hp.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
static/assets/SliceModal-BpM_ev7Q.js


Разлика између датотеке није приказан због своје велике величине
+ 1 - 1
static/assets/SpoolBuddySettingsPage-CHiI5hdn.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 1
static/assets/index-CLHFMRO6.css


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
static/assets/index-CxGVHZzK.js


Разлика између датотеке није приказан због своје велике величине
+ 1 - 0
static/assets/index-Dv5E0ZTV.css


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
static/assets/locationSensorDefaults-Bn_DTObi.js


+ 5 - 5
static/index.html

@@ -26,7 +26,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-BYDR9bb5.js"></script>
+    <script type="module" crossorigin src="/assets/index-CxGVHZzK.js"></script>
     <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-aKtaBQYM.js">
     <link rel="modulepreload" crossorigin href="/assets/client-DSw2TdH4.js">
     <link rel="modulepreload" crossorigin href="/assets/react-dom-PmRWqOqI.js">
@@ -97,7 +97,7 @@
     <link rel="modulepreload" crossorigin href="/assets/printer-qo845RQA.js">
     <link rel="modulepreload" crossorigin href="/assets/filamentSwatchHelpers-C3r5m07g.js">
     <link rel="modulepreload" crossorigin href="/assets/FilamentSwatch-CElzyuMD.js">
-    <link rel="modulepreload" crossorigin href="/assets/PrintModal-BtM1dElr.js">
+    <link rel="modulepreload" crossorigin href="/assets/PrintModal-CJu7nKOF.js">
     <link rel="modulepreload" crossorigin href="/assets/download-nyR-1mBX.js">
     <link rel="modulepreload" crossorigin href="/assets/eye-c0u5gXED.js">
     <link rel="modulepreload" crossorigin href="/assets/folder-kanban-Br4ECQKC.js">
@@ -108,7 +108,7 @@
     <link rel="modulepreload" crossorigin href="/assets/trash-2-CUF18zKt.js">
     <link rel="modulepreload" crossorigin href="/assets/weight-DEayKDXK.js">
     <link rel="modulepreload" crossorigin href="/assets/projectTree-C5LrJHZP.js">
-    <link rel="modulepreload" crossorigin href="/assets/ConfirmOutcomeDialog-ct-PiJ6A.js">
+    <link rel="modulepreload" crossorigin href="/assets/ConfirmOutcomeDialog-DkgbavIS.js">
     <link rel="modulepreload" crossorigin href="/assets/key-DdGCOutm.js">
     <link rel="modulepreload" crossorigin href="/assets/log-out-CDt27Bxj.js">
     <link rel="modulepreload" crossorigin href="/assets/shield-alert-Cjpp4ey-.js">
@@ -120,8 +120,8 @@
     <link rel="modulepreload" crossorigin href="/assets/circle-minus-CpiTIx_j.js">
     <link rel="modulepreload" crossorigin href="/assets/ConnectionDiagnostic-jD6ObGRm.js">
     <link rel="modulepreload" crossorigin href="/assets/plateCheck-fqh7_usj.js">
-    <link rel="modulepreload" crossorigin href="/assets/Layout-CejbX0wr.js">
-    <link rel="stylesheet" crossorigin href="/assets/index-CLHFMRO6.css">
+    <link rel="modulepreload" crossorigin href="/assets/Layout-C9f49H5A.js">
+    <link rel="stylesheet" crossorigin href="/assets/index-Dv5E0ZTV.css">
   </head>
   <body>
     <div id="root"></div>

Неке датотеке нису приказане због велике количине промена