efficient-shell 5.2.0 → 5.6.1
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 +165 -33
- package/THIRD_PARTY_NOTICES.md +32 -0
- package/dist/{Browser-FKZ6M2MK.js → Browser-RXGEZ77D.js} +17 -17
- package/dist/Calculator-MVPKTZZF.js +29 -0
- package/dist/ConfirmDialog-XOYCYFA7.js +1 -0
- package/dist/{CurrencyConverter-OV5ZQIAJ.js → CurrencyConverter-2IGOOR3I.js} +8 -8
- package/dist/{Documents-HGGL3AWI.js → Documents-YBLR7WVX.js} +27 -27
- package/dist/Files-7XNR5IL2.js +87 -0
- package/dist/{Notepad-PO724TVP.js → Notepad-ZQYQXV75.js} +13 -13
- package/dist/PdfViewer-SQVMMA7B.js +1 -0
- package/dist/{PomodoroTimer-LN5MYRZO.js → PomodoroTimer-XIUN4M73.js} +19 -19
- package/dist/{Preview-CUOO5WQ7.js → Preview-YCOA5JPP.js} +129 -129
- package/dist/Sidebar-VI7E3EKI.js +44 -0
- package/dist/{Spreadsheet-2N2JXBCY.js → Spreadsheet-SRAUU3OA.js} +14 -14
- package/dist/{Stock-65WVUGLI.js → Stock-7EKOAQ7C.js} +3 -3
- package/dist/{Weather-TXJICMJN.js → Weather-RWI6NKB2.js} +16 -16
- package/dist/{WorldClock-RIHLKZGC.js → WorldClock-CEHWDFEO.js} +6 -6
- package/dist/apps/index.js +1 -1
- package/dist/bin/ui-check.js +1 -1
- package/dist/brand.css +1 -1
- package/dist/{chunk-TKFAFZRY.js → chunk-3HJTOINK.js} +1 -1
- package/dist/{chunk-YJ6JMGMS.js → chunk-4SWPNARR.js} +4 -4
- package/dist/{chunk-Y553QKEH.js → chunk-5ABLFFC5.js} +15 -15
- package/dist/{chunk-JJIPGAXI.js → chunk-6LTDJKIL.js} +72 -71
- package/dist/{chunk-BFFXVA53.js → chunk-7WC3EOFS.js} +1 -1
- package/dist/chunk-AP2QEJXQ.js +1 -0
- package/dist/{chunk-3PTA2C74.js → chunk-CLPJDJH4.js} +1 -1
- package/dist/{chunk-R3622MOL.js → chunk-F6B3J3GW.js} +1 -1
- package/dist/{chunk-UG4ZPXXU.js → chunk-GG7AE6OC.js} +1 -1
- package/dist/{chunk-MTH7EAH3.js → chunk-IMMHJIV5.js} +21 -21
- package/dist/{chunk-ML22KF7T.js → chunk-JXAAZGLJ.js} +1 -1
- package/dist/{chunk-BFUBBDU6.js → chunk-KXAUDC3A.js} +26 -26
- package/dist/{chunk-KHJ5XSEH.js → chunk-N3A3GWJI.js} +8 -8
- package/dist/{chunk-UAJQB45Y.js → chunk-P4CIOJHP.js} +3 -3
- package/dist/{chunk-6GV4467L.js → chunk-RBDGN4YU.js} +5 -5
- package/dist/chunk-RC2EG6P4.js +1026 -0
- package/dist/{chunk-YQI7KXX3.js → chunk-RL626E7Q.js} +2 -2
- package/dist/{chunk-HIAHERCD.js → chunk-RMJZOKHY.js} +9 -9
- package/dist/chunk-SFCDHHQP.js +1 -0
- package/dist/{chunk-M4OGNYOM.js → chunk-SKB6L53D.js} +11 -11
- package/dist/{chunk-H3UEYXRA.js → chunk-TFETN2GI.js} +1 -1
- package/dist/chunk-TNBCUZVE.js +1 -0
- package/dist/{chunk-TLTFS2D6.js → chunk-VWBQ2P5G.js} +56 -56
- package/dist/chunk-WOEFPBQF.js +1 -0
- package/dist/{chunk-ASSDCWKP.js → chunk-WWF2EQXF.js} +1 -1
- package/dist/chunk-XWSWYMGM.js +2 -0
- package/dist/{chunk-SSBOC52S.js → chunk-YGC7AX3X.js} +9 -9
- package/dist/{chunk-QZNRMJE6.js → chunk-YIH6JLMV.js} +2 -2
- package/dist/{chunk-OTP5WPI4.js → chunk-Z7D6Y5WA.js} +16 -16
- package/dist/file-intake/index.js +1 -1
- package/dist/format/index.d.ts +1 -1
- package/dist/format/index.js +1 -1
- package/dist/i18n/index.d.ts +1 -1
- package/dist/icons/efficient-looks.d.ts +28 -0
- package/dist/icons/efficient-looks.js +920 -0
- package/dist/icons/efficient.d.ts +746 -298
- package/dist/icons/efficient.js +336 -470
- package/dist/icons/heroicons.d.ts +1 -1
- package/dist/icons/index.d.ts +10 -4
- package/dist/icons/index.js +1 -1
- package/dist/icons/lucide.d.ts +1 -1
- package/dist/{index-DpUDK9zG.d.ts → index-CHufemKB.d.ts} +69 -1
- package/dist/{index-Dk5VKml6.d.ts → index-ZI6vl2Wq.d.ts} +367 -78
- package/dist/index.d.ts +5 -5
- package/dist/index.js +231 -231
- package/dist/locales/zh-Hans.d.ts +1 -1
- package/dist/locales/zh-Hans.js +1 -1
- package/dist/map/index.js +7 -7
- package/dist/markdown/index.js +1 -1
- package/dist/page-tabs/index.js +5 -5
- package/dist/pdf/index.js +1 -1
- package/dist/{registry-FHdUeVww.d.ts → registry-J-TU7zKx.d.ts} +14 -4
- package/dist/{strings-aKfRDZWB.d.ts → strings-BVB64Nck.d.ts} +26 -0
- package/dist/ui/index.d.ts +4 -4
- package/dist/ui/index.js +1 -1
- package/dist/ui.css +61 -1
- package/package.json +8 -2
- package/dist/Calculator-LBFBTFSD.js +0 -29
- package/dist/ConfirmDialog-JAEFVRJM.js +0 -1
- package/dist/Files-DAQKIF6W.js +0 -87
- package/dist/PdfViewer-6ALXGVTV.js +0 -1
- package/dist/Sidebar-T4ABK2WV.js +0 -44
- package/dist/chunk-4QBLTEFU.js +0 -1
- package/dist/chunk-AJB5G52B.js +0 -1
- package/dist/chunk-FWOPZB6I.js +0 -1
- package/dist/chunk-PS6PSCOR.js +0 -1018
- 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
|
|
@@ -602,7 +602,9 @@ end. `step` (default 1) is the range's step and the box's ArrowUp/ArrowDown.
|
|
|
602
602
|
`precision` (default: the decimals in `step`) rounds a value. A number typed on
|
|
603
603
|
its way somewhere else ("2" of "25" in a 10–100 box) is not written until it is
|
|
604
604
|
in bounds. Enter or blur commits it: snapped to `step` counted from the box's
|
|
605
|
-
lower bound (`inputMin`, default `min`), then clamped.
|
|
605
|
+
lower bound (`inputMin`, default `min`), then clamped. The snap is float-safe
|
|
606
|
+
and an exact half step goes away from zero: 1.15 at step 0.1 is 1.2, and
|
|
607
|
+
-1.15 is -1.2. Escape puts back the
|
|
606
608
|
value from before the box was entered, including numbers written live while
|
|
607
609
|
typing. These are `InputNumber`'s keys (`snap`, `allowEmpty`), not a copy.
|
|
608
610
|
`emptyLabel` lets the value be `null`: the box shows the word, and emptying it
|
|
@@ -719,6 +721,51 @@ kit `Input` on top and a link row under it, in one box. The row opens the
|
|
|
719
721
|
window under the box, in the panel's own flow. Words come from the `link`
|
|
720
722
|
section of the strings catalogue; `placeholder` sets the address box's text.
|
|
721
723
|
|
|
724
|
+
**`CornersInput`** is the Corners row of a settings panel: the radius in a
|
|
725
|
+
number box (`InputNumber`, an "R" before the number and px after it, compact
|
|
726
|
+
steppers), then four corner buttons, on one row. The four are a SET (`role="group"` of `aria-pressed` `IconButton`s):
|
|
727
|
+
any number may be on, and a press toggles one. Each draws the `corner` glyph
|
|
728
|
+
turned to its corner (`tl` · `tr` · `br` · `bl`) and shows its name in a
|
|
729
|
+
`Tooltip`. Not the theme's `Corners` type, which is the portal-wide corner style.
|
|
730
|
+
|
|
731
|
+
```tsx
|
|
732
|
+
<CornersInput label="Corners" value={sides} onChange={setSides}
|
|
733
|
+
corners={[{ value: 'tl', label: 'Top left' }, { value: 'tr', label: 'Top right' },
|
|
734
|
+
{ value: 'br', label: 'Bottom right' }, { value: 'bl', label: 'Bottom left' }]}
|
|
735
|
+
radius={radius} onRadiusChange={setRadius} radiusPlaceholder="8" />
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
The host owns the words and the values, and resolves "nothing stored" before
|
|
739
|
+
handing `value` in: an empty set means every corner square. Leave out
|
|
740
|
+
`onRadiusChange` and the row is the four corners alone. A corner with
|
|
741
|
+
`disabled` keeps its cell, stays focusable (`aria-disabled`) and says its
|
|
742
|
+
`title` as the reason; `disabled` + `disabledReason` turn the whole row off the
|
|
743
|
+
same way. `label` names the group and is not drawn: the row around it draws
|
|
744
|
+
the label.
|
|
745
|
+
|
|
746
|
+
**`AlignmentGrid`** is the 3×3 position grid — a block's place in its box, a
|
|
747
|
+
picture's focal crop, or any other one-of-nine. It is a `radiogroup` of nine
|
|
748
|
+
real buttons (`role="radio"`, `aria-checked`), each named by its position and
|
|
749
|
+
showing that name in a `Tooltip`. The cells are 36px squares with a line
|
|
750
|
+
between them; each draws a grey dot, and the chosen one is a blue square on a
|
|
751
|
+
light blue cell.
|
|
752
|
+
|
|
753
|
+
```tsx
|
|
754
|
+
<AlignmentGrid label="Placement" value={place} onChange={setPlace}
|
|
755
|
+
points={[{ value: 'tl', label: 'Top left' }, { value: 'tc', label: 'Top' }, /* … nine, in reading order */]} />
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
A point may carry an `icon` (an arrow from the icon set), drawn at 16px
|
|
759
|
+
instead of its dot. A point with `disabled` keeps its cell and says its
|
|
760
|
+
`title` as the reason; `disabled` + `disabledReason` turn the whole grid off.
|
|
761
|
+
Controlled: a press asks `onChange` and the grid lights nothing by itself.
|
|
762
|
+
|
|
763
|
+
The grid is ONE Tab stop (the chosen cell, else the first open one), the
|
|
764
|
+
WAI-ARIA radio group pattern with a roving tabindex. Arrows move focus in two
|
|
765
|
+
dimensions and choose the cell they land on, as native radios do; they stop at
|
|
766
|
+
the grid's edge (no wrap). Space or Enter presses the focused cell. An arrow
|
|
767
|
+
reaches a closed cell, so its reason is heard, but chooses nothing there.
|
|
768
|
+
|
|
722
769
|
**`Tree`**, **`TreeRow`**, **`TreeGroup`**, **`TreeGroupLabel`**,
|
|
723
770
|
**`TreeEmptySlot`** and **`TreeSeam`** draw a layers panel or an outline. They follow the WAI-ARIA
|
|
724
771
|
tree pattern.
|
|
@@ -875,6 +922,36 @@ End go to `min`/`max`, and Enter or a double-click resets to `defaultWidth`. A
|
|
|
875
922
|
drag starts after 3px, so a click never nudges it. `storageKey` restores the
|
|
876
923
|
width after mount and keeps it once a drag settles. Its hint is a `Tooltip`.
|
|
877
924
|
|
|
925
|
+
**`WidthsBar`** sets how a row's columns share its width: a bar split into
|
|
926
|
+
the columns, a handle between each pair, and a typed % box for each column.
|
|
927
|
+
The last column fills itself. It came from the campaign designer's "Column
|
|
928
|
+
widths" field (efficient-admin-portal), which is deleted once this ships.
|
|
929
|
+
|
|
930
|
+
```tsx
|
|
931
|
+
<InputNumber aria-label="Columns" value={count} onChange={n => n && setCount(n)} min={1} max={4} />
|
|
932
|
+
<WidthsBar label="Widths" count={count} value={row.columnShares} onChange={shares => save({ columnShares: shares })} />
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
The value is `ColumnShares`: the first `count - 1` shares, whole percentages.
|
|
936
|
+
The last column is what the others leave, so the widths always add up. No
|
|
937
|
+
column goes under `COLUMN_SHARE_MIN` (10%): raising a box squeezes the columns
|
|
938
|
+
after it, and a handle trades width with its neighbour and stops at the floor.
|
|
939
|
+
A row that chose nothing shows equal widths and stores nothing; "Reset to
|
|
940
|
+
equal" calls `onChange(undefined)` and never stores 34/33, because no value
|
|
941
|
+
renders as the exact `33.34% / 33.33% / 33.33%` strings saved rows carry.
|
|
942
|
+
`count` is 1 to 4; a value means something at one count only and is inert at
|
|
943
|
+
the others, so hiding the bar never needs to clear it. Each handle is a
|
|
944
|
+
`separator`: ArrowLeft/ArrowRight 1%, Shift 10%, PageUp/PageDown 10%, Home and
|
|
945
|
+
End to the floor. A drag starts after 3px. `readOnly` shows everything and
|
|
946
|
+
writes nothing; `disabled` dims it. A drag on the canvas is the host's job.
|
|
947
|
+
|
|
948
|
+
A renderer lays the row out from the same logic, so the panel and the row
|
|
949
|
+
cannot disagree: `resolveColumnWidths(count, shares)` gives the width strings
|
|
950
|
+
(equal strings for no value, a value for another count, or a count past 4),
|
|
951
|
+
`resolveColumnShares(value, count)` clamps a stored value to the floor or
|
|
952
|
+
returns `undefined`, and `COLUMN_COUNT_WIDTHS` / `COLUMN_SHARES_EQUAL` are the
|
|
953
|
+
equal strings and the equal panel figures.
|
|
954
|
+
|
|
878
955
|
**A font list** is not a separate component. `SearchableSelect` takes
|
|
879
956
|
`renderOption(option, { selected, active })` to draw each option, for example
|
|
880
957
|
each face in itself. The `label` still names, filters and fills the field.
|
|
@@ -906,7 +983,8 @@ now in the kit itself:
|
|
|
906
983
|
from before focus. Enter commits like blur, Shift+Arrow steps
|
|
907
984
|
`shiftStep` (default ten steps), and a comma is accepted as the decimal
|
|
908
985
|
separator. `steppers` adds two `IconButton` arrows with hold-to-repeat,
|
|
909
|
-
`snap` puts a committed number on the `step` grid from `min
|
|
986
|
+
`snap` puts a committed number on the `step` grid from `min` (float-safe:
|
|
987
|
+
a typed 1.15 at step 0.1 is 1.2, and an exact half goes away from zero), and
|
|
910
988
|
`allowEmpty={false}` puts an emptied field back to its value.
|
|
911
989
|
- **`IconButton`**: `aria-pressed` true draws the pressed look, the accent wash
|
|
912
990
|
and ink of an active `IconRailButton`. `iconButtonClasses({ pressed })` gives
|
|
@@ -991,7 +1069,7 @@ All exports are named — `import { Modal, ... } from 'efficient-shell'`.
|
|
|
991
1069
|
| `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
1070
|
| `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
1071
|
| `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. |
|
|
1072
|
+
| `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
1073
|
| `ShortcutHelp` | The keyboard cheatsheet shown on `?`. |
|
|
996
1074
|
| `NotificationBell` | Taskbar bell — config via `<Layout notifications={…}>`. |
|
|
997
1075
|
| `BugReportDetail` | Used inside an entity-window registry entry; reads from `<BugReportConfigProvider>`. |
|
|
@@ -1004,7 +1082,7 @@ All exports are named — `import { Modal, ... } from 'efficient-shell'`.
|
|
|
1004
1082
|
| `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
1083
|
| `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
1084
|
| `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. |
|
|
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. |
|
|
1008
1086
|
| `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
1087
|
| `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
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. |
|
|
@@ -1012,12 +1090,12 @@ All exports are named — `import { Modal, ... } from 'efficient-shell'`.
|
|
|
1012
1090
|
| `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
1091
|
| `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
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. |
|
|
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. |
|
|
1093
|
+
| `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
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. |
|
|
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. |
|
|
1095
|
+
| `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
1096
|
| `Card`, `StatCard` | Surface panel (optional header/footer; `description` puts one sentence under the title, outside the heading) + dashboard metric tile. |
|
|
1019
1097
|
| `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. |
|
|
1098
|
+
| `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
1099
|
| `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
1100
|
| `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
1101
|
| `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 +1627,28 @@ classes in `ui.css` (the timeline card's) carry pixel radii of their own.
|
|
|
1549
1627
|
}
|
|
1550
1628
|
```
|
|
1551
1629
|
|
|
1630
|
+
**Type scale, weight, motion, border width.** Font sizes are 9, 11, 12, 14,
|
|
1631
|
+
16, 18, 20, 24 and 30 px, in portals and editor panels alike. 12 and up are
|
|
1632
|
+
Tailwind's own steps (`xs` … `3xl`); `ui.css` adds `--text-3xs` (9px) and
|
|
1633
|
+
`--text-2xs` (11px) as theme keys, so the matching size utilities exist. They
|
|
1634
|
+
carry no line height of their own, like a literal size, so moving a literal
|
|
1635
|
+
onto a step changes nothing on screen. Do not write a literal size; 10px and
|
|
1636
|
+
13px are not on the scale. Weight goes by role: `--weight-body` 400,
|
|
1637
|
+
`--weight-control` 500, `--weight-heading` 600, `--weight-figure` 700. Motion:
|
|
1638
|
+
`--motion-fast` 120ms (hover, press) and `--motion-base` 160ms (open, close).
|
|
1639
|
+
Border width: `--border-hairline` 1px, `--border-strong` 2px, `--border-accent`
|
|
1640
|
+
4px. Code takes the same numbers from `efficient-shell/ui`: `TYPE_SCALE`,
|
|
1641
|
+
`FONT_WEIGHTS`, `MOTION_DURATIONS`, `BORDER_WIDTHS`.
|
|
1642
|
+
|
|
1552
1643
|
**Corners.** A component does not pick a step of the scale for itself; it asks
|
|
1553
|
-
for the radius of what it is.
|
|
1644
|
+
for the radius of what it is. Five role tokens, each defaulting to the step that
|
|
1554
1645
|
role has always drawn, so they change nothing until you set one:
|
|
1555
1646
|
|
|
1556
1647
|
| Token | Default | Drawn by |
|
|
1557
1648
|
|---|---|---|
|
|
1558
1649
|
| `--radius-control` | `--radius-md` (6px) | buttons, fields, a segmented control's options |
|
|
1559
1650
|
| `--radius-card` | `--radius-lg` (8px) | `Card`, `Dialog`, a segmented control's track |
|
|
1651
|
+
| `--radius-panel` | `--radius-xl` (12px) | an editor's side panel |
|
|
1560
1652
|
| `--radius-popup` | `--radius-2xl` (16px) | menus, select lists, the start menu, the search palette |
|
|
1561
1653
|
| `--radius-window` | `--radius-2xl` (16px) | windows and their title bars |
|
|
1562
1654
|
|
|
@@ -1618,6 +1710,16 @@ through. `rosh-reduce-transparency` on `<html>` makes every popup one solid
|
|
|
1618
1710
|
:root { --rosh-glass-surface: var(--popover); --rosh-glass-solid-shadow: 0 4px 12px rgb(0 0 0 / 0.08); }
|
|
1619
1711
|
```
|
|
1620
1712
|
|
|
1713
|
+
**Elevation.** Five levels, one job each: `--elevation-none` (a plain card,
|
|
1714
|
+
border only), `--elevation-raised` (a card or field that sits a little up),
|
|
1715
|
+
`--elevation-panel` (a floating editor panel, a sticky bar), `--elevation-popup`
|
|
1716
|
+
(menus, lists, toasts, tooltips) and `--elevation-dialog` (dialogs, drawers).
|
|
1717
|
+
raised, popup and dialog equal Tailwind's sm, lg and xl, so markup keeps those
|
|
1718
|
+
utilities; md and 2xl are not levels. A component class or an inline style reads
|
|
1719
|
+
the property: `box-shadow: var(--elevation-panel)`. Code takes the names from
|
|
1720
|
+
`efficient-shell/ui` (`ELEVATION_LEVELS`, `RADIUS_ROLES`). In dark, the chart
|
|
1721
|
+
grid (`--viz-grid`) is `--line-strong`, the same as the axis.
|
|
1722
|
+
|
|
1621
1723
|
**Everything else** a kit component paints, each defaulting to the literal it
|
|
1622
1724
|
replaced: `--rosh-media-scrim` and `--rosh-media-scrim-button` (the dark wash
|
|
1623
1725
|
under text and buttons on an upload thumbnail), `--rosh-drop-ring` (the halo
|
|
@@ -3420,6 +3522,10 @@ copied from the account with `syncTimezoneFromProfile`, or the browser's own
|
|
|
3420
3522
|
when unset). Every formatter returns `EMPTY_VALUE` (`—`) for a value that is
|
|
3421
3523
|
missing or cannot be read. All of them are also exported from
|
|
3422
3524
|
`efficient-shell/format`, an entry with no React and no peers, for server code.
|
|
3525
|
+
The image focus maths (`FOCAL_DEFAULT`, `parseFocal`, `formatFocal`,
|
|
3526
|
+
`nudgeFocal`, `focalFromRect`, the `FocalPoint` type; see `ImageFocusPoint`) is
|
|
3527
|
+
exported there too, so a block's defaults and a published page's render can
|
|
3528
|
+
read a stored focus on a server.
|
|
3423
3529
|
|
|
3424
3530
|
| Export | Notes |
|
|
3425
3531
|
|---|---|
|
|
@@ -3463,32 +3569,52 @@ icon by what it **means**, and the set the theme chose draws it:
|
|
|
3463
3569
|
|
|
3464
3570
|
```tsx
|
|
3465
3571
|
import { Icon, registerIconSet } from 'efficient-shell/icons';
|
|
3466
|
-
import {
|
|
3572
|
+
import { efficient } from 'efficient-shell/icons/efficient';
|
|
3573
|
+
import { efficientLooks } from 'efficient-shell/icons/efficient-looks';
|
|
3467
3574
|
|
|
3468
|
-
registerIconSet(
|
|
3575
|
+
registerIconSet(efficient); // once, at startup, in client code
|
|
3576
|
+
registerIconSet(efficientLooks); // only if the host draws other looks
|
|
3469
3577
|
|
|
3470
3578
|
<Icon name="trash" /> // 1em square, the text's colour
|
|
3471
3579
|
<Icon name="search" size={20} tone="muted" />
|
|
3472
3580
|
<Icon name="warning" title="Overdue" /> // named: an image, not decoration
|
|
3581
|
+
<Icon name="trash" look="solid" /> // another look of the same meaning
|
|
3473
3582
|
```
|
|
3474
3583
|
|
|
3475
3584
|
`Icon` and the registry are also exported from `efficient-shell/ui` and the
|
|
3476
3585
|
root entry. They import React and nothing else.
|
|
3477
3586
|
|
|
3478
|
-
**The catalogue.** `IconName` is
|
|
3479
|
-
`
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
**
|
|
3485
|
-
|
|
3587
|
+
**The catalogue.** `IconName` is every meaning the EFFICIENT frontends draw
|
|
3588
|
+
(`add`, `close`, `edit`, `trash`, `search`, `download`, `chevron-down`,
|
|
3589
|
+
`warning`, an editor's `block-heading`, …): 363 names, from the census of every
|
|
3590
|
+
icon in every repository (2026-10-10). `<Icon name>` is typed to it, so a
|
|
3591
|
+
misspelt meaning does not compile.
|
|
3592
|
+
|
|
3593
|
+
**One set.** `efficient` is the company's one icon set (Lewis 2026-10-12). Lewis
|
|
3594
|
+
picked the drawing of every meaning himself: the heroicons outline drawing where
|
|
3595
|
+
heroicons has the meaning, and our own drawing in the heroicons style (24 grid,
|
|
3596
|
+
1.5 line, round ends) where it has not. One meaning, one drawing: a name that
|
|
3597
|
+
means the same as another (`rename` and `edit`, `move-up` and `arrow-up`) is
|
|
3598
|
+
that other's drawing. No two meanings look alike: `tests/efficientSet.test.tsx`
|
|
3599
|
+
measures every pair (Hausdorff distance on the 24 grid) and fails a pair closer
|
|
3600
|
+
than 0.9.
|
|
3601
|
+
|
|
3602
|
+
**Looks.** Each meaning has its default drawing and other looks of it: `solid`
|
|
3603
|
+
(filled, 24 grid) and `mini` (filled, drawn for 20 px) for all of them, and a
|
|
3604
|
+
few more where Lewis kept a second drawing (`<Icon name="edit"
|
|
3605
|
+
look="pencil-square" />`). The looks are a subpath of their own, so a host that
|
|
3606
|
+
draws only the default does not bundle them. A meaning without the look asked
|
|
3607
|
+
for draws its default.
|
|
3486
3608
|
|
|
3487
3609
|
| Import | Set | Draws | Needs |
|
|
3488
3610
|
|---|---|---|---|
|
|
3489
|
-
| `efficient-shell/icons/
|
|
3490
|
-
| `efficient-shell/icons/
|
|
3491
|
-
| `efficient-shell/icons/
|
|
3611
|
+
| `efficient-shell/icons/efficient` | `efficient` | every meaning, default look | nothing |
|
|
3612
|
+
| `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
|
+
|
|
3616
|
+
The heroicons and lucide sets go in the next major release; register `efficient`.
|
|
3617
|
+
Heroicons drawings are copied under the MIT licence: see `THIRD_PARTY_NOTICES.md`.
|
|
3492
3618
|
|
|
3493
3619
|
The first set registered is the default (`setDefaultIconSet(name)` changes
|
|
3494
3620
|
that). A meaning the chosen set does not draw falls back to the default set, so
|
|
@@ -3528,17 +3654,18 @@ registerIcons('editor', { 'block-product': BlockProductGlyph });
|
|
|
3528
3654
|
```
|
|
3529
3655
|
|
|
3530
3656
|
They are found under any theme. `efficient-shell/icons/efficient` also exports
|
|
3531
|
-
each
|
|
3532
|
-
…, `size` 18 by default) for code that wants one drawing
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3657
|
+
each meaning's default drawing as a component (`TrashIcon`, `StylesIcon`,
|
|
3658
|
+
`PaddingTopIcon`, …, `size` 18 by default) for code that wants one drawing
|
|
3659
|
+
whatever the theme.
|
|
3660
|
+
|
|
3661
|
+
**Adding or changing a drawing.** `src/icons/efficient.json` is the source: per
|
|
3662
|
+
name its meaning, where the drawing came from, the default SVG (24 by 24
|
|
3663
|
+
viewBox, 1.5 stroke on `currentColor`, round ends, transparent background) and
|
|
3664
|
+
its looks, or the name it is an alias of. Add the meaning to
|
|
3665
|
+
`src/icons/catalogue.json` too, run `npm run icons`, and commit the lot. Look
|
|
3666
|
+
before you keep a drawing: `node scripts/icons/glyph-sheet.mjs <new>
|
|
3667
|
+
<neighbours…>` draws it at 16px beside the glyphs it could be mistaken for, and
|
|
3668
|
+
`tests/efficientSet.test.tsx` fails it if it looks like another meaning.
|
|
3542
3669
|
|
|
3543
3670
|
With no set registered, `<Icon>` draws nothing and warns once per meaning. The
|
|
3544
3671
|
kit's own components (a window's close button, a filter chip) keep drawing
|
|
@@ -3580,8 +3707,13 @@ itself where the kit has an answer:
|
|
|
3580
3707
|
| `dark` | a `dark:` variant in a class string | the light class alone; the kit remaps it |
|
|
3581
3708
|
| `hex` | a hex colour in a `.tsx` string (not in `href`, `to`, `id`, `key`, `htmlFor`) | a token or a palette class |
|
|
3582
3709
|
| `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 |
|
|
3710
|
+
| `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)` |
|
|
3711
|
+
| `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 |
|
|
3712
|
+
| `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 |
|
|
3713
|
+
| `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
3714
|
| `kit` | an import of a second UI kit (`@headlessui`, `@radix-ui`, `@mui`, `antd`, `sonner`, …) | the kit's component |
|
|
3584
3715
|
| `icon` | an import of an icon package (`@heroicons`, `lucide-react`, `@tabler/icons`, …) | `<Icon name>` from `efficient-shell/icons` |
|
|
3716
|
+
| `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
3717
|
| `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
3718
|
|
|
3587
3719
|
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-
|
|
1
|
+
import{peekBrowserStartUrl as e,claimBrowserStartUrl as t}from"./chunk-LWLCNQY7.js";import{AboutApp as o}from"./chunk-RBDGN4YU.js";import"./chunk-SFCDHHQP.js";import{WindowTitle as r}from"./chunk-VWBQ2P5G.js";import"./chunk-YGC7AX3X.js";import"./chunk-DSSLOQDW.js";import"./chunk-5A7MWTXX.js";import"./chunk-OGXU7QFW.js";import"./chunk-P4CIOJHP.js";import{confirm as a}from"./chunk-IMMHJIV5.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{Icon as h}from"./chunk-XWSWYMGM.js";import"./chunk-TNBCUZVE.js";import"./chunk-C3QOHMR6.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 S(){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(S),[j,z]=g(x||v),[H,D]=g(j),[O,E]=g([j]),[M,P]=g(0),[T,I]=g(0),[J,U]=g(W),[X,F]=g(!1),[G,Q]=g(null),q=p(null),A=p(null);b(()=>{try{localStorage.setItem(w,JSON.stringify(J))}catch{}},[J]);const B=e=>{const t=C(e);t&&(z(t),D(t),E(e=>{const o=e.slice(0,M+1);return o.push(t),o}),P(e=>e+1),F(!1))},Y=()=>{"undefined"!=typeof window&&window.open(j,"_blank","noopener,noreferrer")},K=()=>{const e=G?.trim();e?(U(t=>[...t,{label:e,url:j}]),Q(null)):Q(null)};b(()=>{if(null===G)return;q.current?.focus(),q.current?.select();const e=e=>{const t=e.target;t?.closest("[data-bookmark-popover]")||Q(null)},t=setTimeout(()=>document.addEventListener("mousedown",e),0);return()=>{clearTimeout(t),document.removeEventListener("mousedown",e)}},[null!==G]);const Z=e=>{U(t=>t.filter((t,o)=>o!==e))},$=M>0,V=M<O.length-1,_=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 - ${
|
|
3
|
+
/* @__PURE__ */k(r,{title:`Browser - ${R(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(
|
|
7
|
-
/* @__PURE__ */k("button",{onClick:()=>{if(O
|
|
8
|
-
/* @__PURE__ */k("button",{onClick:()=>
|
|
9
|
-
/* @__PURE__ */k("button",{onClick:()=>
|
|
10
|
-
/* @__PURE__ */k("form",{onSubmit:e=>{e.preventDefault(),
|
|
6
|
+
/* @__PURE__ */k("button",{onClick:()=>{if(M>0){const e=M-1;P(e),z(O[e]),D(O[e]),F(!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;P(e),z(O[e]),D(O[e]),F(!1)}},disabled:!V,className:ee,title:"Forward",children:/* @__PURE__ */k(h,{name:"chevron-right",fallback:l,className:"h-4 w-4",strokeWidth:2})}),
|
|
8
|
+
/* @__PURE__ */k("button",{onClick:()=>I(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:()=>B(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(),B(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:[
|
|
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=>
|
|
13
|
-
/* @__PURE__ */k("button",{"data-bookmark-popover":!0,onClick:()=>
|
|
14
|
-
/* @__PURE__ */k("button",{onClick
|
|
15
|
-
/* @__PURE__ */k("button",{onClick:()=>
|
|
12
|
+
/* @__PURE__ */k("input",{type:"text",value:H,onChange:e=>D(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:()=>D(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:()=>_?Z(J.findIndex(e=>e.url===j)):void Q(R(j)),className:ee+" "+(_?"text-yellow-500":""),title:_?"Remove bookmark":"Add bookmark",children:/* @__PURE__ */k(h,{name:"star",fallback:c,className:"h-4 w-4",fill:_?"currentColor":"none",strokeWidth:1.5})}),
|
|
14
|
+
/* @__PURE__ */k("button",{onClick:Y,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:()=>F(e=>!e),className:ee,title:"Embedding help",children:/* @__PURE__ */k(h,{name:"help",fallback:d,className:"h-4 w-4",strokeWidth:1.5})})]}),null!==G&&/* @__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:
|
|
18
|
+
/* @__PURE__ */k("input",{ref:q,type:"text",value:G,onChange:e=>Q(e.target.value),onKeyDown:e=>{"Enter"===e.key?K():"Escape"===e.key&&Q(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:()=>
|
|
21
|
-
/* @__PURE__ */k("button",{onClick:K,disabled:!
|
|
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:()=>
|
|
20
|
+
/* @__PURE__ */k("button",{onClick:()=>Q(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:!G.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:()=>B(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"})&&Z(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(
|
|
26
|
+
/* @__PURE__ */k("div",{className:"flex-1 relative min-h-0 bg-gray-50",children:/* @__PURE__ */k(L,{url:j,iframeKey:T,iframeRef:A,openExternal:Y,showHelp:X,dismissHelp:()=>F(!1)})})]})}function R(e){try{return new URL(e).hostname.replace(/^www\./,"")}catch{return e}}function L({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:[
|
|
28
|
+
/* @__PURE__ */f("h3",{className:"text-base font-semibold text-gray-800 mb-1",children:[R(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."]}),
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import{loadAppearance as e,WidgetSettingsModal as t}from"./chunk-7WC3EOFS.js";import{useWidgetSettings as n}from"./chunk-VWBQ2P5G.js";import"./chunk-YGC7AX3X.js";import"./chunk-DSSLOQDW.js";import"./chunk-5A7MWTXX.js";import"./chunk-OGXU7QFW.js";import"./chunk-P4CIOJHP.js";import"./chunk-IMMHJIV5.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{Icon as c}from"./chunk-XWSWYMGM.js";import"./chunk-TNBCUZVE.js";import"./chunk-C3QOHMR6.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,W]=a(()=>e(m)),[T,F]=a(!1),[O,H]=a(S),J=i(null);n(o(()=>{H({...S}),F(!0)},[S]));const M=o(e=>{C(""),k(t=>N||"0"===t?(x(!1),"."===e?"0.":e):"."===e&&t.includes(".")?t:t+e)},[N]),X=o(()=>{k("0"),g(null),y(null),x(!1)},[]),A=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}},[]),D=o(e=>{const t=parseFloat(b);if(null!==p&&f&&!N){const e=A(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,A]),I=o(()=>{if(null===p||!f)return;const e=parseFloat(b),t=A(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,A]),L=o(()=>{k(e=>String(parseFloat(e)/100))},[]),P=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"?M(e.key):"."===e.key?M("."):"+"===e.key?D("+"):"-"===e.key?D("-"):"*"===e.key?D("×"):"/"===e.key?(e.preventDefault(),D("÷")):"Enter"===e.key||"="===e.key?("Enter"===e.key&&e.target instanceof Node&&J.current?.contains(e.target)&&e.preventDefault(),I()):"Escape"===e.key?X():"Backspace"===e.key?k(e=>e.length>1?e.slice(0,-1):"0"):"%"===e.key&&L())};return window.addEventListener("keydown",e),()=>window.removeEventListener("keydown",e)},[M,D,I,X,L]);const B="flex items-center justify-center rounded-lg text-sm font-medium transition-colors",Q=`${B} bg-white border border-gray-200 text-gray-900 hover:bg-gray-50 active:bg-gray-100`,Z=`${B} bg-blue-50 border border-blue-200 text-blue-700 hover:bg-blue-100 active:bg-blue-200`,G=`${B} bg-gray-100 border border-gray-200 text-gray-600 hover:bg-gray-200 active:bg-gray-300`,R=`${B} bg-blue-600 text-white hover:bg-blue-700 active:bg-blue-800`;/* @__PURE__ */
|
|
2
|
+
return u(d,{children:[
|
|
3
|
+
/* @__PURE__ */u("div",{ref:J,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:X,className:G,children:"AC"}),
|
|
11
|
+
/* @__PURE__ */h("button",{onClick:P,className:G,children:"+/−"}),
|
|
12
|
+
/* @__PURE__ */h("button",{onClick:L,className:G,children:"%"}),
|
|
13
|
+
/* @__PURE__ */h("button",{onClick:()=>D("÷"),className:`${Z} ${"÷"===f?"ring-2 ring-blue-400":""}`,children:"÷"}),
|
|
14
|
+
/* @__PURE__ */h("button",{onClick:()=>M("7"),className:Q,children:"7"}),
|
|
15
|
+
/* @__PURE__ */h("button",{onClick:()=>M("8"),className:Q,children:"8"}),
|
|
16
|
+
/* @__PURE__ */h("button",{onClick:()=>M("9"),className:Q,children:"9"}),
|
|
17
|
+
/* @__PURE__ */h("button",{onClick:()=>D("×"),className:`${Z} ${"×"===f?"ring-2 ring-blue-400":""}`,children:"×"}),
|
|
18
|
+
/* @__PURE__ */h("button",{onClick:()=>M("4"),className:Q,children:"4"}),
|
|
19
|
+
/* @__PURE__ */h("button",{onClick:()=>M("5"),className:Q,children:"5"}),
|
|
20
|
+
/* @__PURE__ */h("button",{onClick:()=>M("6"),className:Q,children:"6"}),
|
|
21
|
+
/* @__PURE__ */h("button",{onClick:()=>D("-"),className:`${Z} ${"-"===f?"ring-2 ring-blue-400":""}`,children:"−"}),
|
|
22
|
+
/* @__PURE__ */h("button",{onClick:()=>M("1"),className:Q,children:"1"}),
|
|
23
|
+
/* @__PURE__ */h("button",{onClick:()=>M("2"),className:Q,children:"2"}),
|
|
24
|
+
/* @__PURE__ */h("button",{onClick:()=>M("3"),className:Q,children:"3"}),
|
|
25
|
+
/* @__PURE__ */h("button",{onClick:()=>D("+"),className:`${Z} ${"+"===f?"ring-2 ring-blue-400":""}`,children:"+"}),
|
|
26
|
+
/* @__PURE__ */h("button",{onClick:()=>M("0"),className:`${Q} col-span-2`,children:"0"}),
|
|
27
|
+
/* @__PURE__ */h("button",{onClick:()=>M("."),className:Q,children:"."}),
|
|
28
|
+
/* @__PURE__ */h("button",{onClick:I,className:R,children:"="})]})]}),
|
|
29
|
+
/* @__PURE__ */h(t,{open:T,onClose:()=>F(!1),title:"Calculator Settings",appearance:O,onAppearanceChange:H,onSave:()=>{W(O),localStorage.setItem(m,JSON.stringify(O)),F(!1)}})]})}export{b as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{ConfirmProvider,confirm,confirmDestructive,prompt,useConfirm}from"./chunk-IMMHJIV5.js";import"./chunk-AFYQTVZO.js";import"./chunk-X4FJ54PP.js";import"./chunk-6LTDJKIL.js";import"./chunk-XWSWYMGM.js";import"./chunk-TNBCUZVE.js";import"./chunk-C3QOHMR6.js";
|