@stealthscale/component-screen 0.0.1 → 0.2.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 CHANGED
@@ -5,7 +5,8 @@ page, the sidebar of destinations, the switcher at the head of a sidebar, the to
5
5
  the action bar over a selection, the splitter between panels a person resizes and the floating panel
6
6
  a person moves over the page. Every component takes its styles from a recipe, so a theme restyles
7
7
  all nine by extending the recipes. The preset under `./theme` registers the recipes with an
8
- application's compiler.
8
+ application's compiler. The entry under `./shell` adds the switchers of the locale and the theme,
9
+ which render the switcher's recipe.
9
10
 
10
11
  Every value a theme can change is an axis of a recipe, so a caller sets it as a prop and writes no
11
12
  style. A caller changes the element a component renders with `as`.
@@ -16,10 +17,12 @@ style. A caller changes the element a component renders with `as`.
16
17
  pnpm add @stealthscale/component-screen
17
18
  ```
18
19
 
19
- The package peers on `react`, `@stealthscale/theme`, `@stealthscale/hooks`,
20
+ The package peers on `react`, `react-dom`, `@stealthscale/theme`, `@stealthscale/hooks`,
20
21
  `@stealthscale/provider-viewport` and the component packages it renders: `a11y`, `actions`, `data`,
21
22
  `disclosure`, `forms`, `layout`, `modals`, `navigation`, `primitives` and `typography`. An
22
- application lists the preset under `./theme` among the presets its compiler installs.
23
+ application lists the preset under `./theme` among the presets its compiler installs. The `./shell`
24
+ entry also imports `@stealthscale/provider-locale` and `@stealthscale/provider-shell`. Both peers
25
+ are optional, so an application that imports only the main entry installs neither.
23
26
 
24
27
  ## Folding
25
28
 
@@ -29,18 +32,25 @@ open sidebar folds on the width the sidebar leaves it, and a caller does not wri
29
32
  media query. Before the element is laid out, and where the browser does not report a size, the
30
33
  viewport's width decides, so a phone never lays out wide first.
31
34
 
35
+ A toolbar also folds while its controls need more room than its row has. It measures the row while
36
+ no control is folded, and tries the unfolded row again when the row or the window changes width.
37
+ While the folded controls still need more room, the toolbar moves its secondary actions and links
38
+ and its switchers into its menu, the control nearest the row's end first, until the rest fit. The
39
+ row adds up the boxes of its controls, so a touch target larger than its control does not fold it.
40
+
32
41
  `Toolbar.Action`, `Page.Action` and `Section.Action` fold by priority. An action states `priority`,
33
42
  or the action's props imply one:
34
43
 
35
- | Props | Priority | On a narrow row |
36
- | ---------------------------- | ----------- | --------------------------------------- |
37
- | `primary`, no `icon` | `primary` | Unchanged |
38
- | `icon` | `secondary` | Icon alone, the label hidden visually |
39
- | neither `primary` nor `icon` | `tertiary` | Removed from the row, a row in its menu |
44
+ | Props | Priority | On a narrow row |
45
+ | ---------------------------- | ----------- | -------------------------------------------------------- |
46
+ | `primary`, no `icon` | `primary` | Unchanged |
47
+ | `icon` | `secondary` | Icon alone. In a toolbar without room, a row in its menu |
48
+ | neither `primary` nor `icon` | `tertiary` | Removed from the row, a row in its menu |
40
49
 
41
50
  - A secondary action keeps its label for screen readers.
42
51
  - A tertiary action leaves the row and the tab order. The row renders a menu with one row per folded
43
- action.
52
+ action. When one of those rows leads with an icon, the menu pads the others to the icon's gutter,
53
+ so the words of every row start at one line.
44
54
  - `more` names the menu's trigger, `More actions` by default. `moreIcon` replaces the trigger's
45
55
  words with a mark.
46
56
  - A folded link keeps its `href` in its menu row. A menu row calls the action's `onClick`.
@@ -100,7 +110,8 @@ import { ActionBar, Toolbar } from "@stealthscale/component-screen";
100
110
  toolbar renders its menu of folded actions as its last control, beside the actions. Give the main
101
111
  action `priority="primary"` to keep its words in a narrow window.
102
112
  - The bar is as wide as its content, at least `sizes.2xl`, 672px, and at most the window inside the
103
- inset. Its toolbar folds in a narrow window and not because of its own content.
113
+ inset. Its toolbar folds in a narrow window, and while its controls need more room than the window
114
+ leaves the bar.
104
115
  - The positioner is fixed above the safe area at the `banner` layer and takes no pointer events, so
105
116
  the page beside the bar remains pressable. `placement` sets the bar at the start, in the middle or
106
117
  at the end of the bottom edge, and start and end follow the writing direction.
@@ -125,8 +136,8 @@ The bar does not offer these:
125
136
 
126
137
  ## AppShell
127
138
 
128
- Lays out an application: bars across the top and the bottom, a body between them with a panel on
129
- either side of the main region, and a status bar at the foot.
139
+ Lays out an application: bars across the top and the bottom, a body between them with panels on
140
+ either side of the main region or of a pane, and a status bar at the foot.
130
141
 
131
142
  ```tsx
