efficient-shell 5.2.0 → 5.6.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 (87) hide show
  1. package/README.md +160 -31
  2. package/THIRD_PARTY_NOTICES.md +32 -0
  3. package/dist/{Browser-FKZ6M2MK.js → Browser-6QB2QTO7.js} +17 -17
  4. package/dist/Calculator-KOWRV2VK.js +29 -0
  5. package/dist/ConfirmDialog-32NLLK4K.js +1 -0
  6. package/dist/{CurrencyConverter-OV5ZQIAJ.js → CurrencyConverter-YJ47A3L5.js} +7 -7
  7. package/dist/{Documents-HGGL3AWI.js → Documents-TGGI2QBV.js} +24 -24
  8. package/dist/Files-KSDK3EO4.js +87 -0
  9. package/dist/{Notepad-PO724TVP.js → Notepad-EKB6FJPR.js} +10 -10
  10. package/dist/PdfViewer-KW5B44AX.js +1 -0
  11. package/dist/{PomodoroTimer-LN5MYRZO.js → PomodoroTimer-RD5GRRA5.js} +19 -19
  12. package/dist/{Preview-CUOO5WQ7.js → Preview-AQVXKT2I.js} +29 -29
  13. package/dist/Sidebar-VYXN5YIJ.js +44 -0
  14. package/dist/{Spreadsheet-2N2JXBCY.js → Spreadsheet-INXG5UMI.js} +13 -13
  15. package/dist/{Stock-65WVUGLI.js → Stock-CBQN2IJF.js} +4 -4
  16. package/dist/{Weather-TXJICMJN.js → Weather-2EVUTIXX.js} +18 -18
  17. package/dist/{WorldClock-RIHLKZGC.js → WorldClock-TZUF7MOB.js} +5 -5
  18. package/dist/apps/index.js +1 -1
  19. package/dist/bin/ui-check.js +1 -1
  20. package/dist/brand.css +1 -1
  21. package/dist/{chunk-Y553QKEH.js → chunk-3GPU3JOW.js} +12 -12
  22. package/dist/{chunk-TKFAFZRY.js → chunk-3HJTOINK.js} +1 -1
  23. package/dist/{chunk-YJ6JMGMS.js → chunk-4SWPNARR.js} +4 -4
  24. package/dist/{chunk-JJIPGAXI.js → chunk-6LTDJKIL.js} +72 -71
  25. package/dist/chunk-AP2QEJXQ.js +1 -0
  26. package/dist/{chunk-3PTA2C74.js → chunk-CLPJDJH4.js} +1 -1
  27. package/dist/{chunk-SSBOC52S.js → chunk-FROOMUJ7.js} +4 -4
  28. package/dist/{chunk-UG4ZPXXU.js → chunk-GG7AE6OC.js} +1 -1
  29. package/dist/{chunk-UAJQB45Y.js → chunk-HRLPFUPB.js} +1 -1
  30. package/dist/{chunk-OTP5WPI4.js → chunk-JEM73E4C.js} +14 -14
  31. package/dist/{chunk-ML22KF7T.js → chunk-JXAAZGLJ.js} +1 -1
  32. package/dist/{chunk-BFUBBDU6.js → chunk-KXAUDC3A.js} +26 -26
  33. package/dist/{chunk-TLTFS2D6.js → chunk-L5XA5MLK.js} +60 -60
  34. package/dist/{chunk-QZNRMJE6.js → chunk-MLYC5VQI.js} +2 -2
  35. package/dist/{chunk-KHJ5XSEH.js → chunk-N3A3GWJI.js} +8 -8
  36. package/dist/{chunk-BFFXVA53.js → chunk-PKEQ7XOH.js} +1 -1
  37. package/dist/{chunk-ASSDCWKP.js → chunk-R4XSSHDI.js} +1 -1
  38. package/dist/{chunk-R3622MOL.js → chunk-RCHCT7AO.js} +1 -1
  39. package/dist/chunk-RDQ7OXUA.js +1026 -0
  40. package/dist/{chunk-YQI7KXX3.js → chunk-RL626E7Q.js} +2 -2
  41. package/dist/{chunk-HIAHERCD.js → chunk-RMJZOKHY.js} +9 -9
  42. package/dist/{chunk-M4OGNYOM.js → chunk-SKB6L53D.js} +11 -11
  43. package/dist/{chunk-H3UEYXRA.js → chunk-TFETN2GI.js} +1 -1
  44. package/dist/chunk-TNBCUZVE.js +1 -0
  45. package/dist/chunk-WOEFPBQF.js +1 -0
  46. package/dist/chunk-WRBXEZFN.js +1 -0
  47. package/dist/chunk-XWSWYMGM.js +2 -0
  48. package/dist/{chunk-6GV4467L.js → chunk-Z2353HNE.js} +5 -5
  49. package/dist/{chunk-MTH7EAH3.js → chunk-ZUCYJAQH.js} +21 -21
  50. package/dist/file-intake/index.js +1 -1
  51. package/dist/format/index.d.ts +1 -1
  52. package/dist/format/index.js +1 -1
  53. package/dist/i18n/index.d.ts +1 -1
  54. package/dist/icons/efficient-looks.d.ts +28 -0
  55. package/dist/icons/efficient-looks.js +920 -0
  56. package/dist/icons/efficient.d.ts +746 -298
  57. package/dist/icons/efficient.js +336 -470
  58. package/dist/icons/heroicons.d.ts +1 -1
  59. package/dist/icons/index.d.ts +10 -4
  60. package/dist/icons/index.js +1 -1
  61. package/dist/icons/lucide.d.ts +1 -1
  62. package/dist/{index-Dk5VKml6.d.ts → index-BgM75MC6.d.ts} +365 -78
  63. package/dist/{index-DpUDK9zG.d.ts → index-CHufemKB.d.ts} +69 -1
  64. package/dist/index.d.ts +5 -5
  65. package/dist/index.js +211 -211
  66. package/dist/locales/zh-Hans.d.ts +1 -1
  67. package/dist/locales/zh-Hans.js +1 -1
  68. package/dist/map/index.js +5 -5
  69. package/dist/markdown/index.js +1 -1
  70. package/dist/page-tabs/index.js +5 -5
  71. package/dist/pdf/index.js +1 -1
  72. package/dist/{registry-FHdUeVww.d.ts → registry-J-TU7zKx.d.ts} +14 -4
  73. package/dist/{strings-aKfRDZWB.d.ts → strings-BVB64Nck.d.ts} +26 -0
  74. package/dist/ui/index.d.ts +4 -4
  75. package/dist/ui/index.js +1 -1
  76. package/dist/ui.css +61 -1
  77. package/package.json +8 -2
  78. package/dist/Calculator-LBFBTFSD.js +0 -29
  79. package/dist/ConfirmDialog-JAEFVRJM.js +0 -1
  80. package/dist/Files-DAQKIF6W.js +0 -87
  81. package/dist/PdfViewer-6ALXGVTV.js +0 -1
  82. package/dist/Sidebar-T4ABK2WV.js +0 -44
  83. package/dist/chunk-4QBLTEFU.js +0 -1
  84. package/dist/chunk-AJB5G52B.js +0 -1
  85. package/dist/chunk-FWOPZB6I.js +0 -1
  86. package/dist/chunk-PS6PSCOR.js +0 -1018
  87. package/dist/chunk-YKZHOXI4.js +0 -2
package/README.md CHANGED
@@ -582,7 +582,7 @@ menu open; a radio closes it, as a plain item does.
582
582
  </PopupSubmenu>
