@stealthscale/component-screen 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +965 -2
- package/dist/cyclonedx/bom.json +1 -0
- package/dist/index.d.ts +2295 -0
- package/dist/index.js +8217 -0
- package/dist/recipe-DRMsitrw.js +283 -0
- package/dist/recipe-sZAQY6Kq.js +2623 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/root-DDwipXru.d.ts +327 -0
- package/dist/root-qEh4u7y5.js +1182 -0
- package/dist/shell.d.ts +42 -0
- package/dist/shell.js +166 -0
- package/dist/theme.d.ts +12 -0
- package/dist/theme.js +28 -0
- package/package.json +114 -3
package/README.md
CHANGED
|
@@ -1,3 +1,966 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @stealthscale/component-screen
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Lays out an application's screen: the shell around everything, the page inside it, the sections of a
|
|
4
|
+
page, the sidebar of destinations, the switcher at the head of a sidebar, the toolbar over a table,
|
|
5
|
+
the action bar over a selection, the splitter between panels a person resizes and the floating panel
|
|
6
|
+
a person moves over the page. Every component takes its styles from a recipe, so a theme restyles
|
|
7
|
+
all nine by extending the recipes. The preset under `./theme` registers the recipes with an
|
|
8
|
+
application's compiler. The entry under `./shell` adds the switchers of the locale and the theme,
|
|
9
|
+
which render the switcher's recipe.
|
|
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
|
|
12
|
+
style. A caller changes the element a component renders with `as`.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm add @stealthscale/component-screen
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The package peers on `react`, `react-dom`, `@stealthscale/theme`, `@stealthscale/hooks`,
|
|
21
|
+
`@stealthscale/provider-viewport` and the component packages it renders: `a11y`, `actions`, `data`,
|
|
22
|
+
`disclosure`, `forms`, `layout`, `modals`, `navigation`, `primitives` and `typography`. An
|
|
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.
|
|
26
|
+
|
|
27
|
+
## Folding
|
|
28
|
+
|
|
29
|
+
A component folds on its own width, not on the window's. It compares its element's width with the
|
|
30
|
+
width a breakpoint starts at and writes `data-narrow`, which the recipes select on. A page beside an
|
|
31
|
+
open sidebar folds on the width the sidebar leaves it, and a caller does not write a breakpoint or a
|
|
32
|
+
media query. Before the element is laid out, and where the browser does not report a size, the
|
|
33
|
+
viewport's width decides, so a phone never lays out wide first.
|
|
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
|
+
|
|
41
|
+
`Toolbar.Action`, `Page.Action` and `Section.Action` fold by priority. An action states `priority`,
|
|
42
|
+
or the action's props imply one:
|
|
43
|
+
|
|
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 |
|
|
49
|
+
|
|
50
|
+
- A secondary action keeps its label for screen readers.
|
|
51
|
+
- A tertiary action leaves the row and the tab order. The row renders a menu with one row per folded
|
|
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.
|
|
54
|
+
- `more` names the menu's trigger, `More actions` by default. `moreIcon` replaces the trigger's
|
|
55
|
+
words with a mark.
|
|
56
|
+
- A folded link keeps its `href` in its menu row. A menu row calls the action's `onClick`.
|
|
57
|
+
- A control that opens its own overlay cannot run from a menu row. Give it an icon or `primary`.
|
|
58
|
+
- An action writes `data-narrow` on itself from its own row. An action in a wide row inside a narrow
|
|
59
|
+
component keeps its words, such as the toolbar in an action bar, which is fixed to the window and
|
|
60
|
+
wider than the page around it in the document.
|
|
61
|
+
|
|
62
|
+
## ActionBar
|
|
63
|
+
|
|
64
|
+
Renders a bar fixed to the bottom of the window with the actions for what a person has selected. The
|
|
65
|
+
bar contains a toolbar, so its actions have one tab stop, the arrow keys and folding.
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { ActionBar, Toolbar } from "@stealthscale/component-screen";
|
|
69
|
+
|
|
70
|
+
<ActionBar.Root onOpenChange={() => setSelected([])} open={selected.length > 0}>
|
|
71
|
+
<ActionBar.Positioner>
|
|
72
|
+
<ActionBar.Content>
|
|
73
|
+
<Toolbar.Root aria-label="Actions for the selected invoices" size="sm">
|
|
74
|
+
<Toolbar.Start>
|
|
75
|
+
<ActionBar.CloseTrigger aria-label="Clear selection">
|
|
76
|
+
<XIcon />
|
|
77
|
+
</ActionBar.CloseTrigger>
|
|
78
|
+
<Text size="sm">{selected.length} selected</Text>
|
|
79
|
+
</Toolbar.Start>
|
|
80
|
+
<Toolbar.End>
|
|
81
|
+
<Toolbar.Action icon={<DownloadIcon />}>Download</Toolbar.Action>
|
|
82
|
+
<Toolbar.Action icon={<ReceiptIcon />} primary priority="primary">
|
|
83
|
+
Mark as paid
|
|
84
|
+
</Toolbar.Action>
|
|
85
|
+
</Toolbar.End>
|
|
86
|
+
</Toolbar.Root>
|
|
87
|
+
</ActionBar.Content>
|
|
88
|
+
</ActionBar.Positioner>
|
|
89
|
+
</ActionBar.Root>;
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
| Axis | Values | Default |
|
|
93
|
+
| ----------- | -------------------------------------- | -------- |
|
|
94
|
+
| `placement` | `bottom-start`, `bottom`, `bottom-end` | `bottom` |
|
|
95
|
+
|
|
96
|
+
- The caller keeps the selection. `open` is true while anything is selected. `onOpenChange` receives
|
|
97
|
+
`{ open: false }` on Escape inside the bar and on a press of `ActionBar.CloseTrigger`, and the
|
|
98
|
+
caller clears the selection, which closes the bar.
|
|
99
|
+
- Render the root after the list it acts on, so Tab from the list moves into the bar.
|
|
100
|
+
- Focus remains on the selection when the bar opens. A polite live region announces `announcement`,
|
|
101
|
+
`Actions available` by default.
|
|
102
|
+
- When the bar closes with focus inside it, focus returns to the element that had it when the bar
|
|
103
|
+
opened. Focus that a person moved elsewhere remains there.
|
|
104
|
+
- `closeOnEscape={false}` leaves Escape to the caller. An Escape that a control inside the bar
|
|
105
|
+
handles with `preventDefault`, or that a menu portalled out of the bar receives, does not close
|
|
106
|
+
it.
|
|
107
|
+
- `ActionBar.CloseTrigger` renders the actions package's `IconButton`, ghost, as an item of the
|
|
108
|
+
toolbar. Name it with `aria-label`. An `onClick` that calls `preventDefault` keeps the bar open.
|
|
109
|
+
- Put the close trigger and the count in `Toolbar.Start` and the actions in `Toolbar.End`. The
|
|
110
|
+
toolbar renders its menu of folded actions as its last control, beside the actions. Give the main
|
|
111
|
+
action `priority="primary"` to keep its words in a narrow window.
|
|
112
|
+
- The bar is as wide as its content, at least `sizes.2xl`, 672px, and at most the window inside the
|
|
113
|
+
inset. Its toolbar folds in a narrow window, and while its controls need more room than the window
|
|
114
|
+
leaves the bar.
|
|
115
|
+
- The positioner is fixed above the safe area at the `banner` layer and takes no pointer events, so
|
|
116
|
+
the page beside the bar remains pressable. `placement` sets the bar at the start, in the middle or
|
|
117
|
+
at the end of the bottom edge, and start and end follow the writing direction.
|
|
118
|
+
- The bar is not in the document before it first opens, and leaves once its exit motion ends.
|
|
119
|
+
`lazyMount` and `unmountOnExit` default to `true`. It rises from the bottom edge as it fades in,
|
|
120
|
+
and is inert while it leaves.
|
|
121
|
+
- The bar has no `palette` and no `effect` axis, because it is a `bg.panel` surface for controls
|
|
122
|
+
that have their own.
|
|
123
|
+
|
|
124
|
+
| Part | Element |
|
|
125
|
+
| ------------------------ | -------------------------- |
|
|
126
|
+
| `ActionBar.Root` | `div`, `display: contents` |
|
|
127
|
+
| `ActionBar.Positioner` | `div` |
|
|
128
|
+
| `ActionBar.Content` | `div` |
|
|
129
|
+
| `ActionBar.CloseTrigger` | `button` |
|
|
130
|
+
|
|
131
|
+
The bar does not offer these:
|
|
132
|
+
|
|
133
|
+
- A selection trigger. The count is text the caller renders in the toolbar.
|
|
134
|
+
- A bar placed inside a container. The positioner is fixed to the window.
|
|
135
|
+
- Escape outside the bar. A list clears its selection with its own keys.
|
|
136
|
+
|
|
137
|
+
## AppShell
|
|
138
|
+
|
|
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.
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
import { AppShell, Sidebar } from "@stealthscale/component-screen";
|
|
144
|
+
|
|
145
|
+
<AppShell.Root>
|
|
146
|
+
<AppShell.Header sticky>
|
|
147
|
+
<AppShell.Trigger aria-label="Navigation">…</AppShell.Trigger>
|
|
148
|
+
</AppShell.Header>
|
|
149
|
+
<AppShell.Body>
|
|
150
|
+
<AppShell.Navbar collapse="icons" shortcut="b">
|
|
151
|
+
<Sidebar.Root>…</Sidebar.Root>
|
|
152
|
+
</AppShell.Navbar>
|
|
153
|
+
<AppShell.Rail closeLabel="Close navigation" openLabel="Open navigation" />
|
|
154
|
+
<AppShell.Main>…</AppShell.Main>
|
|
155
|
+
<AppShell.Aside aria-label="Invoice detail" folds="under" width="20rem">
|
|
156
|
+
<AppShell.Section grows scrolls>
|
|
157
|
+
…
|
|
158
|
+
</AppShell.Section>
|
|
159
|
+
</AppShell.Aside>
|
|
160
|
+
</AppShell.Body>
|
|
161
|
+
<AppShell.Footer when="narrow">…</AppShell.Footer>
|
|
162
|
+
<AppShell.Status>…</AppShell.Status>
|
|
163
|
+
</AppShell.Root>;
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
| Axis | Values | Default |
|
|
167
|
+
| --------- | ---------------------------- | ------- |
|
|
168
|
+
| `content` | `fluid`, `constrained` | `fluid` |
|
|
169
|
+
| `divided` | `true`, `false` | `true` |
|
|
170
|
+
| `scroll` | `page`, `window` | `page` |
|
|
171
|
+
| `variant` | `floating`, `inset`, `plain` | `plain` |
|
|
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.
|
|
175
|
+
- `divided` renders a hairline on the inner edge of each bar and each panel. A sidebar inside a
|
|
176
|
+
panel sets its ground and no line.
|
|
177
|
+
- `scroll="page"` sets the shell to the window's height and scrolls the main region. While a panel
|
|
178
|
+
is under the page, the body scrolls, so the page and the panel scroll together.
|
|
179
|
+
- `scroll="window"` scrolls the document, pins each `sticky` bar under the bars before it, and
|
|
180
|
+
sticks a panel beside the page under the pinned bars. A pinned bar is filled with `bg.panel`.
|
|
181
|
+
- The body, the main region, a panel's content and a section that scrolls are the primitives
|
|
182
|
+
package's scroll areas, so each bar in the shell is the theme's thin bar, shown under the pointer
|
|
183
|
+
and while the region scrolls. The main region's viewport is the `main` element, and
|
|
184
|
+
`AppShell.Main` passes its props to it. Under `scroll="window"` neither viewport scrolls, and the
|
|
185
|
+
browser's bar scrolls the document.
|
|
186
|
+
- `variant="plain"` fills no region, so the shell shows the ground it is placed on.
|
|
187
|
+
`variant="inset"` gives the main region the panel ground, a hairline and a shadow over a muted
|
|
188
|
+
root. `variant="floating"` gives each panel's content the same instead. A sidebar in a panel
|
|
189
|
+
paints its own ground.
|
|
190
|
+
- The header and the footer pad by `spacing.gap.md`. `AppShell.Footer when="narrow"` renders below
|
|
191
|
+
the `md` breakpoint alone, and `when="wide"` from it.
|
|
192
|
+
- The shell reads its window's height from `WINDOW_HEIGHT`, the custom property
|
|
193
|
+
`--app-shell-window-height`, and `100dvh` without it. A shell drawn in a box of fixed height sets
|
|
194
|
+
it on the box.
|
|
195
|
+
|
|
196
|
+
The shell has no `palette` and no `effect` axis, because its parts are containers.
|
|
197
|
+
|
|
198
|
+
### Parts
|
|
199
|
+
|
|
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 |
|
|
214
|
+
|
|
215
|
+
`AppShell.Navbar` has no landmark, because a sidebar inside it renders its own navigation landmarks.
|
|
216
|
+
Name `AppShell.Aside` with `aria-label` when an application renders more than one.
|
|
217
|
+
|
|
218
|
+
Render `AppShell.Status` after the footer, for the state of the application: the connection, a sync,
|
|
219
|
+
work that runs in the background. It lays its entries out in a row that wraps, padded by
|
|
220
|
+
`spacing.gap.xs` above and below and `spacing.gap.md` at the sides, with a hairline above it under
|
|
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.
|
|
225
|
+
|
|
226
|
+
`AppShell.Section` is a padded band in a panel or in the main region. `grows` gives it the room its
|
|
227
|
+
siblings leave, and `scrolls` makes it a scroll area, so the bands around it remain in place. A band
|
|
228
|
+
that scrolls is for content whose every item takes focus, such as a list of links or a navigation,
|
|
229
|
+
so its viewport is outside the tab order.
|
|
230
|
+
|
|
231
|
+
A page in the main region grows to the region's height, and a sidebar in a panel takes the panel's
|
|
232
|
+
height exactly and scrolls its own list.
|
|
233
|
+
|
|
234
|
+
### Panels
|
|
235
|
+
|
|
236
|
+
`AppShell.Navbar` and `AppShell.Aside` are the same panel on the two sides.
|
|
237
|
+
|
|
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 |
|
|
256
|
+
|
|
257
|
+
- `collapse` sets what closing the panel beside the page leaves: `hide` hides it, and `icons` leaves
|
|
258
|
+
a rail of `railWidth`. A sidebar inside the panel renders as a rail on its own.
|
|
259
|
+
- `folds` sets where the panel goes when the shell is narrower than `foldsBelow`: `over` lays it
|
|
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.
|
|
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.
|
|
272
|
+
- A panel over the page starts closed, and closes again every time the shell narrows. It is a raised
|
|
273
|
+
surface, `bg.panel` with the `lg` shadow, and keeps its width while it slides closed. An
|
|
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.
|
|
276
|
+
- While a panel is over the page, the bars, the main region and the other panels are inert, focus
|
|
277
|
+
moves into the panel, and Escape or a press on the backdrop closes it and returns focus to the
|
|
278
|
+
control that opened it. The panel has no dialog role, because the inert page and the returned
|
|
279
|
+
focus give a reader the same behaviour.
|
|
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
|
+
|
|
312
|
+
### Triggers, the rail and hooks
|
|
313
|
+
|
|
314
|
+
`AppShell.Trigger` renders the library's button, a neutral ghost unless the caller states another
|
|
315
|
+
look. It finds its panel by the name passed as `panel`, `navbar` by default, and sets
|
|
316
|
+
`aria-controls`, `aria-expanded` and `data-state` from it, with no pressed fill. It does not render
|
|
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".
|
|
320
|
+
|
|
321
|
+
`AppShell.Rail` is a 24px strip on the edge between a panel and the page, with a 2px line under the
|
|
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.
|
|
333
|
+
|
|
334
|
+
`useAppShellPanel(name)` reads a panel from an application's own code, for example to close the
|
|
335
|
+
navigation when a destination is pressed:
|
|
336
|
+
|
|
337
|
+
```tsx
|
|
338
|
+
const navbar = useAppShellPanel("navbar");
|
|
339
|
+
|
|
340
|
+
<Link href="/invoices" onClick={() => navbar?.setOpen(false)}>
|
|
341
|
+
Invoices
|
|
342
|
+
</Link>;
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
`useNearestPanel()` reads the panel a part is inside, and `useOverlaid()` returns the panels that
|
|
346
|
+
are open over the page.
|
|
347
|
+
|
|
348
|
+
## FloatingPanel
|
|
349
|
+
|
|
350
|
+
Renders a window over the page that a person opens from a trigger, moves by its header and resizes
|
|
351
|
+
from its edges and corners. The page around the window remains in use.
|
|
352
|
+
|
|
353
|
+
```tsx
|
|
354
|
+
import { FloatingPanel } from "@stealthscale/component-screen";
|
|
355
|
+
|
|
356
|
+
<FloatingPanel.Root>
|
|
357
|
+
<FloatingPanel.Trigger as={Button}>Notes</FloatingPanel.Trigger>
|
|
358
|
+
<Portal>
|
|
359
|
+
<FloatingPanel.Positioner>
|
|
360
|
+
<FloatingPanel.Content>
|
|
361
|
+
<FloatingPanel.Header>
|
|
362
|
+
<FloatingPanel.DragTrigger>
|
|
363
|
+
<GripHorizontalIcon aria-hidden />
|
|
364
|
+
<FloatingPanel.Title>Launch notes</FloatingPanel.Title>
|
|
365
|
+
</FloatingPanel.DragTrigger>
|
|
366
|
+
<FloatingPanel.Control>
|
|
367
|
+
<FloatingPanel.StageTrigger stage="minimized">
|
|
368
|
+
<MinusIcon />
|
|
369
|
+
</FloatingPanel.StageTrigger>
|
|
370
|
+
<FloatingPanel.StageTrigger stage="maximized">
|
|
371
|
+
<Maximize2Icon />
|
|
372
|
+
</FloatingPanel.StageTrigger>
|
|
373
|
+
<FloatingPanel.StageTrigger stage="default">
|
|
374
|
+
<Minimize2Icon />
|
|
375
|
+
</FloatingPanel.StageTrigger>
|
|
376
|
+
<FloatingPanel.CloseTrigger>
|
|
377
|
+
<XIcon />
|
|
378
|
+
</FloatingPanel.CloseTrigger>
|
|
379
|
+
</FloatingPanel.Control>
|
|
380
|
+
</FloatingPanel.Header>
|
|
381
|
+
<FloatingPanel.Body>…</FloatingPanel.Body>
|
|
382
|
+
<FloatingPanel.ResizeTriggers />
|
|
383
|
+
</FloatingPanel.Content>
|
|
384
|
+
</FloatingPanel.Positioner>
|
|
385
|
+
</Portal>
|
|
386
|
+
</FloatingPanel.Root>;
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
The recipe has no axes.
|
|
390
|
+
|
|
391
|
+
- The root takes the machine's options: `open` or `defaultOpen` with `onOpenChange`, `size` or
|
|
392
|
+
`defaultSize` with `onSizeChange` and `onSizeChangeEnd`, `position` or `defaultPosition` with
|
|
393
|
+
`onPositionChange` and `onPositionChangeEnd`, `minSize`, `maxSize`, `getAnchorPosition`,
|
|
394
|
+
`getBoundaryEl`, `allowOverflow`, `draggable`, `resizable`, `disabled`, `lockAspectRatio`,
|
|
395
|
+
`gridSize`, `persistRect`, `closeOnEscape`, `initialFocusEl`, `finalFocusEl`, `restoreFocus` and
|
|
396
|
+
`onStageChange`. The panel is 320 by 240 pixels unless stated, at least 240 by 100, and closes on
|
|
397
|
+
Escape unless `closeOnEscape` is false.
|
|
398
|
+
- The panel opens in the middle of the window, or where `getAnchorPosition` places it from the
|
|
399
|
+
trigger's and the window's rectangles. A drag can take the panel past the window's edge by the
|
|
400
|
+
distance from the pointer to the panel's edge, and `allowOverflow={false}` keeps it inside.
|
|
401
|
+
- A controlled panel remains open until `open` turns false. The trigger, the close trigger and
|
|
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.
|
|
405
|
+
- The positioner is fixed to the window at the `banner` layer plus the panel's place among the open
|
|
406
|
+
panels: above the page's sticky bands, and under a backdrop, a dialog, a popover, a toast and a
|
|
407
|
+
tooltip. Render it in a portal. A press or focus inside a panel brings it in front, and Escape
|
|
408
|
+
closes the panel in front.
|
|
409
|
+
- The trigger opens and closes the panel and sets `aria-expanded` and `aria-haspopup="dialog"`.
|
|
410
|
+
Focus moves to the panel as it opens, or to the element `initialFocusEl` returns, and returns to
|
|
411
|
+
the trigger as it closes.
|
|
412
|
+
- The panel is a `dialog` that is not modal, named by its title. A panel without a title takes
|
|
413
|
+
`aria-label`. Tab moves from the trigger into the panel and from the panel's last control to the
|
|
414
|
+
control after the trigger, wherever the portal puts the panel.
|
|
415
|
+
- While the panel itself has focus, the arrow keys move it in the arrow's direction under both
|
|
416
|
+
writing directions: 1px at a time and 10px with Shift, times `gridSize`. Alt with an arrow resizes
|
|
417
|
+
it from its end and bottom edges and keeps its aspect ratio under `lockAspectRatio`.
|
|
418
|
+
- A drag on `FloatingPanel.DragTrigger` moves the panel, and a double click maximizes it and
|
|
419
|
+
restores it. A press on a button inside the drag trigger does not start a drag. The cursor is
|
|
420
|
+
`move` while the panel can move.
|
|
421
|
+
- The resize triggers are 8px strips inside the panel's edges and 8px squares at its corners. Shift
|
|
422
|
+
keeps the aspect ratio during a drag and Alt resizes from the centre. They are hidden while the
|
|
423
|
+
panel is minimized or maximized, disabled or not resizable. `FloatingPanel.ResizeTriggers` renders
|
|
424
|
+
all eight, or those `axes` lists.
|
|
425
|
+
- `FloatingPanel.StageTrigger` sets `minimized`, the header alone, `maximized`, the whole window, or
|
|
426
|
+
`default`, the size and place before. Minimize and Maximize show while the panel is at its size,
|
|
427
|
+
and Restore while it is minimized or maximized. A stage trigger hidden with focus moves focus to
|
|
428
|
+
the trigger that reverses it. The stage triggers are hidden while the panel is not resizable.
|
|
429
|
+
- `label` names each trigger: "Minimize", "Maximize" and "Restore" by stage, and "Close" for
|
|
430
|
+
`FloatingPanel.CloseTrigger`. Both render the actions `Button` as a ghost `xs` square around the
|
|
431
|
+
caller's glyph.
|
|
432
|
+
- `FloatingPanel.Body` renders the primitives' scroll area, whose viewport is a region named by the
|
|
433
|
+
title while its content overflows. It is hidden while the panel is minimized.
|
|
434
|
+
- The panel has the `bg.panel` surface, the `lg` shadow, a transparent hairline edge that forced
|
|
435
|
+
colours paint in `CanvasText`, and a ring outside it under keyboard focus. It is not in the
|
|
436
|
+
document before it first opens, and leaves once its exit motion ends. `lazyMount` and
|
|
437
|
+
`unmountOnExit` default to `true`.
|
|
438
|
+
|
|
439
|
+
| Part | Element |
|
|
440
|
+
| ------------------------------ | ------------------------------- |
|
|
441
|
+
| `FloatingPanel.Root` | `div`, `display: contents` |
|
|
442
|
+
| `FloatingPanel.Trigger` | `button` |
|
|
443
|
+
| `FloatingPanel.Positioner` | `div` |
|
|
444
|
+
| `FloatingPanel.Content` | `div`, role `dialog` |
|
|
445
|
+
| `FloatingPanel.Header` | `div` |
|
|
446
|
+
| `FloatingPanel.DragTrigger` | `div` |
|
|
447
|
+
| `FloatingPanel.Title` | `h2` |
|
|
448
|
+
| `FloatingPanel.Control` | `div` |
|
|
449
|
+
| `FloatingPanel.StageTrigger` | `button` |
|
|
450
|
+
| `FloatingPanel.CloseTrigger` | `button` |
|
|
451
|
+
| `FloatingPanel.Body` | `div` inside a scroll area |
|
|
452
|
+
| `FloatingPanel.ResizeTrigger` | `div` |
|
|
453
|
+
| `FloatingPanel.ResizeTriggers` | a `ResizeTrigger` for each axis |
|
|
454
|
+
|
|
455
|
+
The panel does not offer these:
|
|
456
|
+
|
|
457
|
+
- `strategy`. The machine computes the panel's place in window coordinates, so the panel is fixed to
|
|
458
|
+
the window.
|
|
459
|
+
- `translations`. The triggers take `label`.
|
|
460
|
+
- The machine's api. A caller controls `open`, `size` and `position`, and the stage triggers set the
|
|
461
|
+
stage.
|
|
462
|
+
- A boundary that scrolls with the page. The machine measures `getBoundaryEl` when it resizes, so
|
|
463
|
+
the panel remains inside a boundary that does not scroll with the page, such as an app shell's
|
|
464
|
+
main region, and leaves one that does.
|
|
465
|
+
|
|
466
|
+
A menu, a select, a combobox or a date picker opened inside the panel stacks at the `popover` layer,
|
|
467
|
+
over the panel.
|
|
468
|
+
|
|
469
|
+
## Page
|
|
470
|
+
|
|
471
|
+
Lays out a page as a column of bands: a banner, a header, a navigation, a toolbar, a body with an
|
|
472
|
+
optional sidebar and aside, and a footer.
|
|
473
|
+
|
|
474
|
+
```tsx
|
|
475
|
+
import { Page } from "@stealthscale/component-screen";
|
|
476
|
+
|
|
477
|
+
<Page.Root measure="wide">
|
|
478
|
+
<Page.Header>
|
|
479
|
+
<Page.Breadcrumbs backIcon={<ArrowLeftIcon />} items={[{ href: "/", label: "Ledger" }]} />
|
|
480
|
+
<Page.Title>Users</Page.Title>
|
|
481
|
+
<Page.Meta when="wide">…</Page.Meta>
|
|
482
|
+
<Page.Description>Everyone with access to this ledger.</Page.Description>
|
|
483
|
+
<Page.Actions more="More actions" moreIcon={<EllipsisIcon />}>
|
|
484
|
+
<Page.Action icon={<SettingsIcon />}>Settings</Page.Action>
|
|
485
|
+
<Page.Action>Preview</Page.Action>
|
|
486
|
+
<Page.Action icon={<PlusIcon />} primary>
|
|
487
|
+
Add user
|
|
488
|
+
</Page.Action>
|
|
489
|
+
</Page.Actions>
|
|
490
|
+
</Page.Header>
|
|
491
|
+
<Page.Nav aria-label="User lists">
|
|
492
|
+
<Page.TabList
|
|
493
|
+
aria-label="Views"
|
|
494
|
+
emptyLabel="No view matches"
|
|
495
|
+
filterLabel="Filter views"
|
|
496
|
+
onValueChange={setView}
|
|
497
|
+
value={view}
|
|
498
|
+
>
|
|
499
|
+
<Page.Tab count={412} value="all">
|
|
500
|
+
All
|
|
501
|
+
</Page.Tab>
|
|
502
|
+
<Page.Tab value="suspended">Suspended</Page.Tab>
|
|
503
|
+
</Page.TabList>
|
|
504
|
+
</Page.Nav>
|
|
505
|
+
<Page.Body>…</Page.Body>
|
|
506
|
+
</Page.Root>;
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
| Axis | Values | Default |
|
|
510
|
+
| --------- | ------------------------ | ------- |
|
|
511
|
+
| `align` | `center`, `start` | `start` |
|
|
512
|
+
| `divided` | `true`, `false` | `true` |
|
|
513
|
+
| `gutter` | `xs` to `4xl` | `xl` |
|
|
514
|
+
| `measure` | `full`, `narrow`, `wide` | `full` |
|
|
515
|
+
| `size` | `sm`, `md`, `lg` | `md` |
|
|
516
|
+
|
|
517
|
+
- Every band runs edge to edge, and its content starts at the gutter. The root sets the gutter and
|
|
518
|
+
the measure as custom properties that every band reads.
|
|
519
|
+
- A page folds below 40rem. A narrow page sets its title one heading size smaller and its actions
|
|
520
|
+
one button size smaller.
|
|
521
|
+
- `size` sets the title one heading size larger than a section's title at the same size.
|
|
522
|
+
- The root has no landmark, because `AppShell.Main` is the `main` landmark. The header takes its
|
|
523
|
+
name from `Page.Title`.
|
|
524
|
+
- `Page.Action` renders the library's button, `outline`, or `solid` with `primary`. `as` renders
|
|
525
|
+
another control in its place, such as a menu's trigger, and the caller sets its look.
|
|
526
|
+
- `when="narrow"` renders a part on a folded page alone, and `when="wide"` on an unfolded page
|
|
527
|
+
alone. `Page.Context`, `Page.Meta`, `Page.Leading`, `Page.Actions`, `Page.Action`, `Page.Nav` and
|
|
528
|
+
`Page.Footer` take it, and `<Page.When when="narrow">` wraps anything else.
|
|
529
|
+
- Each sticky band sticks under the sticky bands before it.
|
|
530
|
+
- `Page.Aside` is a complementary landmark beside the body from the `lg` breakpoint. Name it with
|
|
531
|
+
`aria-label`. Below `lg` it stacks under the body, or leaves the page with `folds="hide"`.
|
|
532
|
+
`sticky` keeps it in view under the shell's pinned bars.
|
|
533
|
+
- A sticky aside renders its content in the primitives package's scroll area. Beside the body the
|
|
534
|
+
aside is at most as tall as the area that scrolls the page, less the pinned bars, the sticky bands
|
|
535
|
+
and a gap at each end. A longer aside scrolls on its own, and its viewport is a region named like
|
|
536
|
+
the aside, with a tab stop.
|
|
537
|
+
- A section scrolled to by its id stops a gap under the shell's pinned bars.
|
|
538
|
+
- The page has no `palette` and no `effect` axis, because it is a layout.
|
|
539
|
+
|
|
540
|
+
### Breadcrumbs
|
|
541
|
+
|
|
542
|
+
`Page.Breadcrumbs` takes the pages above this one as `items`, the nearest last, each with `href`,
|
|
543
|
+
`label` and an optional `onClick`. The navigation package's breadcrumb renders them in the context
|
|
544
|
+
row, named by `label`, with `separator` between the items. On a narrow page one link back to the
|
|
545
|
+
nearest item replaces the trail, with `backIcon` before its words.
|
|
546
|
+
|
|
547
|
+
### Tabs
|
|
548
|
+
|
|
549
|
+
`Page.TabList` selects one of its `Page.Tab` children by value. The caller controls the value with
|
|
550
|
+
`value` and `onValueChange`, or sets `defaultValue`, and the first tab is selected until then.
|
|
551
|
+
`Page.Tab` takes its words as a string and an optional `count`, which renders as a badge.
|
|
552
|
+
|
|
553
|
+
- A wide page renders the disclosure package's tabs on the navigation band, 48px tall at `md`, and
|
|
554
|
+
`Page.Body` is the selected tab's panel.
|
|
555
|
+
- On a narrow page a button with `pickerIcon` replaces the strip and opens a list of the tabs. The
|
|
556
|
+
list filters as the reader types into a field named by `filterLabel`, and shows `emptyLabel` when
|
|
557
|
+
the query matches no tab.
|
|
558
|
+
- `Page.Picker` and `Page.Palette` are that button and its panel, for a caller that composes its own
|
|
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.
|
|
597
|
+
|
|
598
|
+
## Section
|
|
599
|
+
|
|
600
|
+
Renders one part of a page under its own heading, with the actions that apply to it.
|
|
601
|
+
|
|
602
|
+
```tsx
|
|
603
|
+
import { Section } from "@stealthscale/component-screen";
|
|
604
|
+
|
|
605
|
+
<Section.Root variant="surface">
|
|
606
|
+
<Section.Header>
|
|
607
|
+
<Section.Title>Payment methods</Section.Title>
|
|
608
|
+
<Section.Description>Cards this account can be charged on.</Section.Description>
|
|
609
|
+
<Section.Actions>
|
|
610
|
+
<Section.Action icon={<PlusIcon />}>Add a card</Section.Action>
|
|
611
|
+
</Section.Actions>
|
|
612
|
+
</Section.Header>
|
|
613
|
+
<Section.Body bleed>…</Section.Body>
|
|
614
|
+
</Section.Root>;
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
| Axis | Values | Default |
|
|
618
|
+
| ----------- | ------------------ | ------- |
|
|
619
|
+
| `annotated` | `true` | none |
|
|
620
|
+
| `size` | `sm`, `md`, `lg` | `md` |
|
|
621
|
+
| `variant` | `plain`, `surface` | `plain` |
|
|
622
|
+
|
|
623
|
+
- The element is `section`. While a `Section.Title` is mounted, the title names it, so it is a
|
|
624
|
+
region landmark with the title as its name.
|
|
625
|
+
- A section without `size` takes the page's size, so one `size` on `Page.Root` sets every section in
|
|
626
|
+
it.
|
|
627
|
+
- `Section.Action` renders the library's button one size below the section, `outline`, or `solid`
|
|
628
|
+
with `primary`. `Section.Actions` takes `more` and `moreIcon` for its menu of folded actions.
|
|
629
|
+
- A section folds when it is narrower than 40rem. An annotated section folds when it is narrower
|
|
630
|
+
than 48rem, because its header and body share the row.
|
|
631
|
+
- A plain section after another renders a hairline above itself with a gap on either side.
|
|
632
|
+
- `annotated` lays out the header and the body as two columns on a wide section, the layout of a
|
|
633
|
+
settings page.
|
|
634
|
+
- `Section.Body bleed` drops the body's padding in a card and runs it to the card's edges under a
|
|
635
|
+
hairline, for a table or a list whose rows pad themselves.
|
|
636
|
+
- The section has no `palette` and no `effect` axis, because it is a layout.
|
|
637
|
+
|
|
638
|
+
## Sidebar
|
|
639
|
+
|
|
640
|
+
Lays out an application's navigation as a column: a fixed header, scrolling content with nav blocks,
|
|
641
|
+
and a fixed footer.
|
|
642
|
+
|
|
643
|
+
```tsx
|
|
644
|
+
import { NavList } from "@stealthscale/component-navigation";
|
|
645
|
+
import { Sidebar } from "@stealthscale/component-screen";
|
|
646
|
+
|
|
647
|
+
<Sidebar.Root variant="subtle">
|
|
648
|
+
<Sidebar.Header>
|
|
649
|
+
<Sidebar.Search
|
|
650
|
+
aria-label="Find a page"
|
|
651
|
+
clearIndicator={<XIcon />}
|
|
652
|
+
clearLabel="Clear the filter"
|
|
653
|
+
placeholder="Filter…"
|
|
654
|
+
searchIndicator={<SearchIcon />}
|
|
655
|
+
shortcut="k"
|
|
656
|
+
/>
|
|
657
|
+
</Sidebar.Header>
|
|
658
|
+
<Sidebar.Content>
|
|
659
|
+
<Sidebar.Nav>
|
|
660
|
+
<Sidebar.NavLabel>Platform</Sidebar.NavLabel>
|
|
661
|
+
<NavList.Root>…</NavList.Root>
|
|
662
|
+
</Sidebar.Nav>
|
|
663
|
+
<Sidebar.Separator />
|
|
664
|
+
<Sidebar.Nav>
|
|
665
|
+
<Sidebar.NavLabel>Projects</Sidebar.NavLabel>
|
|
666
|
+
<Sidebar.NavAction aria-label="New project">
|
|
667
|
+
<PlusIcon />
|
|
668
|
+
</Sidebar.NavAction>
|
|
669
|
+
<NavList.Root>…</NavList.Root>
|
|
670
|
+
</Sidebar.Nav>
|
|
671
|
+
<Sidebar.Empty>No pages match</Sidebar.Empty>
|
|
672
|
+
</Sidebar.Content>
|
|
673
|
+
<Sidebar.Footer>…</Sidebar.Footer>
|
|
674
|
+
</Sidebar.Root>;
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
| Axis | Values | Default |
|
|
678
|
+
| --------- | --------------------------------------- | ------- |
|
|
679
|
+
| `size` | `sm`, `md`, `lg` | `md` |
|
|
680
|
+
| `variant` | `outline`, `plain`, `subtle`, `surface` | `plain` |
|
|
681
|
+
|
|
682
|
+
- `subtle` is a muted ground without an edge, for a sidebar inside a shell panel, where the shell
|
|
683
|
+
renders the hairline.
|
|
684
|
+
- The content scrolls and the root does not, so the header and the footer remain visible. The
|
|
685
|
+
content is the primitives package's scroll area, with the theme's thin bar. Its viewport is
|
|
686
|
+
outside the tab order, because a link that takes focus scrolls into view. The header and the
|
|
687
|
+
footer pad like the rows, so a search, a switcher or a list in them is as wide as the rows' fills.
|
|
688
|
+
- The size sets the rows of every list inside: 24, 32 and 40px at `sm`, `md` and `lg`. A block's
|
|
689
|
+
label is one row tall.
|
|
690
|
+
- The content's blocks are 12, 16 and 24px apart at `sm`, `md` and `lg`, three to four times the gap
|
|
691
|
+
between rows. A `Sidebar.Separator` between two blocks sets no margin of its own.
|
|
692
|
+
- Each `Sidebar.Nav` is a `nav` landmark named by its `Sidebar.NavLabel` through `aria-labelledby`.
|
|
693
|
+
A block without a label takes `aria-label`, and a caller's `aria-label` replaces the label's name.
|
|
694
|
+
- A block whose rows a query matches none of is hidden, its label included, unless it renders an
|
|
695
|
+
empty message.
|
|
696
|
+
- `Sidebar.NavHeading` renders a heading over one list inside a block. Pass it an `id`, and pass the
|
|
697
|
+
same identifier to the list's `aria-labelledby`.
|
|
698
|
+
- `Sidebar.NavAction` is a 24px ghost square in the navigation list's end column, with the rows' end
|
|
699
|
+
inset. Pass the icon as its child and name it with `aria-label`.
|
|
700
|
+
- The sidebar has no `palette` and no `effect` axis. Its looks are neutral grounds, and the
|
|
701
|
+
navigation list offers its own palette and effect for the current row.
|
|
702
|
+
|
|
703
|
+
### Search
|
|
704
|
+
|
|
705
|
+
`Sidebar.Search` renders the forms package's search input and takes its props, named by
|
|
706
|
+
`aria-label`, `Search` by default. It filters the rows of its scope: every block from the header,
|
|
707
|
+
one block from inside a `Sidebar.Nav`. It hides a row whose words do not contain the query and opens
|
|
708
|
+
a branch that contains a match. When the query clears, every branch returns to the state it had
|
|
709
|
+
before the query.
|
|
710
|
+
|
|
711
|
+
- The down arrow moves focus from the field to the first row its scope shows.
|
|
712
|
+
- `shortcut` moves focus to the field with the platform's modifier held, `k` for ⌘K and Ctrl+K, and
|
|
713
|
+
sets `aria-keyshortcuts` on the field. Inside a closed app shell panel, the shortcut opens the
|
|
714
|
+
panel first.
|
|
715
|
+
- `Sidebar.Empty` renders while its scope shows zero rows, and a polite live region announces it
|
|
716
|
+
while a query is active. Put it in the scope the search filters, and name what was searched:
|
|
717
|
+
`No pages match`.
|
|
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`.
|
|
721
|
+
|
|
722
|
+
### In an app shell
|
|
723
|
+
|
|
724
|
+
- Inside a panel that closes to icons, the sidebar is a rail while the panel is closed. `iconic`
|
|
725
|
+
overrides the panel. The labels, the headings and the words in the header and footer are hidden
|
|
726
|
+
visually and kept for screen readers.
|
|
727
|
+
- On a rail, a search with `searchIndicator` renders a square button with its name in a tooltip. The
|
|
728
|
+
button opens the panel and moves focus to the field. A rail removes a search without
|
|
729
|
+
`searchIndicator` or outside a panel, the empty message and the block controls.
|
|
730
|
+
- In a panel over the page the sidebar renders at `lg`, and a click on a link inside it closes the
|
|
731
|
+
panel.
|
|
732
|
+
- The sidebar provides its size and its rail to every `NavList.Root` inside it through
|
|
733
|
+
`NavList.PropsProvider`. A list's own props apply over them.
|
|
734
|
+
|
|
735
|
+
## Splitter
|
|
736
|
+
|
|
737
|
+
Lays panels out in a row or a column with a resize trigger between each two. An application starts
|
|
738
|
+
the machine with `Splitter.useSplitter` and passes its api to `Splitter.Root`, so a control outside
|
|
739
|
+
the root collapses or resizes a panel.
|
|
740
|
+
|
|
741
|
+
```tsx
|
|
742
|
+
import { Splitter } from "@stealthscale/component-screen";
|
|
743
|
+
|
|
744
|
+
const splitter = Splitter.useSplitter({
|
|
745
|
+
defaultSize: [30, 70],
|
|
746
|
+
panels: [
|
|
747
|
+
{ collapsedSize: 0, collapsible: true, id: "notes", maxSize: 50, minSize: 20 },
|
|
748
|
+
{ id: "note", minSize: 40 },
|
|
749
|
+
],
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
<Splitter.Root splitter={splitter}>
|
|
753
|
+
<Splitter.Panel id="notes">…</Splitter.Panel>
|
|
754
|
+
<Splitter.ResizeTrigger id="notes:note" label="Resize the notes list" />
|
|
755
|
+
<Splitter.Panel id="note">…</Splitter.Panel>
|
|
756
|
+
</Splitter.Root>;
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
| Axis | Values | Default |
|
|
760
|
+
| --------- | ------------------------------------------------------------------ | --------- |
|
|
761
|
+
| `palette` | `primary`, `secondary`, `accent`, `neutral`, and the four statuses | `primary` |
|
|
762
|
+
|
|
763
|
+
- `useSplitter` takes the machine's options: `panels`, each with `id`, `minSize`, `maxSize`,
|
|
764
|
+
`collapsible`, `collapsedSize` and `resizeBehavior`, and `orientation`, `defaultSize` or `size`,
|
|
765
|
+
`onResize`, `onResizeStart`, `onResizeEnd`, `onCollapse`, `onExpand` and `keyboardResizeBy`. A
|
|
766
|
+
number is a percentage of the root, and a string with a unit, such as `"200px"`, resolves against
|
|
767
|
+
the root.
|
|
768
|
+
- The api reads and changes the sizes: `getSizes`, `setSizes`, `resizePanel`, `collapsePanel`,
|
|
769
|
+
`expandPanel`, `isPanelCollapsed`, `isPanelExpanded` and `resetSizes`. With `size`, the
|
|
770
|
+
application resets by setting its sizes, because the machine records a controlled size as its
|
|
771
|
+
first layout.
|
|
772
|
+
- The root is a flex row or column that fills its container, and grows in a flex container, such as
|
|
773
|
+
a column under a toolbar. A panel is a column its content can shrink in. Render content that
|
|
774
|
+
scrolls in the primitives package's scroll area.
|
|
775
|
+
- `Splitter.ResizeTrigger` sits between the two panels its `id` names, `before:after`. It is a
|
|
776
|
+
`separator` named by `label`, "Resize" by default. Its value is the before panel's share of the
|
|
777
|
+
root, `aria-controls` points at that panel, and its orientation is its line's: vertical between
|
|
778
|
+
panels in a row. `disabled` removes it from the tab order and hides its pill.
|
|
779
|
+
- The arrows move a focused trigger by 1%, or by `keyboardResizeBy`, and by 10% with Shift. Home and
|
|
780
|
+
End move it to the before panel's minimum and maximum. Enter collapses a collapsible before panel
|
|
781
|
+
and restores the size it had before the collapse. F6 moves focus to the next trigger.
|
|
782
|
+
- The trigger is 8px across, 24px under a coarse pointer, and stacks above the panels' content. It
|
|
783
|
+
renders a hairline and a pill, which children replace. The palette colours the line and the pill
|
|
784
|
+
while the pointer is over the trigger, while it has focus and while it drags. Under forced colours
|
|
785
|
+
the line is `CanvasText`, an active pill `Highlight` and its ring `CanvasText`.
|
|
786
|
+
|
|
787
|
+
| Part | Element |
|
|
788
|
+
| --------------------------------- | ----------------------- |
|
|
789
|
+
| `Splitter.Root` | `div` |
|
|
790
|
+
| `Splitter.Panel` | `div` |
|
|
791
|
+
| `Splitter.ResizeTrigger` | `div`, role `separator` |
|
|
792
|
+
| `Splitter.ResizeTriggerSeparator` | `div` |
|
|
793
|
+
| `Splitter.ResizeTriggerIndicator` | `div` |
|
|
794
|
+
|
|
795
|
+
The splitter does not offer the machine's `ids`: the hook builds each id from the encoded panel id,
|
|
796
|
+
so an id with a space makes a valid `aria-controls`.
|
|
797
|
+
|
|
798
|
+
## Switcher
|
|
799
|
+
|
|
800
|
+
Renders the control that shows the current workspace, project or environment and opens a menu of the
|
|
801
|
+
others. `Switcher.Root` is the disclosure package's `Menu.Root`, so the roles, the keyboard and the
|
|
802
|
+
positioning are the menu's.
|
|
803
|
+
|
|
804
|
+
```tsx
|
|
805
|
+
import { Switcher } from "@stealthscale/component-screen";
|
|
806
|
+
|
|
807
|
+
<Switcher.Root
|
|
808
|
+
checkIcon={<CheckIcon />}
|
|
809
|
+
indicator={<ChevronsUpDownIcon />}
|
|
810
|
+
items={[
|
|
811
|
+
{ detail: "Pro plan", label: "Ledger", value: "ledger" },
|
|
812
|
+
{ detail: "Free plan", label: "Atlas", value: "atlas" },
|
|
813
|
+
]}
|
|
814
|
+
label="Workspace"
|
|
815
|
+
onValueChange={setWorkspace}
|
|
816
|
+
value={workspace}
|
|
817
|
+
>
|
|
818
|
+
<Switcher.Action icon={<PlusIcon />} onClick={create}>
|
|
819
|
+
New workspace
|
|
820
|
+
</Switcher.Action>
|
|
821
|
+
</Switcher.Root>;
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
| Axis | Values | Default |
|
|
825
|
+
| ----------- | ---------------------------------------------------------------------------------- | ------- |
|
|
826
|
+
| `palette` | `primary`, `secondary`, `accent`, `neutral`, `info`, `success`, `warning`, `error` | none |
|
|
827
|
+
| `placement` | `alone`, `sidebar`, `toolbar` | `alone` |
|
|
828
|
+
| `size` | `sm`, `md`, `lg` | `md` |
|
|
829
|
+
| `variant` | `solid`, `subtle`, `surface`, `outline`, `ghost`, `plain` | `ghost` |
|
|
830
|
+
|
|
831
|
+
- `items` lists the choices, each with `value`, `label` and an optional `detail`, `mark`, `disabled`
|
|
832
|
+
and `href`. A choice without `mark` shows its initials. A choice with `href` is a link row.
|
|
833
|
+
- The value is the caller's with `value` and `onValueChange`, and the root's own otherwise, starting
|
|
834
|
+
at `defaultValue` or the first choice.
|
|
835
|
+
- `label` names what the switcher switches. The trigger renders it as visually hidden text before
|
|
836
|
+
the name, so a screen reader announces `Workspace Ledger`, and the visible name stays in the
|
|
837
|
+
accessible name.
|
|
838
|
+
- `Switcher.Action` renders a menu row with `icon` after a separator under the choices.
|
|
839
|
+
- `Switcher.Root` reads the sidebar or the toolbar around it for `placement` and `size`, unless the
|
|
840
|
+
caller states them.
|
|
841
|
+
- In a sidebar the switcher fills the column and opens a menu as wide as the control. In a sidebar
|
|
842
|
+
closed to icons it is its mark alone, a 32px square at `md`, with its name in a tooltip, and its
|
|
843
|
+
menu opens beside the rail.
|
|
844
|
+
- Alone and in a toolbar the switcher is one line as wide as its words, at least as tall as a button
|
|
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.
|
|
850
|
+
- The looks and the palettes are the button's. The outline look has the button's light edge.
|
|
851
|
+
- Render the menu's positioner in a `Portal` when the switcher is inside an element that clips, such
|
|
852
|
+
as an app shell panel, so the menu renders over the page.
|
|
853
|
+
|
|
854
|
+
Without `items`, the children are the trigger and the menu, composed from the switcher's parts and
|
|
855
|
+
the menu's own:
|
|
856
|
+
|
|
857
|
+
```tsx
|
|
858
|
+
<Switcher.Root placement="toolbar">
|
|
859
|
+
<Toolbar.Item as={Switcher.Trigger} label="Environment">
|
|
860
|
+
<Switcher.Label>
|
|
861
|
+
<Switcher.Name>Production</Switcher.Name>
|
|
862
|
+
</Switcher.Label>
|
|
863
|
+
<Switcher.Indicator>
|
|
864
|
+
<ChevronsUpDownIcon />
|
|
865
|
+
</Switcher.Indicator>
|
|
866
|
+
</Toolbar.Item>
|
|
867
|
+
<Portal>
|
|
868
|
+
<Menu.Positioner>…</Menu.Positioner>
|
|
869
|
+
</Portal>
|
|
870
|
+
</Switcher.Root>
|
|
871
|
+
```
|
|
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
|
+
|
|
904
|
+
## Toolbar
|
|
905
|
+
|
|
906
|
+
Renders a row of controls over a table or a list: a band at each end, a band in the middle, and a
|
|
907
|
+
search that folds to a button on a narrow row.
|
|
908
|
+
|
|
909
|
+
```tsx
|
|
910
|
+
import { Toolbar } from "@stealthscale/component-screen";
|
|
911
|
+
|
|
912
|
+
<Toolbar.Root aria-label="Invoices" more="More actions" moreIcon={<EllipsisIcon />}>
|
|
913
|
+
<Toolbar.Start>
|
|
914
|
+
<Toolbar.Title>Invoices</Toolbar.Title>
|
|
915
|
+
<Toolbar.Link current href="/invoices/drafts">
|
|
916
|
+
Drafts
|
|
917
|
+
</Toolbar.Link>
|
|
918
|
+
<Toolbar.Link href="/invoices/sent">Sent</Toolbar.Link>
|
|
919
|
+
<Toolbar.Separator />
|
|
920
|
+
<Toolbar.Group>
|
|
921
|
+
<Toolbar.Action icon={<BoldIcon />}>Bold</Toolbar.Action>
|
|
922
|
+
<Toolbar.Action icon={<ItalicIcon />}>Italic</Toolbar.Action>
|
|
923
|
+
</Toolbar.Group>
|
|
924
|
+
</Toolbar.Start>
|
|
925
|
+
<Toolbar.End>
|
|
926
|
+
<Toolbar.Search aria-label="Search invoices" searchIndicator={<SearchIcon />} />
|
|
927
|
+
<Toolbar.Action>Export</Toolbar.Action>
|
|
928
|
+
<Toolbar.Action primary>New invoice</Toolbar.Action>
|
|
929
|
+
</Toolbar.End>
|
|
930
|
+
</Toolbar.Root>;
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
| Axis | Values | Default |
|
|
934
|
+
| --------- | ----------------------------- | ------- |
|
|
935
|
+
| `radius` | `l1`, `l2`, `l3` | `l2` |
|
|
936
|
+
| `size` | `sm`, `md`, `lg` | `md` |
|
|
937
|
+
| `variant` | `outline`, `plain`, `surface` | `plain` |
|
|
938
|
+
|
|
939
|
+
- The row has `role="toolbar"`, and the arrow keys move focus between its controls, so the row is
|
|
940
|
+
one tab stop. Name it with `aria-label`.
|
|
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.
|
|
951
|
+
- `Toolbar.Action` renders the library's button, `ghost`, or `solid` with `primary`, and folds by
|
|
952
|
+
its priority.
|
|
953
|
+
- `Toolbar.Link` renders an `a` in the same look with `href`, `current` and `icon`. `current` sets
|
|
954
|
+
`aria-current="page"`. Its `onClick` takes a router's anchor handler, and its menu row calls the
|
|
955
|
+
same handler.
|
|
956
|
+
- `Toolbar.Group` joins controls pressed together, such as bold, italic and underline.
|
|
957
|
+
- `Toolbar.Item` renders a link when it has an `href` and a button otherwise, and both stay in the
|
|
958
|
+
arrow-key group. Pass `as` to render another component as the item: `as={Switcher.Trigger}` or a
|
|
959
|
+
menu's trigger.
|
|
960
|
+
- `Toolbar.Search` renders the forms package's search input and takes its props, named by
|
|
961
|
+
`aria-label`, `Search` by default. On a narrow row it renders a button with `searchIndicator`, or
|
|
962
|
+
with its name without one, that opens the field over the whole row, the padding and the edge of an
|
|
963
|
+
outline or surface row included. Escape on the empty field, or focus leaving it, closes the field
|
|
964
|
+
and returns focus to the button.
|
|
965
|
+
- The toolbar has no `palette` and no `effect` axis, because it is a layout for controls that have
|
|
966
|
+
their own.
|