132
143
  import { AppShell, Sidebar } from "@stealthscale/component-screen";
@@ -154,10 +165,13 @@ import { AppShell, Sidebar } from "@stealthscale/component-screen";
154
165
 
155
166
  | Axis | Values | Default |
156
167
  | --------- | ---------------------------- | ------- |
168
+ | `content` | `fluid`, `constrained` | `fluid` |
157
169
  | `divided` | `true`, `false` | `true` |
158
170
  | `scroll` | `page`, `window` | `page` |
159
171
  | `variant` | `floating`, `inset`, `plain` | `plain` |
160
172
 
173
+ - `content="constrained"` caps the page in the main region at the theme's wide page measure,
174
+ `sizes.page.wide`, and centres it. `fluid` gives the page the whole region.
161
175
  - `divided` renders a hairline on the inner edge of each bar and each panel. A sidebar inside a
162
176
  panel sets its ground and no line.
163
177
  - `scroll="page"` sets the shell to the window's height and scrolls the main region. While a panel
@@ -183,19 +197,20 @@ The shell has no `palette` and no `effect` axis, because its parts are container
183
197
 
184
198
  ### Parts
185
199
 
186
- | Part | Element | Landmark |
187
- | ------------------ | -------- | --------------- |
188
- | `AppShell.Root` | `div` | none |
189
- | `AppShell.Header` | `header` | `banner` |
190
- | `AppShell.Body` | `div` | none |
191
- | `AppShell.Navbar` | `div` | none |
192
- | `AppShell.Rail` | `button` | none |
193
- | `AppShell.Main` | `main` | `main` |
194
- | `AppShell.Aside` | `aside` | `complementary` |
195
- | `AppShell.Section` | `div` | none |
196
- | `AppShell.Footer` | `footer` | `contentinfo` |
197
- | `AppShell.Status` | `div` | none |
198
- | `AppShell.Trigger` | `button` | none |
200
+ | Part | Element | Landmark |
201
+ | ------------------ | --------------------------------------------------------------------- | --------------- |
202
+ | `AppShell.Root` | `div` | none |
203
+ | `AppShell.Header` | `header` | `banner` |
204
+ | `AppShell.Body` | `div` | none |
205
+ | `AppShell.Navbar` | `div` | none |
206
+ | `AppShell.Rail` | `button`, or a `div` in the `separator` role beside a resizable panel | none |
207
+ | `AppShell.Pane` | `div` | none |
208
+ | `AppShell.Main` | `main` | `main` |
209
+ | `AppShell.Aside` | `aside` | `complementary` |
210
+ | `AppShell.Section` | `div` | none |
211
+ | `AppShell.Footer` | `footer` | `contentinfo` |
212
+ | `AppShell.Status` | `section` | `region` |
213
+ | `AppShell.Trigger` | `button` | none |
199
214
 