583
583
  ```
584
584
 
585
- ### Editor controls — `Slider`, `ColorPicker`, `ImageFocusPoint`, `LinkPicker`, `Tree`, `SortableList`, `IconRail`, `ResizeHandle`
585
+ ### Editor controls — `Slider`, `ColorPicker`, `ImageFocusPoint`, `LinkPicker`, `Tree`, `SortableList`, `IconRail`, `ResizeHandle`, `CornersInput`, `AlignmentGrid`, `WidthsBar`
586
586
 
587
587
  These were folded in from `efficient-easel` under harness UI-24, because the
588
588
  website editor and the email designer each drew their own. All of them are on
@@ -719,6 +719,51 @@ kit `Input` on top and a link row under it, in one box. The row opens the
719
719
  window under the box, in the panel's own flow. Words come from the `link`
720
720
  section of the strings catalogue; `placeholder` sets the address box's text.
721
721
 
722
+ **`CornersInput`** is the Corners row of a settings panel: the radius in a
723
+ number box (`InputNumber`, an "R" before the number and px after it, compact
724
+ steppers), then four corner buttons, on one row. The four are a SET (`role="group"` of `aria-pressed` `IconButton`s):
725
+ any number may be on, and a press toggles one. Each draws the `corner` glyph
726
+ turned to its corner (`tl` · `tr` · `br` · `bl`) and shows its name in a
727
+ `Tooltip`. Not the theme's `Corners` type, which is the portal-wide corner style.
728
+
729
+ ```tsx
730
+ <CornersInput label="Corners" value={sides} onChange={setSides}
731
+ corners={[{ value: 'tl', label: 'Top left' }, { value: 'tr', label: 'Top right' },
732
+ { value: 'br', label: 'Bottom right' }, { value: 'bl', label: 'Bottom left' }]}
733
+ radius={radius} onRadiusChange={setRadius} radiusPlaceholder="8" />
734
+ ```
735
+
736
+ The host owns the words and the values, and resolves "nothing stored" before
737
+ handing `value` in: an empty set means every corner square. Leave out
738
+ `onRadiusChange` and the row is the four corners alone. A corner with
739
+ `disabled` keeps its cell, stays focusable (`aria-disabled`) and says its
740
+ `title` as the reason; `disabled` + `disabledReason` turn the whole row off the
741
+ same way. `label` names the group and is not drawn: the row around it draws
742
+ the label.
743
+
744
+ **`AlignmentGrid`** is the 3×3 position grid — a block's place in its box, a
745
+ picture's focal crop, or any other one-of-nine. It is a `radiogroup` of nine
746
+ real buttons (`role="radio"`, `aria-checked`), each named by its position and
747
+ showing that name in a `Tooltip`. The cells are 36px squares with a line
748
+ between them; each draws a grey dot, and the chosen one is a blue square on a
749
+ light blue cell.
750
+
751
+ ```tsx
752
+ <AlignmentGrid label="Placement" value={place} onChange={setPlace}
753
+ points={[{ value: 'tl', label: 'Top left' }, { value: 'tc', label: 'Top' }, /* … nine, in reading order */]} />
754
+ ```
755
+
756
+ A point may carry an `icon` (an arrow from the icon set), drawn at 16px
757
+ instead of its dot. A point with `disabled` keeps its cell and says its
758
+ `title` as the reason; `disabled` + `disabledReason` turn the whole grid off.
759
+ Controlled: a press asks `onChange` and the grid lights nothing by itself.
760
+
761
+ The grid is ONE Tab stop (the chosen cell, else the first open one), the
762
+ WAI-ARIA radio group pattern with a roving tabindex. Arrows move focus in two
763
+ dimensions and choose the cell they land on, as native radios do; they stop at
764
+ the grid's edge (no wrap). Space or Enter presses the focused cell. An arrow
765
+ reaches a closed cell, so its reason is heard, but chooses nothing there.
766
+
722
767
  **`Tree`**, **`TreeRow`**, **`TreeGroup`**, **`TreeGroupLabel`**,
723
768
  **`TreeEmptySlot`** and **`TreeSeam`** draw a layers panel or an outline. They follow the WAI-ARIA
724
769
  tree pattern.
@@ -875,6 +920,36 @@ End go to `min`/`max`, and Enter or a double-click resets to `defaultWidth`. A
875
920
  drag starts after 3px, so a click never nudges it. `storageKey` restores the
876
921
  width after mount and keeps it once a drag settles. Its hint is a `Tooltip`.
877
922
 
923
+ **`WidthsBar`** sets how a row's columns share its width: a bar split into
924
+ the columns, a handle between each pair, and a typed % box for each column.
925
+ The last column fills itself. It came from the campaign designer's "Column
926
+ widths" field (efficient-admin-portal), which is deleted once this ships.
927
+
928
+ ```tsx
929
+ <InputNumber aria-label="Columns" value={count} onChange={n => n && setCount(n)} min={1} max={4} />
930
+ <WidthsBar label="Widths" count={count} value={row.columnShares} onChange={shares => save({ columnShares: shares })} />
931
+ ```
932
+
933
+ The value is `ColumnShares`: the first `count - 1` shares, whole percentages.
934
+ The last column is what the others leave, so the widths always add up. No
935
+ column goes under `COLUMN_SHARE_MIN` (10%): raising a box squeezes the columns
936
+ after it, and a handle trades width with its neighbour and stops at the floor.
937
+ A row that chose nothing shows equal widths and stores nothing; "Reset to
938
+ equal" calls `onChange(undefined)` and never stores 34/33, because no value
939
+ renders as the exact `33.34% / 33.33% / 33.33%` strings saved rows carry.
940
+ `count` is 1 to 4; a value means something at one count only and is inert at
941
+ the others, so hiding the bar never needs to clear it. Each handle is a
942
+ `separator`: ArrowLeft/ArrowRight 1%, Shift 10%, PageUp/PageDown 10%, Home and
943
+ End to the floor. A drag starts after 3px. `readOnly` shows everything and
944
+ writes nothing; `disabled` dims it. A drag on the canvas is the host's job.
945
+
946
+ A renderer lays the row out from the same logic, so the panel and the row
947
+ cannot disagree: `resolveColumnWidths(count, shares)` gives the width strings
948
+ (equal strings for no value, a value for another count, or a count past 4),
949
+ `resolveColumnShares(value, count)` clamps a stored value to the floor or
950
+ returns `undefined`, and `COLUMN_COUNT_WIDTHS` / `COLUMN_SHARES_EQUAL` are the
951
+ equal strings and the equal panel figures.
952
+
878
953
  **A font list** is not a separate component. `SearchableSelect` takes
879
954
  `renderOption(option, { selected, active })` to draw each option, for example
880
955
  each face in itself. The `label` still names, filters and fills the field.
@@ -991,7 +1066,7 @@ All exports are named — `import { Modal, ... } from 'efficient-shell'`.
991
1066
  | `Popover` | A button and the panel it opens, right under it, placed by `DropdownMenu`'s rule (left edges together, or right with `align="end"`, flipping and shifting to stay on screen and in its window, never wider than a phone allows): a non-modal `role="dialog"` named by `title` or `aria-label`, for check boxes, a date, a short form. A kit popup opened inside it (a `DatePicker`'s calendar, a `Select`'s list) does not close it. Focus moves in as it opens; Tab reads it as if it sat right after its trigger; Escape closes it and gives focus back. See [Popovers](#popovers). |
992
1067
  | `Dialog`, `Drawer` | A modal sheet that is not a shell window — centred (`Dialog`) or pinned to an edge (`Drawer`, `side` right / left / bottom) — for a till, a routed page or a form: trapped focus, a locked page, Escape and a backdrop click to close (`blocking` turns both off), a close cross when there is no `footer`. A `Dialog` is never taller than the screen less its gutter: a longer form — on a phone, most of them — scrolls inside the panel, footer included, and a footer wider than the panel wraps. `title` names it; `description` is a line under the title that DESCRIBES it to a screen reader — without one the body does, which suits a confirm whose body is the question and not a form, whose every label would be read out. `role="alertdialog"` (Dialog) for a confirmation that interrupts. A heading or note that lives in the body — a form that loads the record its heading names — names and describes it through `aria-labelledby` / `aria-describedby`. Two open at once — the second inside the first, or beside it on `<body>` where a host portals its dialogs — each hold focus, and only the one in front moves Tab (`useFocusTrap` keeps a stack). |
993
1068
  | `ConfirmProvider`, `confirm` | Imperative `confirm({ title, body })` returning a Promise<boolean>. |
994
- | `GlobalSearch` | Cmd-K command palette. Pass `providers: SearchProvider[]` to add results; a provider is called `(query, { signal })` and the `signal` aborts when a newer query, closing the palette or unmount supersedes the call, so pass it to the request. Only the latest query's response is shown, whether or not a provider honours the signal. A result whose `entity_type` is `__page__` opens a page; an `entity_id` of `/path?search=text` (URL-encoded) opens that page's list with its search box filled in. Portalled to `<body>` on the overlay layer (`Z_LAYERS.overlay`), so it covers every window, pinned or not; opening it closes any open popup (`dismissPopups`). Results are grouped under a header per `result.type` (with a count), in the order each type first appears in the merged responses — providers answer best match first, so the group holding an exact match leads. ↑/↓ run through every row across groups, Tab / Shift+Tab jump to the first row of the next / previous group, Enter opens; the input is an ARIA combobox over a listbox with `role="group"` sections. With fewer than two characters typed it shows an empty state instead: **Recent** (the last 8 results picked, most recent first, deduplicated by `entity_type` + `entity_id`), **Favorites** and **Actions** — each section only when it has rows. `recent` + `onRecord` and `favorites` are props, so a routed app that passes none gets no Recent / Favorites (`pushRecentSearch(recent, picked)` keeps the list for it); `Layout` passes them from the user's preferences (`search_recent`, `favorite_documents`). Picking a result or a Recent row calls `onRecord(result)` then `onSelect(result)`; picking a Favorite or an Action does not record. **While searching**, the previous answer's rows stay drawn (with a spinner in the input row) until the next one lands; "Searching…" shows only when there is nothing to draw yet. **Failures are not "no results"**: an aborted call is silent, but a provider that rejects marks the search failed — every provider down shows `strings.search.searchFailed` instead of "No results", some down shows the rows that arrived plus a one-line `partialFailure` warning. **"See all"**: a provider may answer `SearchResult[]` or a `SearchResponse` (`{ results, truncatedTypes? }`); a group whose type is in `truncatedTypes` counts as "5+", and when `SearchConfig.listRoutes` maps its type to a `SearchListRoute` (`{ path, label }`, label the plural the consumer names its list by, e.g. `customers`) it ends in a "See all {label}" row that calls `onSelect` with a `__page__` result for `${path}?search=${encodeURIComponent(query)}` (so the page must read `usePageSearch()`); it is never recorded in Recent, and a group that is not truncated gets neither the "+" nor the row. `SearchConfig.quickActions` (`{ label, icon?, keywords?, onSelect }[]`) are the Actions: always in the empty state, and in a typed search an "Actions" group at the end for those whose label or keywords contain the query. **A result's kind and its words are separate**: `result.type` is the heading the reader sees, and an optional `result.typeKey` (`sales_order`) is the identity results group by and `typeIcons`, `listRoutes` and `truncatedTypes` are keyed by. Without `typeKey` the heading is the key, as before; a host with a translated interface sets it, because the same kind arrives under a different heading in each language. `SearchConfig.typeLabels` maps a type key to today's words, so a group's heading and the badge on a Recent row saved in another language read in the language the page is in. |
1069
+ | `GlobalSearch` | Cmd-K command palette. Pass `providers: SearchProvider[]` to add results; a provider is called `(query, { signal })` and the `signal` aborts when a newer query, closing the palette or unmount supersedes the call, so pass it to the request. Only the latest query's response is shown, whether or not a provider honours the signal. A result whose `entity_type` is `__page__` opens a page; an `entity_id` of `/path?search=text` (URL-encoded) opens that page's list with its search box filled in. Portalled to `<body>` on the overlay layer (`Z_LAYERS.overlay`), so it covers every window, pinned or not; opening it closes any open popup (`dismissPopups`). Results are grouped under a header per `result.type` (with a count), in the order each type first appears in the merged responses — providers answer best match first, so the group holding an exact match leads. ↑/↓ run through every row across groups, Tab / Shift+Tab jump to the first row of the next / previous group, Enter opens; the input is an ARIA combobox over a listbox with `role="group"` sections. With fewer than two characters typed it shows an empty state instead: **Recent** (the last 8 results picked, most recent first, deduplicated by `entity_type` + `entity_id`), **Favorites** and **Actions** — each section only when it has rows. `recent` + `onRecord` and `favorites` are props, so a routed app that passes none gets no Recent / Favorites (`pushRecentSearch(recent, picked)` keeps the list for it); `Layout` passes them from the user's preferences (`search_recent`, `favorite_documents`). Picking a result or a Recent row calls `onRecord(result)` then `onSelect(result)`; picking a Favorite or an Action does not record. **While searching**, the previous answer's rows stay drawn (with a spinner in the input row) until the next one lands; "Searching…" shows only when there is nothing to draw yet. **Failures are not "no results"**: an aborted call is silent, but a provider that rejects marks the search failed — every provider down shows `strings.search.searchFailed` instead of "No results", some down shows the rows that arrived plus a one-line `partialFailure` warning. **"See all"**: a provider may answer `SearchResult[]` or a `SearchResponse` (`{ results, truncatedTypes? }`); a group whose type is in `truncatedTypes` counts as "5+", and when `SearchConfig.listRoutes` maps its type to a `SearchListRoute` (`{ path, label }`, label the plural the consumer names its list by, e.g. `customers`) it ends in a "See all {label}" row that calls `onSelect` with a `__page__` result for `${path}?search=${encodeURIComponent(query)}` (so the page must read `usePageSearch()`); it is never recorded in Recent, and a group that is not truncated gets neither the "+" nor the row. `SearchConfig.quickActions` (`{ label, icon?, keywords?, onSelect }[]`) are the Actions: always in the empty state, and in a typed search an "Actions" group at the end for those whose label or keywords contain the query. **A result's kind and its words are separate**: `result.type` is the heading the reader sees, and an optional `result.typeKey` (`sales_order`) is the identity results group by and `typeIcons`, `listRoutes` and `truncatedTypes` are keyed by. Without `typeKey` the heading is the key, as before; a host with a translated interface sets it, because the same kind arrives under a different heading in each language. `SearchConfig.typeLabels` maps a type key to today's words, so a group's heading and the badge on a Recent row saved in another language read in the language the page is in. **⌘K / Ctrl+K is shared with the page.** A press that something nearer the user already handled (`preventDefault()`) is left alone, so an embed with its own ⌘K search keeps it. `SearchConfig.hotkey: false` (also a `GlobalSearch` prop) turns the key off altogether for a page whose handler does not claim it; the overlay still opens from a button that calls `openGlobalSearch()`, and Escape still closes it. |
995
1070
  | `ShortcutHelp` | The keyboard cheatsheet shown on `?`. |
996
1071
  | `NotificationBell` | Taskbar bell — config via `<Layout notifications={…}>`. |
997
1072
  | `BugReportDetail` | Used inside an entity-window registry entry; reads from `<BugReportConfigProvider>`. |
@@ -1004,7 +1079,7 @@ All exports are named — `import { Modal, ... } from 'efficient-shell'`.
1004
1079
  | `ProductionTimeline` | The scrubbable production bar, drawn on `TimelineTrack` inside the shared timeline card: a sentence-case heading with the PO as its subject, the window and the lead time as meta, a Play **pill** on the right whose glyph follows its state, and a footer carrying the status line and legend chips drawn with the track's own glyphs — a shipment is a diamond with a truck in it and an inspection a disc with a flask, on the rail and in the chip alike, because every kind is painted in the one accent and the glyph is the only thing left to tell them apart. The thumb only ever rests on a date a report was filed on — drag it and it snaps report to report, arrow it and it steps one report at a time — so the bar never shows an estimate dressed as a fact. Press Play and it **travels** between them: a fresh press rewinds to the first report, the thumb reads each snapshot for 700 ms and then glides to the next with the date chip counting the days it crosses, and the status line under the bar changes on arrival rather than while it is on its way. Pausing freezes it mid-glide and pressing Play again carries on from exactly there; touching the thumb ends the playback and hands it back. The walk keeps time by the clock rather than by the frame rate, so an idle or throttled pane does not stretch it. `onScrubProgress` forwards the track's in-flight fraction — `{ fromKey, toKey, t, ms }` keyed by report id, `null` at a stop — which is how a consumer's items table moves WITH the thumb instead of jumping a whole report's worth of quantities when it lands; it is optional and undefined by default, so a caller that only reads the snapshot needs no change. `onPickReport` selects a report; the optional `onOpenReport` / `onOpenMarker` make the label above the thumb a real button that opens the document behind it. `reportLabel` names a report wherever the bar states its identity — the caption over the thumb, the popover header, each dot's accessible name, the slider's value text and the status line — and defaults to its own `progress_number`, while `resetLabel` replaces the whole of the `Back to …` button: together they are what lets a portal that must not print an internal document number run the same card. A report carries an optional `preview` for its hover/focus popover — the stage row it filed, the note attached to it — and `renderReportPreview` builds one for the reports that carry none, the convenience for a caller whose list arrives straight off an API; the report's own preview wins where both exist, and the popover prints the report number and the date itself. `useProductionTimeline(opts)` owns the slider state and the derived snapshot so the caller's items table and the bar read the same state; `STAGES` / `calcOverall` / `calcReportOverall` are the stage maths it shares with that table. **`variant="rail"`** is the bar alone, for a window whose own header already says what the order is: no card, no heading, no lead-time line, no label under the active report — every report, shipment, inspection and **invoice** (kind `invoice`, a disc with a receipt) is a dot with a popover — the thumb with its date chip, `Start` and `Est. done` flanking the rail, the estimated completion as a hollow mark on the right edge while it is still ahead, and under the rail one row: Play, the return button when the thumb has been moved, and the `<report> · date · N% overall · N pc in stock` line. The rail spreads the reports (the track's `spread` axis) rather than folding two a week apart into a `×2`. `onPlayStart` fires before the thumb moves, on a fresh run and on a resume, so a window that keeps the bar in view while its table shows something else can switch the table first. |
1005
1080
  | `MilestoneTimeline` | The static sibling, on the same `TimelineTrack` and in the same card: one dot per milestone on a date axis, no scrubber. `heading` and `subject` split the title from the record it is about (`Mould development` · **001F/1813**), and the meta line states when it started, how long it has run and how many milestones are dated. The axis **cuts** rather than squeezes: a stretch holding more than 30% of the window's time becomes a fixed notch with a break glyph saying how many days it hides, and every stretch that still keeps time gets at least 48 px. Labels are packed into **two lanes** by their **measured** width, and a run of same-kind revisions folds into one `DFM ×4` pill whose members keep their dots on the rail: hovering it magnifies that stretch until each member can carry its own label, a click pins the popover listing them, Escape closes it. A milestone carries an optional `glyph` (a `default` one that names `doc` is drawn as the signed drawing it is), a `preview` for the popover, and an `onOpen` that the popover's "Open" button fires — `onClick` selects the dot on the bar, `onOpen` opens the drawing behind it, and a card that declares only the first keeps its Open button because the footer falls back to it. Label and date are printed by the popover above the preview, so a consumer supplies neither. A milestone with **no date gets no position**, because the only position available would be a date the record does not have: it is listed beside the bar under "Not yet reached" (two rows, then a real `+N more` button) and takes no part in the range, the fill or a phase bracket. `phase` groups of 2+ get a bracket marking parallel work, the fill never runs past today, and today gets its dashed rule and tag even when it IS the right edge — the commonest case, because a programme with no finish date ends at today. **`variant="rail"`** is the bar alone, for a window whose stepper already names the stages: no card, no heading, no label under any dot — every milestone is a dot with a popover, a revision carries a two-character `caption` over its dot (`v3`; captions that would overprint collapse to `v1–v4`), `edgeCaptions` flank the rail on its own row (`{ start: 'Start', end: 'Ready' }`), nothing is cut (the `spread` axis: every stretch at least 48 px, the idle ones drawn short rather than notched), and what has not happened is one dashed **cap** at the end of a dashed tail — a real button announced as the last undated milestone, naming the rest in its popover. The stepper and the rail then point at each other: `highlightKeys` lights one soft band from the leftmost named milestone to the rightmost and every dot inside it (a single key rings one dot; the cap answers to the pending milestone's key), and `onHoverChange` reports the milestone under the pointer or the focus, `null` on leave. A `provisional` milestone — a sample ORDERED, drawn where the shipment will be — is hollow, never "where we are", and outside the fill. |
1006
1081
  | `SettingRow` | One setting in a settings card: the name with its explanation stacked under it on the left, the control or value on the right (it never shrinks), and an optional body (`children`) that opens beneath behind its own line. Each row draws its own top line (`divider`) and padding, so rows stack in a card. One prop per slot: `label` (the name) and `value` (a control or a read-only fact). `size="compact"` is the editor-panel row. Handles a value that is a control (`controlId` labels it), a read-only fact, a value nobody knows (an em dash in the faint ink, never an empty cell), and a `quiet` row nothing can write yet. A name over a field (children, no value) draws nothing on the right. `DescriptionList` is the record-detail neighbour and stays that. |
1007
- | `Button`, `Input`, `Textarea`, `Select`, `Checkbox`, `Radio`, `Switch`, `FormField`, `Label` | Form controls — controlled (`value`/`onChange`); `Input`/`Textarea` forward native props for react-hook-form. `Button` and `Switch` take `disabledReason`: persistent text beside the control wired with `aria-describedby`, never a `title` — a tooltip needs a hover a disabled control does not reliably get. `Switch` also takes `disabledReasonId`, so a panel whose controls are all dead for one reason states it once and points every control at it. A `Select` option carries an optional `description`, rendered as a second muted line **in the open list only** — the closed trigger stays one line, so one explained option cannot change the height of every field in the row. `NativeSelect` (and so `Select` on touch) ignores it: an `<option>` holds text and nothing else. An option also takes `lang` (a BCP 47 tag) when its label is written in another language than the page's — a language picker naming each language in its own words: it goes on the option in the open list, on the native `<option>` and on the closed trigger's label while chosen, so a screen reader and the font fallback treat each name as its own language (WCAG 3.1.2). On the desktop listbox, `title` joins `aria-label` / `aria-labelledby` / `aria-describedby` on the VISIBLE trigger button — it used to ride the native spread onto the `sr-only` `<select>`, where a tooltip has no pointer that can reach it and a fallback name sits on an `aria-hidden` element. `name`, `required` and the forwarded ref stay on the native control, which is what it is for. A `Checkbox` or `Radio` with both a `label` and a `description` is NAMED by the label and DESCRIBED by the description (`aria-labelledby` / `aria-describedby`); the whole row is still its `<label>`, so its words still tick it. |
1082
+ | `Button`, `Input`, `Textarea`, `Select`, `Checkbox`, `Radio`, `Switch`, `FormField`, `Label` | Form controls — controlled (`value`/`onChange`); `Input`/`Textarea` forward native props for react-hook-form. `Button` and `Switch` take `disabledReason`: persistent text beside the control wired with `aria-describedby`, never a `title` — a tooltip needs a hover a disabled control does not reliably get. `Switch` also takes `disabledReasonId`, so a panel whose controls are all dead for one reason states it once and points every control at it. A `Select` option carries an optional `description`, rendered as a second muted line **in the open list only** — the closed trigger stays one line, so one explained option cannot change the height of every field in the row. `NativeSelect` (and so `Select` on touch) ignores it: an `<option>` holds text and nothing else. An option also takes `lang` (a BCP 47 tag) when its label is written in another language than the page's — a language picker naming each language in its own words: it goes on the option in the open list, on the native `<option>` and on the closed trigger's label while chosen, so a screen reader and the font fallback treat each name as its own language (WCAG 3.1.2). On the desktop listbox, `title` joins `aria-label` / `aria-labelledby` / `aria-describedby` on the VISIBLE trigger button — it used to ride the native spread onto the `sr-only` `<select>`, where a tooltip has no pointer that can reach it and a fallback name sits on an `aria-hidden` element. `name`, `required` and the forwarded ref stay on the native control, which is what it is for. A `Checkbox` or `Radio` with both a `label` and a `description` is NAMED by the label and DESCRIBED by the description (`aria-labelledby` / `aria-describedby`); the whole row is still its `<label>`, so its words still tick it. `FormField` points its one child at the hint or error on screen with `aria-describedby`, and marks it `aria-invalid` while `error` is set. A child that already has its own `aria-describedby` (a react-hook-form binding often does) keeps it: the ids are merged, the child's own first, and each id is said once — a repeated id is a message a screen reader reads twice. |
1008
1083
  | `buttonClasses`, `iconButtonClasses` | `Button`'s and `IconButton`'s look as a class string, for something that must navigate and so cannot be a `<button>`: `<Link href="/organisations/new" className={buttonClasses({ variant: 'secondary' })}>Add a business</Link>`. Same options and defaults as the component (`variant`, `size`, `block`, `className`), and the components render through these functions, so the two cannot drift. What the string cannot give an `<a>`: a disabled state (`disabled:` never matches an anchor — drop the `href`, add `role="link"` and `aria-disabled="true"`, or render a disabled `Button`), and the cursor (an `<a href>` gets the browser's pointer, a `<button>` the host's preflight; set it once for both). Keep its `inline-flex` — ask for `block` rather than passing a display utility. An icon-only link still needs the `aria-label` `IconButton` would have required. Client references, like every kit function: call them in a `'use client'` file. |
1009
1084
  | `labelClasses` | `Label`'s look as a class string, for a `<label>` a host lays out itself — beside a check box, in a row with its control, `sr-only`: `<label htmlFor="x" className={labelClasses({ inline: true, className: 'flex gap-2' })}>`. `inline: true` drops `Label`'s block layout (`mb-1 block`) and keeps its size, weight and ink; without it the string is exactly what `Label` renders. A client reference: call it in a `'use client'` file. |
1010
1085
  | `Segmented` | A row of mutually exclusive options, all visible: a button group, or with `name` a real radio group that submits. Named by `label`, or by an element already on the page (`aria-labelledby`, a list row's own heading) and described by another (`aria-describedby`); an option's `title` says on hover what choosing it means. |
@@ -1012,12 +1087,12 @@ All exports are named — `import { Modal, ... } from 'efficient-shell'`.
1012
1087
  | `RadioGroup` | One choice from a short visible list, with the group chrome each panel used to rebuild: a `role="radiogroup"` named by its own label (`aria-labelledby`, because `<label for>` cannot name a div), the `hint`/`error`/`required` wiring borrowed from `FormField`, and `orientation` for a row that wraps rather than compresses. The options are real native radios sharing one `name`, which is what gives the group its arrow keys, its disabled-skipping and its single tab stop — all from the user agent, none of it hand-rolled. `value: null` is "nothing chosen yet", not a silent first option. |
1013
1088
  | `Snippet` | A value the user copies rather than reads — an endpoint, a tenant id, a shell command. Monospace, truncated with the whole value on its `title` (`multiline` wraps instead), and a copy button that flips to a check for two seconds and says "Copied" through an `aria-live` region. `value` is what reaches the clipboard and `children` is what is shown, so a masked key displays masked and copies whole. `symbol` prints a leading glyph that is never copied and has **no default** — a URL has no `$` in front of it. A refused clipboard toasts rather than looking like it worked. Not a read-only `Input`: a value that can only be copied is not a control and should not take a tab stop or invite an edit. `id` and `aria-describedby` land on the COPY BUTTON — the only focusable thing in the row, and a labelable one — so a wrapping `FormField` really does label it and its hint is really announced; with `hideCopyButton` they fall back to the box, because an id that resolves to nothing is not wiring. |
1014
1089
  | `Kbd` | The badge the shortcut constants (`CMD_ENTER`, `ALT_SHIFT_E`, …) are shown in — a real `<kbd>`, two rungs, `className` layered on so a portal can tint it. It renders the string it is handed; which symbol a platform uses is what the constants already decide. |
1015
- | `FormErrorSummary` | The error list at the top of a failed form (WCAG 3.3.1, the GOV.UK pattern): takes focus when errors appear, each message is a link that focuses the offending control by its `FormField` id. Renders nothing while `errors` is empty. |
1090
+ | `FormErrorSummary` | The error list at the top of a failed form (WCAG 3.3.1, the GOV.UK pattern): takes focus when errors appear, each message is a link that focuses the offending control by its `FormField` id. `fieldId` is optional: an error with no control behind it — a server's `_general` / `non_field_errors`, or a field the form does not draw — is listed as plain text, not a link that goes nowhere. Renders nothing while `errors` is empty. |
1016
1091
  | `TagInput` | Multi-value field — chosen values as removable chips, typing filters the option list in the same dropdown SearchableSelect uses. `allowFreeText` admits unlisted entries; Backspace in the empty input removes the last chip. The value array stays duplicate-free by construction. |
1017
- | `DatePicker`, `TimePicker`, `DateTimePicker`, `DateRangePicker` | Date/time fields, all careful to speak LOCAL dates and wall-clock times (never `toISOString`). `TimePicker` and `DateTimePicker` wrap the platform's own inputs in the kit's field styling; `TimePicker` hands back an `HH:MM` string — a time of day names no calendar day, so it never invents a Date. **`DatePicker`** opens the kit's `Calendar` in a popover (`native` swaps in the platform's date input). Its value posts from a hidden input that takes `name` and `form`; `required` adds "Required" to the trigger's description (`datePicker.required`; a button may not carry `aria-required`) and drops Clear from the footer (`clearable` overrides either way); `invalid` paints the field, and the reader hears the error message the host links through `aria-describedby` (`FormField` does) — the trigger carries no `aria-invalid`, which a button may not either; Today is offered only when `min`/`max` allow today; `aria-*`, `data-*`, `title`, `tabIndex`, `autoFocus`, `onFocus` and `onBlur` reach the trigger, while what only a text box can use (`readOnly`, `pattern`, `autoComplete`…) applies with `native` alone. Named by a `<label htmlFor={id}>` or `aria-labelledby`, the trigger is described by the date spelled out. A browser never validates a hidden input, so `required` cannot block a submit in the popover. **`DateRangePicker`** is a from/to calendar with presets in one popover: side by side on a desktop; where that does not fit the room the box allows (a phone, a narrow shell window — measured, not a media query), the panel takes the full width between the margins and stacks, the presets a wrapping row of chips above the calendar and From/To one line each. `fullWidth` fills a filter-grid cell instead of shrink-wrapping its label. `id` (on the trigger button, for `<label htmlFor>`), `aria-labelledby` and `aria-label` name the trigger, and the chosen range is then its description ("Period, button, 1 August 2026 to 3 October 2026"). `name={{ from, to }}` posts the applied range as two hidden `YYYY-MM-DD` inputs (`''` when unset), so a GET form or a server action reads it as it read two date inputs. `required` adds "Required" to the trigger's description (`dateRange.required`) and, unless `clearable` is given, removes Clear. `min` / `max` (`YYYY-MM-DD`) bound it as they bound `DatePicker`: days outside are disabled, a preset whose range reaches outside is not offered (none at all on a range that starts today — time off, a booking — and then no Custom either), and with nothing chosen the panel opens on the first month that has a day to pick. Both pickers take `weekStartsOn` (0 Sunday, 1 Monday); without it the week starts where the provider's `locale` says. |
1092
+ | `DatePicker`, `TimePicker`, `DateTimePicker`, `DateRangePicker` | Date/time fields, all careful to speak LOCAL dates and wall-clock times (never `toISOString`). `TimePicker` and `DateTimePicker` wrap the platform's own inputs in the kit's field styling; `TimePicker` hands back an `HH:MM` string — a time of day names no calendar day, so it never invents a Date. **`DatePicker`** opens the kit's `Calendar` in a popover (`native` swaps in the platform's date input). Its value posts from a hidden input that takes `name` and `form`; `required` adds "Required" to the trigger's description (`datePicker.required`; a button may not carry `aria-required`) and drops Clear from the footer (`clearable` overrides either way); `invalid` paints the field, and the reader hears the error message the host links through `aria-describedby` (`FormField` does) — the trigger carries no `aria-invalid`, which a button may not either; Today is offered only when `min`/`max` allow today; `aria-*`, `data-*`, `title`, `tabIndex`, `autoFocus`, `onFocus` and `onBlur` reach the trigger, while what only a text box can use (`readOnly`, `pattern`, `autoComplete`…) applies with `native` alone. Named by a `<label htmlFor={id}>` or `aria-labelledby`, the trigger is described by the date spelled out. A browser never validates a hidden input, so `required` cannot block a submit in the popover. **`DateRangePicker`** is a from/to calendar with presets in one popover: side by side on a desktop; where that does not fit the room the box allows (a phone, a narrow shell window — measured, not a media query), the panel takes the full width between the margins and stacks, the presets a wrapping row of chips above the calendar and From/To one line each. `fullWidth` fills a filter-grid cell instead of shrink-wrapping its label. `id` (on the trigger button, for `<label htmlFor>`), `aria-labelledby` and `aria-label` name the trigger, and the chosen range is then its description ("Period, button, 1 August 2026 to 3 October 2026"). `name={{ from, to }}` posts the applied range as two hidden `YYYY-MM-DD` inputs (`''` when unset), so a GET form or a server action reads it as it read two date inputs. `required` adds "Required" to the trigger's description (`dateRange.required`) and, unless `clearable` is given, removes Clear. `min` / `max` (`YYYY-MM-DD`) bound it as they bound `DatePicker`: days outside are disabled, a preset whose range reaches outside is not offered (none at all on a range that starts today — time off, a booking — and then no Custom either), and with nothing chosen the panel opens on the first month that has a day to pick. `disabled` makes the whole control inert: a disabled trigger with the kit's disabled look, no panel, no Clear ×, and the hidden inputs disabled so the range is not posted (an open panel closes). `allowOpenEnd` lets Apply commit ONE end on its own — "from 1 August", "up to 30 June" — and turns the panel's From and To boxes into buttons that choose which end the next day sets (the start first, as before); the open end comes back as `''`. Without it Apply needs both ends, as it always has. Both pickers take `weekStartsOn` (0 Sunday, 1 Monday); without it the week starts where the provider's `locale` says. |
1018
1093
  | `Card`, `StatCard` | Surface panel (optional header/footer; `description` puts one sentence under the title, outside the heading) + dashboard metric tile. |
1019
1094
  | `Avatar`, `AvatarGroup` | User avatar with initials fallback + status dot; overlapping stack with +N overflow. |
1020
- | `Banner` | Static in-flow alert (`tone`: info / success / warning / danger). `action` renders a control at the right edge — after the text, so a screen reader reaches the problem before the remedy, and before the dismiss ×, so "Update card" is never past "close this". Vertically centred against the words and `shrink-0`, so a message that wraps does not drag the button down or squeeze it — and on a narrow banner (a phone, a narrow column: the words keep at least 12rem) it drops UNDER the words rather than leave them a column one word wide. A banner that names a condition and leaves the fix three menus away is the usual shape of a notice nobody acts on. |
1095
+ | `Banner` | Static in-flow alert (`tone`: info / success / warning / danger / neutral). `neutral` is a note that is none of the other four ("Prices exclude GST"): grey in the kit's neutral, the same wash and ink as a neutral `StatusBadge` (`--status-neutral-soft` / `--status-neutral-soft-ink`, `--status-neutral-solid` when `emphasis="solid"`), with the info glyph. `action` renders a control at the right edge — after the text, so a screen reader reaches the problem before the remedy, and before the dismiss ×, so "Update card" is never past "close this". Vertically centred against the words and `shrink-0`, so a message that wraps does not drag the button down or squeeze it — and on a narrow banner (a phone, a narrow column: the words keep at least 12rem) it drops UNDER the words rather than leave them a column one word wide. A banner that names a condition and leaves the fix three menus away is the usual shape of a notice nobody acts on. |
1021
1096
  | `Tabs`, `Accordion`, `Tooltip` | Controlled tab strip, collapsible sections, frosted hover/focus tooltip. A tooltip's `side` is a preference: near an edge the bubble slides along that side to stay 8 px clear of it, and flips to the opposite side when its own has no room, keeping inside the screen and inside the shell window that owns its trigger (UI-11). It never leaves that box: a bubble wider than the room wraps, and one taller than the room is cut off at it. The bubble is drawn on `<body>`, in a `fixed` layer laid over its trigger, so a scrolling pane or an `overflow-hidden` box around the trigger cannot cut it off; it follows the trigger when that moves and closes once the trigger has scrolled out of sight. A small arrow on the edge facing the trigger points at the trigger's centre wherever the bubble ended up, kept clear of the bubble's corners; it is the bubble's own surface (`.rosh-tooltip-arrow` in `ui.css`), solid and seamless under `rosh-reduce-transparency`, and hidden from assistive technology. |
1022
1097
  | `Stepper` | A linear wizard's progress strip — numbered circles, connectors, `aria-current="step"`. Controlled like Tabs; completed steps are clickable to go back (when `onChange` is wired), upcoming steps never are — moving forward belongs to the wizard's own Continue button, behind its validation. |
1023
1098
  | `DataTable` | Read-only table for server-driven lists: it renders and reports, and never sorts or pages its own `data`. Controlled sort and pager, row selection, column groups, pinned columns, windowed rendering. `onRow` gives a row `onClick`, a `className`, any `data-*` attributes for its `<tr>` — a test's or a stylesheet's hook (`data-testid`, `data-state`) — the pointer and the keyboard coming in and out (`onMouseEnter` … `onBlur`) and `current`. `rowHref` makes each row one real link (`onRowPress` a button) with its controls still pressable — see [DataTable rows that are links](#datatable-rows-that-are-links). `stackOnNarrow` turns each row into a labelled card on a narrow container — see [DataTable on a narrow screen](#datatable-on-a-narrow-screen). |
@@ -1549,14 +1624,28 @@ classes in `ui.css` (the timeline card's) carry pixel radii of their own.
1549
1624
  }
1550
1625
  ```
