@stealthscale/component-collections 0.0.1 → 0.1.2
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 +34 -14
- package/package.json +48 -24
|
@@ -0,0 +1,2212 @@
|
|
|
1
|
+
import { COUNTS, PALETTES, below, columnCounts, cornerVariants, defineSlotRecipe, dense, flatVariants, highlightVariants, interactive, onSlot, onSlots, paletteVariants, row, sizeVariants, surface, truncate } from "@stealthscale/theme/authoring";
|
|
2
|
+
//#region src/data-list/recipe.ts
|
|
3
|
+
/**
|
|
4
|
+
* Declares the data list's slot recipe: pairs of a label and a value, down the list or across it.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* The root is a description list. Each item is a `div` that contains one term and its detail,
|
|
8
|
+
* which HTML allows inside `dl`. Down the list, the label is above its value. Across it, the root
|
|
9
|
+
* is a grid of two columns and each item is a subgrid row, so every value starts at the same
|
|
10
|
+
* inline position: the widest label's width, capped at 40% of the list. The value wraps at any
|
|
11
|
+
* character, so a long identifier never widens the list. The root sets the gap between items as
|
|
12
|
+
* `--data-list-gap`, which a divided list also pads each rule by. The list has no color and no
|
|
13
|
+
* box of its own, so it offers no `palette` and no `effect` axis.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Custom property the root sets to the gap between items, which a divided list pads each rule by.
|
|
17
|
+
*/
|
|
18
|
+
const GAP$2 = "--data-list-gap";
|
|
19
|
+
/**
|
|
20
|
+
* Class name of the recipe, which a selector across parts reads.
|
|
21
|
+
*/
|
|
22
|
+
const CLASS$4 = "data-list";
|
|
23
|
+
/**
|
|
24
|
+
* Maps each size to the gap between items and to the gap between the columns across the list.
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
* The gaps measure 8, 12 and 16px between items, and 12, 16 and 24px between the columns, at the
|
|
28
|
+
* foundation's metrics.
|
|
29
|
+
*/
|
|
30
|
+
const GAPS = {
|
|
31
|
+
lg: ["xl", "2xl"],
|
|
32
|
+
md: ["lg", "xl"],
|
|
33
|
+
sm: ["md", "lg"]
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Styles a data list down the page at the middle size, with a muted label.
|
|
37
|
+
*/
|
|
38
|
+
const recipe$8 = defineSlotRecipe({
|
|
39
|
+
base: {
|
|
40
|
+
item: { minInlineSize: "0" },
|
|
41
|
+
itemLabel: {
|
|
42
|
+
alignItems: "center",
|
|
43
|
+
display: "flex",
|
|
44
|
+
gap: "0.375em",
|
|
45
|
+
minInlineSize: "0"
|
|
46
|
+
},
|
|
47
|
+
itemValue: {
|
|
48
|
+
alignItems: "center",
|
|
49
|
+
display: "flex",
|
|
50
|
+
flexWrap: "wrap",
|
|
51
|
+
gap: "0.375em",
|
|
52
|
+
margin: "0",
|
|
53
|
+
minInlineSize: "0",
|
|
54
|
+
overflowWrap: "anywhere"
|
|
55
|
+
},
|
|
56
|
+
root: {
|
|
57
|
+
margin: "0",
|
|
58
|
+
minInlineSize: "0"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
className: CLASS$4,
|
|
62
|
+
defaultVariants: {
|
|
63
|
+
orientation: "vertical",
|
|
64
|
+
size: "md",
|
|
65
|
+
variant: "subtle"
|
|
66
|
+
},
|
|
67
|
+
jsx: [/^DataList\.\w+$/u],
|
|
68
|
+
slots: [
|
|
69
|
+
"root",
|
|
70
|
+
"item",
|
|
71
|
+
"itemLabel",
|
|
72
|
+
"itemValue"
|
|
73
|
+
],
|
|
74
|
+
variants: {
|
|
75
|
+
/**
|
|
76
|
+
* Whether a hairline separates each item from the one before it.
|
|
77
|
+
*
|
|
78
|
+
* @remarks
|
|
79
|
+
* The rule is as far from the item above it as from the item below it: the root's gap above
|
|
80
|
+
* and the same gap as padding below.
|
|
81
|
+
*/
|
|
82
|
+
divided: { true: { item: { [`& + .${CLASS$4}__item`]: {
|
|
83
|
+
borderBlockStartWidth: "hairline",
|
|
84
|
+
borderColor: "border",
|
|
85
|
+
paddingBlockStart: `var(${GAP$2})`
|
|
86
|
+
} } } },
|
|
87
|
+
/**
|
|
88
|
+
* Which way a label and its value run: the label above the value, or beside it.
|
|
89
|
+
*/
|
|
90
|
+
orientation: {
|
|
91
|
+
horizontal: {
|
|
92
|
+
item: {
|
|
93
|
+
alignItems: "baseline",
|
|
94
|
+
display: "grid",
|
|
95
|
+
gridColumn: "1 / -1",
|
|
96
|
+
gridTemplateColumns: "subgrid"
|
|
97
|
+
},
|
|
98
|
+
root: {
|
|
99
|
+
display: "grid",
|
|
100
|
+
gridTemplateColumns: "fit-content(40%) minmax(0, 1fr)",
|
|
101
|
+
rowGap: `var(${GAP$2})`
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
vertical: {
|
|
105
|
+
item: {
|
|
106
|
+
display: "flex",
|
|
107
|
+
flexDirection: "column",
|
|
108
|
+
gap: dense("{spacing.gap.xs}")
|
|
109
|
+
},
|
|
110
|
+
root: {
|
|
111
|
+
display: "flex",
|
|
112
|
+
flexDirection: "column",
|
|
113
|
+
gap: `var(${GAP$2})`
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
/**
|
|
118
|
+
* The text style of the items, one size smaller than the list, and the gaps between them.
|
|
119
|
+
*/
|
|
120
|
+
size: onSlots({
|
|
121
|
+
item: sizeVariants((size) => ({ textStyle: `body.${below(size)}` }), [
|
|
122
|
+
"sm",
|
|
123
|
+
"md",
|
|
124
|
+
"lg"
|
|
125
|
+
]),
|
|
126
|
+
root: sizeVariants((size) => ({
|
|
127
|
+
columnGap: dense(`{spacing.gap.${GAPS[size][1]}}`),
|
|
128
|
+
[GAP$2]: dense(`{spacing.gap.${GAPS[size][0]}}`)
|
|
129
|
+
}), [
|
|
130
|
+
"sm",
|
|
131
|
+
"md",
|
|
132
|
+
"lg"
|
|
133
|
+
])
|
|
134
|
+
}),
|
|
135
|
+
/**
|
|
136
|
+
* Which of the pair is louder.
|
|
137
|
+
*
|
|
138
|
+
* @remarks
|
|
139
|
+
* `subtle` mutes the label, so the value reads first, as in a summary. `bold` sets the label
|
|
140
|
+
* in the label weight and mutes the value, as in a form of settled answers.
|
|
141
|
+
*/
|
|
142
|
+
variant: {
|
|
143
|
+
bold: {
|
|
144
|
+
itemLabel: { fontWeight: "medium" },
|
|
145
|
+
itemValue: { color: "fg.muted" }
|
|
146
|
+
},
|
|
147
|
+
subtle: { itemLabel: { color: "fg.muted" } }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/listbox/metrics.ts
|
|
153
|
+
/**
|
|
154
|
+
* Returns the geometry the listbox recipe is written from: the list's padding, the insets that put
|
|
155
|
+
* a band's and a label's text on the rows' text line, and one row's height.
|
|
156
|
+
*/
|
|
157
|
+
/**
|
|
158
|
+
* Padding of the list around its rows, in every look.
|
|
159
|
+
*
|
|
160
|
+
* @remarks
|
|
161
|
+
* A row needs room around it for the highlight's corners, so the list keeps the padding on a
|
|
162
|
+
* plain look too.
|
|
163
|
+
*/
|
|
164
|
+
const PAD = "{spacing.gap.xs}";
|
|
165
|
+
/**
|
|
166
|
+
* Custom property the list sets to one row's height.
|
|
167
|
+
*/
|
|
168
|
+
const ROW_HEIGHT = "--listbox-row";
|
|
169
|
+
/**
|
|
170
|
+
* Returns one style per column count: the count's template and `display: grid`.
|
|
171
|
+
*
|
|
172
|
+
* @remarks
|
|
173
|
+
* The list is a flex column until a caller sets a count, so each value also sets the display.
|
|
174
|
+
*/
|
|
175
|
+
function tiled() {
|
|
176
|
+
const columned = columnCounts();
|
|
177
|
+
const entries = COUNTS.map((count) => [count, {
|
|
178
|
+
...columned[count],
|
|
179
|
+
display: "grid"
|
|
180
|
+
}]);
|
|
181
|
+
return Object.fromEntries(entries);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Returns the inline padding of a band across the frame, so its text starts on the rows' text
|
|
185
|
+
* line.
|
|
186
|
+
*
|
|
187
|
+
* @remarks
|
|
188
|
+
* The field and the select-all row render in the frame, outside the list, and span the frame's
|
|
189
|
+
* width. Each inset is the list's padding plus the row's own inset.
|
|
190
|
+
* @param start - The row's start inset.
|
|
191
|
+
* @param end - The row's end inset.
|
|
192
|
+
* @returns The start and end padding.
|
|
193
|
+
*/
|
|
194
|
+
function banded(start, end) {
|
|
195
|
+
return {
|
|
196
|
+
paddingInlineEnd: `calc(${PAD} + ${end})`,
|
|
197
|
+
paddingInlineStart: `calc(${PAD} + ${start})`
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Returns the start padding of a part outside the frame, so its text starts on the rows' text
|
|
202
|
+
* line.
|
|
203
|
+
*
|
|
204
|
+
* @remarks
|
|
205
|
+
* The label above the frame and the summary below it take the list's padding plus the row's
|
|
206
|
+
* start inset.
|
|
207
|
+
* @param start - The row's start inset.
|
|
208
|
+
* @returns The start padding.
|
|
209
|
+
*/
|
|
210
|
+
function aligned(start) {
|
|
211
|
+
return { paddingInlineStart: `calc(${PAD} + ${start})` };
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Returns the height of a one-line row at a size, as the value of `ROW_HEIGHT`.
|
|
215
|
+
*
|
|
216
|
+
* @remarks
|
|
217
|
+
* A window and a transfer read the height before any row renders. The height is the row's floor
|
|
218
|
+
* plus its block padding, the two values the row itself reads. A row with a description is taller
|
|
219
|
+
* than this value.
|
|
220
|
+
* @param size - The list's size.
|
|
221
|
+
* @returns The custom property and its value.
|
|
222
|
+
*/
|
|
223
|
+
function rowHeight(size) {
|
|
224
|
+
const floor = "{sizes.6}";
|
|
225
|
+
const padding = dense(`{spacing.gap.${below(size)}}`);
|
|
226
|
+
return { [ROW_HEIGHT]: `calc(${floor} + ${padding} + ${padding})` };
|
|
227
|
+
}
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region src/listbox/selected.ts
|
|
230
|
+
/**
|
|
231
|
+
* Selector of a selected row's description, written with the class the listbox binds on a row.
|
|
232
|
+
*/
|
|
233
|
+
const DESCRIBED = ".listbox__item[data-selected] &";
|
|
234
|
+
/**
|
|
235
|
+
* Fills a selected row with `Highlight` under forced colors.
|
|
236
|
+
*/
|
|
237
|
+
const FORCED$1 = { _highContrast: {
|
|
238
|
+
background: "Highlight",
|
|
239
|
+
color: "HighlightText",
|
|
240
|
+
forcedColorAdjust: "none"
|
|
241
|
+
} };
|
|
242
|
+
/**
|
|
243
|
+
* Restates the forced colors on a selected row under a pointer.
|
|
244
|
+
*
|
|
245
|
+
* @remarks
|
|
246
|
+
* A look's hover fill on a selected row is as specific as the forced colors, and the compiler
|
|
247
|
+
* emits the `(hover: hover)` block after the `(forced-colors: active)` block, so the fill applies
|
|
248
|
+
* over the forced colors. The restatement repeats the selected state's selector, which makes it
|
|
249
|
+
* more specific than the fill.
|
|
250
|
+
*/
|
|
251
|
+
const FORCED_HOVER$1 = { _selected: FORCED$1 };
|
|
252
|
+
/**
|
|
253
|
+
* Draws the highlight's line on a selected row in `HighlightText` under forced colors. The line is
|
|
254
|
+
* `Highlight` on every other row, the color of the selected row's fill.
|
|
255
|
+
*/
|
|
256
|
+
const FOUND = { _highContrast: { outlineColor: "HighlightText" } };
|
|
257
|
+
/**
|
|
258
|
+
* Writes a look that fills a selected row, with the forced inks of the row's checkbox and
|
|
259
|
+
* description on the fill.
|
|
260
|
+
*
|
|
261
|
+
* @param selected - Styles of a selected row in the look.
|
|
262
|
+
*/
|
|
263
|
+
function filled(selected) {
|
|
264
|
+
return {
|
|
265
|
+
item: { _selected: {
|
|
266
|
+
...FORCED$1,
|
|
267
|
+
_highlighted: FOUND,
|
|
268
|
+
...selected
|
|
269
|
+
} },
|
|
270
|
+
itemCheckbox: { "[data-selected] > &": { _highContrast: {
|
|
271
|
+
background: "HighlightText",
|
|
272
|
+
borderColor: "HighlightText",
|
|
273
|
+
color: "Highlight"
|
|
274
|
+
} } },
|
|
275
|
+
itemDescription: { [DESCRIBED]: { _highContrast: { color: "HighlightText" } } }
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Fill of a selected row per look. The solid and subtle looks restate their hover, because a
|
|
280
|
+
* variant applies over the base hover.
|
|
281
|
+
*/
|
|
282
|
+
const SELECTED$1 = {
|
|
283
|
+
none: { item: { _selected: { fontWeight: "inherit" } } },
|
|
284
|
+
plain: filled({ fontWeight: "medium" }),
|
|
285
|
+
solid: filled({
|
|
286
|
+
layerStyle: "flat.solid",
|
|
287
|
+
_hover: {
|
|
288
|
+
...FORCED_HOVER$1,
|
|
289
|
+
background: "colorPalette.solid.hover"
|
|
290
|
+
}
|
|
291
|
+
}),
|
|
292
|
+
subtle: filled({
|
|
293
|
+
layerStyle: "flat.subtle",
|
|
294
|
+
_hover: {
|
|
295
|
+
...FORCED_HOVER$1,
|
|
296
|
+
background: "colorPalette.muted"
|
|
297
|
+
}
|
|
298
|
+
})
|
|
299
|
+
};
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region src/listbox/recipe.ts
|
|
302
|
+
/**
|
|
303
|
+
* Recipe for the listbox: a label, a frame with an optional field and select-all row, the list of
|
|
304
|
+
* rows, and a summary.
|
|
305
|
+
*
|
|
306
|
+
* @remarks
|
|
307
|
+
* A row reads the theme's `row` fragment. Focus rests on the list and the highlight moves over
|
|
308
|
+
* the rows, so a row has no focus ring and no pressed state of its own, and the `highlight` axis
|
|
309
|
+
* reads the three marks a menu reads. A row still fills under a pointer. A row's text reads the
|
|
310
|
+
* body role one size smaller than the list, the rule a menu's rows follow. A group spaces its
|
|
311
|
+
* rows with the list's own gap, and the room above a group's label is the label's padding. A row
|
|
312
|
+
* centres its parts vertically, so the checkbox, the icon and the end mark are at the middle of a
|
|
313
|
+
* row with a description. The content is the primitives package's scroll area, and the frame does
|
|
314
|
+
* not scroll, so the field and the select-all row remain in place while the rows move. The
|
|
315
|
+
* `viewport` and `rows` slots are the scroll area's viewport, the element with the listbox role,
|
|
316
|
+
* and its content: the rows take the list's padding, gap, columns and direction. The area's focus
|
|
317
|
+
* ring is inside its edge, because the frame clips it.
|
|
318
|
+
*/
|
|
319
|
+
/**
|
|
320
|
+
* Class name of the recipe, which the selectors across parts are built from.
|
|
321
|
+
*/
|
|
322
|
+
const CLASS$3 = "listbox";
|
|
323
|
+
/**
|
|
324
|
+
* Custom property the scroll area's root reads for the offset of its focus ring.
|
|
325
|
+
*/
|
|
326
|
+
const RING_OFFSET = "--scroll-area-ring-offset";
|
|
327
|
+
/**
|
|
328
|
+
* Defines the listbox recipe: a plain list at size `md` that tints the highlighted row and fills a
|
|
329
|
+
* selected row with the palette's subtle fill.
|
|
330
|
+
*
|
|
331
|
+
* @remarks
|
|
332
|
+
* The placeholder reads the tertiary ink. The control is the band the field renders in: it
|
|
333
|
+
* carries the block-end rule, the insets and the focus color, and the field renders no box of
|
|
334
|
+
* its own. The clear control renders at the band's end, in the column of the rows' marks. The
|
|
335
|
+
* label, the summary and the group labels take a row's inset, so every text starts on one line.
|
|
336
|
+
* A checkbox hides its mark with `color: transparent`, and Firefox paints a forced border in
|
|
337
|
+
* that color, so the checkbox opts out of forced colors: its edge is `CanvasText`, and a checked
|
|
338
|
+
* box fills with `CanvasText` around a `Canvas` mark.
|
|
339
|
+
*/
|
|
340
|
+
const recipe$7 = defineSlotRecipe({
|
|
341
|
+
base: {
|
|
342
|
+
clearTrigger: {
|
|
343
|
+
...interactive(),
|
|
344
|
+
_hover: { color: "fg" },
|
|
345
|
+
alignItems: "center",
|
|
346
|
+
borderRadius: "l1",
|
|
347
|
+
color: "fg.muted",
|
|
348
|
+
display: "inline-flex",
|
|
349
|
+
flexShrink: "0",
|
|
350
|
+
justifyContent: "center"
|
|
351
|
+
},
|
|
352
|
+
content: {
|
|
353
|
+
[`&:has(.${CLASS$3}__rows:empty)`]: { display: "none" },
|
|
354
|
+
display: "flex",
|
|
355
|
+
flexDirection: "column",
|
|
356
|
+
minBlockSize: "0",
|
|
357
|
+
[RING_OFFSET]: "calc({borderWidths.ring} * -1)"
|
|
358
|
+
},
|
|
359
|
+
control: {
|
|
360
|
+
_focusWithin: { borderBlockEndColor: "colorPalette.solid" },
|
|
361
|
+
alignItems: "center",
|
|
362
|
+
borderBlockEndColor: "border",
|
|
363
|
+
borderBlockEndWidth: "hairline",
|
|
364
|
+
borderStyle: "solid",
|
|
365
|
+
borderWidth: "0",
|
|
366
|
+
display: "flex",
|
|
367
|
+
transitionDuration: "press",
|
|
368
|
+
transitionProperty: "common",
|
|
369
|
+
transitionTimingFunction: "press"
|
|
370
|
+
},
|
|
371
|
+
empty: {
|
|
372
|
+
color: "fg.muted",
|
|
373
|
+
padding: PAD,
|
|
374
|
+
textAlign: "start"
|
|
375
|
+
},
|
|
376
|
+
frame: {
|
|
377
|
+
display: "flex",
|
|
378
|
+
flexDirection: "column",
|
|
379
|
+
minBlockSize: "0",
|
|
380
|
+
minInlineSize: "0",
|
|
381
|
+
overflow: "hidden"
|
|
382
|
+
},
|
|
383
|
+
input: {
|
|
384
|
+
_disabled: { layerStyle: "disabled" },
|
|
385
|
+
_placeholder: { color: "fg.subtle" },
|
|
386
|
+
appearance: "none",
|
|
387
|
+
background: "transparent",
|
|
388
|
+
border: "none",
|
|
389
|
+
borderRadius: "0",
|
|
390
|
+
color: "fg",
|
|
391
|
+
flex: "1",
|
|
392
|
+
minInlineSize: "0",
|
|
393
|
+
outline: "none",
|
|
394
|
+
padding: "0"
|
|
395
|
+
},
|
|
396
|
+
item: {
|
|
397
|
+
...row(),
|
|
398
|
+
_hover: { background: "bg.muted" },
|
|
399
|
+
alignItems: "center",
|
|
400
|
+
justifyContent: "space-between"
|
|
401
|
+
},
|
|
402
|
+
itemCheckbox: {
|
|
403
|
+
_highContrast: {
|
|
404
|
+
borderColor: "CanvasText",
|
|
405
|
+
forcedColorAdjust: "none"
|
|
406
|
+
},
|
|
407
|
+
"[data-selected] > &, [data-state=checked] > &, [data-state=indeterminate] > &": {
|
|
408
|
+
_highContrast: {
|
|
409
|
+
background: "CanvasText",
|
|
410
|
+
borderColor: "CanvasText",
|
|
411
|
+
color: "Canvas"
|
|
412
|
+
},
|
|
413
|
+
background: "colorPalette.solid",
|
|
414
|
+
borderColor: "colorPalette.solid",
|
|
415
|
+
color: "colorPalette.contrast"
|
|
416
|
+
},
|
|
417
|
+
alignItems: "center",
|
|
418
|
+
borderColor: "border.emphasized",
|
|
419
|
+
borderRadius: "l1",
|
|
420
|
+
borderStyle: "solid",
|
|
421
|
+
borderWidth: "control",
|
|
422
|
+
color: "transparent",
|
|
423
|
+
display: "inline-flex",
|
|
424
|
+
flexShrink: "0",
|
|
425
|
+
justifyContent: "center",
|
|
426
|
+
lineHeight: "tight"
|
|
427
|
+
},
|
|
428
|
+
itemDescription: {
|
|
429
|
+
...truncate(),
|
|
430
|
+
color: "fg.subtle",
|
|
431
|
+
display: "block",
|
|
432
|
+
textAlign: "start"
|
|
433
|
+
},
|
|
434
|
+
itemGroup: {
|
|
435
|
+
display: "flex",
|
|
436
|
+
flexDirection: "column",
|
|
437
|
+
minInlineSize: "0"
|
|
438
|
+
},
|
|
439
|
+
itemGroupLabel: {
|
|
440
|
+
color: "fg.subtle",
|
|
441
|
+
fontWeight: "medium"
|
|
442
|
+
},
|
|
443
|
+
itemIndicator: {
|
|
444
|
+
"&[data-state=checked]": { visibility: "visible" },
|
|
445
|
+
alignItems: "center",
|
|
446
|
+
display: "inline-flex",
|
|
447
|
+
flexShrink: "0",
|
|
448
|
+
justifyContent: "center",
|
|
449
|
+
lineHeight: "tight",
|
|
450
|
+
visibility: "hidden"
|
|
451
|
+
},
|
|
452
|
+
itemLines: {
|
|
453
|
+
display: "flex",
|
|
454
|
+
flex: "1",
|
|
455
|
+
flexDirection: "column",
|
|
456
|
+
lineHeight: "tight",
|
|
457
|
+
minInlineSize: "0"
|
|
458
|
+
},
|
|
459
|
+
itemText: {
|
|
460
|
+
...truncate(),
|
|
461
|
+
display: "block",
|
|
462
|
+
flex: "1",
|
|
463
|
+
minInlineSize: "0",
|
|
464
|
+
textAlign: "start"
|
|
465
|
+
},
|
|
466
|
+
label: {
|
|
467
|
+
color: "fg",
|
|
468
|
+
fontWeight: "semibold"
|
|
469
|
+
},
|
|
470
|
+
root: {
|
|
471
|
+
display: "flex",
|
|
472
|
+
flexDirection: "column",
|
|
473
|
+
minBlockSize: "0",
|
|
474
|
+
minInlineSize: "0"
|
|
475
|
+
},
|
|
476
|
+
rows: {
|
|
477
|
+
display: "flex",
|
|
478
|
+
flexDirection: "column",
|
|
479
|
+
padding: PAD
|
|
480
|
+
},
|
|
481
|
+
selectAll: {
|
|
482
|
+
...row(),
|
|
483
|
+
_hover: { background: "bg.muted" },
|
|
484
|
+
borderBlockEndColor: "border",
|
|
485
|
+
borderBlockEndWidth: "hairline",
|
|
486
|
+
borderRadius: "0",
|
|
487
|
+
cursor: "menuitem"
|
|
488
|
+
},
|
|
489
|
+
valueText: {
|
|
490
|
+
...truncate(),
|
|
491
|
+
color: "fg.muted"
|
|
492
|
+
},
|
|
493
|
+
viewport: { scrollPadding: PAD }
|
|
494
|
+
},
|
|
495
|
+
className: CLASS$3,
|
|
496
|
+
defaultVariants: {
|
|
497
|
+
highlight: "tint",
|
|
498
|
+
orientation: "vertical",
|
|
499
|
+
radius: "l1",
|
|
500
|
+
selected: "subtle",
|
|
501
|
+
size: "md",
|
|
502
|
+
variant: "plain"
|
|
503
|
+
},
|
|
504
|
+
jsx: [/^Listbox(\.\w+)?$/u],
|
|
505
|
+
slots: [
|
|
506
|
+
"root",
|
|
507
|
+
"label",
|
|
508
|
+
"control",
|
|
509
|
+
"input",
|
|
510
|
+
"clearTrigger",
|
|
511
|
+
"frame",
|
|
512
|
+
"content",
|
|
513
|
+
"viewport",
|
|
514
|
+
"rows",
|
|
515
|
+
"empty",
|
|
516
|
+
"selectAll",
|
|
517
|
+
"item",
|
|
518
|
+
"itemLines",
|
|
519
|
+
"itemText",
|
|
520
|
+
"itemDescription",
|
|
521
|
+
"itemCheckbox",
|
|
522
|
+
"itemIndicator",
|
|
523
|
+
"itemGroup",
|
|
524
|
+
"itemGroupLabel",
|
|
525
|
+
"valueText"
|
|
526
|
+
],
|
|
527
|
+
staticCss: [{ palette: [...PALETTES] }],
|
|
528
|
+
variants: {
|
|
529
|
+
/**
|
|
530
|
+
* Number of columns of a tiled list.
|
|
531
|
+
*
|
|
532
|
+
* @remarks
|
|
533
|
+
* The grid collection takes the same count, because the machine moves the highlight between
|
|
534
|
+
* neighbours by the collection's count and the recipe places them by this one.
|
|
535
|
+
*/
|
|
536
|
+
columns: onSlot("rows", tiled()),
|
|
537
|
+
/**
|
|
538
|
+
* Glow around a selected row, in the palette's solid at half opacity.
|
|
539
|
+
*/
|
|
540
|
+
effect: onSlot("item", { glow: { _selected: { layerStyle: "glow.sm" } } }),
|
|
541
|
+
/**
|
|
542
|
+
* Mark on the row the highlight is on.
|
|
543
|
+
*/
|
|
544
|
+
highlight: onSlot("item", highlightVariants()),
|
|
545
|
+
/**
|
|
546
|
+
* Direction the rows run in, and the arrow keys that move the highlight.
|
|
547
|
+
*
|
|
548
|
+
* @remarks
|
|
549
|
+
* The machine and the recipe read one prop, so the arrows always follow the layout. A row in
|
|
550
|
+
* a horizontal list takes its own width, and its text does not truncate.
|
|
551
|
+
*/
|
|
552
|
+
orientation: {
|
|
553
|
+
horizontal: {
|
|
554
|
+
item: { inlineSize: "auto" },
|
|
555
|
+
itemText: { flex: "0 0 auto" },
|
|
556
|
+
rows: { flexDirection: "row" }
|
|
557
|
+
},
|
|
558
|
+
vertical: { rows: { flexDirection: "column" } }
|
|
559
|
+
},
|
|
560
|
+
/**
|
|
561
|
+
* Palette the highlight, the selected fill and the checkboxes read.
|
|
562
|
+
*
|
|
563
|
+
* @remarks
|
|
564
|
+
* The palette is set on the root, and every part inherits the palette's custom properties.
|
|
565
|
+
*/
|
|
566
|
+
palette: onSlot("root", paletteVariants()),
|
|
567
|
+
/**
|
|
568
|
+
* Corner radius of a row.
|
|
569
|
+
*/
|
|
570
|
+
radius: onSlot("item", cornerVariants([
|
|
571
|
+
"l1",
|
|
572
|
+
"l2",
|
|
573
|
+
"l3"
|
|
574
|
+
])),
|
|
575
|
+
/**
|
|
576
|
+
* Fill of a selected row, beside the mark at its end.
|
|
577
|
+
*
|
|
578
|
+
* @remarks
|
|
579
|
+
* The selected state and the highlight are separate axes, and a row can show both. The fills
|
|
580
|
+
* are the flat looks, which do not change under a pointer, and each look restates its hover,
|
|
581
|
+
* because a variant applies over the base hover. `plain` sets the text in medium weight and
|
|
582
|
+
* relies on the mark. `none` sets nothing, for a list whose rows each have a checkbox. Under
|
|
583
|
+
* forced colors a selected row fills with `Highlight` in every look but `none`, whose
|
|
584
|
+
* checkboxes show the state.
|
|
585
|
+
*/
|
|
586
|
+
selected: SELECTED$1,
|
|
587
|
+
/**
|
|
588
|
+
* Row height, insets, text sizes and mark sizes. A row's text and a mark read one size smaller
|
|
589
|
+
* than the list, and a description two sizes smaller.
|
|
590
|
+
*
|
|
591
|
+
* @remarks
|
|
592
|
+
* A row takes the inset scale at both ends. The label, the field, the select-all row, the
|
|
593
|
+
* group labels, the empty text and the summary take the same start inset, so every text
|
|
594
|
+
* starts on one line.
|
|
595
|
+
*/
|
|
596
|
+
size: onSlots({
|
|
597
|
+
clearTrigger: sizeVariants((size) => ({ boxSize: dense(`{sizes.icon.${below(size)}}`) }), [
|
|
598
|
+
"sm",
|
|
599
|
+
"md",
|
|
600
|
+
"lg"
|
|
601
|
+
]),
|
|
602
|
+
content: sizeVariants((size) => rowHeight(size), [
|
|
603
|
+
"sm",
|
|
604
|
+
"md",
|
|
605
|
+
"lg"
|
|
606
|
+
]),
|
|
607
|
+
control: sizeVariants((size) => ({
|
|
608
|
+
...banded(dense(`{spacing.inset.${size}}`), dense(`{spacing.inset.${size}}`)),
|
|
609
|
+
columnGap: dense(`{spacing.gap.${below(size)}}`),
|
|
610
|
+
minBlockSize: dense(`{sizes.control.${size}}`)
|
|
611
|
+
}), [
|
|
612
|
+
"sm",
|
|
613
|
+
"md",
|
|
614
|
+
"lg"
|
|
615
|
+
]),
|
|
616
|
+
empty: sizeVariants((size) => ({
|
|
617
|
+
...banded(dense(`{spacing.inset.${size}}`), dense(`{spacing.inset.${size}}`)),
|
|
618
|
+
textStyle: `body.${below(size)}`,
|
|
619
|
+
paddingBlock: dense(`{spacing.gap.${below(size)}}`)
|
|
620
|
+
}), [
|
|
621
|
+
"sm",
|
|
622
|
+
"md",
|
|
623
|
+
"lg"
|
|
624
|
+
]),
|
|
625
|
+
input: sizeVariants((size) => ({ textStyle: `label.${size}` }), [
|
|
626
|
+
"sm",
|
|
627
|
+
"md",
|
|
628
|
+
"lg"
|
|
629
|
+
]),
|
|
630
|
+
item: sizeVariants((size) => ({
|
|
631
|
+
textStyle: `body.${below(size)}`,
|
|
632
|
+
columnGap: dense(`{spacing.gap.${size}}`),
|
|
633
|
+
minBlockSize: "6",
|
|
634
|
+
paddingBlock: dense(`{spacing.gap.${below(size)}}`),
|
|
635
|
+
paddingInline: dense(`{spacing.inset.${size}}`)
|
|
636
|
+
}), [
|
|
637
|
+
"sm",
|
|
638
|
+
"md",
|
|
639
|
+
"lg"
|
|
640
|
+
]),
|
|
641
|
+
itemCheckbox: sizeVariants((size) => ({ boxSize: dense(`{sizes.icon.${below(size)}}`) }), [
|
|
642
|
+
"sm",
|
|
643
|
+
"md",
|
|
644
|
+
"lg"
|
|
645
|
+
]),
|
|
646
|
+
itemDescription: sizeVariants((size) => ({ textStyle: `body.${below(below(size))}` }), [
|
|
647
|
+
"sm",
|
|
648
|
+
"md",
|
|
649
|
+
"lg"
|
|
650
|
+
]),
|
|
651
|
+
itemGroup: sizeVariants((size) => ({ gap: dense(`{spacing.gap.${below(below(size))}}`) }), [
|
|
652
|
+
"sm",
|
|
653
|
+
"md",
|
|
654
|
+
"lg"
|
|
655
|
+
]),
|
|
656
|
+
itemGroupLabel: sizeVariants((size) => ({
|
|
657
|
+
textStyle: `label.${below(below(size))}`,
|
|
658
|
+
paddingBlockStart: dense(`{spacing.gap.${size}}`),
|
|
659
|
+
paddingInline: dense(`{spacing.inset.${size}}`)
|
|
660
|
+
}), [
|
|
661
|
+
"sm",
|
|
662
|
+
"md",
|
|
663
|
+
"lg"
|
|
664
|
+
]),
|
|
665
|
+
itemIndicator: sizeVariants((size) => ({ boxSize: dense(`{sizes.icon.${below(size)}}`) }), [
|
|
666
|
+
"sm",
|
|
667
|
+
"md",
|
|
668
|
+
"lg"
|
|
669
|
+
]),
|
|
670
|
+
label: sizeVariants((size) => ({
|
|
671
|
+
...aligned(dense(`{spacing.inset.${size}}`)),
|
|
672
|
+
textStyle: `label.${size}`
|
|
673
|
+
}), [
|
|
674
|
+
"sm",
|
|
675
|
+
"md",
|
|
676
|
+
"lg"
|
|
677
|
+
]),
|
|
678
|
+
root: sizeVariants((size) => ({ gap: dense(`{spacing.gap.${size}}`) }), [
|
|
679
|
+
"sm",
|
|
680
|
+
"md",
|
|
681
|
+
"lg"
|
|
682
|
+
]),
|
|
683
|
+
rows: sizeVariants((size) => ({ gap: dense(`{spacing.gap.${below(below(size))}}`) }), [
|
|
684
|
+
"sm",
|
|
685
|
+
"md",
|
|
686
|
+
"lg"
|
|
687
|
+
]),
|
|
688
|
+
selectAll: sizeVariants((size) => ({
|
|
689
|
+
...banded(dense(`{spacing.inset.${size}}`), dense(`{spacing.inset.${size}}`)),
|
|
690
|
+
textStyle: `body.${below(size)}`,
|
|
691
|
+
columnGap: dense(`{spacing.gap.${size}}`),
|
|
692
|
+
minBlockSize: "6",
|
|
693
|
+
paddingBlock: dense(`{spacing.gap.${below(size)}}`)
|
|
694
|
+
}), [
|
|
695
|
+
"sm",
|
|
696
|
+
"md",
|
|
697
|
+
"lg"
|
|
698
|
+
]),
|
|
699
|
+
valueText: sizeVariants((size) => ({
|
|
700
|
+
...aligned(dense(`{spacing.inset.${size}}`)),
|
|
701
|
+
textStyle: `label.${below(size)}`
|
|
702
|
+
}), [
|
|
703
|
+
"sm",
|
|
704
|
+
"md",
|
|
705
|
+
"lg"
|
|
706
|
+
])
|
|
707
|
+
}),
|
|
708
|
+
/**
|
|
709
|
+
* Background, edge and corners of the frame. `surface` raises the frame and `plain` leaves it
|
|
710
|
+
* transparent.
|
|
711
|
+
*
|
|
712
|
+
* @remarks
|
|
713
|
+
* The frame contains the field, the select-all row and the rows, and clips its overflow to
|
|
714
|
+
* its corners. The label and the summary render outside it, and the rows scroll inside the
|
|
715
|
+
* content.
|
|
716
|
+
*/
|
|
717
|
+
variant: {
|
|
718
|
+
surface: { frame: surface() },
|
|
719
|
+
plain: { frame: { background: "transparent" } }
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
//#endregion
|
|
724
|
+
//#region src/sortable/recipe.ts
|
|
725
|
+
/**
|
|
726
|
+
* Declares the sortable kit's slot recipe: rows a person drags into another order, in one list or
|
|
727
|
+
* in the lists of a board.
|
|
728
|
+
*
|
|
729
|
+
* @remarks
|
|
730
|
+
* A row has its handle at the start and the caller's content after it, where a row's content part
|
|
731
|
+
* takes the rest of the row. The `card` look renders a row as a panel card with a hairline edge,
|
|
732
|
+
* and the `plain` look renders rows without a fill, a visible edge or block padding that touch
|
|
733
|
+
* each other, as tall as their handle, for a list inside a panel. A fixed row keeps an
|
|
734
|
+
* empty box of the handle's size, so its content starts where the other rows' does. The row a
|
|
735
|
+
* person drags is dnd-kit's own element in the top layer, raised by the large shadow, on the
|
|
736
|
+
* panel's fill in either look. The place it will take is dnd-kit's placeholder, a copy of the row
|
|
737
|
+
* the recipe renders as a dashed slot with its content hidden. A list takes at least one
|
|
738
|
+
* control's height, so an empty list is a place to drop on. A board lays its lists out in a row,
|
|
739
|
+
* each list a well from 15rem to 20rem wide that shares the board's width, and the row scrolls
|
|
740
|
+
* sideways once the lists are wider than the board. A list that refuses the dragged row dashes
|
|
741
|
+
* its edge in the error ink and dims its rows to the muted opacity.
|
|
742
|
+
*/
|
|
743
|
+
/**
|
|
744
|
+
* Class name of the recipe, which a selector across parts reads.
|
|
745
|
+
*/
|
|
746
|
+
const CLASS$2 = "sortable";
|
|
747
|
+
/**
|
|
748
|
+
* Styles the row a person drags, raised by the large shadow, and the place it will take, a dashed
|
|
749
|
+
* slot with its content hidden.
|
|
750
|
+
*
|
|
751
|
+
* @remarks
|
|
752
|
+
* Each look restates these states, because a look's rules apply over the base rules.
|
|
753
|
+
*/
|
|
754
|
+
const MOVING = {
|
|
755
|
+
"&[data-dnd-dragging]": {
|
|
756
|
+
boxShadow: "lg",
|
|
757
|
+
cursor: "dragging"
|
|
758
|
+
},
|
|
759
|
+
"&[data-dnd-placeholder]": {
|
|
760
|
+
"& > *": { visibility: "hidden" },
|
|
761
|
+
background: "transparent",
|
|
762
|
+
borderColor: "border.emphasized",
|
|
763
|
+
borderStyle: "dashed",
|
|
764
|
+
boxShadow: "none"
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
/**
|
|
768
|
+
* Styles the kit's rows, lists and board.
|
|
769
|
+
*/
|
|
770
|
+
const recipe$6 = defineSlotRecipe({
|
|
771
|
+
base: {
|
|
772
|
+
board: { minInlineSize: "0" },
|
|
773
|
+
empty: {
|
|
774
|
+
textStyle: "body.sm",
|
|
775
|
+
color: "fg.muted",
|
|
776
|
+
paddingBlock: dense("{spacing.inset.sm}"),
|
|
777
|
+
paddingInline: dense("{spacing.inset.xs}"),
|
|
778
|
+
textAlign: "center"
|
|
779
|
+
},
|
|
780
|
+
fixed: {
|
|
781
|
+
blockSize: dense("{sizes.control.xs}"),
|
|
782
|
+
flexShrink: "0",
|
|
783
|
+
inlineSize: dense("{sizes.control.xs}")
|
|
784
|
+
},
|
|
785
|
+
handle: {
|
|
786
|
+
cursor: "drag",
|
|
787
|
+
flexShrink: "0",
|
|
788
|
+
touchAction: "none"
|
|
789
|
+
},
|
|
790
|
+
item: {
|
|
791
|
+
textStyle: "body.sm",
|
|
792
|
+
alignItems: "center",
|
|
793
|
+
display: "flex",
|
|
794
|
+
gap: dense("{spacing.gap.sm}"),
|
|
795
|
+
minInlineSize: "0",
|
|
796
|
+
paddingBlock: dense("{spacing.inset.xs}"),
|
|
797
|
+
paddingInlineEnd: dense("{spacing.inset.sm}"),
|
|
798
|
+
paddingInlineStart: dense("{spacing.inset.xs}")
|
|
799
|
+
},
|
|
800
|
+
itemContent: {
|
|
801
|
+
alignItems: "center",
|
|
802
|
+
display: "flex",
|
|
803
|
+
flex: "1",
|
|
804
|
+
gap: dense("{spacing.gap.sm}"),
|
|
805
|
+
minInlineSize: "0"
|
|
806
|
+
},
|
|
807
|
+
items: {
|
|
808
|
+
display: "flex",
|
|
809
|
+
flexDirection: "column",
|
|
810
|
+
gap: dense("{spacing.gap.xs}"),
|
|
811
|
+
listStyle: "none",
|
|
812
|
+
margin: "0",
|
|
813
|
+
minBlockSize: dense("{sizes.control.md}"),
|
|
814
|
+
padding: "0"
|
|
815
|
+
},
|
|
816
|
+
lanes: {
|
|
817
|
+
alignItems: "flex-start",
|
|
818
|
+
display: "flex",
|
|
819
|
+
gap: dense("{spacing.gap.md}")
|
|
820
|
+
},
|
|
821
|
+
list: {
|
|
822
|
+
"&[data-refuses]": {
|
|
823
|
+
[`& .${CLASS$2}__item`]: { opacity: "muted" },
|
|
824
|
+
borderColor: "border.error",
|
|
825
|
+
borderStyle: "dashed"
|
|
826
|
+
},
|
|
827
|
+
background: "bg.subtle",
|
|
828
|
+
borderColor: "transparent",
|
|
829
|
+
borderRadius: "l2",
|
|
830
|
+
borderWidth: "hairline",
|
|
831
|
+
display: "flex",
|
|
832
|
+
flexBasis: "{sizes.60}",
|
|
833
|
+
flexDirection: "column",
|
|
834
|
+
flexGrow: "1",
|
|
835
|
+
flexShrink: "0",
|
|
836
|
+
gap: dense("{spacing.gap.sm}"),
|
|
837
|
+
maxInlineSize: "xs",
|
|
838
|
+
minInlineSize: "{sizes.60}",
|
|
839
|
+
padding: dense("{spacing.inset.sm}")
|
|
840
|
+
},
|
|
841
|
+
root: { minInlineSize: "0" }
|
|
842
|
+
},
|
|
843
|
+
className: CLASS$2,
|
|
844
|
+
defaultVariants: { variant: "card" },
|
|
845
|
+
jsx: [/^Sortable\.\w+$/u],
|
|
846
|
+
slots: [
|
|
847
|
+
"root",
|
|
848
|
+
"board",
|
|
849
|
+
"lanes",
|
|
850
|
+
"list",
|
|
851
|
+
"items",
|
|
852
|
+
"item",
|
|
853
|
+
"handle",
|
|
854
|
+
"fixed",
|
|
855
|
+
"itemContent",
|
|
856
|
+
"empty"
|
|
857
|
+
],
|
|
858
|
+
variants: {
|
|
859
|
+
/**
|
|
860
|
+
* Look of a row: a panel card with a hairline edge, or a plain row without a fill or a visible
|
|
861
|
+
* edge, for rows inside a panel.
|
|
862
|
+
*/
|
|
863
|
+
variant: {
|
|
864
|
+
card: { item: {
|
|
865
|
+
...surface("xs"),
|
|
866
|
+
...MOVING
|
|
867
|
+
} },
|
|
868
|
+
plain: {
|
|
869
|
+
item: {
|
|
870
|
+
...MOVING,
|
|
871
|
+
"&[data-dnd-dragging]": {
|
|
872
|
+
background: "bg.panel",
|
|
873
|
+
borderColor: "border",
|
|
874
|
+
boxShadow: "lg",
|
|
875
|
+
cursor: "dragging"
|
|
876
|
+
},
|
|
877
|
+
borderColor: "transparent",
|
|
878
|
+
borderRadius: "l2",
|
|
879
|
+
borderWidth: "hairline",
|
|
880
|
+
paddingBlock: "0"
|
|
881
|
+
},
|
|
882
|
+
items: { gap: "0" }
|
|
883
|
+
}
|
|
884
|
+
} }
|
|
885
|
+
});
|
|
886
|
+
//#endregion
|
|
887
|
+
//#region src/status-matrix/recipe.ts
|
|
888
|
+
/**
|
|
889
|
+
* Recipe for the status matrix: the styles the matrix adds to the table's parts.
|
|
890
|
+
*
|
|
891
|
+
* @remarks
|
|
892
|
+
* The table's recipe styles the rules, the bands and the cell padding. The mark's tone is the
|
|
893
|
+
* data, so no cell takes a fill per state, and the crosshair fills with `bg.subtle` and no
|
|
894
|
+
* palette. The recipe has no `palette` axis for the same reason, and no `effect` axis, because a
|
|
895
|
+
* glow around a cell overlaps the rules beside it. The root takes the width of its content up to
|
|
896
|
+
* the full width, and the scroller scrolls a wider grid. A column's name renders in a block of
|
|
897
|
+
* its own, because the table's recipe sets the header cell's alignment.
|
|
898
|
+
*/
|
|
899
|
+
/**
|
|
900
|
+
* Sizes the matrix offers, which are the table's.
|
|
901
|
+
*/
|
|
902
|
+
const SIZES$4 = [
|
|
903
|
+
"sm",
|
|
904
|
+
"md",
|
|
905
|
+
"lg"
|
|
906
|
+
];
|
|
907
|
+
/**
|
|
908
|
+
* Styles a cell or a header the crosshair fills.
|
|
909
|
+
*
|
|
910
|
+
* @remarks
|
|
911
|
+
* `bg.subtle` is one step under the panel. The table's stripe is `bg.muted`, so a lit row does
|
|
912
|
+
* not read as a striped one.
|
|
913
|
+
*/
|
|
914
|
+
const LIT = { "&[data-lit]": { background: "bg.subtle" } };
|
|
915
|
+
/**
|
|
916
|
+
* Maps each `data-tone` value to the palette the mark reads.
|
|
917
|
+
*/
|
|
918
|
+
const TONES = {
|
|
919
|
+
"&[data-tone=error]": { colorPalette: "error" },
|
|
920
|
+
"&[data-tone=info]": { colorPalette: "info" },
|
|
921
|
+
"&[data-tone=neutral]": { colorPalette: "neutral" },
|
|
922
|
+
"&[data-tone=success]": { colorPalette: "success" },
|
|
923
|
+
"&[data-tone=warning]": { colorPalette: "warning" }
|
|
924
|
+
};
|
|
925
|
+
/**
|
|
926
|
+
* Defines the status matrix recipe at size `md` by default.
|
|
927
|
+
*/
|
|
928
|
+
const recipe$5 = defineSlotRecipe({
|
|
929
|
+
base: {
|
|
930
|
+
cell: { ...LIT },
|
|
931
|
+
columnHeading: { ...LIT },
|
|
932
|
+
columnLabel: {
|
|
933
|
+
display: "block",
|
|
934
|
+
textAlign: "center"
|
|
935
|
+
},
|
|
936
|
+
dot: {
|
|
937
|
+
background: "colorPalette.solid",
|
|
938
|
+
borderRadius: "full"
|
|
939
|
+
},
|
|
940
|
+
legend: {
|
|
941
|
+
alignItems: "center",
|
|
942
|
+
color: "fg.muted",
|
|
943
|
+
display: "flex",
|
|
944
|
+
flexWrap: "wrap",
|
|
945
|
+
listStyle: "none",
|
|
946
|
+
margin: "0",
|
|
947
|
+
padding: "0"
|
|
948
|
+
},
|
|
949
|
+
legendItem: {
|
|
950
|
+
alignItems: "center",
|
|
951
|
+
display: "flex"
|
|
952
|
+
},
|
|
953
|
+
mark: {
|
|
954
|
+
...TONES,
|
|
955
|
+
alignItems: "center",
|
|
956
|
+
color: "colorPalette.fg",
|
|
957
|
+
display: "flex",
|
|
958
|
+
justifyContent: "center"
|
|
959
|
+
},
|
|
960
|
+
name: { srOnly: true },
|
|
961
|
+
picker: {
|
|
962
|
+
...interactive(),
|
|
963
|
+
_focusVisible: { focusVisibleRing: "inside" },
|
|
964
|
+
appearance: "none",
|
|
965
|
+
background: "transparent",
|
|
966
|
+
borderStyle: "none",
|
|
967
|
+
color: "inherit",
|
|
968
|
+
display: "block",
|
|
969
|
+
font: "inherit",
|
|
970
|
+
inlineSize: "full",
|
|
971
|
+
padding: "0"
|
|
972
|
+
},
|
|
973
|
+
root: {
|
|
974
|
+
alignItems: "stretch",
|
|
975
|
+
display: "flex",
|
|
976
|
+
flexDirection: "column",
|
|
977
|
+
inlineSize: "fit-content",
|
|
978
|
+
maxInlineSize: "full"
|
|
979
|
+
},
|
|
980
|
+
rowHeading: {
|
|
981
|
+
...LIT,
|
|
982
|
+
whiteSpace: "nowrap"
|
|
983
|
+
}
|
|
984
|
+
},
|
|
985
|
+
className: "status-matrix",
|
|
986
|
+
defaultVariants: { size: "md" },
|
|
987
|
+
jsx: [/^StatusMatrix(\.\w+)?$/u],
|
|
988
|
+
slots: [
|
|
989
|
+
"root",
|
|
990
|
+
"columnHeading",
|
|
991
|
+
"columnLabel",
|
|
992
|
+
"rowHeading",
|
|
993
|
+
"cell",
|
|
994
|
+
"picker",
|
|
995
|
+
"mark",
|
|
996
|
+
"dot",
|
|
997
|
+
"name",
|
|
998
|
+
"legend",
|
|
999
|
+
"legendItem"
|
|
1000
|
+
],
|
|
1001
|
+
variants: {
|
|
1002
|
+
/**
|
|
1003
|
+
* Size of the marks and the legend. A mark takes the icon size of the step, and the dot and
|
|
1004
|
+
* the legend's text take one step smaller.
|
|
1005
|
+
*/
|
|
1006
|
+
size: onSlots({
|
|
1007
|
+
dot: sizeVariants((size) => ({ boxSize: dense(`{sizes.icon.${below(size)}}`) }), SIZES$4),
|
|
1008
|
+
legend: sizeVariants((size) => ({
|
|
1009
|
+
textStyle: `label.${below(size)}`,
|
|
1010
|
+
gap: dense(`{spacing.gap.${size}}`),
|
|
1011
|
+
paddingBlock: dense(`{spacing.gap.${size}}`),
|
|
1012
|
+
paddingInline: dense(`{spacing.inset.${size}}`)
|
|
1013
|
+
}), SIZES$4),
|
|
1014
|
+
legendItem: sizeVariants((size) => ({ gap: dense(`{spacing.gap.${below(size)}}`) }), SIZES$4),
|
|
1015
|
+
mark: sizeVariants((size) => ({ "& > svg": { boxSize: dense(`{sizes.icon.${size}}`) } }), SIZES$4)
|
|
1016
|
+
}) }
|
|
1017
|
+
});
|
|
1018
|
+
//#endregion
|
|
1019
|
+
//#region src/table/recipe.ts
|
|
1020
|
+
/**
|
|
1021
|
+
* Recipe for the table: a scroller, the table, and the elements a table is composed from.
|
|
1022
|
+
*
|
|
1023
|
+
* @remarks
|
|
1024
|
+
* Every part binds the element with the table semantics, so the recipe adds styles only.
|
|
1025
|
+
* The stripe and the hover are on the body and select its own rows, because `:nth-of-type` counts
|
|
1026
|
+
* within a parent. Column names read the label role one size smaller, in the muted ink, above a
|
|
1027
|
+
* rule heavier than the row rules. A sorted column's name takes the full ink. A row header that
|
|
1028
|
+
* spans the table is a section heading and takes the column names' style. The caption renders
|
|
1029
|
+
* below the table with a cell's inset. A cell of figures states `data-numeric`, because a slot
|
|
1030
|
+
* recipe resolves its variants once at the root. Borders are separated, and every cell rules its
|
|
1031
|
+
* end on each axis and never its start, so a sticky header cell keeps its own rule. The recipe
|
|
1032
|
+
* has no `effect` axis, because a glow around a row overlaps the rules of the rows beside it. A
|
|
1033
|
+
* row's stripe, hover and selected fills set `--table-row-fill`, which the row paints and a
|
|
1034
|
+
* sticky row header paints over the panel. The stripe skips a selected row. The body's rules
|
|
1035
|
+
* read their ink from `--table-rule` and their width from `--table-rule-width`, which a
|
|
1036
|
+
* composing part sets on a row that ends a group of rows. Under forced colours a selected row
|
|
1037
|
+
* fills with `Highlight`, and its cells' text and rules take `HighlightText`.
|
|
1038
|
+
*/
|
|
1039
|
+
/**
|
|
1040
|
+
* Sizes the table offers.
|
|
1041
|
+
*/
|
|
1042
|
+
const SIZES$3 = [
|
|
1043
|
+
"sm",
|
|
1044
|
+
"md",
|
|
1045
|
+
"lg"
|
|
1046
|
+
];
|
|
1047
|
+
/**
|
|
1048
|
+
* Custom property the scroll area's root reads for the style of its focus ring.
|
|
1049
|
+
*/
|
|
1050
|
+
const RING_STYLE = "--scroll-area-ring-style";
|
|
1051
|
+
/**
|
|
1052
|
+
* Attribute of a cell of figures, which the base aligns to the end in tabular figures.
|
|
1053
|
+
*/
|
|
1054
|
+
const NUMERIC = "data-numeric";
|
|
1055
|
+
/**
|
|
1056
|
+
* Custom property of a row's fill: transparent, the stripe, the hover fill or the selected fill.
|
|
1057
|
+
*
|
|
1058
|
+
* @remarks
|
|
1059
|
+
* The row paints it, and a sticky cell paints it over the panel, so a sticky cell takes the fill
|
|
1060
|
+
* of its row and hides the cells that scroll under it.
|
|
1061
|
+
*/
|
|
1062
|
+
const ROW_FILL = "--table-row-fill";
|
|
1063
|
+
/**
|
|
1064
|
+
* Custom property of the ink a row's cells rule their ends in: `border`, and `HighlightText` in a
|
|
1065
|
+
* selected row under forced colours.
|
|
1066
|
+
*/
|
|
1067
|
+
const RULE_INK = "--table-rule";
|
|
1068
|
+
/**
|
|
1069
|
+
* Custom property of the width of the rule at a row's cells' block end: `hairline`, and a heavier
|
|
1070
|
+
* width a composing part sets on a row that ends a group of rows.
|
|
1071
|
+
*/
|
|
1072
|
+
const RULE_WIDTH = "--table-rule-width";
|
|
1073
|
+
/**
|
|
1074
|
+
* Custom property of a cell's inline inset at the table's size, which a part laid over a cell reads
|
|
1075
|
+
* to start its text where the cell's text starts.
|
|
1076
|
+
*/
|
|
1077
|
+
const CELL_INSET = "--table-cell-inset";
|
|
1078
|
+
/**
|
|
1079
|
+
* Ink of a cell's rules: its row's rule ink, else `border` in a row outside the table's parts.
|
|
1080
|
+
*/
|
|
1081
|
+
const INK = `var(${RULE_INK}, {colors.border})`;
|
|
1082
|
+
/**
|
|
1083
|
+
* Styles the rule at a cell's block end, as wide as its row states.
|
|
1084
|
+
*/
|
|
1085
|
+
const RULE = {
|
|
1086
|
+
borderBlockEndWidth: `var(${RULE_WIDTH}, {borderWidths.hairline})`,
|
|
1087
|
+
borderColor: INK
|
|
1088
|
+
};
|
|
1089
|
+
/**
|
|
1090
|
+
* Styles the rule at the inline end of every cell but the last in its row.
|
|
1091
|
+
*
|
|
1092
|
+
* @remarks
|
|
1093
|
+
* The selector counts children, not types, so a row that starts with a `th` still rules its
|
|
1094
|
+
* first column.
|
|
1095
|
+
*/
|
|
1096
|
+
const BESIDE = { "&:not(:last-child)": {
|
|
1097
|
+
borderColor: INK,
|
|
1098
|
+
borderInlineEndWidth: "hairline"
|
|
1099
|
+
} };
|
|
1100
|
+
/**
|
|
1101
|
+
* Paints a sticky cell: the panel under its row's fill.
|
|
1102
|
+
*/
|
|
1103
|
+
const STUCK = {
|
|
1104
|
+
backgroundColor: "bg.panel",
|
|
1105
|
+
backgroundImage: `linear-gradient(var(${ROW_FILL}), var(${ROW_FILL}))`
|
|
1106
|
+
};
|
|
1107
|
+
/**
|
|
1108
|
+
* Restates a selected row's forced fill under a state that fills the row.
|
|
1109
|
+
*
|
|
1110
|
+
* @remarks
|
|
1111
|
+
* The states are variants, and the compiler layers every variant over the base, where the
|
|
1112
|
+
* selected fill is stated, so a hovered selected row would take the hover's fill.
|
|
1113
|
+
*/
|
|
1114
|
+
const HIGHLIGHTED = { _selected: { _highContrast: { [ROW_FILL]: "Highlight" } } };
|
|
1115
|
+
/**
|
|
1116
|
+
* Styles the rule under the header's last row, at the indicator width.
|
|
1117
|
+
*
|
|
1118
|
+
* @remarks
|
|
1119
|
+
* Every rule uses one color, and the width sets the hierarchy.
|
|
1120
|
+
*/
|
|
1121
|
+
const UNDER = { "& > tr:last-of-type > th": {
|
|
1122
|
+
borderBlockEndWidth: "indicator",
|
|
1123
|
+
borderColor: "border"
|
|
1124
|
+
} };
|
|
1125
|
+
/**
|
|
1126
|
+
* Styles the rule over the footer's first row, at the indicator width.
|
|
1127
|
+
*/
|
|
1128
|
+
const OVER = { "& > tr:first-of-type > *": {
|
|
1129
|
+
borderBlockStartWidth: "indicator",
|
|
1130
|
+
borderColor: "border"
|
|
1131
|
+
} };
|
|
1132
|
+
/**
|
|
1133
|
+
* Removes the rule under a body's last row when a footer follows, because the footer rules its
|
|
1134
|
+
* own start.
|
|
1135
|
+
*/
|
|
1136
|
+
const LAST = { "&:has(+ tfoot) > tr:last-of-type > *": { borderBlockEndWidth: "0" } };
|
|
1137
|
+
/**
|
|
1138
|
+
* Returns a cell's padding: the gap scale on the block axis and the inset scale on the inline
|
|
1139
|
+
* axis, which the cell states in `--table-cell-inset`.
|
|
1140
|
+
*
|
|
1141
|
+
* @param size - The table's size.
|
|
1142
|
+
* @returns The inline inset and the block and inline padding.
|
|
1143
|
+
*/
|
|
1144
|
+
function inset(size) {
|
|
1145
|
+
return {
|
|
1146
|
+
[CELL_INSET]: dense(`{spacing.inset.${size}}`),
|
|
1147
|
+
paddingBlock: dense(`{spacing.gap.${size}}`),
|
|
1148
|
+
paddingInline: `var(${CELL_INSET})`
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Maps each `align` value to the cell's `vertical-align`.
|
|
1153
|
+
*/
|
|
1154
|
+
const VERTICAL = {
|
|
1155
|
+
start: { verticalAlign: "top" },
|
|
1156
|
+
center: { verticalAlign: "middle" },
|
|
1157
|
+
end: { verticalAlign: "bottom" }
|
|
1158
|
+
};
|
|
1159
|
+
/**
|
|
1160
|
+
* Defines the table recipe: a plain table ruled between its rows at size `md` by default.
|
|
1161
|
+
*/
|
|
1162
|
+
const recipe$4 = defineSlotRecipe({
|
|
1163
|
+
base: {
|
|
1164
|
+
caption: {
|
|
1165
|
+
captionSide: "bottom",
|
|
1166
|
+
color: "fg.subtle",
|
|
1167
|
+
textAlign: "start"
|
|
1168
|
+
},
|
|
1169
|
+
cell: {
|
|
1170
|
+
[`&[${NUMERIC}]`]: {
|
|
1171
|
+
fontVariantNumeric: "tabular-nums",
|
|
1172
|
+
textAlign: "end"
|
|
1173
|
+
},
|
|
1174
|
+
textAlign: "start"
|
|
1175
|
+
},
|
|
1176
|
+
column: { borderColor: "border" },
|
|
1177
|
+
columnGroup: { borderColor: "border" },
|
|
1178
|
+
columnHeader: {
|
|
1179
|
+
"&[aria-sort]:not([aria-sort=none])": { color: "fg" },
|
|
1180
|
+
"&[colspan]": { textAlign: "center" },
|
|
1181
|
+
[`&[${NUMERIC}]`]: { textAlign: "end" },
|
|
1182
|
+
color: "fg.muted",
|
|
1183
|
+
fontWeight: "medium",
|
|
1184
|
+
textAlign: "start",
|
|
1185
|
+
whiteSpace: "nowrap"
|
|
1186
|
+
},
|
|
1187
|
+
footer: { fontWeight: "medium" },
|
|
1188
|
+
root: {
|
|
1189
|
+
borderCollapse: "separate",
|
|
1190
|
+
borderSpacing: "0",
|
|
1191
|
+
inlineSize: "full",
|
|
1192
|
+
textAlign: "start"
|
|
1193
|
+
},
|
|
1194
|
+
row: {
|
|
1195
|
+
_selected: {
|
|
1196
|
+
_highContrast: {
|
|
1197
|
+
"& > *": { color: "HighlightText" },
|
|
1198
|
+
color: "HighlightText",
|
|
1199
|
+
forcedColorAdjust: "none",
|
|
1200
|
+
[ROW_FILL]: "Highlight",
|
|
1201
|
+
[RULE_INK]: "HighlightText"
|
|
1202
|
+
},
|
|
1203
|
+
[ROW_FILL]: "colors.colorPalette.subtle"
|
|
1204
|
+
},
|
|
1205
|
+
background: `var(${ROW_FILL})`,
|
|
1206
|
+
[ROW_FILL]: "transparent",
|
|
1207
|
+
[RULE_INK]: "colors.border",
|
|
1208
|
+
[RULE_WIDTH]: "borderWidths.hairline"
|
|
1209
|
+
},
|
|
1210
|
+
rowHeader: {
|
|
1211
|
+
"&[colspan]": { color: "fg.subtle" },
|
|
1212
|
+
color: "fg",
|
|
1213
|
+
fontWeight: "medium",
|
|
1214
|
+
textAlign: "start"
|
|
1215
|
+
},
|
|
1216
|
+
/**
|
|
1217
|
+
* The box around the scroll area, which draws the focus ring while the viewport has keyboard
|
|
1218
|
+
* focus and hides the scroll area's own ring, because the box clips it.
|
|
1219
|
+
*/
|
|
1220
|
+
scroller: {
|
|
1221
|
+
"&:has(.table__viewport:focus-visible)": {
|
|
1222
|
+
outlineColor: "colorPalette.focusRing",
|
|
1223
|
+
outlineOffset: "ring",
|
|
1224
|
+
outlineStyle: "solid",
|
|
1225
|
+
outlineWidth: "ring"
|
|
1226
|
+
},
|
|
1227
|
+
display: "flex",
|
|
1228
|
+
flexDirection: "column",
|
|
1229
|
+
inlineSize: "full",
|
|
1230
|
+
[RING_STYLE]: "none"
|
|
1231
|
+
},
|
|
1232
|
+
sorter: {
|
|
1233
|
+
...interactive(),
|
|
1234
|
+
alignItems: "center",
|
|
1235
|
+
appearance: "none",
|
|
1236
|
+
background: "transparent",
|
|
1237
|
+
borderStyle: "none",
|
|
1238
|
+
color: "inherit",
|
|
1239
|
+
display: "inline-flex",
|
|
1240
|
+
font: "inherit",
|
|
1241
|
+
gap: dense("{spacing.gap.xs}"),
|
|
1242
|
+
padding: "0",
|
|
1243
|
+
textAlign: "inherit"
|
|
1244
|
+
}
|
|
1245
|
+
},
|
|
1246
|
+
className: "table",
|
|
1247
|
+
compoundVariants: [{
|
|
1248
|
+
css: { body: { "& > tr": { _hover: {
|
|
1249
|
+
...HIGHLIGHTED,
|
|
1250
|
+
[ROW_FILL]: "colors.colorPalette.muted"
|
|
1251
|
+
} } } },
|
|
1252
|
+
interactive: true,
|
|
1253
|
+
name: "tracked",
|
|
1254
|
+
striped: true
|
|
1255
|
+
}, {
|
|
1256
|
+
css: { columnHeader: { "&:first-child": { zIndex: "2" } } },
|
|
1257
|
+
name: "cornered",
|
|
1258
|
+
stickyColumn: true,
|
|
1259
|
+
stickyHeader: true
|
|
1260
|
+
}],
|
|
1261
|
+
defaultVariants: {
|
|
1262
|
+
align: "center",
|
|
1263
|
+
layout: "auto",
|
|
1264
|
+
radius: "l2",
|
|
1265
|
+
rules: "rows",
|
|
1266
|
+
size: "md",
|
|
1267
|
+
variant: "plain"
|
|
1268
|
+
},
|
|
1269
|
+
jsx: [/^Table(\.\w+)?$/u],
|
|
1270
|
+
slots: [
|
|
1271
|
+
"scroller",
|
|
1272
|
+
"viewport",
|
|
1273
|
+
"root",
|
|
1274
|
+
"columnGroup",
|
|
1275
|
+
"column",
|
|
1276
|
+
"caption",
|
|
1277
|
+
"header",
|
|
1278
|
+
"body",
|
|
1279
|
+
"footer",
|
|
1280
|
+
"row",
|
|
1281
|
+
"columnHeader",
|
|
1282
|
+
"sorter",
|
|
1283
|
+
"rowHeader",
|
|
1284
|
+
"cell"
|
|
1285
|
+
],
|
|
1286
|
+
staticCss: [{ palette: [...PALETTES] }],
|
|
1287
|
+
variants: {
|
|
1288
|
+
/**
|
|
1289
|
+
* Vertical alignment of a cell's content in a row taller than one line.
|
|
1290
|
+
*/
|
|
1291
|
+
align: onSlots({
|
|
1292
|
+
cell: VERTICAL,
|
|
1293
|
+
columnHeader: VERTICAL,
|
|
1294
|
+
rowHeader: VERTICAL
|
|
1295
|
+
}),
|
|
1296
|
+
/**
|
|
1297
|
+
* Whether a body row fills under the pointer and while a control inside it has focus.
|
|
1298
|
+
*
|
|
1299
|
+
* @remarks
|
|
1300
|
+
* A keyboard reaches the link inside a cell, so the row reads `:focus-within`. A `tr` takes
|
|
1301
|
+
* no focus of its own.
|
|
1302
|
+
*/
|
|
1303
|
+
interactive: { true: { body: { "& > tr": {
|
|
1304
|
+
_focusWithin: {
|
|
1305
|
+
...HIGHLIGHTED,
|
|
1306
|
+
[ROW_FILL]: "colors.colorPalette.subtle"
|
|
1307
|
+
},
|
|
1308
|
+
_hover: {
|
|
1309
|
+
...HIGHLIGHTED,
|
|
1310
|
+
[ROW_FILL]: "colors.colorPalette.subtle"
|
|
1311
|
+
}
|
|
1312
|
+
} } } },
|
|
1313
|
+
/**
|
|
1314
|
+
* Column widths from the content, or from the column declarations and the first row.
|
|
1315
|
+
*/
|
|
1316
|
+
layout: {
|
|
1317
|
+
auto: { root: { tableLayout: "auto" } },
|
|
1318
|
+
fixed: { root: { tableLayout: "fixed" } }
|
|
1319
|
+
},
|
|
1320
|
+
/**
|
|
1321
|
+
* Palette the selected row and the interactive row fill read.
|
|
1322
|
+
*
|
|
1323
|
+
* @remarks
|
|
1324
|
+
* The palette is set on the scroller, and every part inherits the palette's custom
|
|
1325
|
+
* properties.
|
|
1326
|
+
*/
|
|
1327
|
+
palette: onSlot("scroller", paletteVariants()),
|
|
1328
|
+
/**
|
|
1329
|
+
* Corner radius of the scroller, from the layer radii without the pill, which clips the first
|
|
1330
|
+
* and last rows.
|
|
1331
|
+
*/
|
|
1332
|
+
radius: onSlot("scroller", cornerVariants([
|
|
1333
|
+
"l1",
|
|
1334
|
+
"l2",
|
|
1335
|
+
"l3"
|
|
1336
|
+
])),
|
|
1337
|
+
/**
|
|
1338
|
+
* Where the body rules run: `rows` between rows, `all` between rows and between columns, and
|
|
1339
|
+
* `none` nowhere. The rule under the header and the rule over the footer render in every value.
|
|
1340
|
+
*/
|
|
1341
|
+
rules: {
|
|
1342
|
+
all: {
|
|
1343
|
+
body: LAST,
|
|
1344
|
+
cell: {
|
|
1345
|
+
...BESIDE,
|
|
1346
|
+
...RULE
|
|
1347
|
+
},
|
|
1348
|
+
columnHeader: {
|
|
1349
|
+
...BESIDE,
|
|
1350
|
+
...RULE
|
|
1351
|
+
},
|
|
1352
|
+
footer: OVER,
|
|
1353
|
+
header: UNDER,
|
|
1354
|
+
rowHeader: {
|
|
1355
|
+
...BESIDE,
|
|
1356
|
+
...RULE
|
|
1357
|
+
}
|
|
1358
|
+
},
|
|
1359
|
+
none: {
|
|
1360
|
+
footer: OVER,
|
|
1361
|
+
header: UNDER
|
|
1362
|
+
},
|
|
1363
|
+
rows: {
|
|
1364
|
+
body: LAST,
|
|
1365
|
+
cell: { ...RULE },
|
|
1366
|
+
columnHeader: { ...RULE },
|
|
1367
|
+
footer: OVER,
|
|
1368
|
+
header: UNDER,
|
|
1369
|
+
rowHeader: { ...RULE }
|
|
1370
|
+
}
|
|
1371
|
+
},
|
|
1372
|
+
/**
|
|
1373
|
+
* Text size and cell padding: cells read the body role at the size, and the column names and
|
|
1374
|
+
* the caption read the label role one size smaller.
|
|
1375
|
+
*/
|
|
1376
|
+
size: onSlots({
|
|
1377
|
+
caption: sizeVariants((size) => ({
|
|
1378
|
+
textStyle: `label.${below(size)}`,
|
|
1379
|
+
paddingBlock: dense(`{spacing.gap.${size}}`),
|
|
1380
|
+
paddingInline: dense(`{spacing.inset.${size}}`)
|
|
1381
|
+
}), SIZES$3),
|
|
1382
|
+
cell: sizeVariants((size) => inset(size), SIZES$3),
|
|
1383
|
+
columnHeader: sizeVariants((size) => ({
|
|
1384
|
+
...inset(size),
|
|
1385
|
+
textStyle: `label.${below(size)}`
|
|
1386
|
+
}), SIZES$3),
|
|
1387
|
+
root: sizeVariants((size) => ({ textStyle: `body.${size}` }), SIZES$3),
|
|
1388
|
+
rowHeader: sizeVariants((size) => ({
|
|
1389
|
+
...inset(size),
|
|
1390
|
+
"&[colspan]": { textStyle: `label.${below(size)}` }
|
|
1391
|
+
}), SIZES$3)
|
|
1392
|
+
}),
|
|
1393
|
+
/**
|
|
1394
|
+
* Whether the header rows stick to the top of the scroll area's viewport.
|
|
1395
|
+
*
|
|
1396
|
+
* @remarks
|
|
1397
|
+
* The header cells take the panel fill, because the rows stick and the `thead` does not. The
|
|
1398
|
+
* header rows are raised over a sticky column, and a sticky column's first header cell over
|
|
1399
|
+
* both.
|
|
1400
|
+
*/
|
|
1401
|
+
stickyHeader: { true: {
|
|
1402
|
+
columnHeader: { background: "bg.panel" },
|
|
1403
|
+
header: { "& > tr": {
|
|
1404
|
+
insetBlockStart: "0",
|
|
1405
|
+
position: "sticky",
|
|
1406
|
+
zIndex: "2"
|
|
1407
|
+
} }
|
|
1408
|
+
} },
|
|
1409
|
+
/**
|
|
1410
|
+
* Whether the row headers and the first column header stick to the start of the scroll area's
|
|
1411
|
+
* viewport.
|
|
1412
|
+
*/
|
|
1413
|
+
stickyColumn: { true: {
|
|
1414
|
+
columnHeader: { "&:first-child": {
|
|
1415
|
+
background: "bg.panel",
|
|
1416
|
+
insetInlineStart: "0",
|
|
1417
|
+
position: "sticky",
|
|
1418
|
+
zIndex: "1"
|
|
1419
|
+
} },
|
|
1420
|
+
rowHeader: {
|
|
1421
|
+
...STUCK,
|
|
1422
|
+
insetInlineStart: "0",
|
|
1423
|
+
position: "sticky",
|
|
1424
|
+
zIndex: "1"
|
|
1425
|
+
}
|
|
1426
|
+
} },
|
|
1427
|
+
/**
|
|
1428
|
+
* Whether the column names render on `bg.subtle`, in capitals, tracked out.
|
|
1429
|
+
*
|
|
1430
|
+
* @remarks
|
|
1431
|
+
* The fill is the stripe's well, so a banded and striped table uses one tone for both.
|
|
1432
|
+
*/
|
|
1433
|
+
banded: { true: { columnHeader: {
|
|
1434
|
+
background: "bg.subtle",
|
|
1435
|
+
letterSpacing: "wide",
|
|
1436
|
+
textTransform: "uppercase"
|
|
1437
|
+
} } },
|
|
1438
|
+
/**
|
|
1439
|
+
* Whether every odd body row renders on `bg.muted`.
|
|
1440
|
+
*
|
|
1441
|
+
* @remarks
|
|
1442
|
+
* The stripe skips a selected row, so a selected row keeps its selected fill.
|
|
1443
|
+
*/
|
|
1444
|
+
striped: { true: { body: { "& > tr:not([aria-selected=true], [data-selected])": { _odd: { [ROW_FILL]: "colors.bg.muted" } } } } },
|
|
1445
|
+
/**
|
|
1446
|
+
* Surface of the scroller: the panel inside a hairline edge, or transparent.
|
|
1447
|
+
*
|
|
1448
|
+
* @remarks
|
|
1449
|
+
* `surface` does not cast a shadow. After dark a height adds an inset rim under the
|
|
1450
|
+
* scroller's content, and a stripe, a filled row or a sticky cell at the edge covers the
|
|
1451
|
+
* rim.
|
|
1452
|
+
*/
|
|
1453
|
+
variant: {
|
|
1454
|
+
surface: { scroller: {
|
|
1455
|
+
...surface(),
|
|
1456
|
+
boxShadow: "none",
|
|
1457
|
+
overflow: "hidden"
|
|
1458
|
+
} },
|
|
1459
|
+
plain: { scroller: { background: "transparent" } }
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
1463
|
+
//#endregion
|
|
1464
|
+
//#region src/swipe-actions/recipe.ts
|
|
1465
|
+
/**
|
|
1466
|
+
* Declares the swipe actions' slot recipe: a row whose content slides aside to reveal actions at
|
|
1467
|
+
* its inline end.
|
|
1468
|
+
*
|
|
1469
|
+
* @remarks
|
|
1470
|
+
* The root clips its row. The content is a padded flex row that moves towards the inline start by
|
|
1471
|
+
* the width the root states in `--swipe-reveal`, and the actions are clipped to that width at the
|
|
1472
|
+
* inline end, so the actions take no room and show nothing at rest, whatever the row's
|
|
1473
|
+
* background. Both move over the `move` duration and easing, with no transition while a finger
|
|
1474
|
+
* drags the row or under reduced motion. The content takes `touch-action: pan-y`, so a vertical
|
|
1475
|
+
* swipe scrolls the page and a horizontal one moves the row. An action is square, at least as
|
|
1476
|
+
* tall as the row and `sizes.16` wide, with its glyph above its words. Its focus ring is inside
|
|
1477
|
+
* it in the palette's contrast ink, because the row clips a ring drawn outside the action.
|
|
1478
|
+
*/
|
|
1479
|
+
/**
|
|
1480
|
+
* Class name of the recipe, which a selector across parts reads.
|
|
1481
|
+
*/
|
|
1482
|
+
const CLASS$1 = "swipe-actions";
|
|
1483
|
+
/**
|
|
1484
|
+
* Custom property the root sets to the revealed width of the actions, in pixels.
|
|
1485
|
+
*/
|
|
1486
|
+
const REVEAL = "--swipe-reveal";
|
|
1487
|
+
/**
|
|
1488
|
+
* Reads the revealed width.
|
|
1489
|
+
*/
|
|
1490
|
+
const REVEALED = `var(${REVEAL})`;
|
|
1491
|
+
/**
|
|
1492
|
+
* Turns a part's transition off while a finger or a trackpad drags the row.
|
|
1493
|
+
*/
|
|
1494
|
+
const STILL = { "[data-dragging] > &": { transitionDuration: "0s" } };
|
|
1495
|
+
/**
|
|
1496
|
+
* Styles the swipe actions' row, content and actions.
|
|
1497
|
+
*/
|
|
1498
|
+
const recipe$3 = defineSlotRecipe({
|
|
1499
|
+
base: {
|
|
1500
|
+
action: {
|
|
1501
|
+
flexDirection: "column",
|
|
1502
|
+
flexShrink: "0",
|
|
1503
|
+
minBlockSize: "full",
|
|
1504
|
+
minInlineSize: "{sizes.16}"
|
|
1505
|
+
},
|
|
1506
|
+
actions: {
|
|
1507
|
+
...STILL,
|
|
1508
|
+
_motionSafe: {
|
|
1509
|
+
transitionDuration: "move",
|
|
1510
|
+
transitionProperty: "clip-path",
|
|
1511
|
+
transitionTimingFunction: "move"
|
|
1512
|
+
},
|
|
1513
|
+
_rtl: { clipPath: `inset(0 calc(100% - ${REVEALED}) 0 0)` },
|
|
1514
|
+
[`& > .${CLASS$1}__action`]: {
|
|
1515
|
+
focusVisibleRing: "inside",
|
|
1516
|
+
_focusVisible: { outlineOffset: "calc({borderWidths.ring} * -2)" },
|
|
1517
|
+
borderRadius: "none",
|
|
1518
|
+
focusRingColor: "colorPalette.contrast"
|
|
1519
|
+
},
|
|
1520
|
+
alignItems: "stretch",
|
|
1521
|
+
clipPath: `inset(0 0 0 calc(100% - ${REVEALED}))`,
|
|
1522
|
+
display: "flex",
|
|
1523
|
+
insetBlock: "0",
|
|
1524
|
+
insetInlineEnd: "0",
|
|
1525
|
+
position: "absolute"
|
|
1526
|
+
},
|
|
1527
|
+
content: {
|
|
1528
|
+
...STILL,
|
|
1529
|
+
_motionSafe: {
|
|
1530
|
+
transitionDuration: "move",
|
|
1531
|
+
transitionProperty: "translate",
|
|
1532
|
+
transitionTimingFunction: "move"
|
|
1533
|
+
},
|
|
1534
|
+
_rtl: { translate: `${REVEALED} 0` },
|
|
1535
|
+
alignItems: "center",
|
|
1536
|
+
display: "flex",
|
|
1537
|
+
gap: dense("{spacing.gap.md}"),
|
|
1538
|
+
minBlockSize: dense("{sizes.control.lg}"),
|
|
1539
|
+
paddingBlock: dense("{spacing.inset.sm}"),
|
|
1540
|
+
paddingInline: dense("{spacing.inset.md}"),
|
|
1541
|
+
position: "relative",
|
|
1542
|
+
touchAction: "pan-y",
|
|
1543
|
+
translate: `calc(${REVEALED} * -1) 0`
|
|
1544
|
+
},
|
|
1545
|
+
root: {
|
|
1546
|
+
focusVisibleRing: "inside",
|
|
1547
|
+
_focusVisible: { outlineOffset: "calc({borderWidths.ring} * -1)" },
|
|
1548
|
+
overflow: "clip",
|
|
1549
|
+
position: "relative"
|
|
1550
|
+
}
|
|
1551
|
+
},
|
|
1552
|
+
className: CLASS$1,
|
|
1553
|
+
jsx: [/^SwipeActions\.\w+$/u],
|
|
1554
|
+
slots: [
|
|
1555
|
+
"root",
|
|
1556
|
+
"content",
|
|
1557
|
+
"actions",
|
|
1558
|
+
"action"
|
|
1559
|
+
]
|
|
1560
|
+
});
|
|
1561
|
+
//#endregion
|
|
1562
|
+
//#region src/timeline/recipe.ts
|
|
1563
|
+
/**
|
|
1564
|
+
* Declares the timeline's slot recipe: entries in the order they happened, each an indicator on a
|
|
1565
|
+
* rail and the words beside it.
|
|
1566
|
+
*
|
|
1567
|
+
* @remarks
|
|
1568
|
+
* The root is an ordered list laid out as a grid of three columns: the words before the rail,
|
|
1569
|
+
* the rail, and the words after it. Each item is a subgrid row, so dates written before the rail
|
|
1570
|
+
* line up down the list at the widest date's width. `rail` sets where the rail runs: at the start
|
|
1571
|
+
* with the words before it sized to their content, in the centre with both sides taking an equal
|
|
1572
|
+
* share, or at the end. The connector draws the rail down its item as a hairline, and the last
|
|
1573
|
+
* item's rail stops at its indicator unless the timeline is `ongoing`. The indicator is a circle
|
|
1574
|
+
* on the icon scale, 16, 20, 24 and 32px from `sm` to `xl`, opaque and ringed in `bg.panel`, so
|
|
1575
|
+
* the rail never runs through it. A one-line title is as tall as the indicator and centred on it.
|
|
1576
|
+
* The indicators are marks in a list rather than a box a reader acts on, so the timeline offers
|
|
1577
|
+
* no `effect` axis.
|
|
1578
|
+
*/
|
|
1579
|
+
/**
|
|
1580
|
+
* Class name of the recipe, which a selector across parts reads.
|
|
1581
|
+
*/
|
|
1582
|
+
const CLASS = "timeline";
|
|
1583
|
+
/**
|
|
1584
|
+
* Custom property the root sets to the indicator's side, which the rail's column reads.
|
|
1585
|
+
*/
|
|
1586
|
+
const INDICATOR = "--timeline-indicator";
|
|
1587
|
+
/**
|
|
1588
|
+
* Custom property the root sets to the room between the rail and the words.
|
|
1589
|
+
*/
|
|
1590
|
+
const GAP$1 = "--timeline-gap";
|
|
1591
|
+
/**
|
|
1592
|
+
* Custom property the root sets to the room below each entry's words.
|
|
1593
|
+
*/
|
|
1594
|
+
const SPACE = "--timeline-space";
|
|
1595
|
+
/**
|
|
1596
|
+
* Sizes the timeline offers.
|
|
1597
|
+
*/
|
|
1598
|
+
const SIZES$2 = [
|
|
1599
|
+
"sm",
|
|
1600
|
+
"md",
|
|
1601
|
+
"lg",
|
|
1602
|
+
"xl"
|
|
1603
|
+
];
|
|
1604
|
+
/**
|
|
1605
|
+
* Maps each size to its gaps on the gap scale and to the text styles of its parts.
|
|
1606
|
+
*
|
|
1607
|
+
* @remarks
|
|
1608
|
+
* The words are 8, 12, 12 and 16px from the rail and the entries 12, 16, 24 and 24px apart at the
|
|
1609
|
+
* foundation's metrics.
|
|
1610
|
+
*/
|
|
1611
|
+
const METRICS = {
|
|
1612
|
+
lg: {
|
|
1613
|
+
description: "body.sm",
|
|
1614
|
+
gap: "lg",
|
|
1615
|
+
mark: "label.sm",
|
|
1616
|
+
space: "2xl",
|
|
1617
|
+
title: "label.md"
|
|
1618
|
+
},
|
|
1619
|
+
md: {
|
|
1620
|
+
description: "body.xs",
|
|
1621
|
+
gap: "lg",
|
|
1622
|
+
mark: "label.xs",
|
|
1623
|
+
space: "xl",
|
|
1624
|
+
title: "label.sm"
|
|
1625
|
+
},
|
|
1626
|
+
sm: {
|
|
1627
|
+
description: "body.xs",
|
|
1628
|
+
gap: "md",
|
|
1629
|
+
mark: "label.xs",
|
|
1630
|
+
space: "lg",
|
|
1631
|
+
title: "label.xs"
|
|
1632
|
+
},
|
|
1633
|
+
xl: {
|
|
1634
|
+
description: "body.md",
|
|
1635
|
+
gap: "xl",
|
|
1636
|
+
mark: "label.md",
|
|
1637
|
+
space: "2xl",
|
|
1638
|
+
title: "label.lg"
|
|
1639
|
+
}
|
|
1640
|
+
};
|
|
1641
|
+
/**
|
|
1642
|
+
* Styles a neutral timeline with solid indicators at the middle size, the rail at the start.
|
|
1643
|
+
*/
|
|
1644
|
+
const recipe$2 = defineSlotRecipe({
|
|
1645
|
+
base: {
|
|
1646
|
+
connector: {
|
|
1647
|
+
"&::before": {
|
|
1648
|
+
borderColor: "border",
|
|
1649
|
+
borderInlineStartWidth: "hairline",
|
|
1650
|
+
content: "\"\"",
|
|
1651
|
+
insetBlock: "0",
|
|
1652
|
+
insetInlineStart: "50%",
|
|
1653
|
+
marginInlineStart: "calc({borderWidths.hairline} / -2)",
|
|
1654
|
+
position: "absolute"
|
|
1655
|
+
},
|
|
1656
|
+
alignItems: "flex-start",
|
|
1657
|
+
display: "flex",
|
|
1658
|
+
gridColumn: "2",
|
|
1659
|
+
justifyContent: "center",
|
|
1660
|
+
position: "relative"
|
|
1661
|
+
},
|
|
1662
|
+
content: {
|
|
1663
|
+
[`&:has(+ .${CLASS}__connector)`]: {
|
|
1664
|
+
[`& > .${CLASS}__title`]: { justifyContent: "flex-end" },
|
|
1665
|
+
alignItems: "flex-end",
|
|
1666
|
+
paddingInlineEnd: `var(${GAP$1})`,
|
|
1667
|
+
paddingInlineStart: "0",
|
|
1668
|
+
textAlign: "end"
|
|
1669
|
+
},
|
|
1670
|
+
display: "flex",
|
|
1671
|
+
flexDirection: "column",
|
|
1672
|
+
gap: dense("{spacing.gap.xs}"),
|
|
1673
|
+
minInlineSize: "0",
|
|
1674
|
+
paddingBlockEnd: `var(${SPACE})`,
|
|
1675
|
+
paddingInlineStart: `var(${GAP$1})`
|
|
1676
|
+
},
|
|
1677
|
+
description: {
|
|
1678
|
+
color: "fg.muted",
|
|
1679
|
+
minInlineSize: "0"
|
|
1680
|
+
},
|
|
1681
|
+
indicator: {
|
|
1682
|
+
"& > svg": {
|
|
1683
|
+
blockSize: "60%",
|
|
1684
|
+
inlineSize: "60%"
|
|
1685
|
+
},
|
|
1686
|
+
alignItems: "center",
|
|
1687
|
+
backgroundColor: "bg.panel",
|
|
1688
|
+
blockSize: `var(${INDICATOR})`,
|
|
1689
|
+
borderRadius: "full",
|
|
1690
|
+
display: "inline-flex",
|
|
1691
|
+
flexShrink: "0",
|
|
1692
|
+
fontVariantNumeric: "tabular-nums",
|
|
1693
|
+
inlineSize: `var(${INDICATOR})`,
|
|
1694
|
+
justifyContent: "center",
|
|
1695
|
+
lineHeight: "1",
|
|
1696
|
+
outlineColor: "bg.panel",
|
|
1697
|
+
outlineStyle: "solid",
|
|
1698
|
+
outlineWidth: "control",
|
|
1699
|
+
position: "relative"
|
|
1700
|
+
},
|
|
1701
|
+
item: {
|
|
1702
|
+
[`&:last-child > .${CLASS}__connector::before`]: { display: "none" },
|
|
1703
|
+
[`&:last-child > .${CLASS}__content`]: { paddingBlockEnd: "0" },
|
|
1704
|
+
display: "grid",
|
|
1705
|
+
gridColumn: "1 / -1",
|
|
1706
|
+
gridTemplateColumns: "subgrid"
|
|
1707
|
+
},
|
|
1708
|
+
root: {
|
|
1709
|
+
display: "grid",
|
|
1710
|
+
listStyle: "none",
|
|
1711
|
+
margin: "0",
|
|
1712
|
+
minInlineSize: "0",
|
|
1713
|
+
padding: "0"
|
|
1714
|
+
},
|
|
1715
|
+
title: {
|
|
1716
|
+
alignItems: "center",
|
|
1717
|
+
display: "flex",
|
|
1718
|
+
flexWrap: "wrap",
|
|
1719
|
+
gap: "0.375em",
|
|
1720
|
+
minBlockSize: `var(${INDICATOR})`
|
|
1721
|
+
}
|
|
1722
|
+
},
|
|
1723
|
+
className: CLASS,
|
|
1724
|
+
defaultVariants: {
|
|
1725
|
+
palette: "neutral",
|
|
1726
|
+
rail: "start",
|
|
1727
|
+
size: "md",
|
|
1728
|
+
variant: "solid"
|
|
1729
|
+
},
|
|
1730
|
+
jsx: [/^Timeline\.\w+$/u],
|
|
1731
|
+
slots: [
|
|
1732
|
+
"root",
|
|
1733
|
+
"item",
|
|
1734
|
+
"connector",
|
|
1735
|
+
"indicator",
|
|
1736
|
+
"content",
|
|
1737
|
+
"title",
|
|
1738
|
+
"description"
|
|
1739
|
+
],
|
|
1740
|
+
staticCss: [{ palette: [...PALETTES] }],
|
|
1741
|
+
variants: {
|
|
1742
|
+
/**
|
|
1743
|
+
* Whether the run is still going, which draws the rail past the last indicator.
|
|
1744
|
+
*/
|
|
1745
|
+
ongoing: { true: { item: {
|
|
1746
|
+
[`&:last-child > .${CLASS}__connector::before`]: { display: "block" },
|
|
1747
|
+
[`&:last-child > .${CLASS}__content`]: { paddingBlockEnd: `var(${SPACE})` }
|
|
1748
|
+
} } },
|
|
1749
|
+
palette: onSlot("root", paletteVariants()),
|
|
1750
|
+
/**
|
|
1751
|
+
* Where the rail runs, and how the words on either side of it share the width.
|
|
1752
|
+
*/
|
|
1753
|
+
rail: {
|
|
1754
|
+
center: { root: { gridTemplateColumns: `minmax(0, 1fr) var(${INDICATOR}) minmax(0, 1fr)` } },
|
|
1755
|
+
end: { root: { gridTemplateColumns: `minmax(0, 1fr) var(${INDICATOR}) auto` } },
|
|
1756
|
+
start: { root: { gridTemplateColumns: `auto var(${INDICATOR}) minmax(0, 1fr)` } }
|
|
1757
|
+
},
|
|
1758
|
+
/**
|
|
1759
|
+
* The indicator's side and the text styles of the words and the marks.
|
|
1760
|
+
*/
|
|
1761
|
+
size: onSlots({
|
|
1762
|
+
description: sizeVariants((size) => ({ textStyle: METRICS[size].description }), SIZES$2),
|
|
1763
|
+
indicator: sizeVariants((size) => ({ textStyle: METRICS[size].mark }), SIZES$2),
|
|
1764
|
+
root: sizeVariants((size) => ({
|
|
1765
|
+
[GAP$1]: dense(`{spacing.gap.${METRICS[size].gap}}`),
|
|
1766
|
+
[INDICATOR]: dense(`{sizes.icon.${size}}`),
|
|
1767
|
+
[SPACE]: dense(`{spacing.gap.${METRICS[size].space}}`)
|
|
1768
|
+
}), SIZES$2),
|
|
1769
|
+
title: sizeVariants((size) => ({ textStyle: METRICS[size].title }), SIZES$2)
|
|
1770
|
+
}),
|
|
1771
|
+
variant: onSlot("indicator", flatVariants([
|
|
1772
|
+
"solid",
|
|
1773
|
+
"subtle",
|
|
1774
|
+
"outline",
|
|
1775
|
+
"plain"
|
|
1776
|
+
]))
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
//#endregion
|
|
1780
|
+
//#region src/transfer/recipe.ts
|
|
1781
|
+
/**
|
|
1782
|
+
* Recipe for the transfer: two lists side by side and the column of controls between them.
|
|
1783
|
+
*
|
|
1784
|
+
* @remarks
|
|
1785
|
+
* Both sides take an equal share of the width, so the pair keeps its layout as rows move. Both
|
|
1786
|
+
* take the height of the taller side, with a floor of every row's height, which the side reads
|
|
1787
|
+
* from the list's `--listbox-row` and its own `--transfer-rows`, so an empty side is as tall as a
|
|
1788
|
+
* full one. The controls centre on the pair's height. A control's square and its mark are each
|
|
1789
|
+
* one size smaller than the transfer, on the control scale and the icon scale. The palette is
|
|
1790
|
+
* set on the root, and both lists inherit it. The recipe has no `effect` axis, because a pick in
|
|
1791
|
+
* a transfer lasts until the next press of a control, and the checkbox marks it.
|
|
1792
|
+
*/
|
|
1793
|
+
/**
|
|
1794
|
+
* Sizes the transfer offers.
|
|
1795
|
+
*/
|
|
1796
|
+
const SIZES$1 = [
|
|
1797
|
+
"sm",
|
|
1798
|
+
"md",
|
|
1799
|
+
"lg"
|
|
1800
|
+
];
|
|
1801
|
+
/**
|
|
1802
|
+
* Class name of the listbox recipe, whose frame and content a side sizes.
|
|
1803
|
+
*/
|
|
1804
|
+
const LISTBOX = "listbox";
|
|
1805
|
+
/**
|
|
1806
|
+
* Custom property a side sets to the number of rows it keeps room for.
|
|
1807
|
+
*/
|
|
1808
|
+
const ROWS$1 = "--transfer-rows";
|
|
1809
|
+
/**
|
|
1810
|
+
* Defines the transfer recipe at size `md` by default.
|
|
1811
|
+
*/
|
|
1812
|
+
const recipe$1 = defineSlotRecipe({
|
|
1813
|
+
base: {
|
|
1814
|
+
control: {
|
|
1815
|
+
...interactive(),
|
|
1816
|
+
_disabled: { layerStyle: "disabled" },
|
|
1817
|
+
alignItems: "center",
|
|
1818
|
+
borderColor: "border",
|
|
1819
|
+
borderRadius: "l2",
|
|
1820
|
+
borderStyle: "solid",
|
|
1821
|
+
borderWidth: "hairline",
|
|
1822
|
+
color: "fg.muted",
|
|
1823
|
+
display: "inline-flex",
|
|
1824
|
+
justifyContent: "center"
|
|
1825
|
+
},
|
|
1826
|
+
controls: {
|
|
1827
|
+
alignItems: "center",
|
|
1828
|
+
alignSelf: "center",
|
|
1829
|
+
display: "flex",
|
|
1830
|
+
flexDirection: "column",
|
|
1831
|
+
flexShrink: "0"
|
|
1832
|
+
},
|
|
1833
|
+
root: {
|
|
1834
|
+
alignItems: "stretch",
|
|
1835
|
+
display: "flex",
|
|
1836
|
+
minInlineSize: "0"
|
|
1837
|
+
},
|
|
1838
|
+
side: {
|
|
1839
|
+
[`& .${LISTBOX}__content`]: {
|
|
1840
|
+
flex: "1",
|
|
1841
|
+
minBlockSize: `calc(var(${ROW_HEIGHT}) * var(${ROWS$1}))`
|
|
1842
|
+
},
|
|
1843
|
+
[`& .${LISTBOX}__frame`]: { flex: "1" },
|
|
1844
|
+
display: "grid",
|
|
1845
|
+
flex: "1",
|
|
1846
|
+
minInlineSize: "0"
|
|
1847
|
+
}
|
|
1848
|
+
},
|
|
1849
|
+
className: "transfer",
|
|
1850
|
+
defaultVariants: { size: "md" },
|
|
1851
|
+
jsx: [/^Transfer(\.\w+)?$/u],
|
|
1852
|
+
slots: [
|
|
1853
|
+
"root",
|
|
1854
|
+
"side",
|
|
1855
|
+
"controls",
|
|
1856
|
+
"control"
|
|
1857
|
+
],
|
|
1858
|
+
staticCss: [{ palette: [...PALETTES] }],
|
|
1859
|
+
variants: {
|
|
1860
|
+
/**
|
|
1861
|
+
* Palette the lists' checkboxes and fills read.
|
|
1862
|
+
*
|
|
1863
|
+
* @remarks
|
|
1864
|
+
* The palette is set on the root, and both lists inherit its custom properties.
|
|
1865
|
+
*/
|
|
1866
|
+
palette: onSlot("root", paletteVariants()),
|
|
1867
|
+
/**
|
|
1868
|
+
* Gap between the parts and the size of the controls and their marks.
|
|
1869
|
+
*/
|
|
1870
|
+
size: onSlots({
|
|
1871
|
+
control: sizeVariants((size) => ({
|
|
1872
|
+
"& > *": { boxSize: dense(`{sizes.icon.${below(size)}}`) },
|
|
1873
|
+
boxSize: dense(`{sizes.control.${below(size)}}`)
|
|
1874
|
+
}), SIZES$1),
|
|
1875
|
+
controls: sizeVariants((size) => ({ gap: dense(`{spacing.gap.${below(size)}}`) }), SIZES$1),
|
|
1876
|
+
root: sizeVariants((size) => ({ gap: dense(`{spacing.gap.${size}}`) }), SIZES$1)
|
|
1877
|
+
})
|
|
1878
|
+
}
|
|
1879
|
+
});
|
|
1880
|
+
//#endregion
|
|
1881
|
+
//#region src/tree-view/recipe.ts
|
|
1882
|
+
/**
|
|
1883
|
+
* Recipe for the tree view: a label, the tree, and its rows. A branch row opens the rows nested in
|
|
1884
|
+
* it, and an item row ends a path.
|
|
1885
|
+
*
|
|
1886
|
+
* @remarks
|
|
1887
|
+
* A row reads the theme's `row` fragment and shows its focus ring inside its edge, because a row
|
|
1888
|
+
* takes focus itself. The root writes the rows' inset, the gap between a row's parts and the side
|
|
1889
|
+
* of its marks as custom properties per size. Every row indents one mark and one gap per level of
|
|
1890
|
+
* the `--depth` the machine writes, so a child's indicator is under its parent's icon. An item
|
|
1891
|
+
* reserves the column of a branch's indicator, so an item's text and a branch's text at one level
|
|
1892
|
+
* start at one x. A guide is a border, so forced colors paint it in `CanvasText`. Under forced
|
|
1893
|
+
* colors a selected row fills with `Highlight`, and its marks and checkbox read the row's
|
|
1894
|
+
* `HighlightText`. `staticCss` lists every palette, and the `plain` look and the `sm` and `md`
|
|
1895
|
+
* sizes, which the content package's JSON tree view sets at run time.
|
|
1896
|
+
*/
|
|
1897
|
+
/**
|
|
1898
|
+
* Sizes the recipe offers.
|
|
1899
|
+
*/
|
|
1900
|
+
const SIZES = [
|
|
1901
|
+
"sm",
|
|
1902
|
+
"md",
|
|
1903
|
+
"lg"
|
|
1904
|
+
];
|
|
1905
|
+
/**
|
|
1906
|
+
* Inline padding at each end of a row, which the root sets per size.
|
|
1907
|
+
*/
|
|
1908
|
+
const INSET = "var(--tree-view-inset)";
|
|
1909
|
+
/**
|
|
1910
|
+
* Gap between the parts of a row, which the root sets per size.
|
|
1911
|
+
*/
|
|
1912
|
+
const GAP = "var(--tree-view-gap)";
|
|
1913
|
+
/**
|
|
1914
|
+
* Side of a row's indicator, icons and checkbox, which the root sets per size.
|
|
1915
|
+
*/
|
|
1916
|
+
const MARK = "var(--tree-view-mark)";
|
|
1917
|
+
/**
|
|
1918
|
+
* Indent of one level: one mark and one gap.
|
|
1919
|
+
*/
|
|
1920
|
+
const LEVEL = `(${MARK} + ${GAP})`;
|
|
1921
|
+
/**
|
|
1922
|
+
* Height of a row per size, on the scale of the navigation list's rows.
|
|
1923
|
+
*/
|
|
1924
|
+
const ROWS = {
|
|
1925
|
+
lg: "control.md",
|
|
1926
|
+
md: "control.xs",
|
|
1927
|
+
sm: "tag.md"
|
|
1928
|
+
};
|
|
1929
|
+
/**
|
|
1930
|
+
* Styles a focusable row: the theme's row with a focus ring inside its edge.
|
|
1931
|
+
*/
|
|
1932
|
+
const ROW = {
|
|
1933
|
+
...row(),
|
|
1934
|
+
focusVisibleRing: "inside",
|
|
1935
|
+
_focusVisible: { outlineOffset: "calc({borderWidths.ring} * -1)" },
|
|
1936
|
+
_hover: { background: "bg.muted" },
|
|
1937
|
+
"& > svg": {
|
|
1938
|
+
boxSize: MARK,
|
|
1939
|
+
flexShrink: "0"
|
|
1940
|
+
},
|
|
1941
|
+
columnGap: GAP,
|
|
1942
|
+
focusRingColor: "colorPalette.focusRing",
|
|
1943
|
+
paddingInlineEnd: INSET
|
|
1944
|
+
};
|
|
1945
|
+
/**
|
|
1946
|
+
* Styles a row's text: one truncated line that takes the room left, hidden while the row is
|
|
1947
|
+
* renamed.
|
|
1948
|
+
*/
|
|
1949
|
+
const TEXT = {
|
|
1950
|
+
...truncate(),
|
|
1951
|
+
"[data-renaming] > &": { display: "none" },
|
|
1952
|
+
flex: "1",
|
|
1953
|
+
minInlineSize: "0"
|
|
1954
|
+
};
|
|
1955
|
+
/**
|
|
1956
|
+
* Styles a mark at a row's side: a square of the mark's side around the caller's glyph.
|
|
1957
|
+
*/
|
|
1958
|
+
const SQUARE = {
|
|
1959
|
+
"& > svg": { boxSize: "full" },
|
|
1960
|
+
alignItems: "center",
|
|
1961
|
+
boxSize: MARK,
|
|
1962
|
+
display: "inline-flex",
|
|
1963
|
+
flexShrink: "0",
|
|
1964
|
+
justifyContent: "center"
|
|
1965
|
+
};
|
|
1966
|
+
/**
|
|
1967
|
+
* Fills a selected row with `Highlight` under forced colors.
|
|
1968
|
+
*/
|
|
1969
|
+
const FORCED = { _highContrast: {
|
|
1970
|
+
background: "Highlight",
|
|
1971
|
+
color: "HighlightText",
|
|
1972
|
+
forcedColorAdjust: "none"
|
|
1973
|
+
} };
|
|
1974
|
+
/**
|
|
1975
|
+
* Restates the forced colors on a selected row under a pointer.
|
|
1976
|
+
*
|
|
1977
|
+
* @remarks
|
|
1978
|
+
* A look's hover fill on a selected row is as specific as the forced colors under a pointer, and
|
|
1979
|
+
* the compiler emits the `(hover: hover)` block after the `(forced-colors: active)` block, so the
|
|
1980
|
+
* fill applies over the forced colors. The restatement repeats the selected state's selector,
|
|
1981
|
+
* which makes it more specific than the fill.
|
|
1982
|
+
*/
|
|
1983
|
+
const FORCED_HOVER = { _selected: FORCED };
|
|
1984
|
+
/**
|
|
1985
|
+
* Fill of a selected row per look.
|
|
1986
|
+
*/
|
|
1987
|
+
const SELECTED = {
|
|
1988
|
+
plain: { _selected: {
|
|
1989
|
+
...FORCED,
|
|
1990
|
+
fontWeight: "medium"
|
|
1991
|
+
} },
|
|
1992
|
+
solid: { _selected: {
|
|
1993
|
+
...FORCED,
|
|
1994
|
+
layerStyle: "flat.solid",
|
|
1995
|
+
_hover: {
|
|
1996
|
+
...FORCED_HOVER,
|
|
1997
|
+
background: "colorPalette.solid.hover"
|
|
1998
|
+
}
|
|
1999
|
+
} },
|
|
2000
|
+
subtle: { _selected: {
|
|
2001
|
+
...FORCED,
|
|
2002
|
+
layerStyle: "flat.subtle",
|
|
2003
|
+
_hover: {
|
|
2004
|
+
...FORCED_HOVER,
|
|
2005
|
+
background: "colorPalette.muted"
|
|
2006
|
+
},
|
|
2007
|
+
fontWeight: "medium"
|
|
2008
|
+
} }
|
|
2009
|
+
};
|
|
2010
|
+
/**
|
|
2011
|
+
* Glow around a selected row.
|
|
2012
|
+
*/
|
|
2013
|
+
const GLOW = { glow: { _selected: { layerStyle: "glow.sm" } } };
|
|
2014
|
+
/**
|
|
2015
|
+
* Sets a row's height from the size.
|
|
2016
|
+
*/
|
|
2017
|
+
const ROW_SIZES = sizeVariants((size) => ({ minBlockSize: dense(`{sizes.${ROWS[size]}}`) }), SIZES);
|
|
2018
|
+
/**
|
|
2019
|
+
* Defines the tree view recipe: rows at size `md` that fill a selected row with the palette's
|
|
2020
|
+
* subtle fill, by default.
|
|
2021
|
+
*/
|
|
2022
|
+
const recipe = defineSlotRecipe({
|
|
2023
|
+
base: {
|
|
2024
|
+
branch: { minInlineSize: "0" },
|
|
2025
|
+
branchContent: {
|
|
2026
|
+
display: "flex",
|
|
2027
|
+
flexDirection: "column",
|
|
2028
|
+
position: "relative"
|
|
2029
|
+
},
|
|
2030
|
+
branchControl: {
|
|
2031
|
+
...ROW,
|
|
2032
|
+
paddingInlineStart: `calc(${INSET} + (var(--depth) - 1) * ${LEVEL})`
|
|
2033
|
+
},
|
|
2034
|
+
branchIndentGuide: {
|
|
2035
|
+
borderColor: "border",
|
|
2036
|
+
borderInlineStartStyle: "solid",
|
|
2037
|
+
borderInlineStartWidth: "hairline",
|
|
2038
|
+
insetBlock: "0",
|
|
2039
|
+
insetInlineStart: `calc(${INSET} + (var(--depth) - 1) * ${LEVEL} + ${MARK} / 2 - {borderWidths.hairline} / 2)`,
|
|
2040
|
+
pointerEvents: "none",
|
|
2041
|
+
position: "absolute",
|
|
2042
|
+
zIndex: "1"
|
|
2043
|
+
},
|
|
2044
|
+
branchIndicator: {
|
|
2045
|
+
...SQUARE,
|
|
2046
|
+
_highContrast: { color: "inherit" },
|
|
2047
|
+
_motionReduce: { transitionDuration: "0s" },
|
|
2048
|
+
_open: { rotate: "90deg" },
|
|
2049
|
+
_rtl: {
|
|
2050
|
+
_open: { rotate: "90deg" },
|
|
2051
|
+
rotate: "180deg"
|
|
2052
|
+
},
|
|
2053
|
+
color: "fg.muted",
|
|
2054
|
+
transitionDuration: "press",
|
|
2055
|
+
transitionProperty: "rotate",
|
|
2056
|
+
transitionTimingFunction: "press"
|
|
2057
|
+
},
|
|
2058
|
+
branchText: TEXT,
|
|
2059
|
+
branchTrigger: {
|
|
2060
|
+
cursor: "button",
|
|
2061
|
+
display: "inline-flex",
|
|
2062
|
+
flexShrink: "0"
|
|
2063
|
+
},
|
|
2064
|
+
item: {
|
|
2065
|
+
...ROW,
|
|
2066
|
+
paddingInlineStart: `calc(${INSET} + var(--depth) * ${LEVEL})`
|
|
2067
|
+
},
|
|
2068
|
+
itemIndicator: {
|
|
2069
|
+
...SQUARE,
|
|
2070
|
+
_highContrast: { color: "inherit" },
|
|
2071
|
+
color: "colorPalette.fg",
|
|
2072
|
+
marginInlineStart: "auto"
|
|
2073
|
+
},
|
|
2074
|
+
itemText: TEXT,
|
|
2075
|
+
label: {
|
|
2076
|
+
color: "fg",
|
|
2077
|
+
fontWeight: "medium",
|
|
2078
|
+
paddingInline: INSET
|
|
2079
|
+
},
|
|
2080
|
+
nodeCheckbox: {
|
|
2081
|
+
...SQUARE,
|
|
2082
|
+
_highContrast: {
|
|
2083
|
+
borderColor: "CanvasText",
|
|
2084
|
+
forcedColorAdjust: "none"
|
|
2085
|
+
},
|
|
2086
|
+
"[aria-selected=true] > &": {
|
|
2087
|
+
_highContrast: { borderColor: "HighlightText" },
|
|
2088
|
+
"&[data-state=checked], &[data-state=indeterminate]": { _highContrast: {
|
|
2089
|
+
background: "HighlightText",
|
|
2090
|
+
borderColor: "HighlightText",
|
|
2091
|
+
color: "Highlight"
|
|
2092
|
+
} }
|
|
2093
|
+
},
|
|
2094
|
+
"&[data-state=checked], &[data-state=indeterminate]": {
|
|
2095
|
+
_highContrast: {
|
|
2096
|
+
background: "CanvasText",
|
|
2097
|
+
borderColor: "CanvasText",
|
|
2098
|
+
color: "Canvas"
|
|
2099
|
+
},
|
|
2100
|
+
background: "colorPalette.solid",
|
|
2101
|
+
borderColor: "colorPalette.solid",
|
|
2102
|
+
color: "colorPalette.contrast"
|
|
2103
|
+
},
|
|
2104
|
+
borderColor: "border.emphasized",
|
|
2105
|
+
borderRadius: "l1",
|
|
2106
|
+
borderStyle: "solid",
|
|
2107
|
+
borderWidth: "control",
|
|
2108
|
+
color: "transparent",
|
|
2109
|
+
cursor: "button"
|
|
2110
|
+
},
|
|
2111
|
+
nodeRenameInput: {
|
|
2112
|
+
focusVisibleRing: "outside",
|
|
2113
|
+
background: "bg.panel",
|
|
2114
|
+
borderColor: "border.emphasized",
|
|
2115
|
+
borderRadius: "l1",
|
|
2116
|
+
borderStyle: "solid",
|
|
2117
|
+
borderWidth: "hairline",
|
|
2118
|
+
color: "fg",
|
|
2119
|
+
flex: "1",
|
|
2120
|
+
focusRingColor: "colorPalette.focusRing",
|
|
2121
|
+
font: "inherit",
|
|
2122
|
+
minInlineSize: "0",
|
|
2123
|
+
paddingInline: GAP
|
|
2124
|
+
},
|
|
2125
|
+
root: {
|
|
2126
|
+
display: "flex",
|
|
2127
|
+
flexDirection: "column",
|
|
2128
|
+
inlineSize: "full",
|
|
2129
|
+
minInlineSize: "0"
|
|
2130
|
+
},
|
|
2131
|
+
tree: {
|
|
2132
|
+
display: "flex",
|
|
2133
|
+
flexDirection: "column",
|
|
2134
|
+
minInlineSize: "0"
|
|
2135
|
+
}
|
|
2136
|
+
},
|
|
2137
|
+
className: "tree-view",
|
|
2138
|
+
defaultVariants: {
|
|
2139
|
+
selected: "subtle",
|
|
2140
|
+
size: "md"
|
|
2141
|
+
},
|
|
2142
|
+
jsx: [/^TreeView(\.\w+)?$/u],
|
|
2143
|
+
slots: [
|
|
2144
|
+
"root",
|
|
2145
|
+
"label",
|
|
2146
|
+
"tree",
|
|
2147
|
+
"branch",
|
|
2148
|
+
"branchControl",
|
|
2149
|
+
"branchTrigger",
|
|
2150
|
+
"branchIndicator",
|
|
2151
|
+
"branchText",
|
|
2152
|
+
"branchContent",
|
|
2153
|
+
"branchIndentGuide",
|
|
2154
|
+
"item",
|
|
2155
|
+
"itemIndicator",
|
|
2156
|
+
"itemText",
|
|
2157
|
+
"nodeCheckbox",
|
|
2158
|
+
"nodeRenameInput"
|
|
2159
|
+
],
|
|
2160
|
+
staticCss: [
|
|
2161
|
+
{ palette: [...PALETTES] },
|
|
2162
|
+
{ selected: ["plain"] },
|
|
2163
|
+
{ size: ["sm", "md"] }
|
|
2164
|
+
],
|
|
2165
|
+
variants: {
|
|
2166
|
+
/**
|
|
2167
|
+
* Glow around a selected row, in the palette's solid at half opacity.
|
|
2168
|
+
*/
|
|
2169
|
+
effect: onSlots({
|
|
2170
|
+
branchControl: GLOW,
|
|
2171
|
+
item: GLOW
|
|
2172
|
+
}),
|
|
2173
|
+
/**
|
|
2174
|
+
* Palette the selected fill, the checkboxes and the focus ring read.
|
|
2175
|
+
*
|
|
2176
|
+
* @remarks
|
|
2177
|
+
* The palette is set on the root, and every part inherits the palette's custom properties.
|
|
2178
|
+
*/
|
|
2179
|
+
palette: onSlot("root", paletteVariants()),
|
|
2180
|
+
/**
|
|
2181
|
+
* Fill of a selected row.
|
|
2182
|
+
*
|
|
2183
|
+
* @remarks
|
|
2184
|
+
* The fills are the flat looks, and each look restates its hover, because a variant applies
|
|
2185
|
+
* over the base hover. The subtle fill measures 1.2:1 against a card in both color modes, so
|
|
2186
|
+
* `subtle` also sets the text in medium weight, as `plain` does alone. Under forced colors a
|
|
2187
|
+
* selected row fills with `Highlight` in every look.
|
|
2188
|
+
*/
|
|
2189
|
+
selected: onSlots({
|
|
2190
|
+
branchControl: SELECTED,
|
|
2191
|
+
item: SELECTED
|
|
2192
|
+
}),
|
|
2193
|
+
/**
|
|
2194
|
+
* Sets rows of 24, 32 and 40px at `sm`, `md` and `lg`. The text reads the body role and the
|
|
2195
|
+
* marks the icon scale one size smaller than the tree, and the inset two sizes smaller.
|
|
2196
|
+
*/
|
|
2197
|
+
size: onSlots({
|
|
2198
|
+
branchControl: ROW_SIZES,
|
|
2199
|
+
item: ROW_SIZES,
|
|
2200
|
+
label: sizeVariants((size) => ({ textStyle: `label.${size}` }), SIZES),
|
|
2201
|
+
root: sizeVariants((size) => ({
|
|
2202
|
+
textStyle: `body.${below(size)}`,
|
|
2203
|
+
"--tree-view-gap": dense(`{spacing.gap.${below(size)}}`),
|
|
2204
|
+
"--tree-view-inset": dense(`{spacing.inset.${below(below(size))}}`),
|
|
2205
|
+
"--tree-view-mark": dense(`{sizes.icon.${below(size)}}`),
|
|
2206
|
+
gap: dense(`{spacing.gap.${size}}`)
|
|
2207
|
+
}), SIZES)
|
|
2208
|
+
})
|
|
2209
|
+
}
|
|
2210
|
+
});
|
|
2211
|
+
//#endregion
|
|
2212
|
+
export { REVEAL as a, recipe$5 as c, ROW_HEIGHT as d, recipe$8 as f, recipe$2 as i, recipe$6 as l, ROWS$1 as n, recipe$3 as o, recipe$1 as r, recipe$4 as s, recipe as t, recipe$7 as u };
|