200
215
  `AppShell.Navbar` has no landmark, because a sidebar inside it renders its own navigation landmarks.
201
216
  Name `AppShell.Aside` with `aria-label` when an application renders more than one.
@@ -203,8 +218,10 @@ Name `AppShell.Aside` with `aria-label` when an application renders more than on
203
218
  Render `AppShell.Status` after the footer, for the state of the application: the connection, a sync,
204
219
  work that runs in the background. It lays its entries out in a row that wraps, padded by
205
220
  `spacing.gap.xs` above and below and `spacing.gap.md` at the sides, with a hairline above it under
206
- `divided`. It is not a live region, so an entry that reports a change renders its own `output`.
207
- Under `scroll="window"` the bar is at the end of the document.
221
+ `divided`. The bar is a region named by `label`, `Status` by default, so its entries are inside a
222
+ landmark. It is not a live region, so an entry that reports a change renders its own `output`. Under
223
+ `scroll="window"` the bar is at the end of the document. The bar takes no room while every entry in
224
+ it renders nothing.
208
225
 
209
226
  `AppShell.Section` is a padded band in a panel or in the main region. `grows` gives it the room its
210
227
  siblings leave, and `scrolls` makes it a scroll area, so the bands around it remain in place. A band
@@ -218,46 +235,101 @@ height exactly and scrolls its own list.
218
235
 
219
236
  `AppShell.Navbar` and `AppShell.Aside` are the same panel on the two sides.
220
237
 
221
- | Prop | Values | Default |
222
- | -------------- | --------------------- | ---------------------------------------------- |
223
- | `collapse` | `hide`, `icons` | `hide` |
224
- | `folds` | `over`, `under` | `over` |
225
- | `foldsBelow` | a breakpoint, `never` | `md` on the start side, `lg` on the end side |
226
- | `width` | a CSS length | `sizes.sidebar` or `sizes.aside` |
227
- | `railWidth` | a CSS length | `sizes.rail` |
228
- | `defaultOpen` | `true`, `false` | `true` |
229
- | `open` | `true`, `false` | uncontrolled |
230
- | `onOpenChange` | a function | none |
231
- | `name` | a string | `navbar` on the start side, `aside` on the end |
232
- | `shortcut` | a key | none |
238
+ | Prop | Values | Default |
239
+ | -------------- | ----------------------- | ---------------------------------------------- |
240
+ | `collapse` | `hide`, `icons` | `hide` |
241
+ | `folds` | `over`, `under`, `hide` | `over` |
242
+ | `foldsBelow` | a breakpoint, `never` | `md` on the start side, `lg` on the end side |
243
+ | `width` | a CSS length | `sizes.sidebar` or `sizes.aside` |
244
+ | `railWidth` | a CSS length | `sizes.rail` |
245
+ | `defaultOpen` | `true`, `false` | `true` |
246
+ | `open` | `true`, `false` | uncontrolled |
247
+ | `onOpenChange` | a function | none |
248
+ | `name` | a string | `navbar` on the start side, `aside` on the end |
249
+ | `shortcut` | a key | none |
250
+ | `resizable` | `true`, `false` | `false` |
251
+ | `defaultSize` | pixels | `width` |
252
+ | `size` | pixels | uncontrolled |
253
+ | `onSizeChange` | a function | none |
254
+ | `minSize` | pixels | 192 |
255
+ | `maxSize` | pixels | half the window |
233
256
 
234
257
  - `collapse` sets what closing the panel beside the page leaves: `hide` hides it, and `icons` leaves
235
258
  a rail of `railWidth`. A sidebar inside the panel renders as a rail on its own.
236
259
  - `folds` sets where the panel goes when the shell is narrower than `foldsBelow`: `over` lays it