1551
1626
 
1627
+ **Type scale, weight, motion, border width.** Font sizes are 9, 11, 12, 14,
1628
+ 16, 18, 20, 24 and 30 px, in portals and editor panels alike. 12 and up are
1629
+ Tailwind's own steps (`xs` … `3xl`); `ui.css` adds `--text-3xs` (9px) and
1630
+ `--text-2xs` (11px) as theme keys, so the matching size utilities exist. They
1631
+ carry no line height of their own, like a literal size, so moving a literal
1632
+ onto a step changes nothing on screen. Do not write a literal size; 10px and
1633
+ 13px are not on the scale. Weight goes by role: `--weight-body` 400,
1634
+ `--weight-control` 500, `--weight-heading` 600, `--weight-figure` 700. Motion:
1635
+ `--motion-fast` 120ms (hover, press) and `--motion-base` 160ms (open, close).
1636
+ Border width: `--border-hairline` 1px, `--border-strong` 2px, `--border-accent`
1637
+ 4px. Code takes the same numbers from `efficient-shell/ui`: `TYPE_SCALE`,
1638
+ `FONT_WEIGHTS`, `MOTION_DURATIONS`, `BORDER_WIDTHS`.
1639
+
1552
1640
  **Corners.** A component does not pick a step of the scale for itself; it asks
