efficient-shell 4.127.0 → 4.131.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +926 -5
  2. package/dist/{Browser-F5TKMP4K.js → Browser-PSEMJFZE.js} +13 -10
  3. package/dist/Browser-PSEMJFZE.js.map +1 -0
  4. package/dist/{Calculator-T3XG7S73.js → Calculator-ZXJPXG7F.js} +12 -9
  5. package/dist/Calculator-ZXJPXG7F.js.map +1 -0
  6. package/dist/ConfirmDialog-MTC7QMUJ.js +4 -0
  7. package/dist/{ConfirmDialog-LJXMB45A.js.map → ConfirmDialog-MTC7QMUJ.js.map} +1 -1
  8. package/dist/{CurrencyConverter-CASQMKLO.js → CurrencyConverter-WSJ2BBSX.js} +12 -9
  9. package/dist/CurrencyConverter-WSJ2BBSX.js.map +1 -0
  10. package/dist/{Documents-YX67YTZS.js → Documents-PGMZR2MA.js} +14 -11
  11. package/dist/Documents-PGMZR2MA.js.map +1 -0
  12. package/dist/{Files-QB63GO6J.js → Files-2LKCJGYW.js} +17 -14
  13. package/dist/Files-2LKCJGYW.js.map +1 -0
  14. package/dist/{Notepad-3SUD5XUE.js → Notepad-KW6WVABH.js} +15 -12
  15. package/dist/Notepad-KW6WVABH.js.map +1 -0
  16. package/dist/PdfViewer-M2YGZIYX.js +5 -0
  17. package/dist/{PdfViewer-LSP3YNFC.js.map → PdfViewer-M2YGZIYX.js.map} +1 -1
  18. package/dist/{PomodoroTimer-4EHD2QC4.js → PomodoroTimer-DG3LI5CX.js} +14 -11
  19. package/dist/PomodoroTimer-DG3LI5CX.js.map +1 -0
  20. package/dist/{Preview-4ZHR4SKP.js → Preview-E7EJJNXN.js} +16 -13
  21. package/dist/Preview-E7EJJNXN.js.map +1 -0
  22. package/dist/{Sidebar-J76PV4QF.js → Sidebar-BPU7RCZP.js} +5 -4
  23. package/dist/Sidebar-BPU7RCZP.js.map +1 -0
  24. package/dist/{Spreadsheet-G3SC7XJ5.js → Spreadsheet-IDUODRZU.js} +14 -11
  25. package/dist/Spreadsheet-IDUODRZU.js.map +1 -0
  26. package/dist/{Stock-TLT4VN2R.js → Stock-YMPOMUAW.js} +12 -9
  27. package/dist/Stock-YMPOMUAW.js.map +1 -0
  28. package/dist/{Weather-FRKX3DAK.js → Weather-NZO3CWLH.js} +12 -9
  29. package/dist/Weather-NZO3CWLH.js.map +1 -0
  30. package/dist/{WorldClock-GKCITO3M.js → WorldClock-HUC6D3AD.js} +12 -9
  31. package/dist/WorldClock-HUC6D3AD.js.map +1 -0
  32. package/dist/apps/index.js +26 -22
  33. package/dist/apps/index.js.map +1 -1
  34. package/dist/brand.css +1 -1
  35. package/dist/{chunk-XUCIYMN7.js → chunk-2GZQ352J.js} +4 -4
  36. package/dist/{chunk-XUCIYMN7.js.map → chunk-2GZQ352J.js.map} +1 -1
  37. package/dist/chunk-7IN7YZAL.js +547 -0
  38. package/dist/chunk-7IN7YZAL.js.map +1 -0
  39. package/dist/{chunk-YNGPTOL6.js → chunk-7MA5XBWD.js} +3 -3
  40. package/dist/{chunk-YNGPTOL6.js.map → chunk-7MA5XBWD.js.map} +1 -1
  41. package/dist/{chunk-WK7FV7VV.js → chunk-BJ34QOR3.js} +827 -625
  42. package/dist/chunk-BJ34QOR3.js.map +1 -0
  43. package/dist/{chunk-HNH7FOYB.js → chunk-CGXMWHQW.js} +7 -6
  44. package/dist/chunk-CGXMWHQW.js.map +1 -0
  45. package/dist/{chunk-N3CG5HG5.js → chunk-CLGNTGC5.js} +12 -11
  46. package/dist/chunk-CLGNTGC5.js.map +1 -0
  47. package/dist/chunk-D276EMXS.js +57 -0
  48. package/dist/chunk-D276EMXS.js.map +1 -0
  49. package/dist/{chunk-7KIOEKHS.js → chunk-E3TWIN7Z.js} +21 -135
  50. package/dist/chunk-E3TWIN7Z.js.map +1 -0
  51. package/dist/{chunk-2VM2FRZJ.js → chunk-EENVXIVU.js} +47 -67
  52. package/dist/chunk-EENVXIVU.js.map +1 -0
  53. package/dist/{chunk-YG45DAUT.js → chunk-FONEQRBH.js} +17 -11
  54. package/dist/chunk-FONEQRBH.js.map +1 -0
  55. package/dist/chunk-IPXN646P.js +14 -0
  56. package/dist/chunk-IPXN646P.js.map +1 -0
  57. package/dist/chunk-IRWSRJ4K.js +48 -0
  58. package/dist/chunk-IRWSRJ4K.js.map +1 -0
  59. package/dist/{chunk-JFQGPY7W.js → chunk-KWEX4ZAF.js} +20 -17
  60. package/dist/chunk-KWEX4ZAF.js.map +1 -0
  61. package/dist/{chunk-OKEFO6TI.js → chunk-LQTVTOSY.js} +10 -10
  62. package/dist/chunk-LQTVTOSY.js.map +1 -0
  63. package/dist/{chunk-G4KA3SAX.js → chunk-MQPX6ERJ.js} +4 -4
  64. package/dist/{chunk-G4KA3SAX.js.map → chunk-MQPX6ERJ.js.map} +1 -1
  65. package/dist/{chunk-FQB24B42.js → chunk-SEWF4O3H.js} +3 -3
  66. package/dist/{chunk-FQB24B42.js.map → chunk-SEWF4O3H.js.map} +1 -1
  67. package/dist/{chunk-AXWC3332.js → chunk-TFXJRHJV.js} +3 -3
  68. package/dist/{chunk-AXWC3332.js.map → chunk-TFXJRHJV.js.map} +1 -1
  69. package/dist/{chunk-G3HQLQYZ.js → chunk-TQURTLK2.js} +59 -34
  70. package/dist/chunk-TQURTLK2.js.map +1 -0
  71. package/dist/{chunk-6XQHH7D2.js → chunk-TRCWINHF.js} +5 -5
  72. package/dist/{chunk-6XQHH7D2.js.map → chunk-TRCWINHF.js.map} +1 -1
  73. package/dist/chunk-UHPWZ2F4.js +7 -0
  74. package/dist/{chunk-CJZXR7MG.js.map → chunk-UHPWZ2F4.js.map} +1 -1
  75. package/dist/{chunk-NPJCMM6V.js → chunk-VWZIGZFF.js} +4 -4
  76. package/dist/{chunk-NPJCMM6V.js.map → chunk-VWZIGZFF.js.map} +1 -1
  77. package/dist/{chunk-HYZ5QMVO.js → chunk-XH3ELPQZ.js} +22 -19
  78. package/dist/chunk-XH3ELPQZ.js.map +1 -0
  79. package/dist/{chunk-PPUZJFCI.js → chunk-ZKHD4J73.js} +8 -7
  80. package/dist/chunk-ZKHD4J73.js.map +1 -0
  81. package/dist/file-intake/index.js +3 -1
  82. package/dist/file-intake/index.js.map +1 -1
  83. package/dist/{index-dQia6NpB.d.ts → index-COwksV6G.d.ts} +699 -199
  84. package/dist/index.d.ts +2 -2
  85. package/dist/index.js +37 -32
  86. package/dist/index.js.map +1 -1
  87. package/dist/map/index.js +5 -3
  88. package/dist/map/index.js.map +1 -1
  89. package/dist/markdown/index.js +4 -3
  90. package/dist/markdown/index.js.map +1 -1
  91. package/dist/page-tabs/index.js +12 -8
  92. package/dist/page-tabs/index.js.map +1 -1
  93. package/dist/shell.css +17 -17
  94. package/dist/themes.css +15 -0
  95. package/dist/ui/index.d.ts +1 -1
  96. package/dist/ui/index.js +17 -13
  97. package/dist/ui/index.js.map +1 -1
  98. package/dist/ui.css +673 -283
  99. package/package.json +1 -1
  100. package/dist/Browser-F5TKMP4K.js.map +0 -1
  101. package/dist/Calculator-T3XG7S73.js.map +0 -1
  102. package/dist/ConfirmDialog-LJXMB45A.js +0 -3
  103. package/dist/CurrencyConverter-CASQMKLO.js.map +0 -1
  104. package/dist/Documents-YX67YTZS.js.map +0 -1
  105. package/dist/Files-QB63GO6J.js.map +0 -1
  106. package/dist/Notepad-3SUD5XUE.js.map +0 -1
  107. package/dist/PdfViewer-LSP3YNFC.js +0 -4
  108. package/dist/PomodoroTimer-4EHD2QC4.js.map +0 -1
  109. package/dist/Preview-4ZHR4SKP.js.map +0 -1
  110. package/dist/Sidebar-J76PV4QF.js.map +0 -1
  111. package/dist/Spreadsheet-G3SC7XJ5.js.map +0 -1
  112. package/dist/Stock-TLT4VN2R.js.map +0 -1
  113. package/dist/Weather-FRKX3DAK.js.map +0 -1
  114. package/dist/WorldClock-GKCITO3M.js.map +0 -1
  115. package/dist/chunk-2VM2FRZJ.js.map +0 -1
  116. package/dist/chunk-3CUSBZWG.js +0 -59
  117. package/dist/chunk-3CUSBZWG.js.map +0 -1
  118. package/dist/chunk-7KIOEKHS.js.map +0 -1
  119. package/dist/chunk-CJZXR7MG.js +0 -7
  120. package/dist/chunk-G3HQLQYZ.js.map +0 -1
  121. package/dist/chunk-HNH7FOYB.js.map +0 -1
  122. package/dist/chunk-HYZ5QMVO.js.map +0 -1
  123. package/dist/chunk-JFQGPY7W.js.map +0 -1
  124. package/dist/chunk-N3CG5HG5.js.map +0 -1
  125. package/dist/chunk-OKEFO6TI.js.map +0 -1
  126. package/dist/chunk-PPUZJFCI.js.map +0 -1
  127. package/dist/chunk-WK7FV7VV.js.map +0 -1
  128. package/dist/chunk-YG45DAUT.js.map +0 -1