237
- over the page behind a backdrop, and `under` drops it under the main region as a block that is
238
- always shown. `foldsBelow="never"` keeps the panel in the body at every width.
260
+ over the page behind a backdrop, `under` drops it under the main region as a block that is always
261
+ shown, and `hide` takes it out of the shell until the shell is wide enough again, such as a rail
262
+ of apps whose destinations a bar at the foot of the shell lists on a phone. A panel out of the
263
+ shell is hidden and inert. Its trigger and its rail do not render, and its shortcut does not
264
+ toggle it. `foldsBelow="never"` keeps the panel in the body at every width.
239
265
  - `shortcut` toggles the panel with Control or Command held: `shortcut="b"` for ⌘B and Ctrl+B.
266
+ - `resizable` lets a person drag the panel's rail, or focus it and press the arrow keys, to set the
267
+ panel's width. The width a person sets replaces `width`. The panel is at most half the window wide
268
+ unless `maxSize` states another limit, so a width kept from a wide screen fits a narrow one.
269
+ `size` and `onSizeChange` control the width, for an application that keeps it. While a drag runs,
270
+ the root has `data-resizing`: the panels stop their transitions, and the resize cursor shows over
271
+ the whole shell.
240
272
  - A panel over the page starts closed, and closes again every time the shell narrows. It is a raised
241
273
  surface, `bg.panel` with the `lg` shadow, and keeps its width while it slides closed. An
242
- application that passes `open` controls the panel at every width.
274
+ application's `open` and `onOpenChange` apply while the panel is beside the page, so a stored
275
+ choice never opens a sheet over a phone's page.
243
276
  - While a panel is over the page, the bars, the main region and the other panels are inert, focus
244
277
  moves into the panel, and Escape or a press on the backdrop closes it and returns focus to the
245
278
  control that opened it. The panel has no dialog role, because the inert page and the returned
246
279
  focus give a reader the same behaviour.
247
280
 
281
+ ### Panes
282
+
283
+ `AppShell.Pane` is a column in the body for bars that span the page alone. Render the header, the
284
+ main region and the footer inside it, after the panels at the start and before those at the end, and
285
+ the panels run the full height of the body beside the bars.
286
+
287
+ ```tsx
288
+ <AppShell.Root variant="inset">
289
+ <AppShell.Body>
290
+ <AppShell.Navbar folds="hide" name="apps" width="4rem">
291
+ …
292
+ </AppShell.Navbar>
293
+ <AppShell.Navbar name="sections" resizable>
294
+ …
295
+ </AppShell.Navbar>
296
+ <AppShell.Rail panel="sections" resizeLabel="Resize the settings" />
297
+ <AppShell.Pane>
298
+ <AppShell.Header>…</AppShell.Header>
299
+ <AppShell.Main>…</AppShell.Main>
300
+ <AppShell.Footer when="narrow">…</AppShell.Footer>
301
+ </AppShell.Pane>
302
+ </AppShell.Body>
303
+ </AppShell.Root>
304
+ ```
305
+
306
+ - A header in a pane is outside the main region, so it is the `banner` landmark, and the main
307
+ region's scroll bar starts under it.
308
+ - Under `scroll="window"` a pinned bar in a pane sticks under the root's pinned bars, and the panels
309
+ stick under the root's bars alone. A page's sticky bands stick under both.
310
+ - `variant="inset"` gives the pane the main region's card, so the bars in the pane are on the card.
311
+
248
312
  ### Triggers, the rail and hooks
249
313
 
250
314
  `AppShell.Trigger` renders the library's button, a neutral ghost unless the caller states another
251
315
  look. It finds its panel by the name passed as `panel`, `navbar` by default, and sets
252
316
  `aria-controls`, `aria-expanded` and `data-state` from it, with no pressed fill. It does not render
