efficient-shell 5.6.1 → 7.7.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.
- package/README.md +270 -64
- package/THIRD_PARTY_NOTICES.md +212 -0
- package/dist/Browser-56LX7FTE.js +43 -0
- package/dist/Calculator-GBKLJHM6.js +29 -0
- package/dist/ConfirmDialog-PS5BXNAT.js +1 -0
- package/dist/CurrencyConverter-ZXY7XNG6.js +21 -0
- package/dist/Documents-SVKR757H.js +52 -0
- package/dist/Files-KSHU7ROB.js +87 -0
- package/dist/Notepad-5GT2GUCJ.js +25 -0
- package/dist/PdfViewer-YVAWFZVH.js +1 -0
- package/dist/PomodoroTimer-GAGDQ4NF.js +91 -0
- package/dist/Preview-YDCDW5NE.js +182 -0
- package/dist/Sidebar-IH6WUPKT.js +44 -0
- package/dist/Spreadsheet-SYR23K4I.js +37 -0
- package/dist/Stock-LH5CR667.js +16 -0
- package/dist/{Weather-RWI6NKB2.js → Weather-MZGIPR2R.js} +35 -35
- package/dist/WorldClock-DLB2RRLI.js +32 -0
- package/dist/apps/index.js +1 -1
- package/dist/bin/ui-check.js +1 -1
- package/dist/brand.css +1 -1
- package/dist/chunk-2ALIPI3D.js +1 -0
- package/dist/{chunk-JXAAZGLJ.js → chunk-2R5RTN5T.js} +1 -1
- package/dist/chunk-43XFZY4K.js +5 -0
- package/dist/chunk-4JGNJYVL.js +10 -0
- package/dist/{chunk-7WC3EOFS.js → chunk-5TOLXFQ6.js} +1 -1
- package/dist/chunk-6SCHK75C.js +108 -0
- package/dist/chunk-7IQ3B77H.js +1 -0
- package/dist/{chunk-TFETN2GI.js → chunk-B4PGVDB6.js} +1 -1
- package/dist/chunk-BUGGBB5J.js +1 -0
- package/dist/chunk-CJHT4VTF.js +91 -0
- package/dist/chunk-CLFUF3BH.js +9 -0
- package/dist/chunk-CQ2DLPPS.js +3 -0
- package/dist/{chunk-X4FJ54PP.js → chunk-FIEA5CGB.js} +1 -1
- package/dist/chunk-G4CTTSAR.js +11 -0
- package/dist/{chunk-YIH6JLMV.js → chunk-HIRMYFOF.js} +2 -2
- package/dist/chunk-JKHXDQEI.js +24 -0
- package/dist/{chunk-KXAUDC3A.js → chunk-JQRIAZDP.js} +35 -35
- package/dist/chunk-KNUR2UEG.js +1 -0
- package/dist/{chunk-WWF2EQXF.js → chunk-KONXUO5I.js} +1 -1
- package/dist/chunk-M4MCXII7.js +6 -0
- package/dist/{chunk-CLPJDJH4.js → chunk-M4TRVJTO.js} +1 -1
- package/dist/chunk-MSGBGLGI.js +29 -0
- package/dist/chunk-N7JTY3LU.js +21 -0
- package/dist/chunk-NODT4F2C.js +13 -0
- package/dist/chunk-OXTM3XZI.js +1 -0
- package/dist/chunk-PJPK2ADX.js +1 -0
- package/dist/chunk-PT4Z3KIA.js +12 -0
- package/dist/chunk-QRCDQPJA.js +1 -0
- package/dist/{chunk-SKB6L53D.js → chunk-QUVE5JZZ.js} +11 -11
- package/dist/{chunk-N3A3GWJI.js → chunk-R2TPTM4O.js} +12 -12
- package/dist/{chunk-GG7AE6OC.js → chunk-RBKIYZ7H.js} +1 -1
- package/dist/chunk-RUHUTZN4.js +1 -0
- package/dist/chunk-TCYS67ND.js +1016 -0
- package/dist/{chunk-AFYQTVZO.js → chunk-UVBK6YSR.js} +1 -1
- package/dist/client-DLm3Fvbc.d.ts +65 -0
- package/dist/file-intake/index.js +1 -1
- package/dist/i18n/index.d.ts +1 -1
- package/dist/icons/efficient-looks.d.ts +1 -1
- package/dist/icons/efficient-looks.js +4 -4
- package/dist/icons/efficient.d.ts +3 -3
- package/dist/icons/efficient.js +3 -3
- package/dist/icons/index.d.ts +12 -12
- package/dist/icons/index.js +1 -1
- package/dist/{index-ZI6vl2Wq.d.ts → index-BkBsXpfV.d.ts} +468 -161
- package/dist/index.d.ts +6 -4
- package/dist/index.js +635 -639
- package/dist/locales/zh-Hans.d.ts +1 -1
- package/dist/locales/zh-Hans.js +1 -1
- package/dist/map/index.js +22 -22
- package/dist/markdown/index.js +6 -6
- package/dist/markup/index.d.ts +460 -36
- package/dist/markup/index.js +1 -1
- package/dist/page-tabs/index.js +5 -5
- package/dist/pdf/index.js +1 -1
- package/dist/{registry-J-TU7zKx.d.ts → registry-Dw6QO2Ch.d.ts} +12 -9
- package/dist/{strings-BVB64Nck.d.ts → strings-BQtq9300.d.ts} +22 -2
- package/dist/telemetry/index.d.ts +39 -0
- package/dist/telemetry/index.js +1 -0
- package/dist/ui/index.d.ts +4 -3
- package/dist/ui/index.js +1 -1
- package/dist/ui.css +110 -75
- package/package.json +7 -21
- package/dist/Browser-RXGEZ77D.js +0 -43
- package/dist/Calculator-MVPKTZZF.js +0 -29
- package/dist/ConfirmDialog-XOYCYFA7.js +0 -1
- package/dist/CurrencyConverter-2IGOOR3I.js +0 -21
- package/dist/Documents-YBLR7WVX.js +0 -52
- package/dist/Files-7XNR5IL2.js +0 -87
- package/dist/Notepad-ZQYQXV75.js +0 -25
- package/dist/PdfViewer-SQVMMA7B.js +0 -1
- package/dist/PomodoroTimer-XIUN4M73.js +0 -91
- package/dist/Preview-YCOA5JPP.js +0 -182
- package/dist/Sidebar-VI7E3EKI.js +0 -44
- package/dist/Spreadsheet-SRAUU3OA.js +0 -37
- package/dist/Stock-7EKOAQ7C.js +0 -16
- package/dist/WorldClock-CEHWDFEO.js +0 -32
- package/dist/chunk-3HJTOINK.js +0 -1
- package/dist/chunk-4SWPNARR.js +0 -5
- package/dist/chunk-5ABLFFC5.js +0 -24
- package/dist/chunk-6LTDJKIL.js +0 -138
- package/dist/chunk-F6B3J3GW.js +0 -6
- package/dist/chunk-HZZNJHRH.js +0 -1
- package/dist/chunk-IMMHJIV5.js +0 -29
- package/dist/chunk-OGXU7QFW.js +0 -12
- package/dist/chunk-P4CIOJHP.js +0 -3
- package/dist/chunk-RBDGN4YU.js +0 -9
- package/dist/chunk-RC2EG6P4.js +0 -1026
- package/dist/chunk-RL626E7Q.js +0 -13
- package/dist/chunk-RMJZOKHY.js +0 -10
- package/dist/chunk-SFCDHHQP.js +0 -1
- package/dist/chunk-TNBCUZVE.js +0 -1
- package/dist/chunk-VWBQ2P5G.js +0 -108
- package/dist/chunk-XWSWYMGM.js +0 -2
- package/dist/chunk-YGC7AX3X.js +0 -11
- package/dist/chunk-Z7D6Y5WA.js +0 -21
- package/dist/icons/heroicons.d.ts +0 -6
- package/dist/icons/heroicons.js +0 -1
- package/dist/icons/lucide.d.ts +0 -6
- package/dist/icons/lucide.js +0 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ A backend-less playground hosted on GitHub Pages. Wallpapers, themes, sticky not
|
|
|
16
16
|
|
|
17
17
|
**Shell:** `<Layout>`, `<StartMenu>`, `<Desktop>` (with sticky notes + folders), `<WindowManager>`, `<Modal>` (standard / compact / widget styles), `<PopupMenu>`, `<ConfirmDialog>`, `<GlobalSearch>` (Cmd-K), `<ShortcutHelp>`, `<NotificationBell>`, `<StatusBadge>`, `<SearchableSelect>`, frosted-glass theming.
|
|
18
18
|
|
|
19
|
-
**UI primitives:** `<Button>`, `<Input>`, `<Textarea>`, `<Select>`, `<Checkbox>`, `<Radio>` / `<RadioGroup>`, `<FormField>`, `<Label>`, `<Snippet>`, `<Kbd>`, `<Card>` / `<StatCard>`, `<Avatar>` / `<AvatarGroup>`, `<Banner>`, `<Tabs>`, `<Accordion>`, `<Tooltip>`, `<Pagination>`, `<MetricBar>`, `<BudgetBar>`, `<SettingRow>`, and dependency-free `<Sparkline>`
|
|
19
|
+
**UI primitives:** `<Button>`, `<Input>`, `<Textarea>`, `<Select>`, `<Checkbox>`, `<Radio>` / `<RadioGroup>`, `<FormField>`, `<Label>`, `<Snippet>`, `<Kbd>`, `<Card>` / `<StatCard>`, `<Avatar>` / `<AvatarGroup>`, `<Banner>`, `<Tabs>`, `<Accordion>`, `<Tooltip>`, `<Pagination>`, `<MetricBar>`, `<BudgetBar>`, `<SettingRow>`, and the dependency-free chart family (`<Sparkline>` for a tile, and `<ColumnChart>`, `<TimeSeriesChart>`, `<PieChart>` and the rest for charts that are read).
|
|
20
20
|
|
|
21
21
|
**Page templates:** ready-made screens composed from the primitives — `<DashboardTemplate>`, `<DataTablePage>`, `<FormLayoutPage>`, `<CheckoutTemplate>`, `<EmailTemplate>`, `<ChatTemplate>`, `<GalleryTemplate>`, `<AuthScreen>`, `<ErrorPage>`. `<ErrorBoundary>` catches a render crash and shows the 500 page in place of a blank screen.
|
|
22
22
|
|
|
@@ -54,9 +54,12 @@ Peer deps you should already have in a typical React + Tailwind v4 app:
|
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
56
|
npm i react react-dom react-router-dom @tanstack/react-query react-hook-form \
|
|
57
|
-
tailwindcss @headlessui/react
|
|
57
|
+
tailwindcss @headlessui/react
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
No icon package: the shell draws every icon from its own set
|
|
61
|
+
(`efficient-shell/icons/efficient`).
|
|
62
|
+
|
|
60
63
|
All of those except `react` / `react-dom` are declared **optional** peers. The
|
|
61
64
|
shell's components need them — a portal that drops one gets a module-not-found at
|
|
62
65
|
build — but a consumer taking only `efficient-shell/markup` needs none of them, and
|
|
@@ -922,10 +925,28 @@ End go to `min`/`max`, and Enter or a double-click resets to `defaultWidth`. A
|
|
|
922
925
|
drag starts after 3px, so a click never nudges it. `storageKey` restores the
|
|
923
926
|
width after mount and keeps it once a drag settles. Its hint is a `Tooltip`.
|
|
924
927
|
|
|
928
|
+
For the seam between two stacked panes, such as a message box under a thread,
|
|
929
|
+
pass `orientation="horizontal"` and `side="top"` or `side="bottom"` (where the
|
|
930
|
+
pane sits). The word and its values are `Divider`'s and `IconRail`'s, and they
|
|
931
|
+
name the line: a grip between stacked panes is a horizontal line. It drags up
|
|
932
|
+
and down, its cursor is `row-resize`, it is a separator with
|
|
933
|
+
`aria-orientation="horizontal"`, and ArrowUp/ArrowDown move it. Home, End,
|
|
934
|
+
Enter, the double-click and `storageKey` work as above. `width`, `min`, `max`
|
|
935
|
+
and `defaultWidth` are then the pane's height. The default is
|
|
936
|
+
`orientation="vertical"`, and its markup is unchanged.
|
|
937
|
+
|
|
938
|
+
```tsx
|
|
939
|
+
<section className="relative" style={{ height }}>
|
|
940
|
+
<ResizeHandle orientation="horizontal" side="bottom" width={height} onResize={setHeight} min={108} max={520} defaultWidth={220} storageKey="chat.composer" />
|
|
941
|
+
…
|
|
942
|
+
</section>
|
|
943
|
+
```
|
|
944
|
+
|
|
925
945
|
**`WidthsBar`** sets how a row's columns share its width: a bar split into
|
|
926
|
-
the columns, a handle between each pair
|
|
927
|
-
|
|
928
|
-
widths" field (efficient-admin-portal), which is
|
|
946
|
+
the columns, with a handle between each pair. Each segment of the bar is
|
|
947
|
+
where its % is typed; there are no number boxes under it. It came from the
|
|
948
|
+
campaign designer's "Column widths" field (efficient-admin-portal), which is
|
|
949
|
+
deleted once this ships.
|
|
929
950
|
|
|
930
951
|
```tsx
|
|
931
952
|
<InputNumber aria-label="Columns" value={count} onChange={n => n && setCount(n)} min={1} max={4} />
|
|
@@ -934,8 +955,41 @@ widths" field (efficient-admin-portal), which is deleted once this ships.
|
|
|
934
955
|
|
|
935
956
|
The value is `ColumnShares`: the first `count - 1` shares, whole percentages.
|
|
936
957
|
The last column is what the others leave, so the widths always add up. No
|
|
937
|
-
column goes under `COLUMN_SHARE_MIN` (10%)
|
|
938
|
-
|
|
958
|
+
column goes under `COLUMN_SHARE_MIN` (10%).
|
|
959
|
+
|
|
960
|
+
Each segment's "34%" is a field that looks like a label until it is focused.
|
|
961
|
+
Focused, it shows a focus ring and the bare number, selected. Enter or leaving
|
|
962
|
+
the field commits; Escape puts back the value from when the field was
|
|
963
|
+
entered. ArrowUp/ArrowDown step 1, Shift 10. Every column can be typed in, the
|
|
964
|
+
last one too. A typed column takes its room from the next column (the last
|
|
965
|
+
column from the one before it); when that one is at the floor, the room comes
|
|
966
|
+
from the column after it, then from the other side. A focused field in a
|
|
967
|
+
narrow segment grows to at least 40px, over the handles and inside the bar.
|
|
968
|
+
A segment narrower than 36px shows "10" rather than "10%". Each field is a
|
|
969
|
+
`spinbutton` named "Column 1", "Column 2", … (`strings.widths.column`), valued
|
|
970
|
+
in percent and described by the hint. Tab goes column 1, the handle after it,
|
|
971
|
+
column 2, …, the last column, then "Reset to equal".
|
|
972
|
+
|
|
973
|
+
A handle trades width with its neighbour and stops at the floor. Its line is
|
|
974
|
+
2px at rest and 4px on hover, keyboard focus and while dragged; the grab area
|
|
975
|
+
is 12px either way. Under 264px of its own width (a container query, not the
|
|
976
|
+
window), the bar puts "Reset to equal" under the hint, at the right end.
|
|
977
|
+
|
|
978
|
+
A host that draws its own reset and hint passes `footer={false}`: for
|
|
979
|
+
example a settings field with a reset in its heading row (shown only when
|
|
980
|
+
the value has changed) and a hint that folds away. The bar then draws
|
|
981
|
+
nothing under itself, not even a gap. The hint is still there for screen
|
|
982
|
+
readers, as visually hidden text that the group and every field are
|
|
983
|
+
described by. The host's reset calls `onChange(undefined)`, as the bar's own
|
|
984
|
+
does.
|
|
985
|
+
|
|
986
|
+
```tsx
|
|
987
|
+
<WidthsBar label="Widths" count={count} value={row.columnShares} onChange={save} footer={false} />
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
While a field in a narrow segment is focused and grown, a neighbour's label
|
|
991
|
+
that it covers even in part is hidden whole, so only whole numbers are ever
|
|
992
|
+
seen.
|
|
939
993
|
A row that chose nothing shows equal widths and stores nothing; "Reset to
|
|
940
994
|
equal" calls `onChange(undefined)` and never stores 34/33, because no value
|
|
941
995
|
renders as the exact `33.34% / 33.33% / 33.33%` strings saved rows carry.
|
|
@@ -957,13 +1011,33 @@ equal strings and the equal panel figures.
|
|
|
957
1011
|
each face in itself. The `label` still names, filters and fills the field.
|
|
958
1012
|
`Select` takes the same, and `renderValue(chosen)` draws its closed field.
|
|
959
1013
|
|
|
1014
|
+
**A server search that is running, or has failed.** A `SearchableSelect` fed
|
|
1015
|
+
by `onSearchChange` can say both. `loading` puts "Searching..." on a line
|
|
1016
|
+
above the options, in place of "No matches". `error` puts the caller's words
|
|
1017
|
+
there instead ("Couldn't search your catalog."), and `onRetry` adds a "Try
|
|
1018
|
+
again" button to that line; its press keeps focus in the field and the list
|
|
1019
|
+
open. Options still listed (the last answer, or a search that half failed)
|
|
1020
|
+
stay under the line and stay pickable. `loading` wins over `error`. Without
|
|
1021
|
+
them the list is what it was.
|
|
1022
|
+
|
|
1023
|
+
**A typed value only when it is picked.** `SearchableSelect`'s
|
|
1024
|
+
`allowFreeText` saves the typed text on Enter, Tab, blur and a press outside.
|
|
1025
|
+
For a field where typing is mostly searching — a PO number, a shipment —
|
|
1026
|
+
`` freeTextOption={text => `Use “${text}” as manual PO#`} `` instead draws the
|
|
1027
|
+
typed text as one more row at the end of the list, in those words, and saves
|
|
1028
|
+
it only when that row is pressed or reached with the arrow keys and taken with
|
|
1029
|
+
Enter. Every other way out drops it. The row is not drawn while nothing is
|
|
1030
|
+
typed or while the text is exactly an option's label. It turns free text on by
|
|
1031
|
+
itself, and with `multiple` it adds the text and keeps the list open.
|
|
1032
|
+
|
|
960
1033
|
**Gaps the editors found in the form kit** (efficient-easel#199, harness UI-24),
|
|
961
1034
|
now in the kit itself:
|
|
962
1035
|
|
|
963
|
-
- **`Segmented`**: the chosen
|
|
964
|
-
|
|
965
|
-
is chosen (WCAG 1.4.11).
|
|
966
|
-
|
|
1036
|
+
- **`Segmented`**: the chosen option is the solid blue chip pill `Tabs` draw
|
|
1037
|
+
(owner, 2026-10-12), 4.8:1 against the light track and 3.1:1 against the
|
|
1038
|
+
dark one, so a strip of icons shows which option is chosen (WCAG 1.4.11).
|
|
1039
|
+
An option's `title` is shown in the kit's `Tooltip`, not the native
|
|
1040
|
+
attribute. A radio option also gets it as its description.
|
|
967
1041
|
- **`Input`, `Textarea`**: `readOnly` draws the read-only ground (`bg-gray-50`,
|
|
968
1042
|
`--surface-sunken` in dark). The border and focus ring do not change.
|
|
969
1043
|
`inputClasses({ readOnly: true })` gives the same look to anything else.
|
|
@@ -1017,6 +1091,25 @@ Round 4:
|
|
|
1017
1091
|
option checked), `disabled` options (greyed, skipped by the arrows, inert to
|
|
1018
1092
|
a press), and `clearable={false}` to drop the "×". Groups, flattening and the
|
|
1019
1093
|
check are shared with `Select`.
|
|
1094
|
+
- **`SearchableSelect` as a list filter, and with several values** (admin
|
|
1095
|
+
list pages). `filter` marks a field that filters a list ("All Suppliers").
|
|
1096
|
+
While it holds a value it is "on": the accent border and a light accent fill,
|
|
1097
|
+
as a set `FilterBar` chip, in light and dark. Empty or disabled, it is a
|
|
1098
|
+
plain field. With `filter` or `multiple`:
|
|
1099
|
+
- the open list may be **wider than the field** — at least the field's
|
|
1100
|
+
width, growing to fit its options up to the kit's popup width, inside the
|
|
1101
|
+
owning window (UI-11) — so long names are not cut;
|
|
1102
|
+
- **open, the field still shows what is chosen** (in the field's ink) until
|
|
1103
|
+
the person types.
|
|
1104
|
+
|
|
1105
|
+
With `multiple`, a **"Clear all"** row tops the open list while anything is
|
|
1106
|
+
chosen (not with `clearable={false}`). Closed, the field shows **the labels
|
|
1107
|
+
that fit, then "+N"** for the rest ("AA, AC, MF +3"), never a bare "…"; a
|
|
1108
|
+
field too narrow for one label and a count says "6 selected". Hovering a
|
|
1109
|
+
shortened field shows every chosen label in a `Tooltip`. A single-pick field
|
|
1110
|
+
without `filter` is unchanged. New strings: `select.clearAll`,
|
|
1111
|
+
`select.selectedCount(count)`. `Tooltip` shows no bubble while its `content`
|
|
1112
|
+
is empty.
|
|
1020
1113
|
- **`IconButton`**: `pressed` wears the pressed look without `aria-pressed`,
|
|
1021
1114
|
for a menu button that is open (`aria-expanded`), a radio, or a window's
|
|
1022
1115
|
button.
|
|
@@ -1033,6 +1126,9 @@ Round 4:
|
|
|
1033
1126
|
- **`Tabs`**: a `TabItem` takes `tooltip`, a shell `Tooltip` that opens on
|
|
1034
1127
|
hover and on focus, so the arrow keys show each tab's word in turn. Keep the
|
|
1035
1128
|
word in `label` (visually hidden) for an icon-only tab.
|
|
1129
|
+
`variant="pill"` is the same strip `Segmented` draws: the active tab is solid
|
|
1130
|
+
blue with white text on the grey track, 32px tall. The tablist roles, the
|
|
1131
|
+
arrow keys and `idPrefix` stay the tabs' own.
|
|
1036
1132
|
- **`registerModalEscapeInterceptor`** is on `efficient-shell/ui`. A window or
|
|
1037
1133
|
panel a host draws itself registers there, with the element it closes, so
|
|
1038
1134
|
Escape reaches it before the window around it and a `Tooltip` outside it
|
|
@@ -1082,25 +1178,25 @@ All exports are named — `import { Modal, ... } from 'efficient-shell'`.
|
|
|
1082
1178
|
| `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. |
|
|
1083
1179
|
| `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. |
|
|
1084
1180
|
| `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. |
|
|
1085
|
-
| `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. |
|
|
1181
|
+
| `Button`, `Input`, `Textarea`, `Select`, `Checkbox`, `Radio`, `Switch`, `FormField`, `Label` | Form controls — controlled (`value`/`onChange`); `Button`'s `variant` is `primary` (the default), `secondary` (outlined), `secondary-danger` (outlined in red: a destructive action in a footer of outlined buttons, e.g. Delete beside Archive — the same size and shape as `secondary`, red border and text, a light red wash on hover), `ghost`, `ghost-danger` (red text, no box: a destructive action among quiet `ghost` buttons), `danger` (solid red: the one action a confirm dialog asks for) or `link`; `Button` takes `pressed` (or follows `aria-pressed`) for a toggle that is on, with `IconButton`'s accent wash and ink on `ghost` and `secondary` (`secondary` keeps its border, tinted, so it does not move; `buttonClasses({ pressed })` gives a link the same); `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. |
|
|
1086
1182
|
| `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. |
|
|
1087
1183
|
| `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. |
|
|
1088
|
-
| `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. |
|
|
1184
|
+
| `Segmented` | A row of mutually exclusive options, all visible: a button group, or with `name` a real radio group that submits. It looks exactly like `Tabs variant="pill"`: the chosen option is solid blue with white text on the grey track, the others grey text. Both draw through one shared strip, so they cannot drift apart; only the roles differ (a value here, a tab panel there). `size` is `sm` / `md` / `lg` (24 / 32 / 40, the control-height ladder) or `touch` (56, as `Button size="touch"`). Icon-only: give the option its `icon` and its word as a visually hidden label (`sr-only`); the word names it and the icon is hidden from screen readers. 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. |
|
|
1089
1185
|
| `DisplayChips` | Which parts of a section are shown, one chip per part ("Breadcrumb", "Stats", "Intro text"): blue with an eye while shown, grey with a crossed eye while hidden, and a press flips it. The chips wrap, so a long list takes a few short lines instead of one switch row per part. Controlled: `items` (`{ id, label, shown, disabled?, disabledReason? }`) and `onChange(id, shown)`. The group is named by `label` or `aria-labelledby`. Each chip is a real toggle button (`aria-pressed`), named by its part and described by its state in words (`displayChips.shown` / `hidden`), with a hover label that says what a press does; Space and Enter are the button's own. A chip with `disabledReason` stays focusable, its press does nothing and the reason is said, as on `Button`. Not `Segmented` (one choice of several) and not `TagInput` (typing values in). |
|
|
1090
1186
|
| `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. |
|
|
1091
1187
|
| `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. |
|
|
1092
|
-
| `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. |
|
|
1188
|
+
| `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. `tone` says what it sits on: `neutral` (default, grey), `accent` (inside a primary or submit button: the `kbd-submit` chip, with no colour utility of its own) or `warning` (an amber button). It renders the string it is handed; which symbol a platform uses is what the constants already decide. |
|
|
1093
1189
|
| `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. |
|
|
1094
|
-
| `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. |
|
|
1190
|
+
| `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. Opt-in, per chip: `tone(value)` gives a chip `'danger'` (the status red, for a value that is chosen but wrong here) or the default `'accent'`, and `tooltip(value)` gives it the kit's `Tooltip`, shown on hover and when its × has focus. An option's `sublabel` is drawn greyed beside its label in the list (a procedure's name beside its code), and typing it finds the option. `split` (a RegExp, such as `/[\s,/]+/`) takes typed or pasted "AA, AC" as two chips: each finished piece is a chip at once, a piece that names an option (label or value, any case) takes it, and any other piece needs `allowFreeText`. |
|
|
1095
1191
|
| `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. |
|
|
1096
|
-
| `Card`, `StatCard` | Surface panel (optional header/footer; `description` puts one sentence under the title, outside the heading) + dashboard metric tile. |
|
|
1192
|
+
| `Card`, `StatCard` | Surface panel (optional header/footer; `description` puts one sentence under the title, outside the heading; `headingLevel` makes the title a heading and the card a region named by it, and an `aria-label` names the region instead when the title is not the name, such as a count; `tone="warning"` draws the border amber for a card that needs attention, and the card's words still say why; `tone="current"` marks the current card in a list with a light-blue header band, a blue title and `aria-current`) + dashboard metric tile. |
|
|
1097
1193
|
| `Avatar`, `AvatarGroup` | User avatar with initials fallback + status dot; overlapping stack with +N overflow. |
|
|
1098
1194
|
| `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. |
|
|
1099
|
-
| `Tabs`, `Accordion`, `Tooltip` | Controlled tab strip, collapsible sections,
|
|
1195
|
+
| `Tabs`, `Accordion`, `Tooltip` | Controlled tab strip, collapsible sections, solid hover/focus tooltip. `Tabs variant="pill"` draws the same strip as `Segmented` (the active tab solid blue on the grey track); use it when each choice shows its own panel, and `Segmented` when the choice is a value. 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`), seamless, and hidden from assistive technology. The bubble is SOLID, not glass, in every setting: the kit's solid popup surface (`--rosh-glass-surface`, `--rosh-glass-solid-border`, `--rosh-glass-solid-shadow`), so words behind a small bubble never mix with its own. |
|
|
1100
1196
|
| `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. |
|
|
1101
1197
|
| `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). |
|
|
1102
1198
|
| `Pagination` | Numbered page control (pairs with tables; complements `ListFooter`). |
|
|
1103
|
-
| `Sparkline`, `
|
|
1199
|
+
| `Sparkline`, `ScatterChart` | Dependency-free inline-SVG charts. `Sparkline` is the decorative trend for a tile (`currentColor`-themed). For bars use `ColumnChart`, for a trend over time `TimeSeriesChart`, for a donut `PieChart variant="ring"`. `ScatterChart` takes `xDomain` / `yDomain` to override the derived axis, and `xScale` / `yScale` of `'log'` for a long tail; a point outside a supplied domain is dropped and counted in the accessible label. |
|
|
1104
1200
|
| `BulkImportGrid` | Paste-or-upload bulk entry with column mapping, duplicate review and optional sum-merge. Hands resolved rows to `onImport`; owns no persistence. The whole panel takes a dropped CSV; the Upload CSV button opens the same dialog. Its words are the strings catalog's `bulkImport` section (a `description` prop wins), and its totals are written in the provider's `locale`. |
|
|
1105
1201
|
| `UndoProvider`, `UndoControls` | One undo stack per form window, covering its fields, line items and bulk imports. Register state with `useUndoable` and the shell shows the Undo/Redo pair itself, leftmost in the window footer, as soon as the form has state and the window is editing (a draft, a duplicate, Edit mode) or the form has said `useUndoCanEdit(true)` — a view that merely holds state gets nothing — it joins a footer that is there for other reasons and never creates one, and the mobile chrome (footer hidden) gets nothing. A form mounts `UndoControls` by hand only for a place the footer is not (a plain overlay, a section header); a hand mount claims the stack and the shell then mounts nothing for it, so a form shows one pair wherever it chose to put it. Binds ⌘Z / ⇧⌘Z (and Ctrl+Y) except while the caret is in a field, where the browser's own undo wins. `WindowManager` already mounts one per window, scoped with `windowId` so a keypress reaches only the frontmost window; pass `windowId` yourself for any provider you mount outside a `<Modal>`, or two open windows will step back together. Offered to anyone who may edit the record — gate with `canEdit` and/or `perms`; a reader sees no controls and records no history. The shell-level provider cannot know the record's permissions, so a read-only form nests its own `<UndoProvider canEdit={false}>` to shadow it. A provider nested inside a window puts its own pair in the footer on the same footer terms — it joins a footer that is there for other reasons, never creates one or holds it open, and shows nothing on the mobile chrome — but its `canEdit` takes the place of the editing check: the form that nested it has already said whether the record may be edited, so pass `canEdit={isEditing}` (or the lock) rather than leaving the default. |
|
|
1106
1202
|
| `Calendar` | Month grid with full keyboard navigation (arrows, Home/End, PageUp/Down) and `role="grid"` semantics. Single or range. Days of the months either side are quieter than the month's own but keep AA contrast: they are dates a person can press. The shared grid behind `DatePicker` and `DateRangePicker`. `weekStartsOn` defaults to the provider locale's first day of the week (`firstDayOfWeek`), Sunday without one. |
|
|
@@ -1274,8 +1370,8 @@ import 'efficient-shell/ui.css';
|
|
|
1274
1370
|
```
|
|
1275
1371
|
|
|
1276
1372
|
**It reaches nothing but `react` and `react-dom`.** None of the optional peers —
|
|
1277
|
-
no `react-router-dom`, `@tanstack/react-query`, `axios
|
|
1278
|
-
|
|
1373
|
+
no `react-router-dom`, `@tanstack/react-query`, `axios` or `@headlessui/react`
|
|
1374
|
+
— which matters because they are declared *optional*, so a
|
|
1279
1375
|
consumer who has not installed them gets an unresolvable import rather than a
|
|
1280
1376
|
degraded style. CI asserts it against the built artifact
|
|
1281
1377
|
(`scripts/verify-dist.mjs`), not just the source, because leakage can arrive
|
|
@@ -1506,7 +1602,7 @@ families the kit uses: **gray** (nearly all chrome: surfaces, borders, text),
|
|
|
1506
1602
|
**red** (danger and invalid), **green** (success), **amber** and **yellow**
|
|
1507
1603
|
(warning), one **emerald** and one **neutral** (a template's sparkline,
|
|
1508
1604
|
BrandMark's dark plate), plus `white` and `black`. Status badges and charts read
|
|
1509
|
-
their own tokens (`--status-*`, `--viz
|
|
1605
|
+
their own tokens (`--status-*`, `--viz-*`) rather than the palette.
|
|
1510
1606
|
|
|
1511
1607
|
```css
|
|
1512
1608
|
@theme {
|
|
@@ -1552,7 +1648,14 @@ maps `--color-blue-*` sets these as well:
|
|
|
1552
1648
|
|
|
1553
1649
|
Give the chip `kbd-submit` and no colour utility of its own. The dark and
|
|
1554
1650
|
accent themes remap palette utilities as `!important`, so one written beside
|
|
1555
|
-
the class (`Kbd`'s gray ones included) wins over it there
|
|
1651
|
+
the class (`Kbd`'s gray ones included) wins over it there. `<Kbd tone="accent">`
|
|
1652
|
+
is that chip, so write it in React; a bare `<kbd>` is the same chip in HTML:
|
|
1653
|
+
|
|
1654
|
+
```tsx
|
|
1655
|
+
<button className="btn-submit rounded-lg px-4 py-2 text-sm font-medium">
|
|
1656
|
+
Save <Kbd keys={CMD_ENTER} tone="accent" />
|
|
1657
|
+
</button>
|
|
1658
|
+
```
|
|
1556
1659
|
|
|
1557
1660
|
```html
|
|
1558
1661
|
<button class="btn-submit rounded-lg px-4 py-2 text-sm font-medium">
|
|
@@ -1584,7 +1687,7 @@ Tailwind colour the kit always drew:
|
|
|
1584
1687
|
| `--rosh-focus-ring-deep` | blue-500 | charts (plot, legend), a chip's remove, the date-range field |
|
|
1585
1688
|
| `--rosh-focus-ring-accent` | blue-500, follows the accent | HelpCenter's search, the prompt dialog's field |
|
|
1586
1689
|
| `--rosh-focus-ring-zone` | `rgba(59,130,246,.45)` | upload zones and thumbnails |
|
|
1587
|
-
| `--rosh-focus-ring-invalid` | red-400 at 30% | an invalid field, `ghost-danger` |
|
|
1690
|
+
| `--rosh-focus-ring-invalid` | red-400 at 30% | an invalid field, `ghost-danger`, `secondary-danger` |
|
|
1588
1691
|
| `--rosh-focus-ring-invalid-strong` | red-400 at 40% | the `danger` Button |
|
|
1589
1692
|
| `--rosh-focus-ring-invalid-solid` | red-400 | `FormErrorSummary` |
|
|
1590
1693
|
|
|
@@ -1696,8 +1799,8 @@ rows take their height from `--control-height-sm` / `-md` / `-lg` (24 / 32 /
|
|
|
1696
1799
|
`efficient-shell/ui`: `SPACING_SCALE`, `SPACING_ROLES`, `CONTROL_HEIGHTS`, and
|
|
1697
1800
|
`isSpacingStep('2.5')` for a Tailwind step.
|
|
1698
1801
|
|
|
1699
|
-
**Glass.** Menus, popups
|
|
1700
|
-
(`glassStyle()`). `--menu-opacity` (0–1, default `0.95`) sets how much shows
|
|
1802
|
+
**Glass.** Menus, popups and listboxes are frosted glass
|
|
1803
|
+
(`glassStyle()`); a `Tooltip` bubble is always solid (`solidSurfaceStyle()`). `--menu-opacity` (0–1, default `0.95`) sets how much shows
|
|
1701
1804
|
through. `rosh-reduce-transparency` on `<html>` makes every popup one solid
|
|
1702
1805
|
`--rosh-glass-surface` (default: `--surface`) with no blur — and strips
|
|
1703
1806
|
`backdrop-filter` from everything else on the page too, since it is the user's
|
|
@@ -1724,12 +1827,11 @@ grid (`--viz-grid`) is `--line-strong`, the same as the axis.
|
|
|
1724
1827
|
replaced: `--rosh-media-scrim` and `--rosh-media-scrim-button` (the dark wash
|
|
1725
1828
|
under text and buttons on an upload thumbnail), `--rosh-drop-ring` (the halo
|
|
1726
1829
|
while a file is dragged over a gallery), `--rosh-mark-plate-shadow` and
|
|
1727
|
-
`--rosh-mark-plate-shadow-strong` (`BrandMark`'s plates),
|
|
1728
|
-
(`SidebarLayout`'s handle without a custom accent)
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
(`--tl-*`) are documented where `ui.css` declares them.
|
|
1830
|
+
`--rosh-mark-plate-shadow-strong` (`BrandMark`'s plates), and
|
|
1831
|
+
`--rosh-resize-handle` (`SidebarLayout`'s handle without a custom accent).
|
|
1832
|
+
Every chart paints with one palette, `--viz-series-*`. The chart palette
|
|
1833
|
+
(`--viz-*`), the status tiers (`--status-*`) and the timeline (`--tl-*`) are
|
|
1834
|
+
documented where `ui.css` declares them.
|
|
1733
1835
|
|
|
1734
1836
|
### Languages, dates and numbers — `ShellStringsProvider`
|
|
1735
1837
|
|
|
@@ -3295,27 +3397,57 @@ that checks them, live in the EFFICIENT harness (`52-brand-surface.md`).
|
|
|
3295
3397
|
|
|
3296
3398
|
### Editorial markup — `efficient-shell/markup`
|
|
3297
3399
|
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3400
|
+
The ONE grammar for copy a human types into a plain text box — in both editors,
|
|
3401
|
+
on the published page and in the campaign email — so a toolbar button and a
|
|
3402
|
+
parser can never disagree about what a delimiter means. It moved here from
|
|
3403
|
+
efficient-easel (`src/markup`, 6d31d5d) on 2026-10-12 (Lewis, D1) and replaced
|
|
3404
|
+
the smaller grammar this subpath held before.
|
|
3301
3405
|
|
|
3302
3406
|
**Its own subpath because it imports nothing** — no React, no JSX, no DOM, none
|
|
3303
3407
|
of this package's peers. A public site can take the rule without inheriting a 3D
|
|
3304
3408
|
viewer and a PDF renderer; `dist/markup/index.js` is a standalone file with zero
|
|
3305
|
-
import statements
|
|
3409
|
+
import statements, and the `./markup` export carries a `default` condition so a
|
|
3410
|
+
build script running under `tsx` resolves it too.
|
|
3306
3411
|
|
|
3307
3412
|
What is shared is the RULE, not the rendering. A web renderer paints with
|
|
3308
3413
|
theme-token classes and an email renderer must inline every style, so each host
|
|
3309
|
-
keeps its own renderer and walks the same token list.
|
|
3414
|
+
keeps its own renderer and walks the same tree or token list.
|
|
3415
|
+
|
|
3416
|
+
| Mark | Means | Written by a button |
|
|
3417
|
+
|---|---|---|
|
|
3418
|
+
| `**x**` | **Strong** (bold) | yes, ⌘B |
|
|
3419
|
+
| `_x_` | italic, only at a word edge | yes, ⌘I |
|
|
3420
|
+
| `~~x~~` | strikethrough | yes |
|
|
3421
|
+
| `==x==` | **Emphasis** (the brand accent) | `applyMark` writes it; a copy field no longer offers it (`RETIRED_MARKS`) |
|
|
3422
|
+
| `^^x^^` | **Quiet** | `applyMark` writes it; a copy field no longer offers it (`RETIRED_MARKS`) |
|
|
3423
|
+
| `[words](href)` | a link — `http`, `https`, `mailto`, `tel` or no scheme only | yes, ⌘K |
|
|
3424
|
+
| `- ` at the start of a line | a bullet line, one level | yes |
|
|
3425
|
+
| `1. ` at the start of a line | a numbered line, one level: 1-3 digits, `. `, words; the number is kept as typed | yes |
|
|
3426
|
+
| `[x]{underline}`, `[x]{sup}`, `[x]{sub}` | underline, superscript, subscript (tree only) | `applyMark` / `toggleMark` |
|
|
3427
|
+
| `[x]{color=name}` / `[x]{color=#rrggbb}` | a theme colour by name (`isColorName`), or any colour as exactly six hex digits (`isColorValue`; saved lowercase) (tree only) | `applyColor` / `colorOver` |
|
|
3428
|
+
| `@@x@@`, `++x++` | Emphasis, Strong (the storefront's old word styles). `++` opens only at a word start, so `A++ rated tyres, A++ service` is plain text | no — read only |
|
|
3429
|
+
| `*x*`, `#x#`, `[x]` | legacy accent, bold, italic (per product set) | no — read only |
|
|
3430
|
+
|
|
3431
|
+
The three names a merchant sees are in `MARK_NAMES`. A run never crosses a line
|
|
3432
|
+
break, and runs, spans and links nest up to four levels
|
|
3433
|
+
(`[**_[x]{sup}_**]{color=accent}`); a fifth stays as typed. Any block after `]`
|
|
3434
|
+
that is not exactly one of the four (`[x]{foo}`, `[x]{color=#f00}`,
|
|
3435
|
+
`[x]{{first_name}}`) is plain text. The spans and the numbered line are read by
|
|
3436
|
+
`parseMarkup` only: `tokenizeInline` keeps them as text, so a renderer that has
|
|
3437
|
+
not learned them prints the delimiters.
|
|
3310
3438
|
|
|
3311
3439
|
| Export | Notes |
|
|
3312
3440
|
|---|---|
|
|
3313
|
-
| `
|
|
3314
|
-
| `
|
|
3315
|
-
| `
|
|
3316
|
-
| `stripInline(value, rules)` | The
|
|
3317
|
-
| `
|
|
3318
|
-
| `
|
|
3441
|
+
| `parseMarkup(value, rules)` → `MarkupLine[]` | The whole grammar as a tree. Each run keeps the delimiters that were saved. |
|
|
3442
|
+
| `serializeMarkup(lines)` | The inverse: `serializeMarkup(parseMarkup(s, rules)) === s`, for every string. Pinned by `tests/markupRoundTrip.test.ts`; `tests/markupSameAnswers.test.ts` holds 1,609 strings from the repos' own code and tests whose answers were recorded once from easel's copy. |
|
|
3443
|
+
| `isSafeLinkHref(href)` | The link rule. Call it before writing any `href`. |
|
|
3444
|
+
| `tokenizeInline(value, rules)`, `stripInline(value, rules)` | The flat reading older renderers use: runs only, no links, no nesting. Tokens ALTERNATE, starting and ending with a `text` token that may be empty. `stripInline` is the only correct source for an `alt`, an `aria-label` or a structured-data field. |
|
|
3445
|
+
| `applyMark(text, start, end, style, rules?, placeholders?)`, `markedAt`, `MARKUP_TOOLS`, `markupTools`, `COPY_FIELD_TOOLS`, `MARK_PLACEHOLDERS` | The writer behind a toolbar button (pressing the same button again unwraps), whether a button is pressed, and the button descriptors. A copy field offers bold, italic and strike. |
|
|
3446
|
+
| `applyColor(text, start, end, name)`, `isColorName`, `isColorValue`, `spanFor` | A colour carries a value, so it has its own writer: it writes, changes and (`null`) takes off `[x]{color=name}`, and changes nothing for a value `isColorValue` refuses (`#f00`, `rgb()`, a typo). |
|
|
3447
|
+
| `findLink`, `writeLink`, `removeLink` | The link window's reading and writing, with the parser's own link rule. |
|
|
3448
|
+
| `markedLines`, `writeMarkedLines`, `sameMarks` | The saved string as characters, each carrying its runs, and back byte for byte — what an editor that hides the delimiters edits. |
|
|
3449
|
+
| `toggleMark`, `marksIn`, `linkOver`, `unlinkAt`, `colorOver`, `colorIn`, `setList`, `listIn`, `clearMarks`, `clearStyle` | Writers that act on the WORDS whichever runs they sit in, splitting a run rather than crossing a delimiter, and the readers a toolbar draws its pressed state from. |
|
|
3450
|
+
| `STANDARD_MARKUP`, `STOREFRONT_MARKUP`, `CAMPAIGN_MARKUP`, `RETIRED_MARKS` | The rule sets. The last two add each product's LEGACY runs, designed to be deleted once stored content has been converted. |
|
|
3319
3451
|
|
|
3320
3452
|
Two delimiter choices worth knowing. Italic is `_phrase_`, not `*phrase*`,
|
|
3321
3453
|
because a single asterisk already means the accent color in the products that
|
|
@@ -3414,6 +3546,58 @@ so an app importing from both has one hook.
|
|
|
3414
3546
|
| `FileIntakeAlert` | The rejection list as `role="alert"`, styled with Tailwind utilities (`text-xs text-red-600`). A page whose Tailwind does not scan this package renders `rejections` itself, as above — keep the `role="alert"`. |
|
|
3415
3547
|
| `FileIntakeOptions`, `FileIntakeLimits`, `FileRejection`, `FileRejectionReason` | Types. |
|
|
3416
3548
|
|
|
3549
|
+
### Web vitals and errors — `startClientTelemetry` (`efficient-shell/telemetry`)
|
|
3550
|
+
|
|
3551
|
+
Field performance and client errors from a portal, sent to the backend's
|
|
3552
|
+
first-party intake (`POST /api/client-telemetry/web-batches/`, efficient-backend
|
|
3553
|
+
docs/adr/0057). Call it once, after sign-in is known; the host decides whether
|
|
3554
|
+
it runs, what the app is called and what a route is.
|
|
3555
|
+
|
|
3556
|
+
```ts
|
|
3557
|
+
import { startClientTelemetry } from 'efficient-shell'; // or 'efficient-shell/telemetry'
|
|
3558
|
+
|
|
3559
|
+
const telemetry = startClientTelemetry({
|
|
3560
|
+
app: 'admin-portal', // the intake's web app code
|
|
3561
|
+
release: APP_VERSION, // this build's version
|
|
3562
|
+
getAccessToken: () => getAccessToken(),// in-memory bearer token; null = send nothing
|
|
3563
|
+
routeKey: () => currentRouteTemplate(),// '/sales-orders/:id' or a window key, never the URL
|
|
3564
|
+
enabled: true, // the host's switch
|
|
3565
|
+
vitalsSampleRate: 1, // share of page loads whose vitals are collected
|
|
3566
|
+
});
|
|
3567
|
+
// telemetry.flush(), telemetry.reportError(error), telemetry.stop()
|
|
3568
|
+
```
|
|
3569
|
+
|
|
3570
|
+
What it collects:
|
|
3571
|
+
|
|
3572
|
+
- **Web vitals** — LCP, INP, CLS, FCP and TTFB through `web-vitals` 6.2.3,
|
|
3573
|
+
bundled (the host installs nothing). LCP, FCP and TTFB are filed under the
|
|
3574
|
+
route the page loaded on; INP and CLS under the route when they are reported.
|
|
3575
|
+
The device class (`desktop`, `tablet`, `mobile`) comes from the viewport and
|
|
3576
|
+
the pointer.
|
|
3577
|
+
- **Errors** — window `error`, `unhandledrejection`, and the render errors both
|
|
3578
|
+
`ErrorBoundary` and `WindowErrorBoundary` catch (a consumer's own `onError`
|
|
3579
|
+
still runs beside it). Each error is the class, a redacted message (no
|
|
3580
|
+
emails, tokens, URLs, UUIDs or record numbers; 160 characters) and the top
|
|
3581
|
+
five frames as `path:line:column`, unsymbolicated. The same error is sent once
|
|
3582
|
+
a page load with a count, and at most 20 distinct errors a page load.
|
|
3583
|
+
|
|
3584
|
+
How it sends: queued, then posted every minute and when the page is hidden or
|
|
3585
|
+
left, in batches of at most 50 events and 30 KB, with
|
|
3586
|
+
`fetch(…, { keepalive: true })` and the bearer token. Not `sendBeacon`, which
|
|
3587
|
+
cannot carry an `Authorization` header. A failed send is dropped and never
|
|
3588
|
+
retried. No user, URL or query string ever leaves the page; a raw path the host
|
|
3589
|
+
passes as a route is cleaned (query gone, ids made `:id`).
|
|
3590
|
+
|
|
3591
|
+
**The subpath reaches no package at all**, so dealer and POS, which take only
|
|
3592
|
+
`efficient-shell/ui`, can report too. `web-vitals` is a pinned devDependency
|
|
3593
|
+
bundled into it (`tests/telemetryEntryBundlesWebVitals.test.ts`); its licence
|
|
3594
|
+
is in `THIRD_PARTY_NOTICES.md`.
|
|
3595
|
+
|
|
3596
|
+
| Export | Notes |
|
|
3597
|
+
|---|---|
|
|
3598
|
+
| `startClientTelemetry(config)` | Starts collecting; a second call replaces the first. Returns `{ flush, reportError, stop }`. `enabled: false`, a missing `window` or an `app`/`release` that is not version-like starts nothing. |
|
|
3599
|
+
| `ClientTelemetryConfig`, `ClientTelemetry` | Types. The subpath also exports the wire types (`TelemetryBatch`, `TelemetryEvent`, `WebVitalEvent`, `ClientErrorEvent`, `ErrorSource`, `DeviceClass`). |
|
|
3600
|
+
|
|
3417
3601
|
### Open pages as tabs — `efficient-shell/page-tabs`
|
|
3418
3602
|
|
|
3419
3603
|
For a routed app — a sidebar, a header, one page at a time — that wants what
|
|
@@ -3558,7 +3742,7 @@ read a stored focus on a server.
|
|
|
3558
3742
|
| `severityOf(value, warn?, crit?)` | The `SeverityTone` (`success` \| `warning` \| `danger`) a reading earns against **inclusive** bounds; `null` when there's no reading or no usable bounds — the shell hardcodes no threshold. Backs `MetricBar`; use it to roll a `SidebarNavItem severity` up. |
|
|
3559
3743
|
| `isSeverityTone(value)` | Type guard for the three tones. Validate a backend rollup with it at the fetch boundary, where a bad token can still be reported against its payload, rather than letting it surface as a wrong pixel. |
|
|
3560
3744
|
| `toast.success / .error / .info` | Toast notifications — auto-mounts container. Each plays the user's chime; `toast.configure({ sound: false })` keeps them quiet for a host whose screen makes sounds of its own (an alarm, a till's beep), and `{ sound: true }` on one toast still asks for it. |
|
|
3561
|
-
| `Z_LAYERS` | The shell's z-index scale: `window`
|
|
3745
|
+
| `Z_LAYERS` | The shell's z-index scale, in hundreds (owner ruling, 2026-10-12), bottom to top: `raised` / `sticky` / `inBox` 10 / 20 / 30 (inside one box only), `snapPreview` 900, `window` 1000 (+ `windowStep` 2 per window above), `peek` 1999, `pinnedWindow` 2000, `desktopWidget` 2100, `taskbar` 2200, `sidebar` 2300, `taskbarPreview` 2350, `startMenu` 2400, `restoreTab` 2500, `expose` 2600, `overlay` 2700 (the palette, `Dialog`, `Drawer`, the shortcut sheet, startup and logout), `toast` 2800 (toasts, the notification card, the update banner), `popup` = `menu` 2900 (context menus, portalled listboxes, calendars, the server-status card) and `hint` 3000 (tooltips). Menus and popups sit above overlays and toasts, so one opened inside a dialog is never drawn behind it; a pinned window stays under the taskbar and every menu. Apply as inline `zIndex`, or in markup as `z-(--z-<layer>)` from the matching `--z-*` custom property in `ui.css` — an interpolated `z-[…]` class is invisible to Tailwind's scan. Never write a stack order above 30 as a number: name the layer, and add a new one between two hundreds. |
|
|
3562
3746
|
| `dismissPopups()`, `OVERLAY_OPEN_EVENT` | An overlay calls `dismissPopups()` as it opens; every open kit popup (`Select`, `SearchableSelect`, `TagInput`, `DatePicker`, `DateRangePicker`, `PopupMenu` via its `onClose`, `ServerStatusIndicator`) closes rather than floating over the overlay. The kit's own overlays already call it. A consumer's own popup listens for `OVERLAY_OPEN_EVENT` on `window` while open. |
|
|
3563
3747
|
| `Kbd` constants — `MOD`, `ALT`, `SHIFT`, `ENTER`, `ALT_SHIFT_E`, `CMD_K`, … | Symbol constants for rendering keyboard shortcuts. |
|
|
3564
3748
|
|
|
@@ -3590,7 +3774,7 @@ root entry. They import React and nothing else.
|
|
|
3590
3774
|
icon in every repository (2026-10-10). `<Icon name>` is typed to it, so a
|
|
3591
3775
|
misspelt meaning does not compile.
|
|
3592
3776
|
|
|
3593
|
-
**
|
|
3777
|
+
**The default set.** `efficient` is the company's default icon set (Lewis 2026-10-12). Lewis
|
|
3594
3778
|
picked the drawing of every meaning himself: the heroicons outline drawing where
|
|
3595
3779
|
heroicons has the meaning, and our own drawing in the heroicons style (24 grid,
|
|
3596
3780
|
1.5 line, round ends) where it has not. One meaning, one drawing: a name that
|
|
@@ -3606,19 +3790,40 @@ look="pencil-square" />`). The looks are a subpath of their own, so a host that
|
|
|
3606
3790
|
draws only the default does not bundle them. A meaning without the look asked
|
|
3607
3791
|
for draws its default.
|
|
3608
3792
|
|
|
3793
|
+
**A part a host can fill: the `swatch`.** In every look of `text-color`, the
|
|
3794
|
+
letter A itself is the swatch: one closed path (the letter and its counter)
|
|
3795
|
+
marked `data-part="swatch"`. A host paints the chosen colour into it with CSS
|
|
3796
|
+
(`.my-color-button [data-part="swatch"] { fill: var(--chosen) }`). The outline
|
|
3797
|
+
look keeps its ink line round the fill, so a light colour still shows on a
|
|
3798
|
+
light toolbar; with no such rule it draws a hollow A, so "no colour" is told
|
|
3799
|
+
apart from black.
|
|
3800
|
+
|
|
3609
3801
|
| Import | Set | Draws | Needs |
|
|
3610
3802
|
|---|---|---|---|
|
|
3611
3803
|
| `efficient-shell/icons/efficient` | `efficient` | every meaning, default look | nothing |
|
|
3612
3804
|
| `efficient-shell/icons/efficient-looks` | `efficient` (its other looks) | `solid`, `mini` and the kept looks | nothing |
|
|
3613
|
-
| `efficient-shell/icons/heroicons` | `heroicons` | **deprecated**: the 197 meanings it drew | `@heroicons/react` |
|
|
3614
|
-
| `efficient-shell/icons/lucide` | `lucide` | **deprecated**: the 197 meanings it drew | `lucide-react` (0.577 or later) |
|
|
3615
3805
|
|
|
3616
|
-
|
|
3617
|
-
|
|
3806
|
+
No icon package is behind it: the shell imports none, and no host needs
|
|
3807
|
+
`@heroicons/react` or `lucide-react` for it. Heroicons
|
|
3808
|
+
drawings are copied under the MIT licence: see `THIRD_PARTY_NOTICES.md`.
|
|
3809
|
+
|
|
3810
|
+
**More sets: added beside the default.** `efficient` is the default set (Lewis
|
|
3811
|
+
2026-10-12: the set he picked is the default, and we add other sets later). A
|
|
3812
|
+
new set is one module that exports an `IconSet` (its `name`, `label` and the
|
|
3813
|
+
meanings it draws, with `looks` if it has them), registered after the default:
|
|
3814
|
+
|
|
3815
|
+
```tsx
|
|
3816
|
+
registerIconSet(efficient); // the default: every meaning
|
|
3817
|
+
registerIconSet(sketch); // { name: 'sketch', label: 'Sketch', icons: { trash: SketchTrash, … } }
|
|
3818
|
+
```
|
|
3618
3819
|
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3820
|
+
A set need not draw every meaning. Whatever the chosen set does not draw falls
|
|
3821
|
+
back to the default set's drawing, so a new set can start with a handful of
|
|
3822
|
+
icons and never leaves a blank. The first set registered is the default
|
|
3823
|
+
(`setDefaultIconSet(name)` changes that); after the chosen set and the default,
|
|
3824
|
+
every other registered set is asked, which is how a product's own glyphs are
|
|
3825
|
+
found. With a `look`, each set is asked for that look and then for its own
|
|
3826
|
+
default drawing, so a chosen set keeps its style.
|
|
3622
3827
|
|
|
3623
3828
|
**Part of the theme.** The `icon_set` preference sits beside the colour theme
|
|
3624
3829
|
and the corners: `useTheme` and `applyThemePrefs({ icon_set })` choose the set
|
|
@@ -3633,16 +3838,16 @@ drawn in the set that is active. It appears only when a host has registered
|
|
|
3633
3838
|
more than one set, since one set is nothing to choose between.
|
|
3634
3839
|
|
|
3635
3840
|
**The shell's own icons follow the set too.** A window's close button, the
|
|
3636
|
-
sidebar's chevrons, a banner's tone, a number box's steppers:
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3841
|
+
sidebar's chevrons, a banner's tone, a number box's steppers: every icon the
|
|
3842
|
+
shell draws itself is `<Icon name>` (with a `look` where it is filled), so the
|
|
3843
|
+
chosen set draws them. A host that registered no set still sees them: `<Icon>`
|
|
3844
|
+
falls back to `src/icons/kit.tsx` (not a public entry), which the generator
|
|
3845
|
+
writes from the default set's source with exactly the meanings and looks the
|
|
3846
|
+
shell's parts name, so the kit does not bundle the whole set. Size, colour and stroke width stay with the
|
|
3847
|
+
place. What is left inline is not an icon by meaning: charts, spinners,
|
|
3643
3848
|
illustrations, an app's tool palette, a path a host supplies.
|
|
3644
3849
|
`tests/shellIconFallbacks.test.tsx` holds each file to its count; a new icon in
|
|
3645
|
-
the shell is `<Icon name="…"
|
|
3850
|
+
the shell is `<Icon name="…" />`, and `npm run icons` adds it to the kit.
|
|
3646
3851
|
|
|
3647
3852
|
**A product's own glyphs.** An editor's block types and settings rows are not
|
|
3648
3853
|
in the catalogue. Register them and draw them by `glyph`, with no icon package
|
|
@@ -3661,15 +3866,14 @@ whatever the theme.
|
|
|
3661
3866
|
**Adding or changing a drawing.** `src/icons/efficient.json` is the source: per
|
|
3662
3867
|
name its meaning, where the drawing came from, the default SVG (24 by 24
|
|
3663
3868
|
viewBox, 1.5 stroke on `currentColor`, round ends, transparent background) and
|
|
3664
|
-
its looks, or the name it is an alias of.
|
|
3665
|
-
|
|
3869
|
+
its looks, or the name it is an alias of. Run `npm run icons` and commit the
|
|
3870
|
+
lot. Look
|
|
3666
3871
|
before you keep a drawing: `node scripts/icons/glyph-sheet.mjs <new>
|
|
3667
3872
|
<neighbours…>` draws it at 16px beside the glyphs it could be mistaken for, and
|
|
3668
3873
|
`tests/efficientSet.test.tsx` fails it if it looks like another meaning.
|
|
3669
3874
|
|
|
3670
|
-
With no set registered, `<Icon>` draws nothing
|
|
3671
|
-
|
|
3672
|
-
through a built-in fallback, so a host that registers nothing sees no change.
|
|
3875
|
+
With no set registered, `<Icon>` draws nothing for a meaning the shell's own
|
|
3876
|
+
parts do not use, and warns once per meaning. Register `efficient` at startup.
|
|
3673
3877
|
|
|
3674
3878
|
### The UI kit check — `efficient-shell-ui-check`
|
|
3675
3879
|
|
|
@@ -3715,6 +3919,8 @@ itself where the kit has an answer:
|
|
|
3715
3919
|
| `icon` | an import of an icon package (`@heroicons`, `lucide-react`, `@tabler/icons`, …) | `<Icon name>` from `efficient-shell/icons` |
|
|
3716
3920
|
| `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 |
|
|
3717
3921
|
| `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 |
|
|
3922
|
+
| `chart` | an import of a second chart library: `recharts`, `chart.js`, `react-chartjs-2`, `d3` and every `d3-*` module (one imported only for its maths too), `@nivo/*`, `victory` and `victory-*`, `echarts`, `echarts-for-react`, `apexcharts`, `react-apexcharts`, `@visx/*`, `highcharts`, `highcharts-react-official`, `plotly.js`, `plotly.js-dist`, `react-plotly.js`, `lightweight-charts`, `@antv/g2`, `@antv/g2plot`, `chartist`, `billboard.js`, `c3`, `uplot`. A name counts with its subpaths (`chart.js/auto`); a name that only contains one (`react-d3-tree`) does not. Read after `kit` and `icon`, so `@mui/x-charts` stays a `kit` | a chart from `efficient-shell/ui`: `ChartFrame`, `TimeSeriesChart`, `ColumnChart`, `RankedBars`, `PieChart`, `FunnelChart`, `ScatterChart`, …; the one chart system (harness FT-3) |
|
|
3923
|
+
| `chart-dep` | one of those chart libraries declared in `package.json`, whether or not anything imports it (not `devDependencies`) | remove it |
|
|
3718
3924
|
|
|
3719
3925
|
Known gaps: `rgb()` / `hsl()` colours, colours in `.ts` maps, and
|
|
3720
3926
|
`createElement('button')` are not counted.
|