cortena-ui 1.11.0 → 1.13.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/CHANGELOG.md +107 -0
- package/README.md +160 -3
- package/dist/components/app-shell.d.ts +125 -2
- package/dist/components/app-shell.js +471 -89
- package/dist/components/app-shell.js.map +1 -1
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/chart/nivo-theme.js +23 -5
- package/dist/components/chart/nivo-theme.js.map +1 -1
- package/dist/components/chart/recharts-charts.js +9 -14
- package/dist/components/chart/recharts-charts.js.map +1 -1
- package/dist/components/chart/tokens.js +21 -1
- package/dist/components/chart/tokens.js.map +1 -1
- package/dist/components/dropdown-menu.js +21 -21
- package/dist/components/dropdown-menu.js.map +1 -1
- package/dist/core.d.ts +3 -2
- package/dist/core.js +2 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -2
- package/package.json +3 -7
- package/src/components/app-shell.tsx +716 -38
- package/src/components/chart/nivo-theme.ts +23 -4
- package/src/components/chart/recharts-charts.tsx +13 -10
- package/src/components/chart/tokens.ts +19 -0
- package/src/entries/core.ts +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,113 @@
|
|
|
3
3
|
Notable changes per release. Versions before 1.6.0 are recorded in the git log
|
|
4
4
|
and in `../../CONSUMING.md`; this file starts where the changelog does.
|
|
5
5
|
|
|
6
|
+
## 1.13.0
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`AppShell` has a navigation contract** (DESIGN-88). `nav` takes groups of
|
|
11
|
+
items — id, label, lucide icon or registered mark, href, badge, `exact` —
|
|
12
|
+
with an optional section label per group and a `footer` for the one thing in
|
|
13
|
+
a sidebar that is not navigation. Active state comes from `activeId`,
|
|
14
|
+
`isActive(href, item)` or `pathname`, in that order; cortena-ui still has no
|
|
15
|
+
router dependency, and `onNavigate` is where react-router takes over without
|
|
16
|
+
the `href` stopping being real. Below 1024px the sidebar becomes a menu
|
|
17
|
+
button and a drawer. `nav={null}` renders no sidebar, which is a legal state.
|
|
18
|
+
The five extensions in the shared-shell mockup would otherwise have shipped
|
|
19
|
+
five sidebars: five widths, five active treatments, five answers to badges
|
|
20
|
+
and groups.
|
|
21
|
+
- **`headerSlot`**, between the extension name and the avatar, for a live
|
|
22
|
+
status pill, the org the screen is scoped to, or one secondary action —
|
|
23
|
+
and for nothing else. README, "The header slot and the avatar menu", has the
|
|
24
|
+
list of what may not go there.
|
|
25
|
+
- **`menuItems`**, inserted between Settings and Log out in the avatar menu.
|
|
26
|
+
Log out stays last and stays behind its separator.
|
|
27
|
+
- **`bottomInset`**, and `data-bottom-bar` detection when it is not given. The
|
|
28
|
+
theme/help cluster and anything in `agentSlot` lift clear of a mobile bottom
|
|
29
|
+
tab bar instead of covering two of its tabs. The shell publishes
|
|
30
|
+
`--ds-shell-bottom-inset` and `--ds-shell-header-height` on its own element
|
|
31
|
+
and on `<html>`, so a pop-up that portals out of the shell can read them.
|
|
32
|
+
- **`HELP_PANEL_ID` and `NAV_DRAWER_ID`** are exported from `cortena-ui/core`
|
|
33
|
+
and the root barrel — the ids the shell gives the help region and the mobile
|
|
34
|
+
drawer, for a consumer that wires its own control's `aria-controls` to one
|
|
35
|
+
of them. They were exported from the module and reachable from neither entry.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- **The help panel starts below the header.** It was `fixed top-6 right-6`,
|
|
40
|
+
which put it over the 64px header and over the avatar trigger it sits next
|
|
41
|
+
to. It now starts 12px below the header and still stops 12px above the
|
|
42
|
+
cluster (DESIGN-88).
|
|
43
|
+
- **`BottomRightCluster` sets its own `bottom`.** It is now
|
|
44
|
+
`calc(24px + var(--ds-shell-bottom-inset, 0px))` as an inline style rather
|
|
45
|
+
than the `bottom-6` class, because the value is `calc()` over a variable the
|
|
46
|
+
consumer may not have set. **Migration:** an existing
|
|
47
|
+
`className="bottom-22 lg:bottom-6"` on the cluster no longer wins — an
|
|
48
|
+
inline style beats a utility class — and is now wrong twice over, since it
|
|
49
|
+
never moved the agent pop-up in `agentSlot`. Delete the override and give
|
|
50
|
+
the bottom bar `data-bottom-bar`, or pass `bottomInset`; both lift the
|
|
51
|
+
cluster and the pop-up together (DESIGN-88).
|
|
52
|
+
- **The mobile drawer is a real modal.** It was `role="dialog"
|
|
53
|
+
aria-modal="true"` with no focus trap: focus stayed on the menu button,
|
|
54
|
+
Tab walked straight out into the shell under the scrim, and the page
|
|
55
|
+
scrolled behind it. Focus now starts on its close button, Tab and Shift+Tab
|
|
56
|
+
cycle inside it, the rest of the shell is `inert` while it is open, body
|
|
57
|
+
scroll is locked and restored, and focus returns to the menu button
|
|
58
|
+
(DESIGN-88 review).
|
|
59
|
+
- **A modified click belongs to the browser.** `onNavigate` no longer fires —
|
|
60
|
+
and the drawer no longer closes — for a middle-click or a
|
|
61
|
+
cmd/ctrl/shift/alt-click, so "open in a new tab" works without every
|
|
62
|
+
consumer repeating the same guard before its `preventDefault()`. A router's
|
|
63
|
+
`onNavigate` written to the old contract keeps working; its own guard is
|
|
64
|
+
now redundant rather than required (DESIGN-88 review).
|
|
65
|
+
- **`--ds-shell-header-height` is measured**, not restated. The constant is
|
|
66
|
+
declared once and the header takes it as its height; what lands on `<html>`
|
|
67
|
+
and on the shell element is the height the browser laid out.
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- **More than one shell on a page no longer fights over `<html>`.** Every
|
|
72
|
+
shell wrote `--ds-shell-bottom-inset` and `--ds-shell-header-height` there
|
|
73
|
+
and every unmount removed them, so the values were whichever shell committed
|
|
74
|
+
last and the first to leave took them from the rest — visible in the token
|
|
75
|
+
guide, which mounts three. The first shell to mount owns the pair; the last
|
|
76
|
+
to unmount clears it (DESIGN-88 review).
|
|
77
|
+
- **`aria-controls` no longer dangles.** The menu button and the help button
|
|
78
|
+
named an id that was on the page only while the drawer, or the panel, was
|
|
79
|
+
mounted; the attribute is now set only while it is (DESIGN-88 review).
|
|
80
|
+
- **The shell's ResizeObservers survive a re-render.** The cluster's and the
|
|
81
|
+
bottom bar's effects listed `agentSlot`, `helpOpen` and `children` as
|
|
82
|
+
dependencies — fresh element objects every render — so both observers were
|
|
83
|
+
torn down and rebuilt on every render of the page underneath. They are keyed
|
|
84
|
+
on the DOM node now (DESIGN-88 review).
|
|
85
|
+
- **`BrandMark` no longer logs three React errors per page.**
|
|
86
|
+
`safeRootAttributes` spread a mark's root attributes into JSX in their SVG
|
|
87
|
+
spelling — `stroke-width`, `stroke-linecap`, `stroke-linejoin` — and React
|
|
88
|
+
19 warned about each on every screen of every extension. They are camelCased
|
|
89
|
+
on the way in; the values still reach the DOM unchanged (DESIGN-88).
|
|
90
|
+
|
|
91
|
+
## 1.12.0
|
|
92
|
+
|
|
93
|
+
### Changed
|
|
94
|
+
|
|
95
|
+
- **`RenderProp` is exported** from `cortena-ui/core` and the root barrel, so a
|
|
96
|
+
consumer can name the type its wrappers around `Card`, `Breadcrumb` and
|
|
97
|
+
`ButtonLink` already use (DESIGN-51).
|
|
98
|
+
- **`cortena-design` peer range is bounded** at `>=3.4.0 <4`; a future 4.x no
|
|
99
|
+
longer resolves silently (DESIGN-51).
|
|
100
|
+
- **Chart SSR fallbacks match the tokens.** The no-DOM fallbacks for
|
|
101
|
+
`--ds-radius-md` and `--ds-radius-sm` were 6 and 4; the tokens are 12 and 8,
|
|
102
|
+
so a server-rendered Nivo tooltip drew at half radius until hydration. The
|
|
103
|
+
plot margin now reads `--ds-space-2` through `plotMargin()` instead of a raw
|
|
104
|
+
8. Pixel-identical in the browser; asserted by test (DESIGN-51).
|
|
105
|
+
|
|
106
|
+
### Removed
|
|
107
|
+
|
|
108
|
+
- `@nivo/bar`, `@nivo/core`, `@nivo/line`, `@nivo/pie`, `@nivo/scatterplot`
|
|
109
|
+
as dependencies — nothing imported them; those chart types render on the
|
|
110
|
+
Recharts side. `@nivo/core` stays as a transitive of the four Nivo charts
|
|
111
|
+
that remain (DESIGN-51).
|
|
112
|
+
|
|
6
113
|
## 1.11.0
|
|
7
114
|
|
|
8
115
|
### Added
|
package/README.md
CHANGED
|
@@ -122,8 +122,9 @@ regression; `test/chart-bundle.test.tsx` says which line caused it.
|
|
|
122
122
|
`text-[color:var(--ds-foreground)]` and `text-[length:var(--ds-text-caption)]`.
|
|
123
123
|
Backgrounds, borders and radii are unambiguous and need no hint.
|
|
124
124
|
- **Tested in a real browser.** Tests assert computed pixels against the
|
|
125
|
-
token the browser resolved
|
|
126
|
-
|
|
125
|
+
token the browser resolved. A dangling `var()` paints transparent, which the
|
|
126
|
+
tests catch and jsdom cannot. Most tests pin a theme and run once; see
|
|
127
|
+
"Which tests run where".
|
|
127
128
|
- **Every component appears in the guide.** `guide/sections.tsx` is the visual
|
|
128
129
|
check in light, dark and system-dark; add an entry with every variant and
|
|
129
130
|
size when adding a component.
|
|
@@ -135,7 +136,10 @@ how-to-create-a-cortena-extension, audit rule P-10): the registered brand mark
|
|
|
135
136
|
and the extension name top left, the avatar menu — the only settings entry
|
|
136
137
|
point — top right, and one fixed bottom-right cluster holding the theme toggle
|
|
137
138
|
then the help button. `BrandMark`, `BottomRightCluster`, `ThemeToggle`,
|
|
138
|
-
`HelpButton` and `documentTitle` are exported alongside it
|
|
139
|
+
`HelpButton` and `documentTitle` are exported alongside it, and so are
|
|
140
|
+
`HELP_PANEL_ID` and `NAV_DRAWER_ID` — the ids the shell gives the help region
|
|
141
|
+
and the mobile drawer, for a consumer wiring its own `aria-controls` to one of
|
|
142
|
+
them.
|
|
139
143
|
|
|
140
144
|
The mark comes from `cortena-design/marks` by `extension.id`, which is the same
|
|
141
145
|
file the Apps tile, the MCP app card, the agent pop-up pill and the favicon
|
|
@@ -146,6 +150,107 @@ favicon".
|
|
|
146
150
|
`helpPanel` is a slot until `HelpPanel` lands (EXTBP-24); `help.source` is the
|
|
147
151
|
functional document the panel will read.
|
|
148
152
|
|
|
153
|
+
### Navigation
|
|
154
|
+
|
|
155
|
+
`nav` is the left sidebar, and it is the shell's so that five extensions do not
|
|
156
|
+
invent five of them (DESIGN-88):
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
<AppShell
|
|
160
|
+
extension={{ id: "tasks", name: "Tasks" }}
|
|
161
|
+
user={user}
|
|
162
|
+
nav={{
|
|
163
|
+
pathname, // from the router; see below
|
|
164
|
+
groups: [
|
|
165
|
+
{ items: [{ id: "dashboard", label: "Dashboard", icon: LayoutDashboard, href: "/", exact: true }] },
|
|
166
|
+
{
|
|
167
|
+
label: "Projects", // a labelled section
|
|
168
|
+
items: [
|
|
169
|
+
{ id: "all", label: "All Projects", icon: FolderKanban, href: "/projects", exact: true },
|
|
170
|
+
{ id: "alerts", label: "Alerts", icon: Bell, href: "/alerts", badge: 9 },
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
footer: <StorageMeter />, // the one thing that is not navigation
|
|
175
|
+
}}
|
|
176
|
+
/>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
- **`icon`** is a lucide component or the id of a mark registered in
|
|
180
|
+
`cortena-design/marks`.
|
|
181
|
+
- **`badge`** is a count or a short string, painted from tokens. `0` and `""`
|
|
182
|
+
draw nothing — an empty chip is worse than no chip.
|
|
183
|
+
- **Active state** comes from one of three, in this order: `activeId` (the
|
|
184
|
+
item's id), `isActive(href, item)` (a callback), or `pathname` (compared to
|
|
185
|
+
each `href` as a prefix, or exactly for an item marked `exact`). Give none
|
|
186
|
+
and nothing is active: cortena-ui has no router dependency, and reading
|
|
187
|
+
`window.location` here would be right on first paint and stale after every
|
|
188
|
+
navigation.
|
|
189
|
+
- **react-router** passes `pathname: useLocation().pathname` and keeps
|
|
190
|
+
navigation client-side with `onNavigate`, which is called before the browser
|
|
191
|
+
follows the real `href`:
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
onNavigate: (item, event) => {
|
|
195
|
+
event.preventDefault();
|
|
196
|
+
navigate(item.href);
|
|
197
|
+
},
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The `href` stays real either way, so middle-click, "open in new tab" and
|
|
201
|
+
copy-link all still work: the shell does not call `onNavigate` at all for a
|
|
202
|
+
middle-click or a cmd/ctrl/shift/alt-click, so that guard does not belong in
|
|
203
|
+
every consumer.
|
|
204
|
+
- **`href` is compared as a plain path.** A value carrying a query or a hash
|
|
205
|
+
(`/reports?tab=open`, `/docs#intro`) does not prefix-match the pathname and
|
|
206
|
+
will never light up. Give the item its base path and resolve `activeId`
|
|
207
|
+
yourself.
|
|
208
|
+
- **Below 1024px** the sidebar becomes a menu button in the header and a
|
|
209
|
+
drawer, closed by Escape, by the scrim, or by following an item. It is a
|
|
210
|
+
real modal: focus moves to its close button, Tab cycles inside it, the rest
|
|
211
|
+
of the shell is `inert` while it is open, the page under it does not scroll,
|
|
212
|
+
and focus returns to the menu button on close.
|
|
213
|
+
- **`nav={null}`** renders no sidebar and no menu button. That is a legal
|
|
214
|
+
state, and the right one for an extension whose whole navigation lives
|
|
215
|
+
inside one page.
|
|
216
|
+
|
|
217
|
+
### The header slot and the avatar menu
|
|
218
|
+
|
|
219
|
+
`headerSlot` sits between the extension name and the avatar; `menuItems` are
|
|
220
|
+
inserted between Settings and Log out. Both exist because the top bar was
|
|
221
|
+
closed and three extensions had something real to put in it — and both stay
|
|
222
|
+
narrow on purpose (§10):
|
|
223
|
+
|
|
224
|
+
| May go in `headerSlot` | May not |
|
|
225
|
+
| --- | --- |
|
|
226
|
+
| Live status about what is on screen — Assure's "Claude executing" pill | Navigation of any kind. That is `nav`, or it is in the page |
|
|
227
|
+
| The org, workspace or tenant the screen is scoped to — Tasks' "Cortena Labs" | The theme control. It is in the bottom-right cluster, once |
|
|
228
|
+
| **One** secondary action, text or icon, that belongs to the whole extension rather than to the page — Report an issue | Help. It is the help button, and its panel |
|
|
229
|
+
| | Settings, or anything that opens settings. The avatar menu is the only entry point |
|
|
230
|
+
| | A page-level action. Those are `PageHeader`'s `actions` |
|
|
231
|
+
|
|
232
|
+
`menuItems` takes destinations that are the user's rather than the screen's —
|
|
233
|
+
Assure's Permissions, a Report-issue dialog. Log out stays last and stays
|
|
234
|
+
behind its separator: an extension item cannot push itself under the
|
|
235
|
+
destructive action.
|
|
236
|
+
|
|
237
|
+
### The bottom inset, and the two variables the shell publishes
|
|
238
|
+
|
|
239
|
+
`AppShell` sets `--ds-shell-header-height` and `--ds-shell-bottom-inset`, on
|
|
240
|
+
its own element and on `<html>`, so anything that portals out of the shell can
|
|
241
|
+
still tell where the chrome is. `<html>` has one pair of them and a page may
|
|
242
|
+
hold more than one shell — the token guide holds three — so the first shell to
|
|
243
|
+
mount owns the pair and the last to unmount clears it; every shell publishes
|
|
244
|
+
its own values on its own element regardless.
|
|
245
|
+
|
|
246
|
+
An extension with a bottom tab bar gives that bar `data-bottom-bar` and the
|
|
247
|
+
shell measures it — the measurement is 0 while the bar is hidden, so a
|
|
248
|
+
`lg:hidden` tab bar lifts the cluster on a phone and nowhere else. `bottomInset`
|
|
249
|
+
(a number of pixels, or any CSS length) sets the same distance by hand. Both
|
|
250
|
+
move the theme/help cluster **and** whatever `agentSlot` renders, which is what
|
|
251
|
+
the old per-extension `className="bottom-22 lg:bottom-6"` workaround could not
|
|
252
|
+
do.
|
|
253
|
+
|
|
149
254
|
## The agent pop-up
|
|
150
255
|
|
|
151
256
|
`AgentChatPopup` is the extension's own Cortena Agent, in the corner of every
|
|
@@ -696,6 +801,55 @@ action is shaped like A2UI's client-to-server `action`, and the catalogue is a
|
|
|
696
801
|
schema-per-component registry. Swapping the engine later is mechanical. Nothing
|
|
697
802
|
from `@a2ui/*` is installed, so `vitest.config.ts` has no entry for it.
|
|
698
803
|
|
|
804
|
+
## Which tests run where
|
|
805
|
+
|
|
806
|
+
`vitest.config.ts` has four projects. **`light-1`, `light-2` and `light-3`**
|
|
807
|
+
between them run every file once, in a Chromium whose OS colour scheme is
|
|
808
|
+
light; **`dark`** runs a second time, in a dark Chromium, only the files whose
|
|
809
|
+
name ends `.theme.test.tsx`. Tag a test with
|
|
810
|
+
that suffix when its result depends on the browser's own colour scheme — that
|
|
811
|
+
is, when it asserts a theme-dependent value while no `data-theme` attribute is
|
|
812
|
+
set, the `@media (prefers-color-scheme: dark)` branch of `tokens.css`. Nothing
|
|
813
|
+
else needs the tag: an explicit `data-theme` always beats the media query, so a
|
|
814
|
+
test that calls `setTheme("light")` or `setTheme("dark")` computes the same
|
|
815
|
+
bytes in either browser, and the `for (const theme of ["light", "dark"])` loops
|
|
816
|
+
already paint both branches and assert that they differ, inside one run. If one
|
|
817
|
+
test in a file needs the tag, move that test to a sibling `*.theme.test.tsx`
|
|
818
|
+
rather than paying for the whole file twice — six such files exist today, and
|
|
819
|
+
`pnpm test:dark` runs exactly them.
|
|
820
|
+
|
|
821
|
+
The three `light` projects are a wall-clock split, not a behaviour split
|
|
822
|
+
(DESIGN-86). Files run one at a time inside a project — parallel files made
|
|
823
|
+
overlay tests time out and let one file's pointer position leak into the next —
|
|
824
|
+
so 72 light files in one project meant 72 browser-page setups end to end, and
|
|
825
|
+
that, not the test work, was the suite's wall time. Vitest runs *projects* side
|
|
826
|
+
by side, so splitting the light files three ways makes the wall time the slowest
|
|
827
|
+
shard rather than the sum. Nothing about isolation changes: each shard is still
|
|
828
|
+
serial inside itself, and three light shards plus `dark` is four Chromium
|
|
829
|
+
contexts at once, the same kind of concurrency the suite has always had and as
|
|
830
|
+
many as an 8 GB laptop takes without swapping.
|
|
831
|
+
|
|
832
|
+
Measured on the machine this was written on: the full suite went from
|
|
833
|
+
**78.3 s** to **54.3 s** wall for the same 78 file runs and 812 tests, while
|
|
834
|
+
CPU time rose from 73 s to 95 s. It is a third off, not the two-thirds the file
|
|
835
|
+
counts suggest, and the gap is the fixed cost each project pays for itself — a
|
|
836
|
+
Vite server, the `optimizeDeps` pre-bundle above, and a browser launch, worth
|
|
837
|
+
roughly ten seconds before the first file runs — plus what four of those
|
|
838
|
+
contending for 8 GB costs the ones already running. Splitting further buys less
|
|
839
|
+
each time and costs memory sooner; measure before raising `LIGHT_SHARDS`.
|
|
840
|
+
|
|
841
|
+
A file's shard is `hash(path) % 3`, an FNV-1a of its path — deliberately not an
|
|
842
|
+
alphabetical split. Alphabetically, adding one file shifts every file after it
|
|
843
|
+
across a boundary, so shard membership churns on every new test; hashing the
|
|
844
|
+
path leaves every existing file where it was and puts only the new file
|
|
845
|
+
somewhere, and a file moves only when it is renamed. The spread stays close to
|
|
846
|
+
even on its own (21/27/24 over today's 72 files). You never choose a shard, and
|
|
847
|
+
nothing you write needs to know which one it is in: `pnpm test` selects changed
|
|
848
|
+
files across all of them, and a shard with nothing changed in it simply runs
|
|
849
|
+
nothing. If the shards ever drift badly out of balance, raise `LIGHT_SHARDS` in
|
|
850
|
+
`vitest.config.ts` — but keep the light shards plus `dark` at four contexts or
|
|
851
|
+
fewer unless the machine has more memory.
|
|
852
|
+
|
|
699
853
|
## Commands
|
|
700
854
|
|
|
701
855
|
```bash
|
|
@@ -703,6 +857,7 @@ pnpm check # types
|
|
|
703
857
|
pnpm lint:design # no literals where a token exists; no dangling var()
|
|
704
858
|
pnpm build # dist/ via tsdown (ESM + d.ts); also runs on prepack
|
|
705
859
|
pnpm test # changed-only: the suites of this package your diff affects
|
|
860
|
+
pnpm test:dark # the dark project alone: *.theme.test.tsx in dark Chromium
|
|
706
861
|
pnpm test:bundle # bundle budgets alone (builds first, then real Vite apps)
|
|
707
862
|
pnpm bundle:probe # the size table, printed, without asserting anything
|
|
708
863
|
pnpm guide # three-theme guide on a local Vite server
|
|
@@ -725,6 +880,8 @@ src/styles/index.css what consumers import after the tokens: animation
|
|
|
725
880
|
utilities and the Base UI data-attribute variants
|
|
726
881
|
scripts/bundle-probe.mjs builds a consumer app per entry and weighs it
|
|
727
882
|
test/ browser tests; setup.css is the consumer recipe verbatim
|
|
883
|
+
split by path hash across three concurrent light projects
|
|
884
|
+
*.theme.test.tsx also run in a dark Chromium
|
|
728
885
|
*.test.mjs are the Node-side bundle budgets
|
|
729
886
|
guide/ Vite app: ?theme=light|dark|system frames, or all three
|
|
730
887
|
```
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { LucideIcon } from "lucide-react";
|
|
2
3
|
import * as React from "react";
|
|
3
4
|
//#region src/components/app-shell.d.ts
|
|
4
5
|
/**
|
|
@@ -54,8 +55,15 @@ export interface BottomRightClusterProps extends React.ComponentProps<"div"> {
|
|
|
54
55
|
* The one fixed container in the bottom-right corner. It exists so that no
|
|
55
56
|
* extension positions a floating control itself — that is how a third one
|
|
56
57
|
* appears and covers one of the first two.
|
|
58
|
+
*
|
|
59
|
+
* Its distance from the bottom edge is 24px PLUS `--ds-shell-bottom-inset`,
|
|
60
|
+
* which `AppShell` sets from its `bottomInset` prop or from a `data-bottom-bar`
|
|
61
|
+
* element it finds in the page. On a phone with a bottom tab bar the cluster
|
|
62
|
+
* used to sit on top of two of the tabs; Tasks worked around it with
|
|
63
|
+
* `className="bottom-22 lg:bottom-6"` on its own cluster, which the agent
|
|
64
|
+
* pop-up inside `agentSlot` had no equivalent of (DESIGN-88).
|
|
57
65
|
*/
|
|
58
|
-
declare function BottomRightCluster({ className, agentSlot, children, ...props }: BottomRightClusterProps): React.JSX.Element;
|
|
66
|
+
declare function BottomRightCluster({ className, style, agentSlot, children, ...props }: BottomRightClusterProps): React.JSX.Element;
|
|
59
67
|
export interface ThemeToggleProps extends Omit<React.ComponentProps<"button">, "onClick"> {
|
|
60
68
|
/** localStorage key, forwarded to `useCortenaTheme`. */
|
|
61
69
|
storageKey?: string;
|
|
@@ -66,12 +74,90 @@ export interface ThemeToggleProps extends Omit<React.ComponentProps<"button">, "
|
|
|
66
74
|
* a one-tap decision, not a settings visit.
|
|
67
75
|
*/
|
|
68
76
|
declare function ThemeToggle({ className, storageKey, ...props }: ThemeToggleProps): React.JSX.Element;
|
|
77
|
+
/** The id the shell gives its help region, and the button's `aria-controls`. */
|
|
78
|
+
export declare const HELP_PANEL_ID = "cortena-help-panel";
|
|
79
|
+
/** The id the shell gives its mobile nav drawer, and the menu button's `aria-controls`. */
|
|
80
|
+
export declare const NAV_DRAWER_ID = "cortena-nav-drawer";
|
|
69
81
|
export interface HelpButtonProps extends React.ComponentProps<"button"> {
|
|
70
82
|
/** Whether the panel it controls is open. */
|
|
71
83
|
open?: boolean;
|
|
72
84
|
}
|
|
73
85
|
/** The help affordance. Right of the theme toggle, always — order is fixed (§9.3). */
|
|
74
86
|
declare function HelpButton({ className, open, ...props }: HelpButtonProps): React.JSX.Element;
|
|
87
|
+
/**
|
|
88
|
+
* A nav item's glyph: a lucide icon component, or the id of a mark registered
|
|
89
|
+
* in `cortena-design/marks` (so a nav row can wear another extension's mark
|
|
90
|
+
* without importing its SVG).
|
|
91
|
+
*/
|
|
92
|
+
export type AppShellNavIcon = LucideIcon | string;
|
|
93
|
+
export interface AppShellNavItem {
|
|
94
|
+
/** Stable id. What `activeId` names, and what `data-nav-item` carries. */
|
|
95
|
+
id: string;
|
|
96
|
+
label: string;
|
|
97
|
+
icon?: AppShellNavIcon;
|
|
98
|
+
/** The destination. Rendered as a real `href`, so middle-click and copy-link work. */
|
|
99
|
+
href: string;
|
|
100
|
+
/** A count or a short word. Rendered from tokens; `0` and `""` draw nothing. */
|
|
101
|
+
badge?: number | string;
|
|
102
|
+
/** Match `pathname` exactly rather than as a prefix. Ignored under `activeId`/`isActive`. */
|
|
103
|
+
exact?: boolean;
|
|
104
|
+
}
|
|
105
|
+
export interface AppShellNavGroup {
|
|
106
|
+
/** Section heading. A group with no label is separated by a rule instead. */
|
|
107
|
+
label?: string;
|
|
108
|
+
items: AppShellNavItem[];
|
|
109
|
+
}
|
|
110
|
+
export interface AppShellNav {
|
|
111
|
+
groups: AppShellNavGroup[];
|
|
112
|
+
/**
|
|
113
|
+
* Which item is active, decided in one of three ways, in this order:
|
|
114
|
+
*
|
|
115
|
+
* `activeId` the item's own id. The simplest, and the only one that
|
|
116
|
+
* needs nothing from the router.
|
|
117
|
+
* `isActive` a callback per item. Use it when "active" is not a path
|
|
118
|
+
* comparison — a query parameter, a nested layout route.
|
|
119
|
+
* `pathname` the current path, compared to each `href`: a prefix match,
|
|
120
|
+
* or an exact one for an item marked `exact`.
|
|
121
|
+
*
|
|
122
|
+
* cortena-ui has no router dependency and never will, so one of the three
|
|
123
|
+
* has to come from the consumer. With react-router:
|
|
124
|
+
*
|
|
125
|
+
* ```tsx
|
|
126
|
+
* const { pathname } = useLocation();
|
|
127
|
+
* const navigate = useNavigate();
|
|
128
|
+
*
|
|
129
|
+
* <AppShell
|
|
130
|
+
* nav={{
|
|
131
|
+
* groups,
|
|
132
|
+
* pathname,
|
|
133
|
+
* // Keep it a client-side navigation; the href stays real for
|
|
134
|
+
* // middle-click, "open in new tab" and copy-link, and the shell never
|
|
135
|
+
* // calls this for a modified or middle click.
|
|
136
|
+
* onNavigate: (item, event) => {
|
|
137
|
+
* event.preventDefault();
|
|
138
|
+
* navigate(item.href);
|
|
139
|
+
* },
|
|
140
|
+
* }}
|
|
141
|
+
* />
|
|
142
|
+
* ```
|
|
143
|
+
*
|
|
144
|
+
* Give none of the three and nothing is active — deliberately, because the
|
|
145
|
+
* alternative is reading `window.location` here, which would be right on
|
|
146
|
+
* first paint and then silently stale on every navigation after it.
|
|
147
|
+
*/
|
|
148
|
+
activeId?: string;
|
|
149
|
+
isActive?: (href: string, item: AppShellNavItem) => boolean;
|
|
150
|
+
pathname?: string;
|
|
151
|
+
/** Intercept a click. Called before the browser follows the `href`. */
|
|
152
|
+
onNavigate?: (item: AppShellNavItem, event: React.MouseEvent<HTMLAnchorElement>) => void;
|
|
153
|
+
/**
|
|
154
|
+
* Pinned under the items: File Vault's storage meter, Flux's stream status.
|
|
155
|
+
* The one thing in the sidebar that is neither navigation nor chrome.
|
|
156
|
+
*/
|
|
157
|
+
footer?: React.ReactNode;
|
|
158
|
+
/** The landmark's accessible name. Default "Sections". */
|
|
159
|
+
label?: string;
|
|
160
|
+
}
|
|
75
161
|
export interface AppShellExtension {
|
|
76
162
|
/** Registered mark id, from `x-cortena.icon` in the OpenAPI document. */
|
|
77
163
|
id: string;
|
|
@@ -95,9 +181,46 @@ export interface HelpPanelSlotProps {
|
|
|
95
181
|
source?: string;
|
|
96
182
|
onClose: () => void;
|
|
97
183
|
}
|
|
184
|
+
export interface AppShellMenuItem {
|
|
185
|
+
id: string;
|
|
186
|
+
label: string;
|
|
187
|
+
icon?: LucideIcon;
|
|
188
|
+
onSelect: () => void;
|
|
189
|
+
}
|
|
98
190
|
export interface AppShellProps extends Omit<React.ComponentProps<"div">, "title"> {
|
|
99
191
|
extension: AppShellExtension;
|
|
100
192
|
user: AppShellUser;
|
|
193
|
+
/**
|
|
194
|
+
* The left sidebar. `null` (or omitted) renders no sidebar and no menu
|
|
195
|
+
* button — a legal state, and the right one for an extension whose whole
|
|
196
|
+
* navigation is inside one page.
|
|
197
|
+
*/
|
|
198
|
+
nav?: AppShellNav | null;
|
|
199
|
+
/**
|
|
200
|
+
* Between the extension name and the avatar. For state, not for controls:
|
|
201
|
+
* a live-status pill, the org or workspace the screen is scoped to, or one
|
|
202
|
+
* secondary action. Navigation, theme, help and settings do NOT go here —
|
|
203
|
+
* they have exactly one home each and a second one is how two extensions
|
|
204
|
+
* stop looking like one product (§10).
|
|
205
|
+
*/
|
|
206
|
+
headerSlot?: React.ReactNode;
|
|
207
|
+
/**
|
|
208
|
+
* Extra avatar-menu items, inserted between Settings and Log out. For a
|
|
209
|
+
* destination that is the user's, not the screen's — Assure's Permissions,
|
|
210
|
+
* a Report-issue dialog. Not a second way to reach something already in the
|
|
211
|
+
* shell.
|
|
212
|
+
*/
|
|
213
|
+
menuItems?: AppShellMenuItem[];
|
|
214
|
+
/**
|
|
215
|
+
* How far the bottom-right cluster (and anything in `agentSlot`) lifts off
|
|
216
|
+
* the bottom edge, on top of its own 24px. A number is pixels.
|
|
217
|
+
*
|
|
218
|
+
* Omit it and the shell measures the first `[data-bottom-bar]` element
|
|
219
|
+
* inside itself instead, which is 0 when that element is hidden — so a
|
|
220
|
+
* `lg:hidden` mobile tab bar lifts the cluster on a phone and nowhere else,
|
|
221
|
+
* with nothing to keep in sync.
|
|
222
|
+
*/
|
|
223
|
+
bottomInset?: string | number;
|
|
101
224
|
/** Opens the extension's own settings route. The avatar menu is the only way in. */
|
|
102
225
|
onSettings?: () => void;
|
|
103
226
|
onProfile?: () => void;
|
|
@@ -114,7 +237,7 @@ export interface AppShellProps extends Omit<React.ComponentProps<"div">, "title"
|
|
|
114
237
|
/** localStorage key for the theme choice, forwarded to `useCortenaTheme`. */
|
|
115
238
|
themeStorageKey?: string;
|
|
116
239
|
}
|
|
117
|
-
declare function AppShell({ className, extension, user, onSettings, onProfile, onLogout, help, helpPanel, agentSlot, themeStorageKey, children, ...props }: AppShellProps): React.JSX.Element;
|
|
240
|
+
declare function AppShell({ className, style, extension, user, nav, headerSlot, menuItems, bottomInset, onSettings, onProfile, onLogout, help, helpPanel, agentSlot, themeStorageKey, children, ...props }: AppShellProps): React.JSX.Element;
|
|
118
241
|
/**
|
|
119
242
|
* `<title>` for an extension: the extension first, because that is what
|
|
120
243
|
* distinguishes one tab from another, and the product second so a row of tabs
|