@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.
@@ -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 };