253
- while its panel is under the page, because that panel is always shown. Name the trigger for the
254
- panel, such as `Navigation`, so a screen reader announces "Navigation, collapsed, button".
317
+ while its panel is under the page or out of the shell, because nothing it does can show or hide that
318
+ panel. Name the trigger for the panel, such as `Navigation`, so a screen reader announces
319
+ "Navigation, collapsed, button".
255
320
 
256
321
  `AppShell.Rail` is a 24px strip on the edge between a panel and the page, with a 2px line under the
257
- pointer and on focus. Render it after the navbar or before an aside, and name it with `openLabel`
258
- and `closeLabel`. It is not in the tab order, because the trigger and the shortcut open and close
259
- the same panel. It does not render while its panel is over or under the page, and the recipe hides
260
- it under a coarse pointer.
322
+ pointer and on focus. Render it after the navbar or before an aside. It does not render while its
323
+ panel is over or under the page or out of the shell, and the recipe hides it under a coarse pointer.
324
+
325
+ - Beside a panel that is not resizable the rail is a button named by `openLabel` and `closeLabel`.
326
+ It is not in the tab order, because the trigger and the shortcut open and close the same panel.
327
+ - Beside a resizable panel the rail is the panel's `separator`, named by `resizeLabel`, `Resize` by
328
+ default, with a tab stop, as the WAI-ARIA window splitter pattern requires. Its value is the
329
+ panel's width in pixels. The arrows move it by 10px and by 50px with Shift, Home and End move it
330
+ to `minSize` and `maxSize`, and Enter and a press close and open the panel. A drag starts once the
331
+ pointer has moved 3px, and Escape during a drag returns the width the drag started at. Resizing a
332
+ closed panel opens it at the new width.
261
333
 
262
334
  `useAppShellPanel(name)` reads a panel from an application's own code, for example to close the
263
335
  navigation when a destination is pressed:
@@ -328,6 +400,8 @@ The recipe has no axes.
328
400
  distance from the pointer to the panel's edge, and `allowOverflow={false}` keeps it inside.
329
401
  - A controlled panel remains open until `open` turns false. The trigger, the close trigger and
330
402
  Escape call `onOpenChange` with `{ open: false }`.
403
+ - A closing panel plays its exit motion where it is. Each open starts from the position and the size
404
+ of the first open, unless `persistRect` keeps those the panel closed at.
331
405
  - The positioner is fixed to the window at the `banner` layer plus the panel's place among the open
332
406
  panels: above the page's sticky bands, and under a backdrop, a dialog, a popover, a toast and a
333
407
  tooltip. Render it in a portal. A press or focus inside a panel brings it in front, and Escape
@@ -389,13 +463,13 @@ The panel does not offer these:
389
463
  the panel remains inside a boundary that does not scroll with the page, such as an app shell's
390
464
  main region, and leaves one that does.
391
465
 
392
- A menu, a select or a combobox opened inside the panel stacks at the `dropdown` layer, under the
393
- panel.
466
+ A menu, a select, a combobox or a date picker opened inside the panel stacks at the `popover` layer,
467
+ over the panel.
394
468
 
395
469
  ## Page
396
470
 
397
471
  Lays out a page as a column of bands: a banner, a header, a navigation, a toolbar, a body with an
398
- optional aside, and a footer.
472
+ optional sidebar and aside, and a footer.
399
473
 