package/README.md CHANGED
@@ -373,7 +373,7 @@ All exports are named — `import { Modal, ... } from 'efficient-shell'`.
373
373
  |---|---|
374
374
  | `<ShellAuthProvider value={{ hasAnyPerm }}>` | Permission-filter nav items. |
375
375
  | `<ShellPrefsProvider value={{ prefs, save }}>` | Where the shell reads/writes user prefs (theme, taskbar pos, sticky notes, …). Use `useLocalStoragePrefs(key)` for a backend-less default. Since 4.92.0 it is also how `<EntityList>` / `<ResizableTable>` column config and sort persist: with no provider mounted, `useShellPrefs()` reads empty and drops saves, so both silently degrade to localStorage-only and never reach the user's profile. Session restore reads `session_windows` once, at mount: hand the provider prefs that are already loaded by then (an adapter whose prefs arrive later misses that load's replay, though the saved set is kept). |
376
- | `<ShellStringsProvider value={{ … }}>` | Translates the shell's own strings — window controls, taskbar hints, the logout cover, About/What's New, picker and table defaults, the help viewer. English works with NO provider; the override is a typed partial merged per section, so an incomplete catalog falls back rather than breaking. Prop-level text (`emptyText`, placeholders) always wins over the catalog. |
376
+ | `<ShellStringsProvider value={{ … }} locale?>` | Translates every word the shell and the kit draw on their own account — window controls, taskbar hints, dialogs, pickers, pagers, list footers, upload zones, charts, timelines. English works with NO provider; the override is a typed partial merged per section, so an incomplete catalog falls back rather than breaking. Prop-level text (`emptyText`, placeholders) always wins over the catalog. `locale` (a BCP 47 tag) writes dates and numbers that locale's way; without one nothing moves. See [Languages, dates and numbers](#languages-dates-and-numbers--shellstringsprovider) for the full catalog. |
377
377
  | `<ShellEntityFetcherProvider value={(endpoint, id) => …}>` | How the modal stack fetches entity data. |
378
378
  | `<BugReportConfigProvider value={{ submit, list?, resolve? }}>` | Wire the bug-report flow to your backend. |
379
379
  | `<DesktopHostProvider value={{ stickyResolver?, saveShortcuts?, … }}>` | Sticky-note ref resolver + persistence callbacks. |
@@ -403,6 +403,7 @@ All exports are named — `import { Modal, ... } from 'efficient-shell'`.
403
403
  | `useUndoCanEdit(canEdit)` | A form's own word on whether the record may be edited right now — `useUndoCanEdit(!isLocked)`. While false the stack records nothing, ⌘Z is left alone and the footer shows no controls. A nested `<UndoProvider canEdit={false}>` in the form's JSX says the same thing: it shadows the stack for its children AND forwards the read-only claim to the window's stack, which is where the form's own `useUndoableState` calls (made above it in the tree) register. Either form of the claim makes the whole window read-only. The positive claim, `useUndoCanEdit(true)`, is how a window that edits in place outside Edit mode — a detail with inline cells behind a permission — asks the footer for the pair. |
404
404
  | `useMultiModal()` | Manages multi-window stacking + activate/blur. |
405
405
  | `useShellAuth() / useShellPrefs() / useShellEntityFetcher() / useBugReport() / useDesktopHost()` | Context readers — the shell uses these internally; consumers may also call them. |
406
+ | `useShellStrings() / useShellLocale()` | The active catalog, and the provider's `locale` (`undefined` without one) — for a host cell that should write a date or a number the way the kit does. |
406
407
 
407
408
  ### Apps barrel — `efficient-shell/apps`
408
409
 
@@ -491,8 +492,20 @@ What you get: every form control, the display and layout primitives
491
492
  `HelpCenter`, `EditableGrid`, `SearchableSelect`, `PopupMenu`), the charts, all