1553
- for the radius of what it is. Four role tokens, each defaulting to the step that
1641
+ for the radius of what it is. Five role tokens, each defaulting to the step that
1554
1642
  role has always drawn, so they change nothing until you set one:
1555
1643
 
1556
1644
  | Token | Default | Drawn by |
1557
1645
  |---|---|---|
1558
1646
  | `--radius-control` | `--radius-md` (6px) | buttons, fields, a segmented control's options |
1559
1647
  | `--radius-card` | `--radius-lg` (8px) | `Card`, `Dialog`, a segmented control's track |
1648
+ | `--radius-panel` | `--radius-xl` (12px) | an editor's side panel |
1560
1649
  | `--radius-popup` | `--radius-2xl` (16px) | menus, select lists, the start menu, the search palette |
1561
1650
  | `--radius-window` | `--radius-2xl` (16px) | windows and their title bars |
1562
1651
 
@@ -1618,6 +1707,16 @@ through. `rosh-reduce-transparency` on `<html>` makes every popup one solid
1618
1707
  :root { --rosh-glass-surface: var(--popover); --rosh-glass-solid-shadow: 0 4px 12px rgb(0 0 0 / 0.08); }
1619
1708
  ```
1620
1709
 
1710
+ **Elevation.** Five levels, one job each: `--elevation-none` (a plain card,
1711
+ border only), `--elevation-raised` (a card or field that sits a little up),
1712
+ `--elevation-panel` (a floating editor panel, a sticky bar), `--elevation-popup`
1713
+ (menus, lists, toasts, tooltips) and `--elevation-dialog` (dialogs, drawers).
1714
+ raised, popup and dialog equal Tailwind's sm, lg and xl, so markup keeps those
1715
+ utilities; md and 2xl are not levels. A component class or an inline style reads
1716
+ the property: `box-shadow: var(--elevation-panel)`. Code takes the names from
1717
+ `efficient-shell/ui` (`ELEVATION_LEVELS`, `RADIUS_ROLES`). In dark, the chart
1718
+ grid (`--viz-grid`) is `--line-strong`, the same as the axis.
1719
+
1621
1720
  **Everything else** a kit component paints, each defaulting to the literal it
1622
1721
  replaced: `--rosh-media-scrim` and `--rosh-media-scrim-button` (the dark wash
1623
1722
  under text and buttons on an upload thumbnail), `--rosh-drop-ring` (the halo
@@ -3420,6 +3519,10 @@ copied from the account with `syncTimezoneFromProfile`, or the browser's own
3420
3519
  when unset). Every formatter returns `EMPTY_VALUE` (`—`) for a value that is
3421
3520
  missing or cannot be read. All of them are also exported from
3422
3521
  `efficient-shell/format`, an entry with no React and no peers, for server code.
3522
+ The image focus maths (`FOCAL_DEFAULT`, `parseFocal`, `formatFocal`,
3523
+ `nudgeFocal`, `focalFromRect`, the `FocalPoint` type; see `ImageFocusPoint`) is
3524
+ exported there too, so a block's defaults and a published page's render can
3525
+ read a stored focus on a server.
3423
3526
 
3424
3527
  | Export | Notes |
3425
3528
  |---|---|
@@ -3463,32 +3566,52 @@ icon by what it **means**, and the set the theme chose draws it:
3463
3566
 
3464
3567
  ```tsx