400
474
  ```tsx
401
475
  import { Page } from "@stealthscale/component-screen";
@@ -483,6 +557,43 @@ nearest item replaces the trail, with `backIcon` before its words.
483
557
  the query matches no tab.
484
558
  - `Page.Picker` and `Page.Palette` are that button and its panel, for a caller that composes its own
485
559
  picker: `<Menu.Trigger as={Page.Picker}>`, and the menu's content rendered as `Page.Palette`.
560
+ - `Page.Tabs` is the slot for the disclosure package's `Tabs.List`, rendered with `as`, where the
561
+ caller's tabs root also contains the panels. In `Page.Nav` its strip starts at the gutter and its
562
+ tabs take the sizes of `Page.TabList`'s, and the band's hairline replaces the strip's own.
563
+
564
+ ### Sidebar
565
+
566
+ `Page.Sidebar` renders the page's own destinations at the start of the body, a level below the
567
+ shell's sidebar: the child pages of a route, or the sections of a settings page. Render a `nav`
568
+ named by `aria-label` with a navigation list in it.
569
+
570
+ ```tsx
571
+ <Page.Sidebar
572
+ current="Members"
573
+ label="Settings sections"
574
+ pickerIcon={<ChevronDownIcon />}
575
+ resizable
576
+ >
577
+ <nav aria-label="Settings sections">
578
+ <NavList.Root>…</NavList.Root>
579
+ </nav>
580
+ </Page.Sidebar>
581
+ ```
582
+
583
+ - From the `md` breakpoint of the page's own width the sidebar is a column beside the body, as wide
584
+ as `sizes.sidebar`. The column starts at the gutter, so the fills of its rows start where the
585
+ title starts.
586
+ - On a narrower page a `Page.Picker` above the body replaces the column, with `current` as its
587
+ words, `label` where `current` is left out, and `pickerIcon` at its end. It opens the column's
588
+ content in a `Page.Palette` named by `label`, and a press on a link in the panel closes it.
589
+ - `sticky` keeps the column in view under the shell's pinned bars and the page's sticky bands, at
590
+ most as tall as the room they leave, and renders its content in the primitives package's scroll
591
+ area.
592
+ - `resizable` renders a `separator` on the column's end edge, named by `resizeLabel`, `Resize` by
593
+ default, with the keys and the drag of a resizable shell panel. `size`, `defaultSize`,
594
+ `onSizeChange`, `minSize` and `maxSize` take pixels, as a panel's do.
595
+ - A page with a sidebar and an aside lays the three out in one row from the `lg` breakpoint, and
596
+ stacks the aside under the body below it.
486
597
 
487
598
  ## Section
488
599
 
@@ -605,6 +716,8 @@ before the query.
605
716
  while a query is active. Put it in the scope the search filters, and name what was searched:
606
717
  `No pages match`.
607
718
  - With children, `Sidebar.Search` contains the caller's field, and the caller filters the rows.
719
+ - The search is a landmark in the `search` role, named like its field, so a search in the sidebar's
720
+ header is inside a landmark. Give each search on a page its own `aria-label`.
608
721
 
609
722
  ### In an app shell
610
723
 
@@ -729,7 +842,11 @@ import { Switcher } from "@stealthscale/component-screen";
729
842
  closed to icons it is its mark alone, a 32px square at `md`, with its name in a tooltip, and its
730
843
  menu opens beside the rail.
731
844
  - Alone and in a toolbar the switcher is one line as wide as its words, at least as tall as a button
732
- of its size. In a narrow toolbar a switcher with a mark hides its name.
845
+ of its size. In a narrow toolbar a switcher with a mark is its mark alone, a square as tall as a
846
+ button of its size.
847
+ - A narrow toolbar without room for a switcher with `items` moves it into the toolbar's menu. Its
848
+ row there shows `label` with the current name under it, and `indicator`, and it opens the choices
849
+ in a submenu. A switcher composed from parts keeps its place.
733
850
  - The looks and the palettes are the button's. The outline look has the button's light edge.
734
851
  - Render the menu's positioner in a `Portal` when the switcher is inside an element that clips, such
735
852
  as an app shell panel, so the menu renders over the page.
@@ -753,6 +870,37 @@ the menu's own:
753
870
  </Switcher.Root>
754
871
  ```
755
872
 