492
493
  nine page templates, the pageless data primitives (`Pagination`, `Kanban`,
493
494
  `ListFooter`, `ListLoadError`), `toast`, and the theming hooks (`useTheme`,
494
- `resolveTheme`, `applyThemePrefs`) — you need those last ones to reach dark mode
495
- or an accent at all, since both work by stamping `data-theme`.
495
+ `resolveTheme`, `applyThemePrefs`) — the way to an accent theme, which works by
496
+ stamping `data-theme`. Dark mode answers to either `<html data-theme="dark">`
497
+ (what `useTheme` stamps) or `<html class="dark">` (next-themes, shadcn/ui,
498
+ Tailwind's class strategy), so an app whose own theme switcher already sets the
499
+ class gets the kit's dark mode with nothing more to wire.
500
+
501
+ **An app with its own design system** can keep the kit off its markup. Three
502
+ rules in `ui.css` style bare elements rather than the kit's components: dark
503
+ form controls (every `input`, `select` and `textarea`), the 16px font-size that
504
+ stops iOS zooming on focus (narrow or coarse-pointer screens), and dark
505
+ scrollbars. In a portal built on the kit that is what you want; in an app that
506
+ draws its own controls it restyles them. `<html class="rosh-host-elements">`
507
+ turns those three off. The kit's own controls stay dark through their
508
+ remapped utility classes.
496
509
 
497
510
  What you do not get: `Modal` and the window manager, `Layout`, `Desktop`,
498
511
  `StartMenu`, `GlobalSearch`, the settings panels, the bundled apps, and the
@@ -510,6 +523,914 @@ layers without preflight.
510
523
  `efficient-shell` (the root entry) is unchanged and remains a superset: same
511
524
  components, same bindings, plus the shell.
512
525
 
526
+ ### Server rendering (Next.js App Router)
527
+
528
+ Every React entry — the root, `ui`, `apps`, `markdown`, `file-intake`,
529
+ `page-tabs`, `map` — starts with `"use client"`, so a **server component can
530
+ import and render kit components**: they are server-rendered as client
531
+ components and hydrated in the browser.
532
+
533
+ ```tsx
534
+ // app/bookings/page.tsx — a server component
535
+ import { Card, PageHeader, DataTable } from 'efficient-shell/ui';
536
+ ```
537
+
538
+ - **Kit functions and constants are client references.** `toast`, `confirm`,
539
+ `formatDate`, `inputClasses`, `resolveTheme`, `CMD_K`, `Z_LAYERS`… arrive in a
540
+ server component as references, not values: call them from client code (a
541
+ `'use client'` file, an event handler, an effect). From a server component,
542
+ render components and pass them serialisable props.
543
+ - **`markup` is server-safe.** It has no directive and imports no React, so its
544
+ functions are plain functions you can call from server code.
545
+ - **The first render is the server's.** What only a browser knows renders as
546
+ the server would, and switches one render after hydration: the ⌘ forms of the
547
+ shortcut constants (render them through `<Kbd keys={CMD_K} />`, which handles
548
+ the switch; `isMac` is `false` on a server), a `SidebarLayout`'s saved width,
549
+ `useLocalStoragePrefs`, the OS dark-mode setting in `useTheme`, reduced motion,
550
+ and *today* — `Calendar` marks it after hydration, and a `Calendar` with
551
+ nothing to open on (no `value`, `month` or `defaultMonth`) draws an empty frame
552
+ until then, so pass `defaultMonth` if the server knows the month. Popups and
553
+ portals open after hydration. A root that was never server-rendered sees none
554
+ of this: it reads the browser on its first render, as before.
555
+ - `formatDate` writes the default format on a server. A page that also lets
556
+ readers choose a format (`user_date_format`) should format those dates in the
557
+ browser after mount.
558
+
559
+ ### Theming the kit from a host app
560
+
561
+ An app with its own design system — its own neutrals, its own primary, square
562
+ corners, one focus treatment for the whole app — can put the kit under its look
563
+ without forking a component. Every colour, radius, typeface and focus ring the
564
+ kit draws comes from a token, and the knobs below are all of them. (A colour
565
+ written as a literal in a kit module fails the build:
566
+ `tests/kitColourLiterals.test.ts`.)
567
+
568
+ Write light classes in your own markup and never `dark:` variants. The kit's
569
+ dark mode remaps utility classes under `<html data-theme="dark">` or
570
+ `<html class="dark">` — so a next-themes or shadcn/ui host already has it, and
571
+ points it at its own palette through the neutral roles below — and its accent
572
+ themes under `data-theme="pink" | "green" | "grey" | "blue"`, which only a host
573
+ that stamps one gets. `rosh-host-elements` on `<html>` keeps the three rules
574
+ that style bare `input`/`select`/`textarea` and scrollbars off your own markup.
575
+
576
+ Your `@theme` wins whichever side of the import it is on (the kit's is
577
+ `@theme default`). Your `:root` and dark overrides of the kit's TOKENS are plain
578
+ declarations, where source order decides, so import the kit first:
579
+
580
+ ```css
581
+ @import "tailwindcss";
582
+ @import "efficient-shell/ui.css";
583
+ /* your @theme and :root blocks after this line */
584
+ ```
585
+
586
+ **Palette.** Kit components paint with Tailwind palette utilities, which
587
+ compile to `var(--color-<family>-<step>)`, so `@theme` reaches them. The
588
+ families the kit uses: **gray** (nearly all chrome: surfaces, borders, text),
589
+ **blue** (the accent: primary buttons, links, selection, the chosen tab),
590
+ **red** (danger and invalid), **green** (success), **amber** and **yellow**
591
+ (warning), one **emerald** and one **neutral** (a template's sparkline,
592
+ BrandMark's dark plate), plus `white` and `black`. Status badges and charts read
593
+ their own tokens (`--status-*`, `--viz-*`, `--rosh-donut-*`) rather than the palette.
594
+
595
+ ```css
596
+ @theme {
597
+ /* a neutral gray ramp, and a near-black primary */
598
+ --color-gray-50: oklch(98.5% 0 0); /* … through --color-gray-950 */
599
+ --color-blue-600: oklch(20.5% 0 0); /* the primary Button's fill */
600
+ --color-blue-700: oklch(14.5% 0 0); /* its hover */
601
+ }
602
+ ```
603
+
604
+ **Accent, picked at runtime.** `applyThemePrefs({ accent_color })` (or
605
+ `useTheme`) stamps `data-custom-accent` on `<html>` and the `--accent-50` …
606
+ `--accent-700` scale inline; `themes.css` then remaps the blue accent utilities
607
+ to that scale, and the timeline, `SidebarLayout`'s resize handle and the accent
608
+ focus ring follow it. A host can stamp the same attribute and scale itself. For
609
+ a fixed brand colour, `@theme` above is simpler.
610
+
611
+ **The neutral roles.** Twelve tokens, declared for light on `:root` and for dark
612
+ under `:is([data-theme="dark"], .dark)`: `--surface`, `--surface-sunken`, `--surface-raised`,
613
+ `--line-subtle`, `--line`, `--line-strong`, `--ink-faintest`, `--ink-faint`,
614
+ `--ink-muted`, `--ink`, `--ink-strong`, `--ink-strongest`. Dark mode's remaps,
615
+ the timeline card and the frosted glass read them.
616
+
617
+ ```css
618
+ :root { --surface: #ffffff; --line: oklch(92.2% 0 0); --ink-strongest: oklch(14.5% 0 0); }
619
+ :is([data-theme="dark"], .dark) { --surface: oklch(14.5% 0 0); --line: oklch(26.9% 0 0); }
620
+ ```
621
+
622
+ **Focus.** Kit controls ring on `:focus-visible`, so a button does not ring
623
+ when it is clicked and a text field does (it takes typing, so the browser shows
624
+ where the caret went). The ring colours are tokens; each default is the
625
+ Tailwind colour the kit always drew:
626
+
627
+ | Token | Default | Where |
628
+ |---|---|---|
629
+ | `--rosh-focus-ring` | blue-400 at 30% | fields, outlined and quiet buttons, Checkbox, Radio |
630
+ | `--rosh-focus-ring-strong` | blue-400 at 40% | primary Button, Switch, TileButton, a badge's close |
631
+ | `--rosh-focus-ring-solid` | blue-400 | edgeless controls: a dialog's close, calendar days, table rows, crumbs, steps |
632
+ | `--rosh-focus-ring-deep` | blue-500 | charts (plot, legend), a chip's remove, the date-range field |
633
+ | `--rosh-focus-ring-accent` | blue-500, follows the accent | HelpCenter's search, the prompt dialog's field |
634
+ | `--rosh-focus-ring-zone` | `rgba(59,130,246,.45)` | upload zones and thumbnails |
635
+ | `--rosh-focus-ring-invalid` | red-400 at 30% | an invalid field, `ghost-danger` |
636
+ | `--rosh-focus-ring-invalid-strong` | red-400 at 40% | the `danger` Button |
637
+ | `--rosh-focus-ring-invalid-solid` | red-400 | `FormErrorSummary` |
638
+
639
+ To give the whole app **one** focus treatment, hand focus to the host: put
640
+ `rosh-host-focus` on `<html>` (or on any ancestor of the kit). The kit then
641
+ draws no ring and no outline of its own and removes nothing, so your rule is
642
+ what every kit control shows — including one in `@layer base`, which the kit
643
+ outranks otherwise:
644
+
645
+ ```html
646
+ <html class="rosh-host-focus">
647
+ ```
648
+
649
+ ```css
650
+ @layer base {
651
+ :where(a, button, input, select, textarea, summary, [role="button"], [role="tab"],
652
+ [tabindex]:not([tabindex="-1"])):focus-visible {
653
+ outline: 2px solid var(--ring);
654
+ outline-offset: 2px;
655
+ }
656
+ }
657
+ ```
658
+
659
+ A class rather than a token, because no token value can give an outline back:
660
+ the kit takes the browser's outline off its controls while it draws a ring, and
661
+ only a selector that stops matching can stop that. A focused text field still
662
+ changes its border colour (`focus:border-blue-400`): that is the field's edge,
663
+ so the palette above governs it.
664
+
665
+ **Radius and type.** `ui.css` declares `--radius-sm` … `--radius-2xl`,
666
+ `--font-sans` and `--font-mono` as `@theme default`; declare your own anywhere. Bare
667
+ `rounded` and `rounded-full` (pills, switches, avatars, badges) compile to fixed
668
+ values unless you name `--radius` and `--radius-full` too. A few kit component
669
+ classes in `ui.css` (the timeline card's) carry pixel radii of their own.
670
+
671
+ ```css
672
+ @theme {
673
+ --radius: 0; --radius-sm: 0; --radius-md: 0; --radius-lg: 0; --radius-xl: 0; --radius-2xl: 0;
674
+ --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
675
+ }
676
+ ```
677
+
678
+ **Glass.** Menus, popups, listboxes and tooltips are frosted glass
679
+ (`glassStyle()`). `--menu-opacity` (0–1, default `0.95`) sets how much shows
680
+ through. `rosh-reduce-transparency` on `<html>` makes every popup one solid
681
+ `--rosh-glass-surface` (default: `--surface`) with no blur — and strips
682
+ `backdrop-filter` from everything else on the page too, since it is the user's
683
+ "Reduce transparency" preference. The frosted look itself is
684
+ `--rosh-glass-surface`, `--rosh-glass-surface-mid`, `--rosh-glass-border` and
685
+ `--rosh-glass-shadow`; the solid one adds `--rosh-glass-solid-border` and
686
+ `--rosh-glass-solid-shadow`. All have light and dark values.
687
+
688
+ ```css
689
+ :root { --rosh-glass-surface: var(--popover); --rosh-glass-solid-shadow: 0 4px 12px rgb(0 0 0 / 0.08); }
690
+ ```
691
+
692
+ **Everything else** a kit component paints, each defaulting to the literal it
693
+ replaced: `--rosh-media-scrim` and `--rosh-media-scrim-button` (the dark wash
694
+ under text and buttons on an upload thumbnail), `--rosh-drop-ring` (the halo
695
+ while a file is dragged over a gallery), `--rosh-mark-plate-shadow` and
696
+ `--rosh-mark-plate-shadow-strong` (`BrandMark`'s plates), `--rosh-resize-handle`
697
+ (`SidebarLayout`'s handle without a custom accent), and `--rosh-donut-1` …
698
+ `--rosh-donut-8` with `--rosh-donut-track` (`DonutChart`'s default palette —
699
+ point them at `--viz-series-*` for one palette across every chart). The chart
700
+ palette (`--viz-*`), the status tiers (`--status-*`) and the timeline
701
+ (`--tl-*`) are documented where `ui.css` declares them.
702
+
703
+ ### Languages, dates and numbers — `ShellStringsProvider`
704
+
705
+ Every word the kit draws on its own account — a dialog's "Close", a pager's
706
+ "Previous page", a chart's tooltip rows, a calendar's "Next month", an upload
707
+ zone's refusal — comes from one typed catalog. With no provider the catalog is
708
+ American English and the markup is exactly what it has always been. Mount the
709
+ provider once, near the top of the app, with whatever you translate:
710
+
711
+ ```tsx
712
+ import { ShellStringsProvider, type ShellStringsOverride } from 'efficient-shell/ui';
713
+
714
+ // Your own catalog, your own plural rules — the kit asks for nothing else.
715
+ function kitStrings(t: (key: string, values?: object) => string): ShellStringsOverride {
716
+ return {
717
+ dialog: {
718
+ close: t('kit.close'), cancel: t('kit.cancel'), ok: t('kit.ok'),
719
+ // Word order is the translation's: the kit draws the <kbd> wherever
720
+ // the sentence puts the word.
721
+ typeToConfirm: word => t('kit.typeToConfirm', { word }),
722
+ },
723
+ pagination: { previous: t('kit.previousPage'), next: t('kit.nextPage') },
724
+ // A sentence with a value in it is a function. Counts arrive as numbers,
725
+ // so a plural rule can choose the words: "1 запись", "5 записей".
726
+ list: {
727
+ allLoaded: (total, label) => t('kit.allLoaded', { total, label }),
728
+ loadedOf: (loaded, total, label) => t('kit.loadedOf', { loaded, total, label }),
729
+ },
730
+ };
731
+ }
732
+
733
+ <ShellStringsProvider value={kitStrings(t)} locale="de-DE">
734
+ <App />
735
+ </ShellStringsProvider>
736
+ ```
737
+
738
+ **How the catalog merges.** `value` is a partial: per section, keys you leave
739
+ out keep their English default, so a catalog that falls behind a release still
740
+ works. A prop on the component (`emptyText`, `placeholder`, `label`,
741
+ `emptyLabel`, a chart's `formatValue`) always wins over the catalog — the
742
+ catalog replaces the kit's defaults, never your words. A nested provider's
743
+ `value` merges onto the English defaults, not onto its parent's.
744
+
745
+ **Sentences are functions, deliberately not templates.** The catalog is not an
746
+ i18n library: no message syntax, no plural rules. An entry that carries a value
747
+ is a plain function, and you write it with whatever you already use. Two rules
748
+ make the arguments predictable:
749
+
750
+ - a **count** arrives as a `number`, unformatted, because the words depend on it
751
+ — format it yourself (the English defaults format it in the provider's
752
+ `locale`);
753
+ - a **value the kit has already written** — a date, a file size, the output of
754
+ your own `formatValue` — arrives as the `string` that will be printed.
755
+
756
+ Three older entries are halves the kit concatenates (`select.remove` before a
757
+ chip's label, `pageTabs.close` before a tab's title, a count before
758
+ `pageTabs.discardMany`). They still work; each now has an optional
759
+ whole-sentence function beside it (`select.removeItem`, `pageTabs.closeTab`,
760
+ `pageTabs.discardCount`) that wins when you supply it, for a language whose
761
+ word order a concatenation cannot express.
762
+
763
+ **`locale` — how dates and numbers are written.** A BCP 47 tag. With one:
764
+
765
+ - `Calendar`, `DatePicker` and `DateRangePicker` take month and weekday names —
766
+ and the full date each day is announced as — from `Intl.DateTimeFormat` in
767
+ that locale (`Oktober`, `Mo.`, `2026年10月2日`). The calendar is always
768
+ Gregorian, whatever the locale's default.
769
+ - `DatePicker native`, `DateTimePicker` and `TimePicker` are drawn by the
770
+ browser, so they get the locale the one way they can: a `lang` attribute.
771
+ - `formatDate`, and the timelines' dates, are written by `Intl` in the locale
772
+ (`dateStyle: 'medium'` by default). The stored `user_date_format` is not read
773
+ — a locale is the host saying how dates read for everyone it serves.
774
+ - Numbers the kit formats itself use `Intl.NumberFormat` in the locale: list
775
+ footers, page numbers, `Statistic`, `CountBadge`, chart axes, tooltips,
776
+ percentages and the meters (`1.234,5`, `37,5 %`).
777
+
778
+ With no `locale` — no provider, or a provider that only translates strings —
779
+ nothing changes: the same English month names, `DD/MM/YYYY` from the stored
780
+ setting, `toLocaleString()` with no argument, `String(v)`. Mounting the provider
781
+ for its strings alone moves no digit. A nested provider without a `locale`
782
+ keeps its parent's; a malformed tag (`en_US`) is reported once on the console
783
+ and ignored rather than thrown in render. Which day a week starts on is still
784
+ `weekStartsOn`, not the locale's to change. `InputNumber` keeps a `.` decimal in
785
+ the field, because what is typed there is parsed back.
786
+
787
+ `useShellLocale()` reads the tag for your own cells — pass it on as
788
+ `formatDate(value, { locale, style })` or `fmtSliderDate(ms, locale)`, or hand it
789
+ to `Intl` directly — so a host's dates and numbers read like the kit's.
790
+
791
+ **What is not in the catalog, on purpose:** month and weekday names (the
792
+ `locale` supplies them); file-format names in `BrandAssetEditor`'s default
793
+ `acceptHint` and the `20ft`/`40ft` container codes (notation, not language); the
794
+ `Kbd` shortcut constants (key symbols); `STAGES` labels (data a consumer's own
795
+ tables read); the sample content of the starter templates (`DashboardTemplate`,
796
+ `AuthScreen` and the rest are demos to copy, not components to configure); and
797
+ console warnings, which are for developers.
798
+
799
+ **The full catalog** — 39 sections, 359 keys. A function's default is shown
800
+ with sample arguments.
801
+
802
+ <details><summary><code>window</code> — 17 keys</summary>
803
+
804
+ | Key | Type | English default |
805
+ |---|---|---|
806
+ | `minimize` | `string` | `Minimize` |
807
+ | `restore` | `string` | `Restore` |
808
+ | `maximize` | `string` | `Maximize` |
809
+ | `windowed` | `string` | `Windowed` |
810
+ | `pinOnTop` | `string` | `Pin on top` |
811
+ | `unpin` | `string` | `Unpin from top` |
812
+ | `close` | `string` | `Close` |
813
+ | `windowSuffix` | `string` | `window` |
814
+ | `prev` | `string` | `Prev` |
815
+ | `next` | `string` | `Next` |
816
+ | `snapLayouts` | `string` | `Snap layouts` |
817
+ | `snapLeft` | `string` | `Snap left half` |
818
+ | `snapRight` | `string` | `Snap right half` |
819
+ | `snapTopLeft` | `string` | `Snap top-left quarter` |
820
+ | `snapTopRight` | `string` | `Snap top-right quarter` |
821
+ | `snapBottomLeft` | `string` | `Snap bottom-left quarter` |
822
+ | `snapBottomRight` | `string` | `Snap bottom-right quarter` |
823
+
824
+ </details>
825
+
826
+ <details><summary><code>taskbar</code> — 9 keys</summary>
827
+
828
+ | Key | Type | English default |
829
+ |---|---|---|
830
+ | `closeWindow` | `string` | `Close window` |
831
+ | `offscreenHint` | `string` | `This window is off screen — click to bring it back` |
832
+ | `offscreenDot` | `string` | `Off screen — click to bring this window back` |
833
+ | `expose` | `string` | `Exposé — show all open windows as thumbnails` |
834
+ | `addToDesktop` | `string` | `Add to desktop` |
835
+ | `removeFromDesktop` | `string` | `Remove from desktop` |
836
+ | `minimizeAllWindows` | `string` | `Minimize all` |
837
+ | `restoreAllWindows` | `string` | `Restore all` |
838
+ | `closeAllWindows` | `string` | `Close all` |
839
+
840
+ </details>
841
+
842
+ <details><summary><code>logout</code> — 2 keys</summary>
843
+
844
+ | Key | Type | English default |
845
+ |---|---|---|
846
+ | `goodbye` | `string` | `Goodbye` |
847
+ | `seeYou` | `string` | `See you next time` |
848
+
849
+ </details>
850
+
851
+ <details><summary><code>about</code> — 5 keys</summary>
852
+
853
+ | Key | Type | English default |
854
+ |---|---|---|
855
+ | `aboutPrefix` | `string` | `About` |
856
+ | `thisApp` | `string` | `this app` |
857
+ | `openSourceLicenses` | `string` | `Open Source Licenses` |
858
+ | `whatsNew` | `string` | `What's New` |
859
+ | `noChangelog` | `string` | `No changelog available.` |
860
+
861
+ </details>
862
+
863
+ <details><summary><code>table</code> — 4 keys</summary>
864
+
865
+ | Key | Type | English default |
866
+ |---|---|---|
867
+ | `empty` | `string` | `Nothing to show` |
868
+ | `loading` | `string` | `Loading` |
869
+ | `selectAll` | `string` | `Select all rows` |
870
+ | `selectRow` | `string` | `Select row` |
871
+
872
+ </details>
873
+
874
+ <details><summary><code>select</code> — 6 keys</summary>
875
+
876
+ | Key | Type | English default |
877
+ |---|---|---|
878
+ | `none` | `string` | `— None —` |
879
+ | `noMatches` | `string` | `No matches` |
880
+ | `allSelected` | `string` | `All options selected` |
881
+ | `clear` | `string` | `Clear selection` |
882
+ | `remove` | `string` | `Remove` |
883
+ | `removeItem?` | `(label: string) => string` | *(none — the older halves are concatenated)* |
884
+
885
+ </details>
886
+
887
+ <details><summary><code>notifications</code> — 9 keys</summary>
888
+
889
+ | Key | Type | English default |
890
+ |---|---|---|
891
+ | `title` | `string` | `Notifications` |
892
+ | `markAllRead` | `string` | `Mark all read` |
893
+ | `dnd` | `string` | `Do not disturb` |
894
+ | `dndOn` | `string` | `Do not disturb is on` |
895
+ | `caughtUp` | `string` | `All caught up` |
896
+ | `empty` | `string` | `No notifications yet` |
897
+ | `viewAll` | `string` | `View all notifications` |
898
+ | `cardLabel` | `string` | `Notification` |
899
+ | `dismiss` | `string` | `Dismiss notification` |
900
+
901
+ </details>
902
+
903
+ <details><summary><code>form</code> — 1 keys</summary>
904
+
905
+ | Key | Type | English default |
906
+ |---|---|---|
907
+ | `errorSummaryTitle` | `string` | `There is a problem` |
908
+
909
+ </details>
910
+
911
+ <details><summary><code>help</code> — 10 keys</summary>
912
+
913
+ | Key | Type | English default |
914
+ |---|---|---|
915
+ | `title` | `string` | `Help` |
916
+ | `searchPlaceholder` | `string` | `Search help…` |
917
+ | `empty` | `string` | `No help articles yet.` |
918
+ | `noResults` | `string` | `No articles match your search.` |
919
+ | `loading` | `string` | `Loading…` |
920
+ | `pickArticle` | `string` | `Pick a help article from the left.` |
921
+ | `noBody` | `string` | `This article has no body yet.` |
922
+ | `draft` | `string` | `Draft` |
923
+ | `newArticle` | `string` | `+ New` |
924
+ | `edit` | `string` | `Edit` |
925
+
926
+ </details>
927
+
928
+ <details><summary><code>update</code> — 2 keys</summary>
929
+
930
+ | Key | Type | English default |
931
+ |---|---|---|
932
+ | `available` | `string` | `A new version is available.` |
933
+ | `refreshNow` | `string` | `Refresh now` |
934
+
935
+ </details>
936
+
937
+ <details><summary><code>pageTabs</code> — 13 keys</summary>
938
+
939
+ | Key | Type | English default |
940
+ |---|---|---|
941
+ | `label` | `string` | `Open pages` |
942
+ | `close` | `string` | `Close` |
943
+ | `closeTab?` | `(title: string) => string` | *(none — the older halves are concatenated)* |
944
+ | `closeOthers` | `string` | `Close other tabs` |
945
+ | `closeRight` | `string` | `Close tabs to the right` |
946
+ | `closeAll` | `string` | `Close all tabs` |
947
+ | `unsaved` | `string` | `Unsaved changes` |
948
+ | `discardTitle` | `string` | `Discard unsaved changes?` |
949
+ | `discardOne` | `string` | `This page has changes that have not been saved. Close it anyway?` |
950
+ | `discardMany` | `string` | `pages have changes that have not been saved. Close them anyway?` |
951
+ | `discardCount?` | `(count: number) => string` | *(none — the older halves are concatenated)* |
952
+ | `discardConfirm` | `string` | `Discard and close` |
953
+ | `keepEditing` | `string` | `Keep editing` |
954
+
955
+ </details>
956
+
957
+ <details><summary><code>dialog</code> — 9 keys</summary>
958
+
959
+ | Key | Type | English default |
960
+ |---|---|---|
961
+ | `close` | `string` | `Close` |
962
+ | `cancel` | `string` | `Cancel` |
963
+ | `ok` | `string` | `OK` |
964
+ | `confirmTitle` | `string` | `Confirm` |
965
+ | `destructiveTitle` | `string` | `Confirm Action` |
966
+ | `destructiveCancel` | `string` | `Dismiss` |
967
+ | `destructiveConfirm` | `string` | `Delete` |
968
+ | `typeToConfirm` | `(word: string) => string` | `Type {word} to confirm:` |
969
+ | `promptTitle` | `string` | `Enter a value` |
970
+
971
+ </details>
972
+
973
+ <details><summary><code>calendar</code> — 10 keys</summary>
974
+
975
+ | Key | Type | English default |
976
+ |---|---|---|
977
+ | `previousMonth` | `string` | `Previous month` |
978
+ | `nextMonth` | `string` | `Next month` |
979
+ | `previousYear` | `string` | `Previous year` |
980
+ | `nextYear` | `string` | `Next year` |
981
+ | `previousYears` | `string` | `Previous years` |
982
+ | `nextYears` | `string` | `Next years` |
983
+ | `monthPanel` | `string` | `Choose a month` |
984
+ | `yearPanel` | `string` | `Choose a year` |
985
+ | `chooseMonth` | `(month: string) => string` | `Choose a month, currently {month}` |
986
+ | `chooseYear` | `(year: number) => string` | `Choose a year, currently 2026` |
987
+
988
+ </details>
989
+
990
+ <details><summary><code>datePicker</code> — 4 keys</summary>
991
+
992
+ | Key | Type | English default |
993
+ |---|---|---|
994
+ | `placeholder` | `string` | `Select a date` |
995
+ | `dialog` | `string` | `Choose a date` |
996
+ | `clear` | `string` | `Clear` |
997
+ | `today` | `string` | `Today` |
998
+
999
+ </details>
1000
+
1001
+ <details><summary><code>dateRange</code> — 18 keys</summary>
1002
+
1003
+ | Key | Type | English default |
1004
+ |---|---|---|
1005
+ | `placeholder` | `string` | `Date Range` |
1006
+ | `clearRange` | `string` | `Clear date range` |
1007
+ | `dialog` | `string` | `Date range` |
1008
+ | `from` | `string` | `From` |
1009
+ | `to` | `string` | `To` |
1010
+ | `start` | `string` | `Start` |
1011
+ | `end` | `string` | `End` |
1012
+ | `fromOnly` | `(date: string) => string` | `From {date}` |
1013
+ | `toOnly` | `(date: string) => string` | `To {date}` |
1014
+ | `last2Weeks` | `string` | `Last 2 Weeks` |
1015
+ | `lastMonth` | `string` | `Last Month` |
1016
+ | `last3Months` | `string` | `Last 3 Months` |
1017
+ | `last6Months` | `string` | `Last 6 Months` |
1018
+ | `last12Months` | `string` | `Last 12 Months` |
1019
+ | `custom` | `string` | `Custom` |
1020
+ | `clear` | `string` | `Clear` |
1021
+ | `cancel` | `string` | `Cancel` |
1022
+ | `apply` | `string` | `Apply` |
1023
+
1024
+ </details>
1025
+
1026
+ <details><summary><code>pagination</code> — 5 keys</summary>
1027
+
1028
+ | Key | Type | English default |
1029
+ |---|---|---|
1030
+ | `label` | `string` | `Pagination` |
1031
+ | `first` | `string` | `First page` |
1032
+ | `previous` | `string` | `Previous page` |
1033
+ | `next` | `string` | `Next page` |
1034
+ | `last` | `string` | `Last page` |
1035
+
1036
+ </details>
1037
+
1038
+ <details><summary><code>list</code> — 8 keys</summary>
1039
+
1040
+ | Key | Type | English default |
1041
+ |---|---|---|
1042
+ | `selected` | `(count: number) => string` | `3 selected` |
1043
+ | `allLoaded` | `(total: number, label: string) => string` | `All 3 {label} loaded` |
1044
+ | `count` | `(total: number, label: string) => string` | `3 {label}` |
1045
+ | `loadedOf` | `(loaded: number, total: number, label: string) => string` | `3 of 3 {label}` |
1046
+ | `loadingMore` | `string` | `Loading more...` |
1047
+ | `loadError` | `string` | `Couldn't load this list` |
1048
+ | `loadErrorMessage` | `string` | `Something went wrong fetching these records. Check your connection and try again.` |
1049
+ | `retry` | `string` | `Try again` |
1050
+
1051
+ </details>
1052
+
1053
+ <details><summary><code>kanban</code> — 4 keys</summary>
1054
+
1055
+ | Key | Type | English default |
1056
+ |---|---|---|
1057
+ | `loading` | `string` | `Loading…` |
1058
+ | `empty` | `string` | `No items.` |
1059
+ | `dropHere` | `string` | `Drop here` |
1060
+ | `addItem` | `string` | `Add item` |
1061
+
1062
+ </details>
1063
+
1064
+ <details><summary><code>files</code> — 13 keys</summary>
1065
+
1066
+ | Key | Type | English default |
1067
+ |---|---|---|
1068
+ | `attach` | `string` | `Attach files` |
1069
+ | `drop` | `string` | `Drop files to attach` |
1070
+ | `remove` | `(name: string) => string` | `Remove {name}` |
1071
+ | `chooseMany` | `string` | `Choose files` |
1072
+ | `chooseOne` | `string` | `Choose a file` |
1073
+ | `dragMany` | `string` | `or drag them here` |
1074
+ | `dragOne` | `string` | `or drag it here` |
1075
+ | `notAccepted` | `(name: string, hint: string) => string` | `{name} is not an accepted file type ({hint})` |
1076
+ | `tooLarge` | `(name: string, size: string, limit: string) => string` | `{name} is {size} — the limit is {limit}` |
1077
+ | `tooMany` | `(name: string, max: number) => string` | `{name} was not added — 3 files is the limit` |
1078
+ | `bytes` | `(value: number) => string` | `3 B` |
1079
+ | `kilobytes` | `(value: number) => string` | `3 KB` |
1080
+ | `megabytes` | `(value: number) => string` | `3.0 MB` |
1081
+
1082
+ </details>
1083
+
1084
+ <details><summary><code>media</code> — 14 keys</summary>
1085
+
1086
+ | Key | Type | English default |
1087
+ |---|---|---|
1088
+ | `uploadImage` | `string` | `Upload an image` |
1089
+ | `uploadVideo` | `string` | `Upload a video` |
1090
+ | `uploadFile` | `string` | `Upload a file` |
1091
+ | `upload` | `string` | `Upload media` |
1092
+ | `cta` | `string` | `Choose from library or upload` |
1093
+ | `replace` | `string` | `Replace` |
1094
+ | `remove` | `string` | `Remove` |
1095
+ | `uploading` | `string` | `Uploading…` |
1096
+ | `cover` | `string` | `Cover` |
1097
+ | `add` | `string` | `Add` |
1098
+ | `addMedia` | `string` | `Add media` |
1099
+ | `empty` | `string` | `No media.` |
1100
+ | `removeItem` | `(name: string) => string` | `Remove {name}` |
1101
+ | `reorder` | `(name: string, position: number, total: number) => string` | `{name}, item 2 of 3. Use arrow keys to reorder.` |
1102
+
1103
+ </details>
1104
+
1105
+ <details><summary><code>brandAsset</code> — 10 keys</summary>
1106
+
1107
+ | Key | Type | English default |
1108
+ |---|---|---|
1109
+ | `save` | `(noun: string, assetName: string) => string` | `Save {noun}` |
1110
+ | `remove` | `(noun: string, assetName: string) => string` | `Remove {noun}` |
1111
+ | `discard` | `string` | `Discard change` |
1112
+ | `saveFailed` | `(noun: string, assetName: string) => string` | `Could not save {noun}.` |
1113
+ | `removeFailed` | `(noun: string, assetName: string) => string` | `Could not remove {noun}.` |
1114
+ | `region` | `(assetName: string, subjectName: string) => string` | `{assetName} for {subjectName}` |
1115
+ | `usedFor` | `(subjectName: string) => string` | `Used for {subjectName}.` |
1116
+ | `previews` | `(assetName: string) => string` | `{assetName} previews` |
1117
+ | `preview` | `string` | `Preview` |
1118
+ | `alt` | `(subjectName: string, assetName: string) => string` | `{subjectName} {assetName}` |
1119
+
1120
+ </details>
1121
+
1122
+ <details><summary><code>copy</code> — 5 keys</summary>
1123
+
1124
+ | Key | Type | English default |
1125
+ |---|---|---|
1126
+ | `copy` | `string` | `Copy` |
1127
+ | `copyLabel` | `(label: string) => string` | `Copy {label}` |
1128
+ | `copied` | `string` | `Copied` |
1129
+ | `copiedLabel` | `(label: string) => string` | `{label} copied` |
1130
+ | `failed` | `string` | `Could not copy to the clipboard` |
1131
+
1132
+ </details>
1133
+
1134
+ <details><summary><code>keypad</code> — 2 keys</summary>
1135
+
1136
+ | Key | Type | English default |
1137
+ |---|---|---|
1138
+ | `backspace` | `string` | `Backspace` |
1139
+ | `enter` | `string` | `Enter` |
1140
+
1141
+ </details>
1142
+
1143
+ <details><summary><code>banner</code> — 1 keys</summary>
1144
+
1145
+ | Key | Type | English default |
1146
+ |---|---|---|
1147
+ | `dismiss` | `string` | `Dismiss` |
1148
+
1149
+ </details>
1150
+
1151
+ <details><summary><code>badge</code> — 2 keys</summary>
1152
+
1153
+ | Key | Type | English default |
1154
+ |---|---|---|
1155
+ | `remove` | `string` | `Remove` |
1156
+ | `removeItem` | `(label: string) => string` | `Remove {label}` |
1157
+
1158
+ </details>
1159
+
1160
+ <details><summary><code>breadcrumbs</code> — 2 keys</summary>
1161
+
1162
+ | Key | Type | English default |
1163
+ |---|---|---|
1164
+ | `label` | `string` | `Breadcrumb` |
1165
+ | `hidden` | `string` | `Hidden crumbs` |
1166
+
1167
+ </details>
1168
+
1169
+ <details><summary><code>emptyState</code> — 1 keys</summary>
1170
+
1171
+ | Key | Type | English default |
1172
+ |---|---|---|
1173
+ | `message` | `string` | `Nothing here yet.` |
1174
+
1175
+ </details>
1176
+
1177
+ <details><summary><code>errors</code> — 9 keys</summary>
1178
+
1179
+ | Key | Type | English default |
1180
+ |---|---|---|
1181
+ | `notFound` | `string` | `Page not found` |
1182
+ | `notFoundBody` | `string` | `The page you're looking for doesn't exist or was moved.` |
1183
+ | `forbidden` | `string` | `Access denied` |
1184
+ | `forbiddenBody` | `string` | `You don't have permission to view this page.` |
1185
+ | `noAccess` | `string` | `You don't have access to this` |
1186
+ | `serverError` | `string` | `Something went wrong` |
1187
+ | `serverErrorBody` | `string` | `An unexpected error occurred. Please try again.` |
1188
+ | `retry` | `string` | `Try again` |
1189
+ | `details` | `string` | `Error details` |
1190
+
1191
+ </details>
1192
+
1193
+ <details><summary><code>search</code> — 10 keys</summary>
1194
+
1195
+ | Key | Type | English default |
1196
+ |---|---|---|
1197
+ | `placeholder` | `string` | `Search...` |
1198
+ | `escBadge` | `string` | `ESC` |
1199
+ | `searching` | `string` | `Searching...` |
1200
+ | `noResults` | `string` | `No results found.` |
1201
+ | `minChars` | `(min: number) => string` | `Type at least 3 characters to search...` |
1202
+ | `navigate` | `string` | `navigate` |
1203
+ | `open` | `string` | `open` |
1204
+ | `close` | `string` | `close` |
1205
+ | `enterKey` | `string` | `Enter` |
1206
+ | `escKey` | `string` | `Esc` |
1207
+
1208
+ </details>
1209
+
1210
+ <details><summary><code>grid</code> — 9 keys</summary>
1211
+
1212
+ | Key | Type | English default |
1213
+ |---|---|---|
1214
+ | `selectAll` | `string` | `Select all` |
1215
+ | `insertRowAbove` | `string` | `Insert row above` |
1216
+ | `insertRowBelow` | `string` | `Insert row below` |
1217
+ | `deleteRow` | `(row: number) => string` | `Delete row 4` |
1218
+ | `insertColumnLeft` | `string` | `Insert column left` |
1219
+ | `insertColumnRight` | `string` | `Insert column right` |
1220
+ | `deleteColumn` | `(column: string) => string` | `Delete column {column}` |
1221
+ | `resize` | `string` | `Drag to resize · double-click to auto-fit` |
1222
+ | `fill` | `string` | `Drag to fill` |
1223
+
1224
+ </details>
1225
+
1226
+ <details><summary><code>sidebar</code> — 1 keys</summary>
1227
+
1228
+ | Key | Type | English default |
1229
+ |---|---|---|
1230
+ | `resize` | `string` | `Drag to resize · double-click to reset` |
1231
+
1232
+ </details>
1233
+
1234
+ <details><summary><code>markdown</code> — 1 keys</summary>
1235
+
1236
+ | Key | Type | English default |
1237
+ |---|---|---|
1238
+ | `screenshot` | `string` | `Screenshot` |
1239
+
1240
+ </details>
1241
+
1242
+ <details><summary><code>severity</code> — 4 keys</summary>
1243
+
1244
+ | Key | Type | English default |
1245
+ |---|---|---|
1246
+ | `success` | `string` | `ok` |
1247
+ | `warning` | `string` | `warning` |
1248
+ | `danger` | `string` | `critical` |
1249
+ | `unrecognised` | `(token: string) => string` | `unrecognised severity {token}` |
1250
+
1251
+ </details>
1252
+
1253
+ <details><summary><code>meter</code> — 10 keys</summary>
1254
+
1255
+ | Key | Type | English default |
1256
+ |---|---|---|
1257
+ | `noData` | `string` | `no data` |
1258
+ | `warningAt` | `(bound: number) => string` | `warning ≥ 3` |
1259
+ | `criticalAt` | `(bound: number) => string` | `critical ≥ 3` |
1260
+ | `noReading` | `string` | `no reading` |
1261
+ | `noBudget` | `string` | `no budget` |
1262
+ | `ofBudget` | `(value: string) => string` | `{value} of budget` |
1263
+ | `budget` | `string` | `budget` |
1264
+ | `hoursMinutes` | `(hours: number, minutes: number) => string` | `3h 3m` |
1265
+ | `minutesSeconds` | `(minutes: number, seconds: number) => string` | `3m 3s` |
1266
+ | `seconds` | `(seconds: number) => string` | `3s` |
1267
+
1268
+ </details>
1269
+
1270
+ <details><summary><code>charts</code> — 81 keys</summary>
1271
+
1272
+ | Key | Type | English default |
1273
+ |---|---|---|
1274
+ | `showTable` | `string` | `Show the data as a table` |
1275
+ | `loading` | `string` | `Loading chart data` |
1276
+ | `noData` | `string` | `No data in this window.` |
1277
+ | `windowStart` | `string` | `Window start` |
1278
+ | `windowEnd` | `string` | `Window end` |
1279
+ | `window` | `(from: string, to: string, count: number) => string` | `Time window — showing {from} to {to} of 3 intervals` |
1280
+ | `overIntervals` | `(series: string[], count: number) => string` | `{series1}, {series2} over 3 intervals` |
1281
+ | `acrossCategories` | `(series: string[], count: number) => string` | `{series1}, {series2} across 3 categories` |
1282
+ | `acrossMeasures` | `(series: string[], count: number) => string` | `{series1}, {series2} across 3 measures` |
1283
+ | `breakdown` | `(total: string, segments: number) => string` | `Breakdown of {total} across 3 segments` |
1284
+ | `funnel` | `(stages: number, first: string, last: string) => string` | `Funnel of 3 stages, {first} down to {last}` |
1285
+ | `radialTrack` | `(count: number) => string` | `3 categories, as concentric proportions` |
1286
+ | `radialCycle` | `(count: number) => string` | `3 categories, around a cycle` |
1287
+ | `tableFrom` | `string` | `From` |
1288
+ | `tableTo` | `string` | `To` |
1289
+ | `tableItem` | `string` | `Item` |
1290
+ | `tableLevel` | `string` | `Level` |
1291
+ | `tableSeries` | `string` | `Series` |
1292
+ | `inBucket` | `(total: string) => string` | `{total} in this bucket` |
1293
+ | `inCategory` | `(total: string) => string` | `{total} in this category` |
1294
+ | `value` | `string` | `Value` |
1295
+ | `share` | `string` | `Share` |
1296
+ | `count` | `string` | `Count` |
1297
+ | `flow` | `string` | `Flow` |
1298
+ | `totalFlow` | `string` | `Total flow` |
1299
+ | `throughput` | `string` | `Throughput` |
1300
+ | `open` | `string` | `Open` |
1301
+ | `high` | `string` | `High` |
1302
+ | `low` | `string` | `Low` |
1303
+ | `close` | `string` | `Close` |
1304
+ | `maximum` | `string` | `Maximum` |
1305
+ | `q3` | `string` | `Q3` |
1306
+ | `median` | `string` | `Median` |
1307
+ | `q1` | `string` | `Q1` |
1308
+ | `minimum` | `string` | `Minimum` |
1309
+ | `distribution` | `string` | `Distribution` |
1310
+ | `fiveNumber` | `(label: string, count: number) => string` | `{label}: five-number summary for 3 categories` |
1311
+ | `outliers` | `(count: number) => string` | `3 outliers beyond the whiskers` |
1312
+ | `price` | `string` | `Price` |
1313
+ | `ohlc` | `(label: string, count: number) => string` | `{label}: open, high, low and close across 3 periods` |
1314
+ | `closedAbove` | `string` | `Closed above its open` |
1315
+ | `closedBelow` | `string` | `Closed below its open` |
1316
+ | `noFlows` | `string` | `No flows to chart.` |
1317
+ | `chord` | `(count: number) => string` | `Flows between 3 participants` |
1318
+ | `noStages` | `string` | `No stages to chart.` |
1319
+ | `ofFirstStage` | `string` | `Of first stage` |
1320
+ | `droppedHere` | `string` | `Dropped here` |
1321
+ | `intensity` | `string` | `Intensity` |
1322
+ | `noActivity` | `string` | `No activity in this window.` |
1323
+ | `heatmap` | `(label: string, rows: number, columns: number) => string` | `{label} across 4 rows and 3 columns` |
1324
+ | `countAxis` | `string` | `count` |
1325
+ | `histogram` | `(label: string, bins: number, observations: number) => string` | `{label}: 3 bins over 3 observations` |
1326
+ | `observations` | `string` | `Observations` |
1327
+ | `notEnough` | `string` | `Not enough eligible requests to judge.` |
1328
+ | `objectiveMet` | `(objective: string) => string` | `objective {objective} met` |
1329
+ | `objectiveMissed` | `(objective: string) => string` | `objective {objective} missed` |
1330
+ | `nothingToPlot` | `string` | `Nothing to plot.` |
1331
+ | `nothingToCompare` | `string` | `Nothing to compare.` |
1332
+ | `other` | `string` | `Other` |
1333
+ | `nothingToBreakDown` | `string` | `Nothing to break down.` |
1334
+ | `range` | `string` | `Range` |
1335
+ | `rangeLabel` | `(label: string, count: number) => string` | `{label}: low and high across 3 categories` |
1336
+ | `nothingToRank` | `string` | `Nothing to rank in this window.` |
1337
+ | `emphasised` | `string` | `(emphasised)` |
1338
+ | `sankey` | `(nodes: number, links: number) => string` | `Flow between 3 nodes over 3 links` |
1339
+ | `noObservations` | `string` | `No observations in this window.` |
1340
+ | `xAxis` | `string` | `x` |
1341
+ | `yAxis` | `string` | `y` |
1342
+ | `size` | `string` | `Size` |
1343
+ | `scatter` | `(series: string[], yLabel: string, xLabel: string) => string` | `{series1}, {series2}: {yLabel} against {xLabel}` |
1344
+ | `outsideRange` | `(count: number) => string` | `3 points outside the shown range` |
1345
+ | `waterfall` | `(steps: number, from: string, to: string) => string` | `Waterfall of 3 steps from {from} to {to}` |
1346
+ | `balance` | `string` | `Balance` |
1347
+ | `increase` | `string` | `Increase` |
1348
+ | `decrease` | `string` | `Decrease` |
1349
+ | `runningTotal` | `string` | `Running total` |
1350
+ | `sunburst` | `(total: string, depth: number) => string` | `Hierarchy of {total} across 3 levels` |
1351
+ | `treemap` | `(count: number) => string` | `Treemap of 3 items` |
1352
+ | `trendUp` | `string` | `up` |
1353
+ | `trendDown` | `string` | `down` |
1354
+ | `trendFlat` | `string` | `flat` |
1355
+
1356
+ </details>
1357
+
1358
+ <details><summary><code>timeline</code> — 17 keys</summary>
1359
+
1360
+ | Key | Type | English default |
1361
+ |---|---|---|
1362
+ | `days` | `(count: number) => string` | `3 days` |
1363
+ | `daysToToday` | `(count: number) => string` | `3 days to today` |
1364
+ | `notReached` | `string` | `Not yet reached` |
1365
+ | `next` | `string` | `Next` |
1366
+ | `more` | `(count: number) => string` | `+3 more` |
1367
+ | `today` | `string` | `Today` |
1368
+ | `todayTitle` | `(date: string) => string` | `Today — {date}` |
1369
+ | `open` | `(name: string) => string` | `Open {name}` |
1370
+ | `openButton` | `string` | `Open` |
1371
+ | `scrub` | `string` | `Scrub the timeline` |
1372
+ | `alsoToCome` | `(names: string[]) => string` | `Also to come: {names1}, {names2}` |
1373
+ | `start` | `(date: string) => string` | `Start · {date}` |
1374
+ | `events` | `(count: number, names: string[]) => string` | `3 events · {names1}, {names2}` |
1375
+ | `started` | `(date: string) => string` | `started {date}` |
1376
+ | `duration` | `(days: number) => string` | `3 days` |
1377
+ | `reached` | `(dated: number, total: number) => string` | `3 of 3 milestones` |
1378
+ | `milestones` | `(title: string) => string` | `{title} milestones` |
1379
+
1380
+ </details>
1381
+
1382
+ <details><summary><code>production</code> — 20 keys</summary>
1383
+
1384
+ | Key | Type | English default |
1385
+ |---|---|---|
1386
+ | `heading` | `string` | `Production progress` |
1387
+ | `start` | `string` | `Start` |
1388
+ | `estimated` | `string` | `Estimated` |
1389
+ | `estimatedCompletion` | `string` | `Estimated completion` |
1390
+ | `estDone` | `string` | `Est. done` |
1391
+ | `play` | `string` | `Play` |
1392
+ | `pause` | `string` | `Pause` |
1393
+ | `playTitle` | `string` | `Play the timeline, report by report` |
1394
+ | `backTo` | `(report: string) => string` | `Back to {report}` |
1395
+ | `showing` | `(report: string) => string` | `Showing {report}` |
1396
+ | `overall` | `(percent: number) => string` | `3% overall` |
1397
+ | `inStock` | `(pieces: number) => string` | `3 pc in stock` |
1398
+ | `noReport` | `string` | `No production report filed yet` |
1399
+ | `leadTime` | `(days: number) => string` | `3 days lead time` |
1400
+ | `report` | `string` | `Production report` |
1401
+ | `shipment` | `string` | `Shipment` |
1402
+ | `inspection` | `string` | `Inspection` |
1403
+ | `invoice` | `string` | `Invoice` |
1404
+ | `scrub` | `string` | `Scrub the production timeline` |
1405
+ | `label` | `(from: string, to: string) => string` | `Production reports and events, {from} to {to}` |
1406
+
1407
+ </details>
1408
+
1409
+ <details><summary><code>containers</code> — 10 keys</summary>
1410
+
1411
+ | Key | Type | English default |
1412
+ |---|---|---|
1413
+ | `items` | `(count: number) => string` | `3 items` |
1414
+ | `pieces` | `(count: number) => string` | `3 pcs` |
1415
+ | `split` | `(instructed: number, loaded: number) => string` | `(3 instr / 3 loaded)` |
1416
+ | `added` | `(count: number, pieces: number) => string` | `3 new (3 pcs)` |
1417
+ | `fill` | `(volume: number, capacity: number) => string` | `3.0 / 3 m³` |
1418
+ | `total` | `string` | `Total:` |
1419
+ | `instructed` | `(volume: number) => string` | `3.00 m³ instruction` |
1420
+ | `loaded` | `(volume: number) => string` | `3.00 m³ loaded` |
1421
+ | `volume` | `(volume: number) => string` | `3.00 m³` |
1422
+ | `noVolume` | `string` | `Volume: N/A (no volume data for these part numbers)` |
1423
+
1424
+ </details>
1425
+
1426
+ <details><summary><code>toast</code> — 1 keys</summary>
1427
+
1428
+ | Key | Type | English default |
1429
+ |---|---|---|
1430
+ | `notification` | `string` | `Notification` |
1431
+
1432
+ </details>
1433
+
513
1434
  ### Brand surfaces — `efficient-shell/brand.css`
514
1435
 
515
1436
  For output that is not a portal window — a report, a published artifact, a
@@ -753,7 +1674,7 @@ copying both from `node_modules/maplibre-gl/dist`). Tiles come from
753
1674
 
754
1675
  | Export | Notes |
755
1676
  |---|---|
756
- | `EntityMap` | The map. `pins` (`MapPin`: glyph or `icon` paths, `badge`, `tone` classes, `approximate`, `faded`), `selectedKey` + `onSelect`, `focus` to fly to a pin, `routes` (`MapRoute`, dashed when an estimate), `fitTo`, `attribution`. Follows `data-theme="dark"` on `<html>`; resizes with its box (a shell window sends no window resize). Without WebGL it says so and its children still draw. |
1677
+ | `EntityMap` | The map. `pins` (`MapPin`: glyph or `icon` paths, `badge`, `tone` classes, `approximate`, `faded`), `selectedKey` + `onSelect`, `focus` to fly to a pin, `routes` (`MapRoute`, dashed when an estimate), `fitTo`, `attribution`. Follows `data-theme="dark"` or a `dark` class on `<html>`; resizes with its box (a shell window sends no window resize). Without WebGL it says so and its children still draw. |
757
1678
  | `LocationsMap` | A record's compact map over an endpoint: legend, "Locating N addresses…", a note per record with no pin, the chosen pin's card. `palette` names the categories the portal's way; `numbers` gives each pin the number the page's own list shows. |
758
1679
  | `MapLegend`, `SelectedCard` | The overlays, for a map that lays out its own list (the admin portal's list-wide Map view). |
759
1680
  | `useMapPoints(queryKey, queryFn)` | react-query, re-asked every few seconds while `pending > 0`; `mapPointsRefetchInterval` is the same rule for a hand-written query. |
@@ -769,7 +1690,7 @@ copying both from `node_modules/maplibre-gl/dist`). Tiles come from
769
1690
  | `isPageEntry`, `isEntityEntry` | Type guards for `WindowRegistryEntry`. |
770
1691
  | `glassStyle()` | Returns the theme-aware frosted-glass `style` object. |
771
1692
  | `reportBug(submit)` | Captures a screenshot via `getDisplayMedia`, opens the dialog, hands the payload to your `submit`. |
772
- | `formatDate(iso)` | Locale-aware date formatter. |
1693
+ | `formatDate(iso, { locale?, style? })` | A `YYYY-MM-DD` (or the date part of a timestamp) in the user's stored date format — or, given a `locale`, written by `Intl` in it (`style` defaults to `{ dateStyle: 'medium' }`). Returns `—` for nothing. |
773
1694
  | `installStaleChunkReload(options?)` | Call once before the app mounts. A deploy replaces every lazy chunk; a tab opened before it crashes on the first lazy window it opens ("Failed to fetch dynamically imported module", or a `ChunkLoadError`) and no remount can fix it. This reloads the page instead — once per 30 s cooldown, kept in `sessionStorage`, so a chunk still missing after the reload (a broken deploy) crashes visibly instead of looping. It hears Vite's `vite:preloadError` (whose throw it swallows) and, for Next.js (Turbopack) and webpack, a chunk under `chunkPathPrefix` (default `NEXT_CHUNK_PATH_PREFIX`, `/_next/static/`; a path from this origin or a full URL for a CDN `assetPrefix`; `null` for Vite only) that fails as a `<script>`, a stylesheet or a script/style/module preload — or that came back as HTML and threw a `SyntaxError`, which is how a missing chunk surfaces without `X-Content-Type-Options: nosniff`. Prefetches, fonts and images are ignored. Before a chunk reload it asks the server (`HEAD`, `no-store`) whether the file is really gone — a 404/410, or the SPA fallback's HTML — so a dropped connection is left to the loader's own retry and an offline tab is never reloaded onto the browser's error page. Both signals share one cooldown. Returns the uninstaller. |
774
1695
  | `isStaleChunkError(error)` | True for the three engines' "module the server did not return" messages, Vite's CSS-preload line, and any Error named `ChunkLoadError` (Turbopack, webpack) — what `WindowCrashedFallback` uses to offer "Reload page" instead of "Reload window". |
775
1696
  | `budgetState(elapsed, budget)` | `no-reading` \| `no-budget` \| `within` \| `over` — the verdict `BudgetBar` draws by, exported so a run list sorts and filters by the same rule rather than re-deriving "late" a second time. |