3465
3568
  import { Icon, registerIconSet } from 'efficient-shell/icons';
3466
- import { heroicons } from 'efficient-shell/icons/heroicons';
3569
+ import { efficient } from 'efficient-shell/icons/efficient';
3570
+ import { efficientLooks } from 'efficient-shell/icons/efficient-looks';
3467
3571
 
3468
- registerIconSet(heroicons); // once, at startup, in client code
3572
+ registerIconSet(efficient); // once, at startup, in client code
3573
+ registerIconSet(efficientLooks); // only if the host draws other looks
3469
3574
 
3470
3575
  <Icon name="trash" /> // 1em square, the text's colour
3471
3576
  <Icon name="search" size={20} tone="muted" />
3472
3577
  <Icon name="warning" title="Overdue" /> // named: an image, not decoration
3578
+ <Icon name="trash" look="solid" /> // another look of the same meaning
3473
3579
  ```
3474
3580
 
3475
3581
  `Icon` and the registry are also exported from `efficient-shell/ui` and the
3476
3582
  root entry. They import React and nothing else.
3477
3583
 
3478
- **The catalogue.** `IconName` is 197 meanings (`add`, `close`, `edit`, `trash`,
3479
- `search`, `download`, `chevron-down`, `warning`, …), built from the icons the
3480
- EFFICIENT frontends import today. `<Icon name>` is typed to it, so a misspelt
3481
- meaning does not compile. To add one, add it to `src/icons/catalogue.json` with
3482
- its icon in every package and run `npm run icons`.
3483
-
3484
- **The sets.** Each is its own subpath, so a host bundles only what it
3485
- registers, and the icon package behind it is an optional peer:
3584
+ **The catalogue.** `IconName` is every meaning the EFFICIENT frontends draw
3585
+ (`add`, `close`, `edit`, `trash`, `search`, `download`, `chevron-down`,
3586
+ `warning`, an editor's `block-heading`, …): 363 names, from the census of every
3587
+ icon in every repository (2026-10-10). `<Icon name>` is typed to it, so a
3588
+ misspelt meaning does not compile.
3589
+
3590
+ **One set.** `efficient` is the company's one icon set (Lewis 2026-10-12). Lewis
3591
+ picked the drawing of every meaning himself: the heroicons outline drawing where
3592
+ heroicons has the meaning, and our own drawing in the heroicons style (24 grid,
3593
+ 1.5 line, round ends) where it has not. One meaning, one drawing: a name that
3594
+ means the same as another (`rename` and `edit`, `move-up` and `arrow-up`) is
3595
+ that other's drawing. No two meanings look alike: `tests/efficientSet.test.tsx`
3596
+ measures every pair (Hausdorff distance on the 24 grid) and fails a pair closer
3597
+ than 0.9.
3598
+
3599
+ **Looks.** Each meaning has its default drawing and other looks of it: `solid`
3600
+ (filled, 24 grid) and `mini` (filled, drawn for 20 px) for all of them, and a
3601
+ few more where Lewis kept a second drawing (`<Icon name="edit"
3602
+ look="pencil-square" />`). The looks are a subpath of their own, so a host that
3603
+ draws only the default does not bundle them. A meaning without the look asked
3604
+ for draws its default.
3486
3605
 
3487
3606
  | Import | Set | Draws | Needs |
3488
3607
  |---|---|---|---|
3489
- | `efficient-shell/icons/heroicons` | `heroicons` | the whole catalogue | `@heroicons/react` |
3490
- | `efficient-shell/icons/lucide` | `lucide` | the whole catalogue | `lucide-react` (0.577 or later) |
3491
- | `efficient-shell/icons/efficient` | `efficient` | the 165 house glyphs, which draw 58 of the catalogue meanings | nothing |
3608
+ | `efficient-shell/icons/efficient` | `efficient` | every meaning, default look | nothing |
3609
+ | `efficient-shell/icons/efficient-looks` | `efficient` (its other looks) | `solid`, `mini` and the kept looks | nothing |
3610
+ | `efficient-shell/icons/heroicons` | `heroicons` | **deprecated**: the 197 meanings it drew | `@heroicons/react` |
3611
+ | `efficient-shell/icons/lucide` | `lucide` | **deprecated**: the 197 meanings it drew | `lucide-react` (0.577 or later) |
3612
+
3613
+ The heroicons and lucide sets go in the next major release; register `efficient`.
3614
+ Heroicons drawings are copied under the MIT licence: see `THIRD_PARTY_NOTICES.md`.
3492
3615
 
3493
3616
  The first set registered is the default (`setDefaultIconSet(name)` changes
3494
3617
  that). A meaning the chosen set does not draw falls back to the default set, so
@@ -3528,17 +3651,18 @@ registerIcons('editor', { 'block-product': BlockProductGlyph });
3528
3651
  ```
3529
3652
 
3530
3653
  They are found under any theme. `efficient-shell/icons/efficient` also exports
3531
- each house glyph as a component (`TrashIcon`, `StylesIcon`, `PaddingTopIcon`,
3532
- …, `size` 18 by default) for code that wants one drawing whatever the theme.
3533
-
3534
- **Adding or changing a house glyph.** `src/icons/efficient.json` is the source
3535
- (one meaning, one SVG: a 24 by 24 viewBox, stroke 1.7 on `currentColor`,
3536
- transparent background). Edit it, run `npm run icons`, and commit both files.
3537
- Look before you keep a drawing: `node scripts/icons/glyph-sheet.mjs <new>
3538
- <neighbours…>` writes a sheet that draws it at 16px, the size a settings row
3539
- wears it, beside the glyphs it could be mistaken for.
3540
- `tests/efficientGlyphs.test.tsx` holds the standard and every decision made
3541
- about the set so far.
3654
+ each meaning's default drawing as a component (`TrashIcon`, `StylesIcon`,
3655
+ `PaddingTopIcon`, …, `size` 18 by default) for code that wants one drawing
3656
+ whatever the theme.
3657
+
3658
+ **Adding or changing a drawing.** `src/icons/efficient.json` is the source: per
3659
+ name its meaning, where the drawing came from, the default SVG (24 by 24
3660
+ viewBox, 1.5 stroke on `currentColor`, round ends, transparent background) and
3661
+ its looks, or the name it is an alias of. Add the meaning to
3662
+ `src/icons/catalogue.json` too, run `npm run icons`, and commit the lot. Look
3663
+ before you keep a drawing: `node scripts/icons/glyph-sheet.mjs <new>
3664
+ <neighbours…>` draws it at 16px beside the glyphs it could be mistaken for, and
3665
+ `tests/efficientSet.test.tsx` fails it if it looks like another meaning.
3542
3666
 
3543
3667
  With no set registered, `<Icon>` draws nothing and warns once per meaning. The
3544
3668
  kit's own components (a window's close button, a filter chip) keep drawing
@@ -3580,8 +3704,13 @@ itself where the kit has an answer:
3580
3704
  | `dark` | a `dark:` variant in a class string | the light class alone; the kit remaps it |
3581
3705
  | `hex` | a hex colour in a `.tsx` string (not in `href`, `to`, `id`, `key`, `htmlFor`) | a token or a palette class |
3582
3706
  | `radius` | a literal corner radius: an arbitrary-value radius class (`rounded-[4px]`) or an inline `borderRadius` number or length (not `inherit`, `var(…)`, `50%` or `9999`) | a scale utility or a role token; the theme's Corners setting shapes them |
3707
+ | `spacing` | a padding, margin or gap step off the scale 2/4/8/12/16/20/24/32/40/48 (Tailwind steps 1.5, 2.5, 3.5, 7 …), an arbitrary spacing value that is a length, or an inline `padding*` / `margin*` / `gap` off the scale (owner ruling 2026-10-12) | a step on the scale, or a role token such as `var(--space-control-x)` |
3708
+ | `font-size` | a literal font size: an arbitrary-value text size, or an inline `fontSize` (not `var(…)` or `inherit`) | a type step 9/11/12/14/16/18/20/24/30 |
3709
+ | `z-index` | a stack order above 30 written as a number: a z step, an arbitrary value or an inline `zIndex` | a named layer, `Z_LAYERS` or its `--z-*` property |
3710
+ | `shadow` | a literal shadow (an arbitrary value, an inline `boxShadow`) or a size that is not an elevation level (xs, 2xs, md, 2xl) | an elevation: the sm, lg and xl sizes, or `var(--elevation-panel)` and its siblings |
3583
3711
  | `kit` | an import of a second UI kit (`@headlessui`, `@radix-ui`, `@mui`, `antd`, `sonner`, …) | the kit's component |
3584
3712
  | `icon` | an import of an icon package (`@heroicons`, `lucide-react`, `@tabler/icons`, …) | `<Icon name>` from `efficient-shell/icons` |
