@stealthscale/component-collections 0.0.1 → 0.1.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 +684 -4
- package/dist/cyclonedx/bom.json +1 -1
- package/dist/index.d.ts +2148 -1
- package/dist/index.js +5697 -6
- package/dist/recipe-C1oaF8pO.js +2212 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/theme.d.ts +3 -4
- package/dist/theme.js +15 -5
- package/package.json +48 -24
package/README.md
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
# @stealthscale/component-collections
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
React components that render a set of records, styled by the theme's recipes.
|
|
4
|
+
|
|
5
|
+
| Component | Renders |
|
|
6
|
+
| -------------------------------- | ------------------------------------------------------ |
|
|
7
|
+
| `Table` | A table of records |
|
|
8
|
+
| `Listbox` | A list of rows a person selects from |
|
|
9
|
+
| `StatusMatrix` | A grid of states, one row per item |
|
|
10
|
+
| `Transfer` | Two lists and the controls that move rows between them |
|
|
11
|
+
| `DataList` | Pairs of a label and a value |
|
|
12
|
+
| `Timeline` | Entries in the order they happened, along a rail |
|
|
13
|
+
| `TreeView`, `TreeCollection` | Nested rows that open and close, walked by the keys |
|
|
14
|
+
| `Sortable` | Rows a person drags into another order |
|
|
15
|
+
| `SwipeActions` | A row whose actions a swipe reveals at its end |
|
|
16
|
+
| `useListCollection`, `useFilter` | The rows of a list, filtered by typed text |
|
|
17
|
+
|
|
18
|
+
Every value a theme can change is an axis of a component's recipe. Set it as a prop, and write no
|
|
19
|
+
style. Change the element a component renders with `as`. A component with parts is exported as a
|
|
20
|
+
namespace, such as `Table.Root`.
|
|
6
21
|
|
|
7
22
|
## Install
|
|
8
23
|
|
|
@@ -10,4 +25,669 @@ recipe. The preset under `./theme` registers the recipes with an application's c
|
|
|
10
25
|
pnpm add @stealthscale/component-collections
|
|
11
26
|
```
|
|
12
27
|
|
|
13
|
-
The package peers on `react`
|
|
28
|
+
The package peers on `react`, `react-dom`, `@stealthscale/hooks`, `@stealthscale/theme` and
|
|
29
|
+
`@stealthscale/component-primitives`. It depends on dnd-kit 0.5.0 (`@dnd-kit/abstract`,
|
|
30
|
+
`@dnd-kit/dom`, `@dnd-kit/helpers` and `@dnd-kit/react`), pinned exactly, for `Sortable`, and on
|
|
31
|
+
`@stealthscale/component-actions` for the sortable handle and the swipe actions. List the preset
|
|
32
|
+
under `./theme` among the presets your compiler installs. Add the primitives package's preset, which
|
|
33
|
+
styles the scroll areas of the table, the listbox and the board, and the actions package's preset,
|
|
34
|
+
which styles the handle and the swipe actions.
|
|
35
|
+
|
|
36
|
+
## Table
|
|
37
|
+
|
|
38
|
+
`Table` renders a table of records with the semantics of the HTML table. `Table.Simple` renders a
|
|
39
|
+
whole table from a list of columns and a list of rows.
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { ArrowDownIcon } from "lucide-react";
|
|
43
|
+
|
|
44
|
+
import { Table } from "@stealthscale/component-collections";
|
|
45
|
+
|
|
46
|
+
<Table.Simple
|
|
47
|
+
caption="Payouts raised this quarter"
|
|
48
|
+
columns={[
|
|
49
|
+
{ key: "name", label: "Account", rowHeader: true },
|
|
50
|
+
{
|
|
51
|
+
key: "amount",
|
|
52
|
+
label: (
|
|
53
|
+
<>
|
|
54
|
+
Amount <ArrowDownIcon aria-hidden size="1em" />
|
|
55
|
+
</>
|
|
56
|
+
),
|
|
57
|
+
numeric: true,
|
|
58
|
+
sorted: "descending",
|
|
59
|
+
sortLabel: "Sort by amount",
|
|
60
|
+
},
|
|
61
|
+
]}
|
|
62
|
+
onSort={sortBy}
|
|
63
|
+
rows={accounts}
|
|
64
|
+
rowToKey={(account) => account.id}
|
|
65
|
+
total={(column) => totals[column.key]}
|
|
66
|
+
variant="surface"
|
|
67
|
+
/>;
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
| Axis | Values | Default |
|
|
71
|
+
| -------------- | ----------------------------------------------------------------- | -------- |
|
|
72
|
+
| `variant` | `surface`, `plain` | `plain` |
|
|
73
|
+
| `size` | `sm`, `md`, `lg` | `md` |
|
|
74
|
+
| `align` | `start`, `center`, `end` | `center` |
|
|
75
|
+
| `layout` | `auto`, `fixed` | `auto` |
|
|
76
|
+
| `radius` | `l1`, `l2`, `l3` | `l2` |
|
|
77
|
+
| `rules` | `rows`, `all`, `none` | `rows` |
|
|
78
|
+
| `palette` | `primary`, `secondary`, `accent`, `neutral` and the four statuses | none |
|
|
79
|
+
| `striped` | `true` | off |
|
|
80
|
+
| `banded` | `true` | off |
|
|
81
|
+
| `interactive` | `true` | off |
|
|
82
|
+
| `stickyHeader` | `true` | off |
|
|
83
|
+
| `stickyColumn` | `true` | off |
|
|
84
|
+
|
|
85
|
+
| Part | Element | What it renders |
|
|
86
|
+
| -------------- | ---------- | ----------------------------------------------------- |
|
|
87
|
+
| `Scroller` | `div` | The box around the table's scroll area, with variants |
|
|
88
|
+
| `Root` | `table` | The table |
|
|
89
|
+
| `ColumnGroup` | `colgroup` | The column declarations |
|
|
90
|
+
| `Column` | `col` | One column's width |
|
|
91
|
+
| `Caption` | `caption` | The table's name, under the table |
|
|
92
|
+
| `Header` | `thead` | The header rows |
|
|
93
|
+
| `Body` | `tbody` | A group of body rows |
|
|
94
|
+
| `Footer` | `tfoot` | The total row |
|
|
95
|
+
| `Row` | `tr` | One row |
|
|
96
|
+
| `ColumnHeader` | `th` | A column's name |
|
|
97
|
+
| `Sorter` | `button` | The button that sorts a column, inside its header |
|
|
98
|
+
| `RowHeader` | `th` | A row's name |
|
|
99
|
+
| `Cell` | `td` | One value |
|
|
100
|
+
| `Simple` | `div` | A whole table composed from the parts |
|
|
101
|
+
|
|
102
|
+
- `Table.Simple` takes `caption`, `columns`, `rows`, `rowToKey`, `total`, `empty`, `groupBy`,
|
|
103
|
+
`groupLabel` and `onSort`, beside every prop of the scroller.
|
|
104
|
+
- A column states `key`, `label`, `cell`, `numeric`, `rowHeader`, `width`, `sorted` and `sortLabel`.
|
|
105
|
+
A column with `columns` in place of `key` spans the columns under it and adds a header row.
|
|
106
|
+
- The scroller receives the variants. A composed table sets them on `Table.Scroller`.
|
|
107
|
+
- The table scrolls in both axes in the primitives package's scroll area inside the scroller, under
|
|
108
|
+
the theme's thin bars. While the table overflows, the area's viewport is a `region` in the tab
|
|
109
|
+
order, so a keyboard can scroll it, and the scroller draws the focus ring. `Table.Simple` names
|
|
110
|
+
the region from `caption`, or from `aria-label` where the caller passes one, such as a table whose
|
|
111
|
+
caption repeats the title of the section around it. A composed table gives `Table.Caption` an `id`
|
|
112
|
+
and passes `aria-labelledby` to `Table.Scroller`, which sets it on the viewport.
|
|
113
|
+
- `focusable={false}` on `Table.Scroller` keeps the viewport out of the tab order, for a table whose
|
|
114
|
+
cells take focus, such as a grid with one roving tab stop. Focus on a cell scrolls the cell into
|
|
115
|
+
view.
|
|
116
|
+
- `viewportRef` on `Table.Scroller` receives the viewport, the element that scrolls, for a caller
|
|
117
|
+
that renders only the rows in view.
|
|
118
|
+
- A column of figures states `data-numeric` on its cells and its header, and `Table.Simple` sets it
|
|
119
|
+
from `numeric`. The cells align to their end in tabular figures.
|
|
120
|
+
- Sorting, filtering and pagination are the caller's. `Table.Sorter` reports the press, the column
|
|
121
|
+
header states `aria-sort`, and the caller passes the sorted rows. The package does not include
|
|
122
|
+
icons, so put the direction mark in the column's label.
|
|
123
|
+
- `interactive` fills a row under the pointer and while a link inside it has focus. Put a real link
|
|
124
|
+
in a cell for a row a person opens.
|
|
125
|
+
- A row with `aria-selected="true"` fills with the palette's subtle fill. Forced colours fill it
|
|
126
|
+
with `Highlight`.
|
|
127
|
+
- A sticky header needs a maximum height on the scroller. A sticky column needs a table wider than
|
|
128
|
+
the scroller, so declare the column widths and set `layout="fixed"`. With both set, the corner
|
|
129
|
+
cell sticks to both edges, above the header and the column. Both stick to the scroll area's
|
|
130
|
+
viewport.
|
|
131
|
+
- `variant="surface"` does not cast a shadow. After dark a shadow adds a rim inside the edge, and a
|
|
132
|
+
stripe, a filled row or a sticky cell at the edge covers the rim.
|
|
133
|
+
|
|
134
|
+
## Listbox
|
|
135
|
+
|
|
136
|
+
`Listbox` renders a list of rows a person selects from, with the keyboard interaction of the ARIA
|
|
137
|
+
listbox pattern. `Listbox.Simple` renders a whole list from props.
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
import { CheckIcon, XIcon } from "lucide-react";
|
|
141
|
+
|
|
142
|
+
import { Listbox, useFilter, useListCollection } from "@stealthscale/component-collections";
|
|
143
|
+
|
|
144
|
+
const filter = useFilter();
|
|
145
|
+
const { collection, narrow } = useListCollection({
|
|
146
|
+
filter: filter.contains,
|
|
147
|
+
itemToString: (client) => client.name,
|
|
148
|
+
itemToValue: (client) => client.id,
|
|
149
|
+
rows: clients,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
<Listbox.Simple
|
|
153
|
+
collection={collection}
|
|
154
|
+
description={(client) => client.terms}
|
|
155
|
+
empty="No client matches."
|
|
156
|
+
label="Clients"
|
|
157
|
+
mark={<CheckIcon size="100%" />}
|
|
158
|
+
narrowing={{
|
|
159
|
+
clearIndicator: <XIcon size="100%" />,
|
|
160
|
+
clearLabel: "Clear the filter",
|
|
161
|
+
onNarrow: narrow,
|
|
162
|
+
placeholder: "Filter clients",
|
|
163
|
+
}}
|
|
164
|
+
variant="surface"
|
|
165
|
+
/>;
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
| Axis | Values | Default |
|
|
169
|
+
| ------------- | ----------------------------------------------------------------- | ---------- |
|
|
170
|
+
| `variant` | `surface`, `plain` | `plain` |
|
|
171
|
+
| `size` | `sm`, `md`, `lg` | `md` |
|
|
172
|
+
| `selected` | `none`, `plain`, `solid`, `subtle` | `subtle` |
|
|
173
|
+
| `highlight` | `bar`, `fill`, `tint` | `tint` |
|
|
174
|
+
| `palette` | `primary`, `secondary`, `accent`, `neutral` and the four statuses | none |
|
|
175
|
+
| `effect` | `glow` | none |
|
|
176
|
+
| `radius` | `l1`, `l2`, `l3` | `l1` |
|
|
177
|
+
| `orientation` | `vertical`, `horizontal` | `vertical` |
|
|
178
|
+
| `columns` | `1` to `12` | none |
|
|
179
|
+
|
|
180
|
+
| Part | Element | What it renders |
|
|
181
|
+
| ----------------- | -------- | ----------------------------------------------------- |
|
|
182
|
+
| `Root` | `div` | The root, and the machine its parts share |
|
|
183
|
+
| `Label` | `span` | The list's label |
|
|
184
|
+
| `Frame` | `div` | The box around the field, the rows and the empty text |
|
|
185
|
+
| `Input` | `input` | The filter field and its clear control |
|
|
186
|
+
| `SelectAll` | `button` | The row that selects every row |
|
|
187
|
+
| `Content` | `div` | The scroll area whose viewport is the `listbox` |
|
|
188
|
+
| `Item` | `div` | One row, with `role="option"` |
|
|
189
|
+
| `ItemCheckbox` | `span` | The checkbox at a row's start |
|
|
190
|
+
| `ItemLines` | `span` | The column of a row's text and description |
|
|
191
|
+
| `ItemText` | `span` | A row's text |
|
|
192
|
+
| `ItemDescription` | `span` | A row's description |
|
|
193
|
+
| `ItemIndicator` | `span` | The mark at a selected row's end |
|
|
194
|
+
| `ItemGroup` | `div` | A group of rows, with `role="group"` |
|
|
195
|
+
| `ItemGroupLabel` | `span` | A group's label |
|
|
196
|
+
| `Empty` | `span` | The text of an empty list |
|
|
197
|
+
| `ValueText` | `span` | The selected rows' text |
|
|
198
|
+
| `Window` | `div` | The rows of a long list near the viewport |
|
|
199
|
+
| `Row` | `div` | A ready-made row composed from the item parts |
|
|
200
|
+
| `Simple` | `div` | A whole list composed from the parts |
|
|
201
|
+
|
|
202
|
+
- `Listbox.Simple` takes `label`, `narrowing`, `selectAll`, `description`, `icon`, `groupBy`,
|
|
203
|
+
`groupLabel`, `empty`, `summary` and `tall`, beside every prop of the root.
|
|
204
|
+
- The machine sets every role, identifier and key. The arrow keys move the highlight, typing moves
|
|
205
|
+
it to a matching row, and `selectionMode` is `single`, `multiple` or `extended`.
|
|
206
|
+
- Focus stays on the list or on the filter field, and `highlight` marks the highlighted row.
|
|
207
|
+
`selected` sets the fill of a selected row.
|
|
208
|
+
- Under forced colors a selected row fills with `Highlight` in every `selected` look but `none`. Its
|
|
209
|
+
text, description, checkbox and highlight line read `HighlightText`. A list with `selected="none"`
|
|
210
|
+
shows the state in its checkboxes.
|
|
211
|
+
- The rows scroll in the primitives package's scroll area, with the theme's thin bar. Its viewport
|
|
212
|
+
is the `listbox`, so the element that has focus is the element that scrolls, and the machine
|
|
213
|
+
scrolls a row the keys highlight into view by the least distance. The focus ring is inside the
|
|
214
|
+
list's edge. `Content` takes no `as`, and its props go to the `listbox`.
|
|
215
|
+
- The root states `boxed` and the marks once. A boxed list renders a checkbox at every row's start
|
|
216
|
+
and defaults `selected` to `none`. Otherwise a selected row renders its mark at its end.
|
|
217
|
+
- A row centres its checkbox, icon and mark on its height, and takes the inset scale at both ends.
|
|
218
|
+
- Filtering is the caller's: the field reports the text, and the caller passes the collection
|
|
219
|
+
`useListCollection` returns.
|
|
220
|
+
- `Listbox.Input autoHighlight` highlights the first row of each filtered collection while the field
|
|
221
|
+
has text, so Enter chooses the best match. The command palette sets it.
|
|
222
|
+
- `tall` sets the list's height in rows and renders only the rows near the viewport, so a list of
|
|
223
|
+
ten thousand rows renders about twenty.
|
|
224
|
+
- The package does not include icons. Pass the marks and the clear icon.
|
|
225
|
+
|
|
226
|
+
## StatusMatrix
|
|
227
|
+
|
|
228
|
+
`StatusMatrix` renders the state of one set of items against another, with a mark at every crossing,
|
|
229
|
+
a column with each row's worst state, and a legend. It is built from the table's parts.
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
import { CheckIcon, CircleDashedIcon, TriangleAlertIcon, XIcon } from "lucide-react";
|
|
233
|
+
|
|
234
|
+
import { StatusMatrix } from "@stealthscale/component-collections";
|
|
235
|
+
|
|
236
|
+
<StatusMatrix
|
|
237
|
+
caption="Service health by region"
|
|
238
|
+
cells={[
|
|
239
|
+
{ column: "EU", row: "checkout-api", state: "fine" },
|
|
240
|
+
{ column: "US", row: "checkout-api", state: "down" },
|
|
241
|
+
]}
|
|
242
|
+
columns={[
|
|
243
|
+
{ id: "EU", label: "EU" },
|
|
244
|
+
{ id: "US", label: "US" },
|
|
245
|
+
]}
|
|
246
|
+
corner="Service"
|
|
247
|
+
legend="States"
|
|
248
|
+
rollup="Worst"
|
|
249
|
+
rows={[{ group: "Payments", id: "checkout-api", label: "checkout-api" }]}
|
|
250
|
+
states={{
|
|
251
|
+
down: { label: "Down", mark: <XIcon />, tone: "error" },
|
|
252
|
+
fine: { label: "Healthy", mark: <CheckIcon />, tone: "success" },
|
|
253
|
+
slow: { label: "Degraded", mark: <TriangleAlertIcon />, tone: "warning" },
|
|
254
|
+
}}
|
|
255
|
+
unmeasured={{ label: "Not measured", mark: <CircleDashedIcon />, tone: "neutral" }}
|
|
256
|
+
variant="surface"
|
|
257
|
+
/>;
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
| Axis | Values | Default |
|
|
261
|
+
| ------ | ---------------- | ------- |
|
|
262
|
+
| `size` | `sm`, `md`, `lg` | `md` |
|
|
263
|
+
|
|
264
|
+
- The scroller takes the table's axes, such as `variant`, `rules` and `radius`. `size` sets both the
|
|
265
|
+
table and the marks.
|
|
266
|
+
- `cells` is sparse. A pair without a cell renders `unmeasured`, and the later of two cells for one
|
|
267
|
+
pair applies.
|
|
268
|
+
- `rollup` adds a last column with each row's worst state. From worst to best the rank is error,
|
|
269
|
+
warning, a gap, info, success and neutral, so a row with an unmeasured crossing never rolls up to
|
|
270
|
+
a pass.
|
|
271
|
+
- A state's `tone` sets its palette. A state without `mark` renders a filled dot, so give two states
|
|
272
|
+
with one tone a mark each.
|
|
273
|
+
- Rows with `group` render one `tbody` per group under a `scope="rowgroup"` heading. The matrix
|
|
274
|
+
renders groups only when every row has one.
|
|
275
|
+
- The crosshair fills the row and the column under the pointer with `bg.subtle`.
|
|
276
|
+
- Without `onSelectCell` the matrix has no tab stop. With it every crossing is a button, a gap
|
|
277
|
+
included. Pass `cellLabel` to name each button by its row and column.
|
|
278
|
+
- `legend` names the list of states under the grid, with `unmeasured` last.
|
|
279
|
+
- The package does not include icons. Pass each state's mark.
|
|
280
|
+
|
|
281
|
+
## Transfer
|
|
282
|
+
|
|
283
|
+
`Transfer` renders two lists and the controls that move checked rows between them. Each side is a
|
|
284
|
+
boxed listbox with multiple selection.
|
|
285
|
+
|
|
286
|
+
```tsx
|
|
287
|
+
import { CheckIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
|
288
|
+
|
|
289
|
+
import { Transfer } from "@stealthscale/component-collections";
|
|
290
|
+
|
|
291
|
+
<Transfer
|
|
292
|
+
giveBackLabel="Move the checked rows back"
|
|
293
|
+
giveBackMark={<ChevronLeftIcon size="100%" />}
|
|
294
|
+
itemToString={(client) => client.name}
|
|
295
|
+
itemToValue={(client) => client.id}
|
|
296
|
+
mark={<CheckIcon size="100%" />}
|
|
297
|
+
nothing="No clients here"
|
|
298
|
+
offeredTitle="Available"
|
|
299
|
+
onValueChange={setChosen}
|
|
300
|
+
rows={clients}
|
|
301
|
+
takeLabel="Move the checked rows across"
|
|
302
|
+
takeMark={<ChevronRightIcon size="100%" />}
|
|
303
|
+
takenTitle="Chosen"
|
|
304
|
+
value={chosen}
|
|
305
|
+
/>;
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
| Axis | Values | Default |
|
|
309
|
+
| --------- | ----------------------------------------------------------------- | ------- |
|
|
310
|
+
| `size` | `sm`, `md`, `lg` | `md` |
|
|
311
|
+
| `palette` | `primary`, `secondary`, `accent`, `neutral` and the four statuses | none |
|
|
312
|
+
|
|
313
|
+
- `value` and `onValueChange` control the set of moved rows, and `defaultValue` sets it once. The
|
|
314
|
+
checked rows on each side are the transfer's own state.
|
|
315
|
+
- A control is disabled while no row on its side is checked. A move clears the checked rows of the
|
|
316
|
+
side it moves them from.
|
|
317
|
+
- Both sides take an equal share of the width, and the height of the taller side with a floor of
|
|
318
|
+
every row's height, so the layout does not move as rows cross.
|
|
319
|
+
- `description` adds a second line to each row. `palette` sets the palette of both lists.
|
|
320
|
+
- `takeLabel` and `giveBackLabel` name the controls, because their content is a mark without text.
|
|
321
|
+
- The package does not include icons. Pass the marks.
|
|
322
|
+
|
|
323
|
+
## DataList
|
|
324
|
+
|
|
325
|
+
`DataList` renders pairs of a label and a value as a description list: the details beside a record,
|
|
326
|
+
a summary panel or a settled form.
|
|
327
|
+
|
|
328
|
+
```tsx
|
|
329
|
+
import { DataList } from "@stealthscale/component-collections";
|
|
330
|
+
import { Badge } from "@stealthscale/component-data";
|
|
331
|
+
|
|
332
|
+
<DataList.Root divided orientation="horizontal" size="sm">
|
|
333
|
+
<DataList.Item>
|
|
334
|
+
<DataList.ItemLabel>Account</DataList.ItemLabel>
|
|
335
|
+
<DataList.ItemValue>Bridge Ledger</DataList.ItemValue>
|
|
336
|
+
</DataList.Item>
|
|
337
|
+
<DataList.Item>
|
|
338
|
+
<DataList.ItemLabel>State</DataList.ItemLabel>
|
|
339
|
+
<DataList.ItemValue>
|
|
340
|
+
<Badge palette="success">Settled</Badge>
|
|
341
|
+
</DataList.ItemValue>
|
|
342
|
+
</DataList.Item>
|
|
343
|
+
</DataList.Root>;
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
| Axis | Values | Default |
|
|
347
|
+
| ------------- | ------------------------ | ---------- |
|
|
348
|
+
| `orientation` | `vertical`, `horizontal` | `vertical` |
|
|
349
|
+
| `size` | `sm`, `md`, `lg` | `md` |
|
|
350
|
+
| `variant` | `subtle`, `bold` | `subtle` |
|
|
351
|
+
| `divided` | `true` | off |
|
|
352
|
+
|
|
353
|
+
| Part | Element | What it renders |
|
|
354
|
+
| ----------- | ------- | ----------------------------------- |
|
|
355
|
+
| `Root` | `dl` | The list, with the variants |
|
|
356
|
+
| `Item` | `div` | One pair |
|
|
357
|
+
| `ItemLabel` | `dt` | The label, a row of words and marks |
|
|
358
|
+
| `ItemValue` | `dd` | The value, words or components |
|
|
359
|
+
|
|
360
|
+
- `vertical` sets each label above its value. `horizontal` lays the pairs on two columns, so every
|
|
361
|
+
value starts at the same position: the widest label's width, capped at 40% of the list.
|
|
362
|
+
- `size` sets the text one size smaller than the list, 12.6, 14.2 and 16px, and the gaps with it: 8,
|
|
363
|
+
12 and 16px between the pairs, and 12, 16 and 24px between the columns.
|
|
364
|
+
- `subtle` mutes the label, so the value reads first. `bold` sets the label in the label weight and
|
|
365
|
+
mutes the value.
|
|
366
|
+
- `divided` renders a hairline between the pairs, as far from each pair as the gap between them.
|
|
367
|
+
- A value wraps at any character, so a long identifier never widens the list. A label and a value
|
|
368
|
+
are rows with a gap, so a control or a mark beside the words is centred on their line.
|
|
369
|
+
- The list has no `palette` and no `effect` axis, because it has no color and no box of its own.
|
|
370
|
+
|
|
371
|
+
## Timeline
|
|
372
|
+
|
|
373
|
+
`Timeline` renders entries in the order they happened, such as a history, an order's shipping or an
|
|
374
|
+
activity feed. It is an ordered list, and each entry places an indicator on a rail beside its words.
|
|
375
|
+
|
|
376
|
+
```tsx
|
|
377
|
+
import { Timeline } from "@stealthscale/component-collections";
|
|
378
|
+
|
|
379
|
+
<Timeline.Root>
|
|
380
|
+
<Timeline.Item>
|
|
381
|
+
<Timeline.Connector>
|
|
382
|
+
<Timeline.Indicator>1</Timeline.Indicator>
|
|
383
|
+
</Timeline.Connector>
|
|
384
|
+
<Timeline.Content>
|
|
385
|
+
<Timeline.Title>Offer raised</Timeline.Title>
|
|
386
|
+
<Timeline.Description>Tuesday</Timeline.Description>
|
|
387
|
+
</Timeline.Content>
|
|
388
|
+
</Timeline.Item>
|
|
389
|
+
</Timeline.Root>;
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
| Axis | Values | Default |
|
|
393
|
+
| --------- | ------------------------------------------------------------------ | --------- |
|
|
394
|
+
| `size` | `sm`, `md`, `lg`, `xl` | `md` |
|
|
395
|
+
| `variant` | `solid`, `subtle`, `outline`, `plain` | `solid` |
|
|
396
|
+
| `palette` | `primary`, `secondary`, `accent`, `neutral`, and the four statuses | `neutral` |
|
|
397
|
+
| `rail` | `start`, `center`, `end` | `start` |
|
|
398
|
+
| `ongoing` | `true` | off |
|
|
399
|
+
|
|
400
|
+
| Part | Element | What it renders |
|
|
401
|
+
| ------------- | ------- | --------------------------------------------------------- |
|
|
402
|
+
| `Root` | `ol` | The list, a grid of three columns |
|
|
403
|
+
| `Item` | `li` | One entry, a row across the three columns |
|
|
404
|
+
| `Connector` | `div` | The rail's cell, which contains the indicator |
|
|
405
|
+
| `Indicator` | `span` | A number, an icon or an empty circle, hidden from readers |
|
|
406
|
+
| `Content` | `div` | The words on one side of the rail |
|
|
407
|
+
| `Title` | `div` | The headline, a row that wraps |
|
|
408
|
+
| `Description` | `div` | The supporting text, in the muted ink |
|
|
409
|
+
|
|
410
|
+
- A `Content` written before the `Connector` goes on the side before the rail and aligns to it. One
|
|
411
|
+
written after goes on the side after the rail.
|
|
412
|
+
- Each entry is a row of the root's grid, so the words before the rail line up down the list at the
|
|
413
|
+
widest entry's width. `start` sizes them to their words, `center` gives both sides an equal share,
|
|
414
|
+
and `end` puts the rail at the end.
|
|
415
|
+
- The indicator is 16, 20, 24 and 32px from `sm` to `xl`. A one-line title is as tall as the
|
|
416
|
+
indicator and centred on it, and the words are 8, 12, 12 and 16px from it.
|
|
417
|
+
- `solid` fills the indicator in the palette's solid, `subtle` tints it, `outline` rings it and
|
|
418
|
+
`plain` leaves the panel's ground. Every look is opaque and ringed in `bg.panel`, so the rail
|
|
419
|
+
never runs through an indicator.
|
|
420
|
+
- The rail stops at the last indicator. `ongoing` draws it past, for a run that has not finished.
|
|
421
|
+
- The indicator is `aria-hidden`, because the list's order numbers the entries and an icon repeats
|
|
422
|
+
the title. State what an entry means in its words.
|
|
423
|
+
- The timeline has no `effect` axis, because its indicators are marks in a list rather than a box a
|
|
424
|
+
reader acts on.
|
|
425
|
+
|
|
426
|
+
## TreeView
|
|
427
|
+
|
|
428
|
+
`TreeView` renders nested rows that open and close, such as a file tree, a documentation index or a
|
|
429
|
+
set of permissions. The tree reads a `TreeCollection`, and `TreeView.Nodes` calls a function that
|
|
430
|
+
renders each node's row from the node and its state.
|
|
431
|
+
|
|
432
|
+
```tsx
|
|
433
|
+
import { TreeCollection, TreeView } from "@stealthscale/component-collections";
|
|
434
|
+
|
|
435
|
+
const files = new TreeCollection<File>({
|
|
436
|
+
nodeToString: (node) => node.name,
|
|
437
|
+
nodeToValue: (node) => node.id,
|
|
438
|
+
rootNode,
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
<TreeView.Root collection={files} defaultExpandedValue={["src"]}>
|
|
442
|
+
<TreeView.Label>Files</TreeView.Label>
|
|
443
|
+
<TreeView.Tree>
|
|
444
|
+
<TreeView.Nodes
|
|
445
|
+
render={({ node, nodeState }: TreeView.NodeDetails<File>) =>
|
|
446
|
+
nodeState.isBranch ? (
|
|
447
|
+
<TreeView.BranchControl>
|
|
448
|
+
<TreeView.BranchIndicator>
|
|
449
|
+
<ChevronRightIcon />
|
|
450
|
+
</TreeView.BranchIndicator>
|
|
451
|
+
<TreeView.BranchText>{node.name}</TreeView.BranchText>
|
|
452
|
+
</TreeView.BranchControl>
|
|
453
|
+
) : (
|
|
454
|
+
<TreeView.Item>
|
|
455
|
+
<TreeView.ItemText>{node.name}</TreeView.ItemText>
|
|
456
|
+
</TreeView.Item>
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
/>
|
|
460
|
+
</TreeView.Tree>
|
|
461
|
+
</TreeView.Root>;
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
| Axis | Values | Default |
|
|
465
|
+
| ---------- | ------------------------------------------------------------------ | --------- |
|
|
466
|
+
| `size` | `sm`, `md`, `lg` | `md` |
|
|
467
|
+
| `selected` | `subtle`, `solid`, `plain` | `subtle` |
|
|
468
|
+
| `palette` | `primary`, `secondary`, `accent`, `neutral`, and the four statuses | inherited |
|
|
469
|
+
| `effect` | `glow` | none |
|
|
470
|
+
|
|
471
|
+
| Part | Element | What it renders |
|
|
472
|
+
| ------------------- | ------------ | ---------------------------------------------------------- |
|
|
473
|
+
| `Root` | `div` | The label and the tree, which starts the machine |
|
|
474
|
+
| `Label` | `div` | The words that name the tree |
|
|
475
|
+
| `Tree` | `div` | The element with the `tree` role, which handles the keys |
|
|
476
|
+
| `Nodes` | none | Every node, with a group around an open branch's children |
|
|
477
|
+
| `BranchControl` | `div` | A branch's row, with the `treeitem` role |
|
|
478
|
+
| `BranchTrigger` | `span` | The part of a branch's row that opens it without selecting |
|
|
479
|
+
| `BranchIndicator` | `span` | The mark that turns a quarter as the branch opens |
|
|
480
|
+
| `BranchText` | `span` | A branch's text |
|
|
481
|
+
| `BranchIndentGuide` | `div` | The line down an open branch's children |
|
|
482
|
+
| `Item` | `div` or `a` | An item's row, with the `treeitem` role |
|
|
483
|
+
| `ItemText` | `span` | An item's text |
|
|
484
|
+
| `ItemIndicator` | `span` | The mark at the end of a selected item's row |
|
|
485
|
+
| `NodeCheckbox` | `span` | The box that shows a node's check |
|
|
486
|
+
| `NodeRenameInput` | `input` | The field that replaces a row's text while it is renamed |
|
|
487
|
+
|
|
488
|
+
- The focusable row has the tree semantics: the `treeitem` role, `aria-level`, `aria-posinset`,
|
|
489
|
+
`aria-setsize`, and `aria-expanded`, `aria-selected`, `aria-disabled` and `aria-busy` where they
|
|
490
|
+
apply. A screen reader announces them on the row that has focus.
|
|
491
|
+
- The tree takes its name from `TreeView.Label` while the label is mounted. Without one, pass
|
|
492
|
+
`aria-label` to `TreeView.Tree`.
|
|
493
|
+
- The arrows move focus, ArrowRight opens a branch and moves into it, ArrowLeft closes a branch or
|
|
494
|
+
moves to its parent, Home and End move to the ends, `*` opens the siblings of the focused branch,
|
|
495
|
+
and a typed letter moves to the next row that starts with it. Enter and Space select.
|
|
496
|
+
- A focused row shows its ring while the last input was a key. It sets `data-focus-visible`, which
|
|
497
|
+
the ring's condition matches in every browser. Firefox keeps `:focus-visible` off a focus that a
|
|
498
|
+
script moves after a press.
|
|
499
|
+
- Rows measure 24, 32 and 40px from `sm` to `lg`. Each level indents one mark and one gap, so a
|
|
500
|
+
child's indicator is under its parent's icon. An item reserves the column of a branch's indicator,
|
|
501
|
+
so the texts of one level start at one x.
|
|
502
|
+
- `subtle` fills a selected row with the palette's subtle role, which measures 1.2:1 against a card,
|
|
503
|
+
and sets its text in medium weight. `solid` fills it with the palette's solid color, and `plain`
|
|
504
|
+
sets the medium weight alone. Under forced colors a selected row fills with `Highlight`.
|
|
505
|
+
- `selectionMode="multiple"` lets Ctrl or Cmd with a press add a row, and Shift with a press or an
|
|
506
|
+
arrow extend the selection.
|
|
507
|
+
- `checkable` puts `aria-checked` on every row, and Space toggles the check in place of selecting. A
|
|
508
|
+
branch is checked while every node under it is, and partly checked while some are.
|
|
509
|
+
`TreeView.NodeCheckbox` shows the check and toggles it under a pointer.
|
|
510
|
+
- An `Item` with an `href` renders an `a`, which follows its address on a press and on Enter.
|
|
511
|
+
- A branch whose node declares `childrenCount` loads its children through `loadChildren` when it
|
|
512
|
+
first opens, and reports `aria-busy` meanwhile. Keep the collection `onLoadChildrenComplete`
|
|
513
|
+
returns.
|
|
514
|
+
- With `canRename`, F2 opens `TreeView.NodeRenameInput` on the focused row. Enter keeps the new name
|
|
515
|
+
and Escape cancels. Replace the node in your collection in `onRenameComplete`.
|
|
516
|
+
- `expandOnClick={false}` makes a press on a row select it alone, and `TreeView.BranchTrigger` opens
|
|
517
|
+
it. A collapsed branch renders no group, so its rows are not in the document.
|
|
518
|
+
- `TreeCollection` is the engine's own class. Its `filter`, `replace`, `remove`, `insertAfter` and
|
|
519
|
+
`insertBefore` return a new collection, which you pass to the root in place of the old one.
|
|
520
|
+
|
|
521
|
+
## Sortable
|
|
522
|
+
|
|
523
|
+
`Sortable` renders rows a person drags into another order, in one list or in the lists of a board.
|
|
524
|
+
It is built on dnd-kit, and a person drags each row by its handle, with the pointer or the keys.
|
|
525
|
+
|
|
526
|
+
```tsx
|
|
527
|
+
import { GripVerticalIcon } from "lucide-react";
|
|
528
|
+
|
|
529
|
+
import { Sortable } from "@stealthscale/component-collections";
|
|
530
|
+
|
|
531
|
+
<Sortable.Root
|
|
532
|
+
canMove={(card, from, to) => from !== "todo" || to !== "done"}
|
|
533
|
+
items={columns}
|
|
534
|
+
onItemMove={save}
|
|
535
|
+
onItemsChange={setColumns}
|
|
536
|
+
>
|
|
537
|
+
<Sortable.Board>
|
|
538
|
+
{lists.map((list) => (
|
|
539
|
+
<Sortable.List key={list.id} label={list.title} limit={list.limit} value={list.id}>
|
|
540
|
+
<Sortable.Items>
|
|
541
|
+
{columns[list.id].map((card, index) => (
|
|
542
|
+
<Sortable.Item index={index} key={card.id} label={card.title} value={card.id}>
|
|
543
|
+
<Sortable.Handle>
|
|
544
|
+
<GripVerticalIcon />
|
|
545
|
+
</Sortable.Handle>
|
|
546
|
+
<Sortable.ItemContent>{card.title}</Sortable.ItemContent>
|
|
547
|
+
</Sortable.Item>
|
|
548
|
+
))}
|
|
549
|
+
</Sortable.Items>
|
|
550
|
+
<Sortable.Empty>No cards yet.</Sortable.Empty>
|
|
551
|
+
</Sortable.List>
|
|
552
|
+
))}
|
|
553
|
+
</Sortable.Board>
|
|
554
|
+
</Sortable.Root>;
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
| Axis | Values | Default |
|
|
558
|
+
| --------- | --------------- | ------- |
|
|
559
|
+
| `variant` | `card`, `plain` | `card` |
|
|
560
|
+
|
|
561
|
+
| Part | Element | What it renders |
|
|
562
|
+
| ------------- | -------- | ----------------------------------------------------------------- |
|
|
563
|
+
| `Root` | `div` | dnd-kit's provider around the lists, with the hidden instructions |
|
|
564
|
+
| `Board` | `div` | A board's lists in a row that scrolls sideways |
|
|
565
|
+
| `List` | `div` | One list of a board, a drop target as a whole |
|
|
566
|
+
| `Items` | `ul` | A list's rows |
|
|
567
|
+
| `Item` | `li` | One row |
|
|
568
|
+
| `Handle` | `button` | The actions `Button` a drag starts from |
|
|
569
|
+
| `ItemContent` | `div` | A row's words and marks, which take the rest of the row |
|
|
570
|
+
| `Empty` | `div` | A list's message while it has no row, outside the `ul` |
|
|
571
|
+
|
|
572
|
+
- `card` renders each row as a panel card with a hairline edge. `plain` renders rows without a fill
|
|
573
|
+
or a visible edge, as tall as their handle and touching each other, for a list inside a panel such
|
|
574
|
+
as a popover. In either look the row a person drags lifts on the panel's fill.
|
|
575
|
+
- The root is controlled. `items` is an array for one list, or a record of a board's lists keyed by
|
|
576
|
+
their ids. `onItemsChange` reports the items to render after a drop, a move into another list
|
|
577
|
+
during a drag, a cancel and a move without a drag. `onItemMove` reports each finished move once,
|
|
578
|
+
from the place the item left to the place it took.
|
|
579
|
+
- An item's id is unique across the lists and differs from every list's id, because dnd-kit keeps
|
|
580
|
+
one registry of drop targets.
|
|
581
|
+
- A list's `limit` refuses an item from another list while the list has that many items.
|
|
582
|
+
`canMove(item, from, to)` refuses a move from the list the item was picked up from. A refusing
|
|
583
|
+
list sets `data-refuses` while the dragged item is over it, and the recipe dashes its edge in the
|
|
584
|
+
error ink. A drop on it puts the item back.
|
|
585
|
+
- A handle is named `Move <row>` by default, with the role description `sortable` and the
|
|
586
|
+
instructions as its description. Space or Enter lifts the row, the arrow keys move it, Space or
|
|
587
|
+
Enter drops it, and Escape puts it back. A mouse drags the row from its handle at once, and a
|
|
588
|
+
finger after 250ms.
|
|
589
|
+
- The kit announces a pick-up, each place a drag passes, a refusal and a drop in dnd-kit's live
|
|
590
|
+
region. `liftedLabel`, `movedLabel`, `droppedLabel` and `cancelledLabel` write a sentence from the
|
|
591
|
+
item, its position, its list's count and, on a board, the list's name. `fullLabel` and
|
|
592
|
+
`refusedLabel` write a refusal. `handleLabel`, `roleDescription` and `instructions` set the
|
|
593
|
+
handle's words. Each word defaults to English.
|
|
594
|
+
- `Sortable.useMove()` inside the root, or a render function among its children, returns
|
|
595
|
+
`move(id, { index, list })`, which moves an item without a drag. The move follows the drag's rules
|
|
596
|
+
and reports as a drop does. It is announced, and it returns whether the list took the item. A row
|
|
597
|
+
menu with "Move up" and "Move down" gives a person who cannot drag the same result (WCAG 2.5.7).
|
|
598
|
+
- A `disabled` row renders an inert box of the handle's size in place of its handle, and dnd-kit
|
|
599
|
+
neither lifts it nor drops another row on it.
|
|
600
|
+
- The dragged row is dnd-kit's element in the top layer with the large shadow, and the place it will
|
|
601
|
+
take is a dashed slot.
|
|
602
|
+
- A board's lists are 15 to 20rem wide and share the board's width. The board scrolls sideways in
|
|
603
|
+
the primitives package's scroll area once the lists are wider.
|
|
604
|
+
- The package does not include icons. Pass the handle's glyph.
|
|
605
|
+
|
|
606
|
+
## SwipeActions
|
|
607
|
+
|
|
608
|
+
`SwipeActions` renders a row whose actions wait behind it at its inline end. A finger or a pen
|
|
609
|
+
swipes the row aside, a trackpad's horizontal swipe does the same, and Tab moves to the actions with
|
|
610
|
+
no gesture.
|
|
611
|
+
|
|
612
|
+
```tsx
|
|
613
|
+
import { ArchiveIcon, Trash2Icon } from "lucide-react";
|
|
614
|
+
|
|
615
|
+
import { SwipeActions } from "@stealthscale/component-collections";
|
|
616
|
+
|
|
617
|
+
<SwipeActions.Root>
|
|
618
|
+
<SwipeActions.Content>Invoice 1042 is ready to send</SwipeActions.Content>
|
|
619
|
+
<SwipeActions.Actions>
|
|
620
|
+
<SwipeActions.Action aria-label="Archive invoice 1042" onClick={archive} palette="neutral">
|
|
621
|
+
<ArchiveIcon size="1em" />
|
|
622
|
+
Archive
|
|
623
|
+
</SwipeActions.Action>
|
|
624
|
+
<SwipeActions.Action aria-label="Delete invoice 1042" onClick={remove} palette="error">
|
|
625
|
+
<Trash2Icon size="1em" />
|
|
626
|
+
Delete
|
|
627
|
+
</SwipeActions.Action>
|
|
628
|
+
</SwipeActions.Actions>
|
|
629
|
+
</SwipeActions.Root>;
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
The recipe has no axis.
|
|
633
|
+
|
|
634
|
+
| Part | Element | What it renders |
|
|
635
|
+
| --------- | -------- | ------------------------------------------------- |
|
|
636
|
+
| `Root` | `div` | The row, which clips its content and its actions |
|
|
637
|
+
| `Content` | `div` | The row's content, which a swipe moves aside |
|
|
638
|
+
| `Actions` | `div` | The actions at the row's inline end |
|
|
639
|
+
| `Action` | `button` | One action, the actions `Button` as tall as a row |
|
|
640
|
+
|
|
641
|
+
- The actions follow the content in the document, so Tab stops on the row's own links first. Focus
|
|
642
|
+
entering the actions opens the row, and focus leaving them closes it. Escape closes the row and
|
|
643
|
+
moves focus to it, which the root's `tabIndex={-1}` allows.
|
|
644
|
+
- A touch or pen drag moves the row under the finger. A release at or past half the actions' width
|
|
645
|
+
leaves them open, and one short of half closes them. A horizontal trackpad swipe moves the row the
|
|
646
|
+
same way and settles 150ms after its last delta. `settleSwipe(revealed, width)` is the rule.
|
|
647
|
+
- A mouse drag does not move the row, so a mouse selects the row's text. A mouse user opens the
|
|
648
|
+
actions with Tab.
|
|
649
|
+
- A press outside an open row closes it, so opening another row closes the first.
|
|
650
|
+
- An action closes its row and moves focus to it, then runs `onClick`. When the action removes the
|
|
651
|
+
row, move focus to the next row or to what replaces the list, because focus on a removed row falls
|
|
652
|
+
to the document's body.
|
|
653
|
+
- Name each action after its row, such as `aria-label="Archive invoice 1042"`, because every row
|
|
654
|
+
repeats the same visible words. Start the name with the visible words.
|
|
655
|
+
- The root sets `data-open` while the actions rest open, `data-dragging` while a drag moves the row,
|
|
656
|
+
and `--swipe-reveal` to the revealed width. The actions are clipped to that width, so they show
|
|
657
|
+
nothing at rest, whatever the row's background.
|
|
658
|
+
- The content is a padded flex row with `touch-action: pan-y`, so a vertical swipe scrolls the page.
|
|
659
|
+
The row moves over the theme's `move` duration. It moves with no transition during a drag and
|
|
660
|
+
under reduced motion.
|
|
661
|
+
- An action is square, at least 64px wide at the default scale and as tall as the row, with its
|
|
662
|
+
glyph above its words. Its focus ring is inside it in the palette's contrast ink. Use the solid
|
|
663
|
+
look, the button's default.
|
|
664
|
+
- In a row laid out right to left the actions wait at the left end, and a swipe towards the right
|
|
665
|
+
reveals them.
|
|
666
|
+
- The package does not include icons. Pass the actions' glyphs.
|
|
667
|
+
|
|
668
|
+
## useListCollection and useFilter
|
|
669
|
+
|
|
670
|
+
`useListCollection` keeps a list's rows and narrows them to the typed text. `useFilter` returns the
|
|
671
|
+
functions that match a row's text.
|
|
672
|
+
|
|
673
|
+
```tsx
|
|
674
|
+
const filter = useFilter({ sensitivity: "base" });
|
|
675
|
+
const { collection, narrow } = useListCollection({
|
|
676
|
+
filter: filter.contains,
|
|
677
|
+
itemToString: (client) => client.name,
|
|
678
|
+
itemToValue: (client) => client.id,
|
|
679
|
+
rows: clients,
|
|
680
|
+
});
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
- `useFilter` returns `contains`, `startsWith` and `endsWith`, each built on `Intl.Collator`, so
|
|
684
|
+
`Jose` matches `José` and `strasse` matches `Straße`. The default sensitivity ignores case and
|
|
685
|
+
accents.
|
|
686
|
+
- `useListCollection` returns the collection of matching rows and `narrow`, which sets the text.
|
|
687
|
+
`narrow("")` restores every row.
|
|
688
|
+
- Pass `filter` to match on the text a reader sees, or pass a predicate of your own to match on any
|
|
689
|
+
field of the row.
|
|
690
|
+
|
|
691
|
+
## Licence
|
|
692
|
+
|
|
693
|
+
MIT. See [LICENSE](LICENSE).
|