873
+ ## LocaleSwitcher and ThemeSwitcher
874
+
875
+ `@stealthscale/component-screen/shell` exports the switchers of what an application's shell
876
+ provides. Each renders `Switcher.Root` with a row per choice on offer, so the switcher's axes and
877
+ the menu's keys apply to it.
878
+
879
+ ```tsx
880
+ import { LocaleSwitcher, ThemeSwitcher } from "@stealthscale/component-screen/shell";
881
+
882
+ <Toolbar.End>
883
+ <LocaleSwitcher checkIcon={<CheckIcon />} indicator={<ChevronsUpDownIcon />} />
884
+ <ThemeSwitcher checkIcon={<CheckIcon />} indicator={<ChevronsUpDownIcon />} />
885
+ </Toolbar.End>;
886
+ ```
887
+
888
+ - `LocaleSwitcher` reads `useLocale` from `@stealthscale/provider-locale`. Each row shows its
889
+ language's name in that language, `Nederlands` for `nl`, with the language subtag in capitals as
890
+ its mark. A pick sets the locale, so the page is translated, and a language written right to left
891
+ mirrors the page. The control is named `Language` unless `label` names it otherwise.
892
+ - `ThemeSwitcher` reads `useThemeChoice` from `@stealthscale/provider-shell`. Each row shows the
893
+ theme's name with a swatch of its primary colour, which the swatch resolves in its own theme
894
+ through `data-theme`. A pick sets the shell's theme, and the shell keeps the choice under the
895
+ application's name. The control is named `Theme` unless `label` names it otherwise.
896
+ - Each takes every prop of `Switcher.Root` except the choices and the value: `checkIcon`,
897
+ `indicator`, `variant`, `palette`, `size`, `placement`, `positioning`, and `Switcher.Action` rows
898
+ as children.
899
+ - Each renders only where the application offers two choices or more.
900
+ - `useLocale` throws outside a `LocaleProvider`, and outside a `Shell` the theme choice offers no
901
+ theme.
902
+ - The locale switcher does not mark a change in progress, which `useLocale().isPending` reports.
903
+
756
904
  ## Toolbar
757
905
 
758
906
  Renders a row of controls over a table or a list: a band at each end, a band in the middle, and a
@@ -763,6 +911,7 @@ import { Toolbar } from "@stealthscale/component-screen";
763
911
 
764
912
  <Toolbar.Root aria-label="Invoices" more="More actions" moreIcon={<EllipsisIcon />}>
765
913
  <Toolbar.Start>
914
+ <Toolbar.Title>Invoices</Toolbar.Title>
766
915
  <Toolbar.Link current href="/invoices/drafts">
767
916
  Drafts
768
917
  </Toolbar.Link>
@@ -789,8 +938,16 @@ import { Toolbar } from "@stealthscale/component-screen";
789
938
 
790
939
  - The row has `role="toolbar"`, and the arrow keys move focus between its controls, so the row is
791
940
  one tab stop. Name it with `aria-label`.
792
- - The row measures its own width and writes `data-narrow` below `sm`. Its gap is the gap token of
793
- its size, and every library button in it renders at its size.
941
+ - The row folds its controls and writes `data-narrow` below `sm` of its own width, and while its
942
+ controls need more room than it has. Its gap is the gap token of its size, and every library
943
+ button in it renders at its size.
944
+ - A folded row that still has no room moves a secondary `Toolbar.Action`, a secondary `Toolbar.Link`
945
+ and a switcher into its menu, the one nearest its end first. `Toolbar.Item` and `Toolbar.Search`
946
+ keep their place, because they have no row in a menu.
947
+ - `Toolbar.Title` renders the row's title, a name or a brand, in semibold with twice the row's gap
948
+ after it, so the control after it reads as the row's first control and not as part of the title. A
949
+ narrow row keeps it with the row's gap alone, because the row folds the controls the wider gap
950
+ sets apart. A link inside it renders as a `Toolbar.Item`, one of the row's controls.
794
951
  - `Toolbar.Action` renders the library's button, `ghost`, or `solid` with `primary`, and folds by
795
952
  its priority.
796
953
  - `Toolbar.Link` renders an `a` in the same look with `href`, `current` and `icon`. `current` sets