3713
+ | `svg` | an `<svg>` element written in JSX: an icon drawn in place (one per element, however many shapes it holds; a component named `Svg` and the word in a string are not counted) | `<Icon name>` from `efficient-shell/icons`. A chart, an illustration or a mark a host supplies stays on the baseline |
3585
3714
  | `kit-dep` | one of those packages declared in `package.json` (not `devDependencies`, and not one the shell needs from an entry the repository imports: `@headlessui/react` and `@heroicons/react` for the root entry, an icon set's own package) | remove it |
3586
3715
 
3587
3716
  Known gaps: `rgb()` / `hsl()` colours, colours in `.ts` maps, and
@@ -0,0 +1,32 @@
1
+ # Third-party notices
2
+
3
+ efficient-shell is Copyright (c) Efficient Pty Ltd (see `LICENSE`). It includes
4
+ the third-party material below, under that material's own licence.
5
+
6
+ ## Heroicons
7
+
8
+ Many drawings in the `efficient` icon set (`efficient-shell/icons/efficient` and
9
+ `efficient-shell/icons/efficient-looks`) are copied from Heroicons 2.2.0
10
+ (https://github.com/tailwindlabs/heroicons).
11
+
12
+ MIT License
13
+
14
+ Copyright (c) Tailwind Labs, Inc.
15
+
16
+ Permission is hereby granted, free of charge, to any person obtaining a copy
17
+ of this software and associated documentation files (the "Software"), to deal
18
+ in the Software without restriction, including without limitation the rights
19
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
20
+ copies of the Software, and to permit persons to whom the Software is
21
+ furnished to do so, subject to the following conditions:
22
+
23
+ The above copyright notice and this permission notice shall be included in all
24
+ copies or substantial portions of the Software.
25
+
26
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
29
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
30
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
31
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
32
+ SOFTWARE.
@@ -1,31 +1,31 @@
1
- import{peekBrowserStartUrl as e,claimBrowserStartUrl as t}from"./chunk-LWLCNQY7.js";import{AboutApp as o}from"./chunk-6GV4467L.js";import{WindowTitle as r}from"./chunk-TLTFS2D6.js";import"./chunk-DSSLOQDW.js";import"./chunk-UAJQB45Y.js";import"./chunk-4QBLTEFU.js";import"./chunk-SSBOC52S.js";import"./chunk-5A7MWTXX.js";import"./chunk-OGXU7QFW.js";import{confirm as a}from"./chunk-MTH7EAH3.js";import"./chunk-HIAHERCD.js";import"./chunk-AFYQTVZO.js";import"./chunk-HZZNJHRH.js";import"./chunk-X4FJ54PP.js";import"./chunk-D65T4H6J.js";import{ChevronLeftFallback as n,ChevronRightFallback as l,RefreshFallback as s,HomeFallback as i,StarFallback as c,ExternalFallback as m,HelpFallback as d,NoSymbolFallback as u}from"./chunk-JJIPGAXI.js";import"./chunk-AJB5G52B.js";import"./chunk-C3QOHMR6.js";import{Icon as h}from"./chunk-YKZHOXI4.js";import{useRef as p,useEffect as b,useState as g}from"react";import{jsxs as f,jsx as k,Fragment as x}from"react/jsx-runtime";var w="react-os-shell:browser-bookmarks",y="react-os-shell:browser-homepage",v="https://en.wikipedia.org/wiki/Main_Page",N=[{label:"Wikipedia",url:"https://en.wikipedia.org/wiki/Main_Page"},{label:"DevDocs",url:"https://devdocs.io"},{label:"Example",url:"https://example.com"}],j=new Set(["ac ad ae af ag ai al am ao aq ar as at au aw ax az ba bb bd be bf bg bh bi","bj bm bn bo br bs bt bw by bz ca cc cd cf cg ch ci ck cl cm cn co cr cu cv","cw cx cy cz de dj dk dm do dz ec ee eg er es et eu fi fj fk fm fo fr ga gd","ge gf gg gh gi gl gm gn gp gq gr gs gt gu gw gy hk hm hn hr ht hu id ie il","im in io iq ir is it je jm jo jp ke kg kh ki km kn kp kr kw ky kz la lb lc","li lk lr ls lt lu lv ly ma mc md me mg mh mk ml mm mn mo mp mq mr ms mt mu","mv mw mx my mz na nc ne nf ng ni nl no np nr nu nz om pa pe pf pg ph pk pl","pm pn pr ps pt pw py qa re ro rs ru rw sa sb sc sd se sg sh si sk sl sm sn","so sr ss st su sv sx sy sz tc td tf tg th tj tk tl tm tn to tr tt tv tw tz","ua ug uk us uy uz va vc ve vg vi vn vu wf ws ye yt za zm zw","com net org edu gov mil int info biz name pro app dev xyz online site tech","store shop cloud design digital agency studio media news blog live life","world today email systems solutions services software page link space host","press wiki team group works careers finance capital energy academy"].join(" ").split(" "));function C(e){let t=e.trim();return t?/^[a-z][a-z0-9+.-]*:\/\//i.test(t)?t:function(e){const t=/^([\w-]+\.)+([a-z]{2,})(\/.*)?$/i.exec(e);return null!==t&&j.has(t[2].toLowerCase())}(t)?"https://"+t:"https://duckduckgo.com/?q="+encodeURIComponent(t):""}var z=["duckduckgo.com","developer.mozilla.org","google.com","gmail.com","youtube.com","facebook.com","instagram.com","whatsapp.com","twitter.com","x.com","github.com","gitlab.com","linkedin.com","reddit.com","pinterest.com","amazon.com","amazon.ca","amazon.co.uk","apple.com","icloud.com","microsoft.com","outlook.com","live.com","office.com","netflix.com","spotify.com","paypal.com","stripe.com","chat.openai.com","chatgpt.com","claude.ai"];function S(){if("undefined"==typeof window)return N;try{const e=localStorage.getItem(w);if(!e)return N;const t=JSON.parse(e);if(Array.isArray(t)&&t.every(e=>"string"==typeof e?.url))return t}catch{}return N}function W(){return"undefined"==typeof window?v:localStorage.getItem(y)||v}function H(){const u=p(void 0);void 0===u.current&&(u.current=e()),b(()=>{null!=u.current&&t(u.current)},[]);const x=u.current?C(u.current.url):"",[v,N]=g(W),[j,z]=g(x||v),[H,E]=g(j),[T,D]=g([j]),[O,A]=g(0),[I,F]=g(0),[J,U]=g(S),[B,P]=g(!1),[X,G]=g(null),M=p(null),Q=p(null);b(()=>{try{localStorage.setItem(w,JSON.stringify(J))}catch{}},[J]);const q=e=>{const t=C(e);t&&(z(t),E(t),D(e=>{const o=e.slice(0,O+1);return o.push(t),o}),A(e=>e+1),P(!1))},$=()=>{"undefined"!=typeof window&&window.open(j,"_blank","noopener,noreferrer")},K=()=>{const e=X?.trim();e?(U(t=>[...t,{label:e,url:j}]),G(null)):G(null)};b(()=>{if(null===X)return;M.current?.focus(),M.current?.select();const e=e=>{const t=e.target;t?.closest("[data-bookmark-popover]")||G(null)},t=setTimeout(()=>document.addEventListener("mousedown",e),0);return()=>{clearTimeout(t),document.removeEventListener("mousedown",e)}},[null!==X]);const Y=e=>{U(t=>t.filter((t,o)=>o!==e))},Z=O>0,_=O<T.length-1,V=J.some(e=>e.url===j),ee="p-1.5 rounded hover:bg-gray-200 transition-colors text-gray-600 disabled:opacity-30";/* @__PURE__ */
1
+ import{peekBrowserStartUrl as e,claimBrowserStartUrl as t}from"./chunk-LWLCNQY7.js";import{AboutApp as o}from"./chunk-Z2353HNE.js";import"./chunk-WRBXEZFN.js";import{WindowTitle as r}from"./chunk-L5XA5MLK.js";import"./chunk-FROOMUJ7.js";import"./chunk-DSSLOQDW.js";import"./chunk-5A7MWTXX.js";import"./chunk-OGXU7QFW.js";import"./chunk-HRLPFUPB.js";import{confirm as a}from"./chunk-ZUCYJAQH.js";import"./chunk-RMJZOKHY.js";import"./chunk-AFYQTVZO.js";import"./chunk-HZZNJHRH.js";import"./chunk-X4FJ54PP.js";import"./chunk-D65T4H6J.js";import{ChevronLeftFallback as n,ChevronRightFallback as l,RefreshFallback as s,HomeFallback as i,StarFallback as c,ExternalFallback as m,HelpFallback as d,NoSymbolFallback as u}from"./chunk-6LTDJKIL.js";import"./chunk-TNBCUZVE.js";import"./chunk-C3QOHMR6.js";import{Icon as h}from"./chunk-XWSWYMGM.js";import{useRef as p,useEffect as b,useState as g}from"react";import{jsxs as f,jsx as k,Fragment as x}from"react/jsx-runtime";var w="react-os-shell:browser-bookmarks",y="react-os-shell:browser-homepage",v="https://en.wikipedia.org/wiki/Main_Page",N=[{label:"Wikipedia",url:"https://en.wikipedia.org/wiki/Main_Page"},{label:"DevDocs",url:"https://devdocs.io"},{label:"Example",url:"https://example.com"}],j=new Set(["ac ad ae af ag ai al am ao aq ar as at au aw ax az ba bb bd be bf bg bh bi","bj bm bn bo br bs bt bw by bz ca cc cd cf cg ch ci ck cl cm cn co cr cu cv","cw cx cy cz de dj dk dm do dz ec ee eg er es et eu fi fj fk fm fo fr ga gd","ge gf gg gh gi gl gm gn gp gq gr gs gt gu gw gy hk hm hn hr ht hu id ie il","im in io iq ir is it je jm jo jp ke kg kh ki km kn kp kr kw ky kz la lb lc","li lk lr ls lt lu lv ly ma mc md me mg mh mk ml mm mn mo mp mq mr ms mt mu","mv mw mx my mz na nc ne nf ng ni nl no np nr nu nz om pa pe pf pg ph pk pl","pm pn pr ps pt pw py qa re ro rs ru rw sa sb sc sd se sg sh si sk sl sm sn","so sr ss st su sv sx sy sz tc td tf tg th tj tk tl tm tn to tr tt tv tw tz","ua ug uk us uy uz va vc ve vg vi vn vu wf ws ye yt za zm zw","com net org edu gov mil int info biz name pro app dev xyz online site tech","store shop cloud design digital agency studio media news blog live life","world today email systems solutions services software page link space host","press wiki team group works careers finance capital energy academy"].join(" ").split(" "));function C(e){let t=e.trim();return t?/^[a-z][a-z0-9+.-]*:\/\//i.test(t)?t:function(e){const t=/^([\w-]+\.)+([a-z]{2,})(\/.*)?$/i.exec(e);return null!==t&&j.has(t[2].toLowerCase())}(t)?"https://"+t:"https://duckduckgo.com/?q="+encodeURIComponent(t):""}var z=["duckduckgo.com","developer.mozilla.org","google.com","gmail.com","youtube.com","facebook.com","instagram.com","whatsapp.com","twitter.com","x.com","github.com","gitlab.com","linkedin.com","reddit.com","pinterest.com","amazon.com","amazon.ca","amazon.co.uk","apple.com","icloud.com","microsoft.com","outlook.com","live.com","office.com","netflix.com","spotify.com","paypal.com","stripe.com","chat.openai.com","chatgpt.com","claude.ai"];function W(){if("undefined"==typeof window)return N;try{const e=localStorage.getItem(w);if(!e)return N;const t=JSON.parse(e);if(Array.isArray(t)&&t.every(e=>"string"==typeof e?.url))return t}catch{}return N}function R(){return"undefined"==typeof window?v:localStorage.getItem(y)||v}function L(){const u=p(void 0);void 0===u.current&&(u.current=e()),b(()=>{null!=u.current&&t(u.current)},[]);const x=u.current?C(u.current.url):"",[v,N]=g(R),[j,z]=g(x||v),[L,E]=g(j),[O,D]=g([j]),[M,T]=g(0),[U,F]=g(0),[J,P]=g(W),[X,Z]=g(!1),[A,I]=g(null),q=p(null),B=p(null);b(()=>{try{localStorage.setItem(w,JSON.stringify(J))}catch{}},[J]);const K=e=>{const t=C(e);t&&(z(t),E(t),D(e=>{const o=e.slice(0,M+1);return o.push(t),o}),T(e=>e+1),Z(!1))},Q=()=>{"undefined"!=typeof window&&window.open(j,"_blank","noopener,noreferrer")},G=()=>{const e=A?.trim();e?(P(t=>[...t,{label:e,url:j}]),I(null)):I(null)};b(()=>{if(null===A)return;q.current?.focus(),q.current?.select();const e=e=>{const t=e.target;t?.closest("[data-bookmark-popover]")||I(null)},t=setTimeout(()=>document.addEventListener("mousedown",e),0);return()=>{clearTimeout(t),document.removeEventListener("mousedown",e)}},[null!==A]);const Y=e=>{P(t=>t.filter((t,o)=>o!==e))},$=M>0,_=M<O.length-1,V=J.some(e=>e.url===j),ee="p-1.5 rounded hover:bg-gray-200 transition-colors text-gray-600 disabled:opacity-30";/* @__PURE__ */
2
2
  return f("div",{className:"relative flex flex-col h-full bg-white",children:[
3
- /* @__PURE__ */k(r,{title:`Browser - ${L(j)}`}),
3
+ /* @__PURE__ */k(r,{title:`Browser - ${S(j)}`}),
4
4
  /* @__PURE__ */k(o,{app:"browser"}),
5
5
  /* @__PURE__ */f("div",{className:"flex items-center gap-1 px-2 py-1.5 border-b border-gray-200 bg-gray-50 shrink-0",children:[
6
- /* @__PURE__ */k("button",{onClick:()=>{if(O>0){const e=O-1;A(e),z(T[e]),E(T[e]),P(!1)}},disabled:!Z,className:ee,title:"Back",children:/* @__PURE__ */k(h,{name:"chevron-left",fallback:n,className:"h-4 w-4",strokeWidth:2})}),
7
- /* @__PURE__ */k("button",{onClick:()=>{if(O<T.length-1){const e=O+1;A(e),z(T[e]),E(T[e]),P(!1)}},disabled:!_,className:ee,title:"Forward",children:/* @__PURE__ */k(h,{name:"chevron-right",fallback:l,className:"h-4 w-4",strokeWidth:2})}),
6
+ /* @__PURE__ */k("button",{onClick:()=>{if(M>0){const e=M-1;T(e),z(O[e]),E(O[e]),Z(!1)}},disabled:!$,className:ee,title:"Back",children:/* @__PURE__ */k(h,{name:"chevron-left",fallback:n,className:"h-4 w-4",strokeWidth:2})}),
7
+ /* @__PURE__ */k("button",{onClick:()=>{if(M<O.length-1){const e=M+1;T(e),z(O[e]),E(O[e]),Z(!1)}},disabled:!_,className:ee,title:"Forward",children:/* @__PURE__ */k(h,{name:"chevron-right",fallback:l,className:"h-4 w-4",strokeWidth:2})}),
8
8
  /* @__PURE__ */k("button",{onClick:()=>F(e=>e+1),className:ee,title:"Refresh",children:/* @__PURE__ */k(h,{name:"refresh",fallback:s,className:"h-4 w-4",strokeWidth:1.8})}),
9
- /* @__PURE__ */k("button",{onClick:()=>q(v),className:ee,title:"Home",children:/* @__PURE__ */k(h,{name:"home",fallback:i,className:"h-4 w-4",strokeWidth:1.8})}),
10
- /* @__PURE__ */k("form",{onSubmit:e=>{e.preventDefault(),q(H)},className:"flex-1 flex items-center mx-1",children:/* @__PURE__ */f("div",{className:"flex items-center w-full bg-white border border-gray-300 rounded focus-within:border-blue-400 focus-within:ring-1 focus-within:ring-blue-200",children:[
9
+ /* @__PURE__ */k("button",{onClick:()=>K(v),className:ee,title:"Home",children:/* @__PURE__ */k(h,{name:"home",fallback:i,className:"h-4 w-4",strokeWidth:1.8})}),
10
+ /* @__PURE__ */k("form",{onSubmit:e=>{e.preventDefault(),K(L)},className:"flex-1 flex items-center mx-1",children:/* @__PURE__ */f("div",{className:"flex items-center w-full bg-white border border-gray-300 rounded focus-within:border-blue-400 focus-within:ring-1 focus-within:ring-blue-200",children:[
11
11
  /* @__PURE__ */k("span",{className:"px-2 text-gray-400 text-xs",children:j.startsWith("https://")?"🔒":"⚠️"}),
12
- /* @__PURE__ */k("input",{type:"text",value:H,onChange:e=>E(e.target.value),onFocus:e=>e.target.select(),className:"flex-1 px-1 py-1 text-sm bg-transparent outline-none font-mono",spellCheck:!1,placeholder:"Enter URL or search…"}),j!==H&&/* @__PURE__ */k("button",{type:"button",onClick:()=>E(j),className:"px-1.5 text-[10px] text-gray-400 hover:text-gray-700",title:"Reset to current URL",children:"×"})]})}),
13
- /* @__PURE__ */k("button",{"data-bookmark-popover":!0,onClick:()=>V?Y(J.findIndex(e=>e.url===j)):void G(L(j)),className:ee+" "+(V?"text-yellow-500":""),title:V?"Remove bookmark":"Add bookmark",children:/* @__PURE__ */k(h,{name:"star",fallback:c,className:"h-4 w-4",fill:V?"currentColor":"none",strokeWidth:1.5})}),
14
- /* @__PURE__ */k("button",{onClick:$,className:ee,title:"Open in new tab",children:/* @__PURE__ */k(h,{name:"external",fallback:m,className:"h-4 w-4",strokeWidth:1.5})}),
15
- /* @__PURE__ */k("button",{onClick:()=>P(e=>!e),className:ee,title:"Embedding help",children:/* @__PURE__ */k(h,{name:"help",fallback:d,className:"h-4 w-4",strokeWidth:1.5})})]}),null!==X&&/* @__PURE__ */f("div",{"data-bookmark-popover":!0,className:"absolute right-3 top-12 z-30 w-72 bg-white border border-gray-200 rounded-md shadow-lg p-3",children:[
12
+ /* @__PURE__ */k("input",{type:"text",value:L,onChange:e=>E(e.target.value),onFocus:e=>e.target.select(),className:"flex-1 px-1 py-1 text-sm bg-transparent outline-none font-mono",spellCheck:!1,placeholder:"Enter URL or search…"}),j!==L&&/* @__PURE__ */k("button",{type:"button",onClick:()=>E(j),className:"px-1.5 text-[10px] text-gray-400 hover:text-gray-700",title:"Reset to current URL",children:"×"})]})}),
13
+ /* @__PURE__ */k("button",{"data-bookmark-popover":!0,onClick:()=>V?Y(J.findIndex(e=>e.url===j)):void I(S(j)),className:ee+" "+(V?"text-yellow-500":""),title:V?"Remove bookmark":"Add bookmark",children:/* @__PURE__ */k(h,{name:"star",fallback:c,className:"h-4 w-4",fill:V?"currentColor":"none",strokeWidth:1.5})}),
14
+ /* @__PURE__ */k("button",{onClick:Q,className:ee,title:"Open in new tab",children:/* @__PURE__ */k(h,{name:"external",fallback:m,className:"h-4 w-4",strokeWidth:1.5})}),
15
+ /* @__PURE__ */k("button",{onClick:()=>Z(e=>!e),className:ee,title:"Embedding help",children:/* @__PURE__ */k(h,{name:"help",fallback:d,className:"h-4 w-4",strokeWidth:1.5})})]}),null!==A&&/* @__PURE__ */f("div",{"data-bookmark-popover":!0,className:"absolute right-3 top-12 z-30 w-72 bg-white border border-gray-200 rounded-md shadow-lg p-3",children:[
16
16
  /* @__PURE__ */k("div",{className:"text-[11px] font-semibold uppercase tracking-wide text-gray-500 mb-1",children:"Bookmark this page"}),
17
17
  /* @__PURE__ */k("div",{className:"text-[11px] text-gray-400 truncate mb-2 font-mono",title:j,children:j}),
18
- /* @__PURE__ */k("input",{ref:M,type:"text",value:X,onChange:e=>G(e.target.value),onKeyDown:e=>{"Enter"===e.key?K():"Escape"===e.key&&G(null)},placeholder:"Name",className:"w-full text-sm px-2 py-1.5 border border-gray-300 rounded focus:outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-200"}),
18
+ /* @__PURE__ */k("input",{ref:q,type:"text",value:A,onChange:e=>I(e.target.value),onKeyDown:e=>{"Enter"===e.key?G():"Escape"===e.key&&I(null)},placeholder:"Name",className:"w-full text-sm px-2 py-1.5 border border-gray-300 rounded focus:outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-200"}),
19
19
  /* @__PURE__ */f("div",{className:"flex justify-end gap-1 mt-2",children:[
20
- /* @__PURE__ */k("button",{onClick:()=>G(null),className:"px-2 py-1 text-xs text-gray-600 hover:bg-gray-100 rounded",children:"Cancel"}),
21
- /* @__PURE__ */k("button",{onClick:K,disabled:!X.trim(),className:"px-2 py-1 text-xs bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-40",children:"Save"})]})]}),
22
- /* @__PURE__ */f("div",{className:"flex items-center gap-0.5 px-2 py-1 border-b border-gray-200 bg-white shrink-0 overflow-x-auto",children:[0===J.length&&/* @__PURE__ */k("span",{className:"text-[11px] text-gray-400 italic px-2",children:"No bookmarks yet — star the address bar to add one."}),J.map((e,t)=>/* @__PURE__ */f("button",{onClick:()=>q(e.url),onContextMenu:async o=>{o.preventDefault();await a({title:"Remove bookmark",message:`"${e.label}" will be removed from your bookmarks.`,confirmLabel:"Remove",variant:"danger"})&&Y(t)},className:"flex items-center gap-1.5 px-2 py-0.5 rounded text-[12px] text-gray-700 hover:bg-gray-100 whitespace-nowrap",title:`${e.url}\n(right-click to remove)`,children:[
20
+ /* @__PURE__ */k("button",{onClick:()=>I(null),className:"px-2 py-1 text-xs text-gray-600 hover:bg-gray-100 rounded",children:"Cancel"}),
21
+ /* @__PURE__ */k("button",{onClick:G,disabled:!A.trim(),className:"px-2 py-1 text-xs bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-40",children:"Save"})]})]}),
22
+ /* @__PURE__ */f("div",{className:"flex items-center gap-0.5 px-2 py-1 border-b border-gray-200 bg-white shrink-0 overflow-x-auto",children:[0===J.length&&/* @__PURE__ */k("span",{className:"text-[11px] text-gray-400 italic px-2",children:"No bookmarks yet — star the address bar to add one."}),J.map((e,t)=>/* @__PURE__ */f("button",{onClick:()=>K(e.url),onContextMenu:async o=>{o.preventDefault();await a({title:"Remove bookmark",message:`"${e.label}" will be removed from your bookmarks.`,confirmLabel:"Remove",variant:"danger"})&&Y(t)},className:"flex items-center gap-1.5 px-2 py-0.5 rounded text-[12px] text-gray-700 hover:bg-gray-100 whitespace-nowrap",title:`${e.url}\n(right-click to remove)`,children:[
23
23
  /* @__PURE__ */k("img",{src:`https://icons.duckduckgo.com/ip3/${new URL(e.url).hostname}.ico`,alt:"",className:"h-3.5 w-3.5",onError:e=>{e.target.style.visibility="hidden"}}),e.label]},t)),
24
24
  /* @__PURE__ */k("div",{className:"flex-1"}),
25
25
  /* @__PURE__ */k("button",{onClick:()=>{N(j);try{localStorage.setItem(y,j)}catch{}},className:"px-2 py-0.5 text-[11px] text-gray-500 hover:text-gray-800",title:"Set current page as homepage",children:"Set as home"})]}),
26
- /* @__PURE__ */k("div",{className:"flex-1 relative min-h-0 bg-gray-50",children:/* @__PURE__ */k(R,{url:j,iframeKey:I,iframeRef:Q,openExternal:$,showHelp:B,dismissHelp:()=>P(!1)})})]})}function L(e){try{return new URL(e).hostname.replace(/^www\./,"")}catch{return e}}function R({url:e,iframeKey:t,iframeRef:o,openExternal:r,showHelp:a,dismissHelp:n}){const[l,s]=g(!1);b(()=>{s(!1)},[e]);return function(e){try{const t=new URL(e).hostname.toLowerCase();return z.some(e=>t===e||t.endsWith("."+e))}catch{return!1}}(e)&&!l?/* @__PURE__ */k("div",{className:"absolute inset-0 flex items-center justify-center bg-white p-8",children:/* @__PURE__ */f("div",{className:"max-w-md text-center",children:[
26
+ /* @__PURE__ */k("div",{className:"flex-1 relative min-h-0 bg-gray-50",children:/* @__PURE__ */k(H,{url:j,iframeKey:U,iframeRef:B,openExternal:Q,showHelp:X,dismissHelp:()=>Z(!1)})})]})}function S(e){try{return new URL(e).hostname.replace(/^www\./,"")}catch{return e}}function H({url:e,iframeKey:t,iframeRef:o,openExternal:r,showHelp:a,dismissHelp:n}){const[l,s]=g(!1);b(()=>{s(!1)},[e]);return function(e){try{const t=new URL(e).hostname.toLowerCase();return z.some(e=>t===e||t.endsWith("."+e))}catch{return!1}}(e)&&!l?/* @__PURE__ */k("div",{className:"absolute inset-0 flex items-center justify-center bg-white p-8",children:/* @__PURE__ */f("div",{className:"max-w-md text-center",children:[
27
27
  /* @__PURE__ */k(h,{name:"no-symbol",fallback:u,className:"h-14 w-14 mx-auto text-gray-300 mb-3",strokeWidth:1.2}),
28
- /* @__PURE__ */f("h3",{className:"text-base font-semibold text-gray-800 mb-1",children:[L(e)," can't be embedded"]}),
28
+ /* @__PURE__ */f("h3",{className:"text-base font-semibold text-gray-800 mb-1",children:[S(e)," can't be embedded"]}),
29
29
  /* @__PURE__ */f("p",{className:"text-sm text-gray-500 mb-4",children:["This site sends an ",
30
30
  /* @__PURE__ */k("span",{className:"font-mono text-xs",children:"X-Frame-Options"})," ","or ",
31
31
  /* @__PURE__ */k("span",{className:"font-mono text-xs",children:"Content-Security-Policy"})," header that refuses iframe embedding. The browser blocks the load before our app can do anything about it."]}),
@@ -40,4 +40,4 @@ return f("div",{className:"relative flex flex-col h-full bg-white",children:[
40
40
  /* @__PURE__ */f("p",{children:["Hit the ",
41
41
  /* @__PURE__ */k("span",{className:"font-medium",children:"↗"})," button to open the page in a real new tab. Sites that ",
42
42
  /* @__PURE__ */k("em",{children:"do"})," allow embedding (Wikipedia, DevDocs, your own apps) work fine in here."]}),
43
- /* @__PURE__ */k("button",{onClick:n,className:"mt-2 text-[11px] text-blue-600 hover:underline",children:"Got it"})]})]})}export{H as default};
43
+ /* @__PURE__ */k("button",{onClick:n,className:"mt-2 text-[11px] text-blue-600 hover:underline",children:"Got it"})]})]})}export{L as default};
@@ -0,0 +1,29 @@
1
+ import{loadAppearance as e,WidgetSettingsModal as t}from"./chunk-PKEQ7XOH.js";import{useWidgetSettings as n}from"./chunk-L5XA5MLK.js";import"./chunk-FROOMUJ7.js";import"./chunk-DSSLOQDW.js";import"./chunk-5A7MWTXX.js";import"./chunk-OGXU7QFW.js";import"./chunk-HRLPFUPB.js";import"./chunk-ZUCYJAQH.js";import"./chunk-RMJZOKHY.js";import"./chunk-AFYQTVZO.js";import"./chunk-HZZNJHRH.js";import"./chunk-X4FJ54PP.js";import"./chunk-D65T4H6J.js";import{CheckFallbackB as l,CopyFallbackB as r}from"./chunk-6LTDJKIL.js";import"./chunk-TNBCUZVE.js";import"./chunk-C3QOHMR6.js";import{Icon as c}from"./chunk-XWSWYMGM.js";import{useState as a,useRef as i,useCallback as o,useEffect as s}from"react";import{jsxs as u,Fragment as d,jsx as h}from"react/jsx-runtime";var m="calc_appearance";function b(){const[b,k]=a("0"),[p,g]=a(null),[f,y]=a(null),[N,x]=a(!1),[v,C]=a(""),[$,j]=a([]),[w,E]=a(!1),[S,T]=a(()=>e(m)),[F,O]=a(!1),[W,H]=a(S),L=i(null);n(o(()=>{H({...S}),O(!0)},[S]));const A=o(e=>{C(""),k(t=>N||"0"===t?(x(!1),"."===e?"0.":e):"."===e&&t.includes(".")?t:t+e)},[N]),J=o(()=>{k("0"),g(null),y(null),x(!1)},[]),M=o((e,t,n)=>{switch(n){case"+":return e+t;case"-":return e-t;case"×":return e*t;case"÷":return 0===t?0:e/t;default:return t}},[]),X=o(e=>{const t=parseFloat(b);if(null!==p&&f&&!N){const e=M(p,t,f);k(String(e)),g(e),j(n=>[`${p} ${f} ${t} = ${e}`,...n].slice(0,20))}else g(t);y(e),x(!0)},[b,p,f,N,M]),D=o(()=>{if(null===p||!f)return;const e=parseFloat(b),t=M(p,e,f),n=`${p} ${f} ${e} =`;j(e=>[`${n} ${t}`,...e].slice(0,20)),C(n),k(String(t)),g(null),y(null),x(!0)},[b,p,f,M]),P=o(()=>{k(e=>String(parseFloat(e)/100))},[]),Q=o(()=>{k(e=>e.startsWith("-")?e.slice(1):"0"===e?e:"-"+e)},[]);s(()=>{const e=e=>{const t=e.target.tagName;"INPUT"!==t&&"TEXTAREA"!==t&&"SELECT"!==t&&(e.key>="0"&&e.key<="9"?A(e.key):"."===e.key?A("."):"+"===e.key?X("+"):"-"===e.key?X("-"):"*"===e.key?X("×"):"/"===e.key?(e.preventDefault(),X("÷")):"Enter"===e.key||"="===e.key?("Enter"===e.key&&e.target instanceof Node&&L.current?.contains(e.target)&&e.preventDefault(),D()):"Escape"===e.key?J():"Backspace"===e.key?k(e=>e.length>1?e.slice(0,-1):"0"):"%"===e.key&&P())};return window.addEventListener("keydown",e),()=>window.removeEventListener("keydown",e)},[A,X,D,J,P]);const R="flex items-center justify-center rounded-lg text-sm font-medium transition-colors",U=`${R} bg-white border border-gray-200 text-gray-900 hover:bg-gray-50 active:bg-gray-100`,Z=`${R} bg-blue-50 border border-blue-200 text-blue-700 hover:bg-blue-100 active:bg-blue-200`,B=`${R} bg-gray-100 border border-gray-200 text-gray-600 hover:bg-gray-200 active:bg-gray-300`,K=`${R} bg-blue-600 text-white hover:bg-blue-700 active:bg-blue-800`;/* @__PURE__ */
2
+ return u(d,{children:[
3
+ /* @__PURE__ */u("div",{ref:L,className:"flex flex-col h-full",style:{opacity:S.activeOpacity/100,backdropFilter:S.activeBlur>0?`blur(${S.activeBlur}px)`:void 0},children:[
4
+ /* @__PURE__ */u("div",{className:"bg-slate-200 rounded-t-lg px-4 py-2 group/display relative flex flex-col justify-end min-h-[120px] border-b border-slate-300",children:[
5
+ /* @__PURE__ */h("div",{className:"flex flex-col justify-end flex-1 overflow-hidden",children:$.slice(0,3).reverse().map((e,t)=>/* @__PURE__ */h("div",{className:"text-[11px] text-slate-400 text-right font-mono truncate",children:e},t))}),(v||f&&null!==p)&&/* @__PURE__ */h("div",{className:"text-base text-slate-600 text-right font-mono",children:v||`${p} ${f} ${N?"":b}`}),
6
+ /* @__PURE__ */u("div",{className:"flex items-center gap-2",children:[
7
+ /* @__PURE__ */h("button",{onClick:()=>{navigator.clipboard.writeText(b),E(!0),setTimeout(()=>E(!1),1200)},title:"Copy result",className:"shrink-0 text-slate-300 hover:text-slate-500 transition-colors opacity-0 group-hover/display:opacity-100",children:/* @__PURE__ */h(c,w?{name:"check",fallback:l,className:"h-5 w-5 text-green-500",strokeWidth:2}:{name:"copy",fallback:r,className:"h-5 w-5",strokeWidth:2})}),
8
+ /* @__PURE__ */h("div",{className:"text-right text-4xl font-mono font-bold text-slate-800 truncate flex-1",children:(e=>{const t=parseFloat(e);return isNaN(t)||e.endsWith(".")||e.endsWith(".0")||Math.abs(t)<1e15&&e.length<16?e:t.toExponential(6)})(b)})]})]}),
9
+ /* @__PURE__ */u("div",{className:"grid grid-cols-4 grid-rows-5 gap-1.5 flex-1 min-h-0 p-2",children:[
10
+ /* @__PURE__ */h("button",{onClick:J,className:B,children:"AC"}),
11
+ /* @__PURE__ */h("button",{onClick:Q,className:B,children:"+/−"}),
12
+ /* @__PURE__ */h("button",{onClick:P,className:B,children:"%"}),
13
+ /* @__PURE__ */h("button",{onClick:()=>X("÷"),className:`${Z} ${"÷"===f?"ring-2 ring-blue-400":""}`,children:"÷"}),
14
+ /* @__PURE__ */h("button",{onClick:()=>A("7"),className:U,children:"7"}),
15
+ /* @__PURE__ */h("button",{onClick:()=>A("8"),className:U,children:"8"}),
16
+ /* @__PURE__ */h("button",{onClick:()=>A("9"),className:U,children:"9"}),
17
+ /* @__PURE__ */h("button",{onClick:()=>X("×"),className:`${Z} ${"×"===f?"ring-2 ring-blue-400":""}`,children:"×"}),
18
+ /* @__PURE__ */h("button",{onClick:()=>A("4"),className:U,children:"4"}),
19
+ /* @__PURE__ */h("button",{onClick:()=>A("5"),className:U,children:"5"}),
20
+ /* @__PURE__ */h("button",{onClick:()=>A("6"),className:U,children:"6"}),
21
+ /* @__PURE__ */h("button",{onClick:()=>X("-"),className:`${Z} ${"-"===f?"ring-2 ring-blue-400":""}`,children:"−"}),
22
+ /* @__PURE__ */h("button",{onClick:()=>A("1"),className:U,children:"1"}),
23
+ /* @__PURE__ */h("button",{onClick:()=>A("2"),className:U,children:"2"}),
24
+ /* @__PURE__ */h("button",{onClick:()=>A("3"),className:U,children:"3"}),
25
+ /* @__PURE__ */h("button",{onClick:()=>X("+"),className:`${Z} ${"+"===f?"ring-2 ring-blue-400":""}`,children:"+"}),
26
+ /* @__PURE__ */h("button",{onClick:()=>A("0"),className:`${U} col-span-2`,children:"0"}),
27
+ /* @__PURE__ */h("button",{onClick:()=>A("."),className:U,children:"."}),
28
+ /* @__PURE__ */h("button",{onClick:D,className:K,children:"="})]})]}),
29
+ /* @__PURE__ */h(t,{open:F,onClose:()=>O(!1),title:"Calculator Settings",appearance:W,onAppearanceChange:H,onSave:()=>{T(W),localStorage.setItem(m,JSON.stringify(W)),O(!1)}})]})}export{b as default};
@@ -0,0 +1 @@
1
+ export{ConfirmProvider,confirm,confirmDestructive,prompt,useConfirm}from"./chunk-ZUCYJAQH.js";import"./chunk-AFYQTVZO.js";import"./chunk-X4FJ54PP.js";import"./chunk-6LTDJKIL.js";import"./chunk-TNBCUZVE.js";import"./chunk-C3QOHMR6.js";import"./chunk-XWSWYMGM.js";