Three console issues observed while idle on an offer:
1. Heartbeat flooded the console with 401s. The lock-keepalive interval
fired apiFetch(...).catch(() => {}) every 10s; apiFetch RESOLVES with a
401 (it doesn't throw), so .catch never saw it and the interval never
stopped. Once the refresh token had also expired, every tick 401'd
forever. Now we inspect the response and clearInterval on a 401 (which
only occurs after a failed refresh = session truly gone); the healthy
path still returns 200 and keeps ticking (verified in Chrome).
2. Duplicate React key "item-1" in the line-items table. The form/items/
sections useState initializers restored the create-mode draft
UNCONDITIONALLY — even in edit mode — and reused the draft's stale
item _key values while itemKeyCounter restarts at 0 each load, so an
added row collided. Now the draft is only restored when !isEdit, and
restored items are re-keyed with fresh unique _keys.
3. framer-motion "motion() is deprecated" warning — PageEnter used the
deprecated motion(Box) factory; switched to motion.create(Box).
tsc -b --noEmit, npm run build, vitest 152/152 clean. Console verified
clean in Chrome (only benign dev notices remain).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-status row backgrounds used MUI's solid `.light` palette shades
(error.light/success.light/warning.light). Those are LIGHT colors in both
schemes, so in dark mode the row's white text sat on a light fill and was
effectively invisible; in light mode they read as garish saturated blocks.
Replaced with low-alpha washes via the palette channel vars
(rgba(var(--mui-palette-X-mainChannel) / 0.12), 0.18 on hover), which keep
the row's normal theme text colour fully readable in BOTH schemes:
- Offers: completed = subtle green wash; invalidated = faded/muted
(opacity + secondary text, the original "voided" look) instead of a
solid red block; expired = subtle amber wash.
- DataTable rowDanger (kit-wide — Warehouse, WarehouseReports): subtle
error wash for both the desktop row and the mobile card (card keeps its
error.main border as the danger cue).
- Invoices overdue rows: subtle amber wash.
- Dashboard 2FA banner Card: subtle error wash (keeps the error.main
border) so the banner copy stays readable in dark mode.
Verified live in Chrome on Offers (completed + invalidated, light + dark):
faint tint + normal readable text, channel switches per scheme. tsc -b
--noEmit, npm run build, vitest 152/152 all clean. (Left untouched: small
icon tiles / callout boxes that pair a .light bg with an explicit dark or
coloured text/glyph — those are readable in both schemes by construction.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 3 of the "fully MUI" cleanup.
- New src/admin/ui/RichText.tsx exports two theme-aware styled() wrappers:
RichEditorRoot (the .admin-rich-editor Quill overrides) and RichTextView
(the .admin-rich-text-view read-only output). All colors resolve via
theme.vars; picker/tooltip shadows use theme.applyStyles for the
light/dark difference; the active-button tint uses the primary channel.
- Dropped the ~170 dead font/size picker rules from the old stylesheet —
the editor toolbar exposes neither picker. Kept the .ql-font-* content
classes (shared by editor + view) so legacy stored HTML still renders.
- RichEditor uses <RichEditorRoot> (min-height still via --re-min-height);
InvoiceDetail + OrderDetail use <RichTextView> for the sanitized output.
- offers.css deleted; its import removed from AdminApp.
tsc -b --noEmit and npm run build clean. Verified the editor in Chrome
(light + dark): toolbar, editor surface, text, icon strokes and min-height
all correct and theme-reactive.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 2 of the "fully MUI" cleanup — eliminates two more stylesheets.
- base.css -> src/admin/GlobalStyles.tsx: the reset, typography,
scrollbar/::selection, theme cross-fade timing and all utility
classes now live in a single theme-aware MUI <GlobalStyles>, rendered
inside MuiProvider so every rule resolves against theme.vars (verified
reactive in both schemes). base.css deleted.
- attendance.css -> a styled("div") LocationMap in AttendanceLocation;
attendance.css deleted.
- App.tsx bootstrap loader is now fully self-contained (inline spinner
+ keyframes, theme-aware background read from the data-theme attr),
since it renders before MuiProvider/GlobalStyles mount — it no longer
depends on the removed .admin-spinner / var(--bg-primary).
tsc -b --noEmit and npm run build clean; verified in Chrome (light +
dark): body bg, text, fonts, .text-warning/.link-accent all correct.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Správa kategorií modal footer had two buttons that both just close it (Zrušit + Zavřít). Added an opt-in hideCancel prop to the kit Modal (default false → every other modal unchanged) and set it on PlanCategoriesModal, leaving only 'Zavřít'. Verified: footer shows only Zavřít; close/Escape/backdrop intact.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On screens below the sm breakpoint, a wide multi-column table forced horizontal scrolling and truncated every cell (e.g. 'NA-202…'). DataTable now renders each row as a stacked label:value card on phones: every column becomes a 'HEADER: value' line with full, wrapping content; the actions column (key 'actions') drops to a right-aligned button row at the bottom; rowDanger/rowInactive/rowSx/onRowClick are all honored (action buttons stopPropagation so they don't trigger the row click). Tablets/desktop (>= sm) keep the normal table. Applies to every list table app-wide. Verified on Offers (actions) and Warehouse items (onRowClick): no truncation, no page horizontal scroll.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Since kit buttons now keep their natural width (flexShrink:0 + nowrap), a row of them can't shrink — so button groups that were flexShrink:0 with no wrap got clipped on phones (the page has overflow-x:hidden). The draft-concept Alert actions (Offers + Invoices) and the shared PageHeader actions now use flexWrap:wrap and drop flexShrink:0, so the group conforms to the available width and its buttons wrap onto stacked lines. Verified at 430px: the Offers draft alert no longer overflows (buttons stack), page has no horizontal scroll.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the nowrap fix: MUI's default flexShrink:1 was still squeezing buttons in flex rows below their content width, so with nowrap the label overflowed its padding and touched the button edges (measured: '+ Přidat položku' was 96px wide for 98px of content). Adding flexShrink:0 keeps each button at its natural width (now 131px, full padding restored). Verified via DOM measurement + visual on the offer detail.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In flex rows (e.g. the add-item/add-section buttons next to a 'Ze šablony…' select on OfferDetail/OrderConfirmationModal/InvoiceDetail), flexbox shrank a button below its text width and wrapped the label onto two lines, making the button tall. The kit Button now defaults to whiteSpace: nowrap (merged with any caller sx, still overridable), so labels stay single-line app-wide.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The kit Select now passes displayEmpty:true (so a selected value="" option like an "All" filter renders its label instead of a blank control) and MenuProps.PaperProps maxHeight:320 (so long option lists scroll inside the popover instead of listing every record). Both apply via slotProps.select and are overridable per call site. Verified app-wide: no call site regresses (audit of all 74 usages).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each top-level child rises+fades in, staggered (0.4s, ease-out), so every page enters the same way and nothing appears instantly; honors prefers-reduced-motion. Replaces ad-hoc per-page motion.div entrance wrappers. One component → the look is tunable in one place.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the three remaining hand-written admin-loading/admin-spinner
markup blocks with the kit <LoadingState /> component:
- AdminApp.tsx Suspense fallback
- AppShell.tsx loading branch
- PlanGrid.tsx no-data branch (plan-* classes untouched)
After this the only remaining admin-* classes in TSX are
admin-rich-editor (RichEditor/Quill) and admin-rich-text-view
(sanitized HTML in Order/Invoice detail).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Removed MuiColorSchemeSync: it called MUI setMode which ALSO wrote data-theme, fighting ThemeContext (the other writer) → theme flip-flop loop. ThemeContext is now the sole owner; MUI's cssVariables CSS (scoped by [data-theme]) applies from the attribute, no JS bridge needed. New animated ThemeToggle kit button (sun/moon crossfade+rotate) replaces the static AppShell toggle. DashAttendanceToday avatars now solid-fill + white initials (matches StatCard/activity badges).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DataTable: optional per-column width; when any column sets one, the table uses table-layout:fixed with a colgroup so columns keep a stable size instead of reflowing as rows are filtered (overflow clipped with ellipsis). Tables without widths keep auto layout. Projects: assign column widths; restore the original '(přiděleno automaticky)' suffix on the next-number hint.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After clicking the confirm button, it shows 'Zpracovávám…' (disabled) while the action is in flight — matching how the form modal shows 'Ukládám…'. The earlier freeze keeps it through the close fade (no flash back to the confirm label).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On save/delete success the parent flips loading false a beat before the dialog finishes its fade-out, so the button flashed back to 'Uložit změny'/'Smazat'. Both dialogs now freeze the loading flag (and the label props) while open, so the closing dialog keeps showing 'Ukládám…'/'Pracuji…'. Same derive-state-from-props pattern as the title freeze.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Scroll: MUI only locks <body>, but base.css 'html{overflow-x:hidden}' makes <html> the vertical scroller (overflow-x:hidden forces overflow-y:auto), so the page scrolled behind modals. Added a ref-counted useDialogScrollLock that locks <html> (with scrollbar-width padding) + disableScrollLock on the kit dialogs. Labels: Modal now freezes title/subtitle/submitText while closing (derive-state-from-props), so editing then saving no longer briefly flashes the 'create' title/button during the fade-out.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Select value/option restricted to string (MUI sets the literal child value; string|number + onChange:string was a type lie). DataTable sortDirection uses ?? 'asc' so aria-sort is never undefined on the active column. StatusChip sx-merge drops undefined; Pagination clamps page into range; DataTable dev-warns on sortKey without onSort; Alert onClose JSDoc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DateField (committed in 58a3bca) renders MUI X DatePicker, which requires a LocalizationProvider ancestor — without it, it throws at render time (tsc/build don't catch it). Added to MuiProvider.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the useRef-during-render snapshot with React's blessed 'adjust state when a prop changes' pattern (useState + guarded setState during render). Render-pure, same self-contained behavior. Avoids writing to a ref during render.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The message was bound to the caller's selected row, which gets nulled on close at the same instant the Dialog begins its fade-out — so it faded out with empty text. Now the dialog caches the last shown title/message and renders that through the exit transition.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add src/admin/ui/SidebarNav.tsx: drawer content component using MUI
List/ListItemButton with NavLink polymorphism. Active item uses the
`selected` prop for the white "pill" styling (not NavLink active class).
Renders logo header, permission-filtered nav sections, and user/logout
footer. Not yet wired — next task mounts it in a Drawer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move MenuItem/MenuSection interfaces and the full menuSections array (6 sections,
~30 items with inline SVG icons) verbatim from Sidebar.tsx into src/admin/ui/navData.tsx.
Add exported isItemActive/hasItemPermission pure helpers (folding in the matchAlso
branch that was previously inline in Sidebar's NavLink className callback).
Sidebar updated to import and use these; rendering and active-state behavior unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wraps AdminApp in MuiProvider (ThemeProvider v7, defaultMode=dark) and
adds MuiColorSchemeSync to mirror ThemeContext's data-theme attribute
into MUI's JS color-scheme state so useColorScheme().mode stays in sync.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>