@stealthscale/component-disclosure 0.1.0 → 0.2.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,2130 @@
1
+ import { PALETTES, below, controlSizes, defineRecipe, defineSlotRecipe, dense, divider, highlightVariants, iconSizes, insetSizes, interactive, justifyVariants, link, motion, onSlot, onSlots, paletteVariants, row, sizeVariants, surface, textSizes, truncate } from "@stealthscale/theme/authoring";
2
+ //#region src/accordion/recipe.ts
3
+ /**
4
+ * Recipe for the accordion: a column of items, each a heading whose trigger shows and hides the
5
+ * content under it.
6
+ *
7
+ * @remarks
8
+ * The trigger fills its heading, sets its title in the label style and puts the indicator at its
9
+ * end, so an icon or an avatar before the title leaves the indicators in one column. The trigger
10
+ * is at least a control's height and grows with a title that wraps. A heading with a second child
11
+ * after the trigger, such as an action button, places that child at the end of the row, outside
12
+ * the button. The content animates to the height the collapsible machine measures, and the body
13
+ * inside it has the padding, so the animation runs down to zero height. The subtle, surface
14
+ * and outline looks inset the trigger and the body from the box's edges. The flushed and plain
15
+ * looks align both with the text around the accordion. The recipe has no `effect` axis, because
16
+ * the box contains content as well as the triggers, and a glow marks a control.
17
+ */
18
+ /**
19
+ * Sizes the recipe offers.
20
+ */
21
+ const SIZES$4 = [
22
+ "sm",
23
+ "md",
24
+ "lg"
25
+ ];
26
+ /**
27
+ * Selects a heading that contains a second child after its trigger.
28
+ */
29
+ const FOLLOWED = "&:has(> :nth-child(2))";
30
+ /**
31
+ * Places the focus ring one ring width inside the trigger's edge, for the looks whose root clips
32
+ * what falls outside it.
33
+ */
34
+ const INSIDE = { _focusVisible: { outlineOffset: "calc({borderWidths.ring} * -1)" } };
35
+ /**
36
+ * Removes the inline inset of the trigger and the body, for the looks that align their text with
37
+ * the text around the accordion.
38
+ */
39
+ const FLUSH = { paddingInline: "0" };
40
+ /**
41
+ * Removes the end inset a heading gives the child after its trigger, for the same looks.
42
+ */
43
+ const FLUSH_END = { [FOLLOWED]: { paddingInlineEnd: "0" } };
44
+ /**
45
+ * Rules a hairline under every item but the last.
46
+ */
47
+ const DIVIDED = {
48
+ _last: { borderBlockEndWidth: "0" },
49
+ borderBlockEndWidth: "hairline"
50
+ };
51
+ /**
52
+ * Defines the accordion recipe: flushed items at size `md` in the neutral palette, sliding open,
53
+ * by default.
54
+ */
55
+ const recipe$11 = defineSlotRecipe({
56
+ base: {
57
+ item: { overflowAnchor: "none" },
58
+ itemContent: { overflow: "hidden" },
59
+ itemHeading: {
60
+ alignItems: "center",
61
+ display: "flex"
62
+ },
63
+ itemIndicator: {
64
+ _motionReduce: { transitionDuration: "none" },
65
+ _open: { rotate: "180deg" },
66
+ alignItems: "center",
67
+ color: "fg.muted",
68
+ display: "inline-flex",
69
+ flexShrink: "0",
70
+ justifyContent: "center",
71
+ marginInlineStart: "auto",
72
+ transitionDuration: "press",
73
+ transitionProperty: "rotate, color",
74
+ transitionTimingFunction: "press"
75
+ },
76
+ itemTrigger: {
77
+ ...interactive(),
78
+ _hover: { "& .accordion__itemIndicator": { color: "fg" } },
79
+ "& > svg": { flexShrink: "0" },
80
+ alignItems: "center",
81
+ display: "flex",
82
+ flex: "1",
83
+ minInlineSize: "0",
84
+ textAlign: "start"
85
+ },
86
+ root: { inlineSize: "full" }
87
+ },
88
+ className: "accordion",
89
+ defaultVariants: {
90
+ motion: "slide",
91
+ palette: "neutral",
92
+ size: "md",
93
+ variant: "flushed"
94
+ },
95
+ jsx: [/^Accordion(\.\w+)?$/u],
96
+ slots: [
97
+ "root",
98
+ "item",
99
+ "itemHeading",
100
+ "itemTrigger",
101
+ "itemIndicator",
102
+ "itemContent",
103
+ "itemBody"
104
+ ],
105
+ staticCss: [{ palette: [...PALETTES] }],
106
+ variants: {
107
+ /**
108
+ * Animation of the content as it opens and closes.
109
+ */
110
+ motion: {
111
+ fade: { itemContent: {
112
+ _closed: { animationStyle: "fade.out" },
113
+ _open: { animationStyle: "fade.in" }
114
+ } },
115
+ none: { itemContent: { animation: "none" } },
116
+ slide: { itemContent: {
117
+ _closed: { animationStyle: "collapse.out" },
118
+ _open: { animationStyle: "collapse.in" }
119
+ } }
120
+ },
121
+ /**
122
+ * Palette the looks read, set on the root.
123
+ */
124
+ palette: onSlot("root", paletteVariants()),
125
+ /**
126
+ * Size of the trigger on the control scale, the indicator on the icon scale, and the body's
127
+ * padding and text.
128
+ *
129
+ * @remarks
130
+ * A trigger is at least a control's height. Its block padding is the gap one step below, so a
131
+ * one-line title centres in that height and a title that wraps adds its lines. A leading icon
132
+ * is on the icon scale one step below the indicator. The body starts one gap below the
133
+ * trigger and ends one inset above the next item.
134
+ */
135
+ size: onSlots({
136
+ itemBody: sizeVariants((size) => ({
137
+ textStyle: `body.${size}`,
138
+ paddingBlockEnd: dense(`{spacing.inset.${size}}`),
139
+ paddingBlockStart: dense(`{spacing.gap.${below(size)}}`),
140
+ paddingInline: dense(`{spacing.inset.${size}}`)
141
+ }), SIZES$4),
142
+ itemHeading: sizeVariants((size) => ({
143
+ [FOLLOWED]: { paddingInlineEnd: dense(`{spacing.inset.${below(size)}}`) },
144
+ gap: dense(`{spacing.gap.${size}}`)
145
+ }), SIZES$4),
146
+ itemIndicator: iconSizes(SIZES$4),
147
+ itemTrigger: sizeVariants((size) => ({
148
+ textStyle: `label.${size}`,
149
+ "&:not(:last-child)": { paddingInlineEnd: "0" },
150
+ "& > svg": { boxSize: dense(`{sizes.icon.${below(size)}}`) },
151
+ gap: dense(`{spacing.gap.${size}}`),
152
+ minBlockSize: dense(`{sizes.control.${size}}`),
153
+ paddingBlock: dense(`{spacing.gap.${below(size)}}`),
154
+ paddingInline: dense(`{spacing.inset.${size}}`)
155
+ }), SIZES$4)
156
+ }),
157
+ /**
158
+ * Look of the items and the box around them.
159
+ *
160
+ * @remarks
161
+ * `subtle` fills the open item from the palette and fills a trigger under the pointer.
162
+ * `surface` is the theme's panel, with its shadow, and `outline` a hairline box, both with a
163
+ * hairline between items. `flushed` rules a hairline under every item and renders no box.
164
+ * `plain` renders no line. The boxed looks clip their content to the box's corners and place
165
+ * the focus ring inside the trigger. The rules and the outline read the palette's muted role,
166
+ * and the surface's rules read the panel's edge color.
167
+ */
168
+ variant: {
169
+ subtle: {
170
+ item: {
171
+ _open: { background: "colorPalette.subtle" },
172
+ borderRadius: "l2"
173
+ },
174
+ itemTrigger: {
175
+ ...INSIDE,
176
+ _hover: { background: "colorPalette.subtle" },
177
+ _open: {
178
+ _hover: { background: "colorPalette.muted" },
179
+ borderEndEndRadius: "0",
180
+ borderEndStartRadius: "0"
181
+ },
182
+ borderRadius: "l2"
183
+ }
184
+ },
185
+ surface: {
186
+ item: {
187
+ ...DIVIDED,
188
+ borderBlockEndColor: "border"
189
+ },
190
+ itemTrigger: {
191
+ ...INSIDE,
192
+ _hover: { background: "colorPalette.subtle" }
193
+ },
194
+ root: {
195
+ ...surface(),
196
+ overflow: "clip"
197
+ }
198
+ },
199
+ outline: {
200
+ item: {
201
+ ...DIVIDED,
202
+ borderBlockEndColor: "colorPalette.muted"
203
+ },
204
+ itemTrigger: {
205
+ ...INSIDE,
206
+ _hover: { background: "colorPalette.subtle" }
207
+ },
208
+ root: {
209
+ borderColor: "colorPalette.muted",
210
+ borderRadius: "l2",
211
+ borderWidth: "hairline",
212
+ overflow: "clip"
213
+ }
214
+ },
215
+ flushed: {
216
+ item: {
217
+ borderBlockEndColor: "colorPalette.muted",
218
+ borderBlockEndWidth: "hairline"
219
+ },
220
+ itemBody: FLUSH,
221
+ itemHeading: FLUSH_END,
222
+ itemTrigger: FLUSH
223
+ },
224
+ plain: {
225
+ itemBody: FLUSH,
226
+ itemHeading: FLUSH_END,
227
+ itemTrigger: FLUSH
228
+ }
229
+ }
230
+ }
231
+ });
232
+ //#endregion
233
+ //#region src/collapsible/recipe.ts
234
+ /**
235
+ * Recipe for the collapsible: a trigger that shows and hides the content beneath it, and an
236
+ * indicator that turns as it does.
237
+ *
238
+ * @remarks
239
+ * The trigger takes the full width and places the indicator at its end, and reads the control
240
+ * scale, so its height matches a button of the same size. The content animates to the height the
241
+ * machine measures, with the theme's animation styles, so the theme settles reduced motion. The
242
+ * palette axis offers the eight palettes. The recipe has no `effect` axis, because the box holds
243
+ * content as well as the trigger, and a glow marks a control.
244
+ */
245
+ /**
246
+ * Defines the collapsible recipe: a plain collapsible at size `md` in the neutral palette, sliding
247
+ * open, by default.
248
+ */
249
+ const recipe$10 = defineSlotRecipe({
250
+ base: {
251
+ content: { overflow: "hidden" },
252
+ indicator: {
253
+ _motionReduce: { transitionDuration: "none" },
254
+ _open: { rotate: "180deg" },
255
+ alignItems: "center",
256
+ display: "inline-flex",
257
+ flexShrink: "0",
258
+ justifyContent: "center",
259
+ transitionDuration: "press",
260
+ transitionProperty: "rotate",
261
+ transitionTimingFunction: "press"
262
+ },
263
+ root: { width: "full" },
264
+ trigger: {
265
+ ...interactive(),
266
+ alignItems: "center",
267
+ display: "flex",
268
+ justifyContent: "space-between",
269
+ textAlign: "start",
270
+ width: "full"
271
+ }
272
+ },
273
+ className: "collapsible",
274
+ defaultVariants: {
275
+ motion: "slide",
276
+ palette: "neutral",
277
+ size: "md",
278
+ variant: "plain"
279
+ },
280
+ jsx: [/^Collapsible(\.\w+)?$/u],
281
+ slots: [
282
+ "root",
283
+ "trigger",
284
+ "content",
285
+ "indicator"
286
+ ],
287
+ staticCss: [{ palette: [...PALETTES] }],
288
+ variants: {
289
+ /**
290
+ * Animation of the content as it opens and closes.
291
+ */
292
+ motion: {
293
+ fade: { content: {
294
+ _closed: { animationStyle: "fade.out" },
295
+ _open: { animationStyle: "fade.in" }
296
+ } },
297
+ none: { content: { animation: "none" } },
298
+ slide: { content: {
299
+ _closed: { animationStyle: "collapse.out" },
300
+ _open: { animationStyle: "collapse.in" }
301
+ } }
302
+ },
303
+ /**
304
+ * Palette the looks read, set on the root.
305
+ */
306
+ palette: onSlot("root", paletteVariants()),
307
+ /**
308
+ * Size of the trigger on the control scale, the indicator on the icon scale and the content's
309
+ * padding on the inset scale.
310
+ */
311
+ size: onSlots({
312
+ content: insetSizes(),
313
+ indicator: iconSizes(),
314
+ trigger: controlSizes()
315
+ }),
316
+ /**
317
+ * Look of the box around the trigger and the content.
318
+ *
319
+ * @remarks
320
+ * `surface` uses the theme's `surface()` fragment, with its shadow. Every look reads the
321
+ * palette, so the palette tints the fill and the edge. The subtle trigger takes the root's
322
+ * radius, so its hover fill follows the root's corners, and its end corners are square while
323
+ * the content is open under it. Under forced colors the subtle box, which the browser's fill
324
+ * replaces, takes a `CanvasText` hairline outline.
325
+ */
326
+ variant: {
327
+ subtle: {
328
+ root: {
329
+ _highContrast: {
330
+ outlineColor: "CanvasText",
331
+ outlineOffset: "calc({borderWidths.hairline} * -1)",
332
+ outlineStyle: "solid",
333
+ outlineWidth: "hairline"
334
+ },
335
+ background: "colorPalette.subtle",
336
+ borderRadius: "l2"
337
+ },
338
+ trigger: {
339
+ _hover: { background: "colorPalette.muted" },
340
+ _open: {
341
+ borderEndEndRadius: "0",
342
+ borderEndStartRadius: "0"
343
+ },
344
+ borderRadius: "l2"
345
+ }
346
+ },
347
+ surface: {
348
+ root: surface(),
349
+ trigger: { _open: {
350
+ borderBlockEndColor: "colorPalette.muted",
351
+ borderBlockEndWidth: "hairline"
352
+ } }
353
+ },
354
+ outline: {
355
+ root: {
356
+ borderColor: "colorPalette.muted",
357
+ borderRadius: "l2",
358
+ borderWidth: "hairline"
359
+ },
360
+ trigger: { _open: {
361
+ borderBlockEndColor: "colorPalette.muted",
362
+ borderBlockEndWidth: "hairline"
363
+ } }
364
+ },
365
+ plain: { root: { borderWidth: "0" } }
366
+ }
367
+ }
368
+ });
369
+ //#endregion
370
+ //#region src/details/recipe.ts
371
+ /**
372
+ * Recipe for the details: a native `details` element whose summary shows and hides the content
373
+ * under it, and an indicator that turns a quarter as it opens.
374
+ *
375
+ * @remarks
376
+ * The browser opens and closes the element, so every open style keys on the root's `open`
377
+ * attribute. The summary drops the browser's marker and is a control's height for one line, and
378
+ * a summary that wraps grows. The indicator goes before the words and turns a quarter
379
+ * clockwise, and a quarter the other way under `dir="rtl"`, for a chevron that points to the
380
+ * end. The looks and the palette are the collapsible's, so a details beside a collapsible looks
381
+ * the same.
382
+ */
383
+ /**
384
+ * Selects the summary of an open details.
385
+ */
386
+ const OPENED = ".details__root[open] > &";
387
+ /**
388
+ * Defines the details recipe: an outlined details at size `md` in the neutral palette by default.
389
+ */
390
+ const recipe$9 = defineSlotRecipe({
391
+ base: {
392
+ indicator: {
393
+ _motionReduce: { transitionDuration: "none" },
394
+ "& svg": { boxSize: "full" },
395
+ alignItems: "center",
396
+ display: "inline-flex",
397
+ flexShrink: "0",
398
+ justifyContent: "center",
399
+ [".details__root[open] > .details__summary > &"]: {
400
+ _rtl: { rotate: "-90deg" },
401
+ rotate: "90deg"
402
+ },
403
+ transitionDuration: "press",
404
+ transitionProperty: "rotate",
405
+ transitionTimingFunction: "press"
406
+ },
407
+ root: { width: "full" },
408
+ summary: {
409
+ ...interactive(),
410
+ "&::-webkit-details-marker": { display: "none" },
411
+ alignItems: "center",
412
+ display: "flex",
413
+ fontWeight: "medium",
414
+ listStyle: "none",
415
+ textAlign: "start"
416
+ }
417
+ },
418
+ className: "details",
419
+ defaultVariants: {
420
+ palette: "neutral",
421
+ size: "md",
422
+ variant: "outline"
423
+ },
424
+ jsx: [/^Details(\.\w+)?$/u],
425
+ slots: [
426
+ "root",
427
+ "summary",
428
+ "indicator",
429
+ "content"
430
+ ],
431
+ staticCss: [{ palette: [...PALETTES] }],
432
+ variants: {
433
+ /**
434
+ * Palette the looks read, set on the root.
435
+ */
436
+ palette: onSlot("root", paletteVariants()),
437
+ /**
438
+ * Size of the summary, the indicator on the icon scale and the content's padding on the inset
439
+ * scale.
440
+ *
441
+ * @remarks
442
+ * The summary pads its ends by half of a control's height less its tallest line, one line of
443
+ * text or the indicator, and is at least a control's height. A one-line summary is then a
444
+ * collapsible trigger's height at the same size, and a longer one grows by its lines. It pads
445
+ * its sides on the inset scale.
446
+ */
447
+ size: onSlots({
448
+ content: insetSizes(),
449
+ indicator: iconSizes(),
450
+ summary: sizeVariants((size) => ({
451
+ textStyle: `label.${size}`,
452
+ gap: dense(`{spacing.gap.${size}}`),
453
+ minBlockSize: dense(`{sizes.control.${size}}`),
454
+ paddingBlock: `calc((${dense(`{sizes.control.${size}}`)} - max(1lh, ${dense(`{sizes.icon.${size}}`)})) / 2)`,
455
+ paddingInline: dense(`{spacing.inset.${size}}`)
456
+ }))
457
+ }),
458
+ /**
459
+ * Look of the box around the summary and the content.
460
+ *
461
+ * @remarks
462
+ * The looks are the collapsible's. The subtle summary takes the root's radius, so its hover
463
+ * fill follows the root's corners, and its end corners are square while the content is open
464
+ * under it. Under forced colors the subtle box, which the browser's fill replaces, takes a
465
+ * `CanvasText` hairline outline. The plain look draws no box and pads no side, so its summary
466
+ * and content line up with the text around them.
467
+ */
468
+ variant: {
469
+ subtle: {
470
+ root: {
471
+ _highContrast: {
472
+ outlineColor: "CanvasText",
473
+ outlineOffset: "calc({borderWidths.hairline} * -1)",
474
+ outlineStyle: "solid",
475
+ outlineWidth: "hairline"
476
+ },
477
+ background: "colorPalette.subtle",
478
+ borderRadius: "l2"
479
+ },
480
+ summary: {
481
+ _hover: { background: "colorPalette.muted" },
482
+ borderRadius: "l2",
483
+ [OPENED]: {
484
+ borderEndEndRadius: "0",
485
+ borderEndStartRadius: "0"
486
+ }
487
+ }
488
+ },
489
+ surface: {
490
+ root: surface(),
491
+ summary: { [OPENED]: {
492
+ borderBlockEndColor: "colorPalette.muted",
493
+ borderBlockEndWidth: "hairline"
494
+ } }
495
+ },
496
+ outline: {
497
+ root: {
498
+ borderColor: "colorPalette.muted",
499
+ borderRadius: "l2",
500
+ borderWidth: "hairline"
501
+ },
502
+ summary: { [OPENED]: {
503
+ borderBlockEndColor: "colorPalette.muted",
504
+ borderBlockEndWidth: "hairline"
505
+ } }
506
+ },
507
+ plain: {
508
+ content: { paddingInline: "0" },
509
+ root: { borderWidth: "0" },
510
+ summary: { paddingInline: "0" }
511
+ }
512
+ }
513
+ }
514
+ });
515
+ //#endregion
516
+ //#region src/hover-card/recipe.ts
517
+ /**
518
+ * Recipe for the hover card: a panel beside a link that previews what the link leads to, with an
519
+ * arrow.
520
+ *
521
+ * @remarks
522
+ * The machine has no root part. The recipe adds a root with `display: contents`, because the
523
+ * trigger and the positioner are siblings and a slot recipe passes its variants from an element
524
+ * above both. The machine writes the positioner's position inline, so the recipe sets no
525
+ * position. The trigger reads `link()` and is underlined at rest, because the link ink alone
526
+ * measures 1.64:1 against body text in light mode and 1.38:1 in dark mode on the ink theme, under
527
+ * the 3:1 that WCAG technique G183 asks for. The panel slides in from the trigger's side, which
528
+ * it reads from `data-placement`. It is at most `sizes.xs` wide, or as wide as the room the
529
+ * machine measures on its side of the trigger, `--available-width`, where that is less. The
530
+ * recipe has no `palette` axis, because every look uses a neutral surface, and no `effect` axis,
531
+ * because a panel is not a control.
532
+ */
533
+ /**
534
+ * Steps of the size axis.
535
+ */
536
+ const SIZES$3 = [
537
+ "xs",
538
+ "sm",
539
+ "md",
540
+ "lg"
541
+ ];
542
+ /**
543
+ * Defines the hover card recipe: the surface look at size `md` by default.
544
+ */
545
+ const recipe$8 = defineSlotRecipe({
546
+ base: {
547
+ arrow: {
548
+ "--arrow-background": "var(--hover-card-surface)",
549
+ "--arrow-size": "sizes.icon.sm"
550
+ },
551
+ arrowTip: {
552
+ borderInlineStartWidth: "hairline",
553
+ borderTopWidth: "hairline"
554
+ },
555
+ content: {
556
+ ...motion("slide-fade.in", "slide-fade.out"),
557
+ display: "flex",
558
+ flexDirection: "column",
559
+ maxInlineSize: "min({sizes.xs}, var(--available-width, {sizes.xs}))",
560
+ position: "relative",
561
+ zIndex: "popover"
562
+ },
563
+ positioner: { position: "relative" },
564
+ root: { display: "contents" },
565
+ trigger: {
566
+ ...link(),
567
+ borderRadius: "l1",
568
+ textDecoration: "underline"
569
+ }
570
+ },
571
+ className: "hover-card",
572
+ defaultVariants: {
573
+ size: "md",
574
+ variant: "surface"
575
+ },
576
+ jsx: [/^HoverCard(\.\w+)?$/u],
577
+ slots: [
578
+ "root",
579
+ "trigger",
580
+ "positioner",
581
+ "content",
582
+ "arrow",
583
+ "arrowTip"
584
+ ],
585
+ variants: {
586
+ /**
587
+ * Padding of the panel on the inset scale and its text on the body role.
588
+ */
589
+ size: onSlot("content", sizeVariants((size) => ({
590
+ textStyle: `body.${size}`,
591
+ padding: dense(`{spacing.inset.${size}}`)
592
+ }), SIZES$3)),
593
+ /**
594
+ * Surface of the panel: the popover surface inside a hairline edge, the panel surface with a
595
+ * large shadow, or the glass layer style.
596
+ *
597
+ * @remarks
598
+ * The elevated panel has a transparent hairline edge, which forced colors paint in
599
+ * `CanvasText`, so the panel keeps its outline where the shadow is not drawn.
600
+ */
601
+ variant: {
602
+ elevated: {
603
+ arrowTip: { borderColor: "var(--hover-card-surface)" },
604
+ content: {
605
+ "--hover-card-surface": "colors.bg.panel",
606
+ background: "var(--hover-card-surface)",
607
+ borderColor: "transparent",
608
+ borderRadius: "l3",
609
+ borderStyle: "solid",
610
+ borderWidth: "hairline",
611
+ boxShadow: "xl"
612
+ }
613
+ },
614
+ glass: {
615
+ arrowTip: { borderColor: "border" },
616
+ content: {
617
+ layerStyle: "glass",
618
+ "--hover-card-surface": "colors.bg.popover",
619
+ borderRadius: "l3"
620
+ }
621
+ },
622
+ surface: {
623
+ arrowTip: { borderColor: "border" },
624
+ content: {
625
+ "--hover-card-surface": "colors.bg.popover",
626
+ background: "var(--hover-card-surface)",
627
+ borderColor: "border",
628
+ borderRadius: "l3",
629
+ borderWidth: "hairline",
630
+ boxShadow: "lg"
631
+ }
632
+ }
633
+ }
634
+ }
635
+ });
636
+ //#endregion
637
+ //#region src/menu/recipe.ts
638
+ /**
639
+ * Recipe for the menu: a trigger, a panel of rows and groups, and submenus opened from a row.
640
+ *
641
+ * @remarks
642
+ * The machine has no root part. The recipe adds a root with `display: contents`, because the
643
+ * trigger and the positioner are siblings and a slot recipe passes its variants from an element
644
+ * above both. A submenu's root is a child of the parent menu's panel. The machine writes the
645
+ * positioner's position inline and `--available-height`, which caps the panel, so a menu near the
646
+ * window's edge scrolls its rows in the primitives package's scroll area inside the panel. The
647
+ * `viewport` and `rows` slots are that scroll area's viewport, the element with the menu role,
648
+ * and its content: the rows take the panel's padding, and the viewport's scroll padding keeps a
649
+ * revealed row that padding from the edge. The panel is at least `sizes.44` wide and grows to its
650
+ * widest row. The panel takes the `popover` z-index plus its depth in the nest, because the
651
+ * machine writes `z-index: var(--z-index)` inline on the positioner from the panel's value. The
652
+ * `popover` level is above a page's sticky bands and the app shell's bars, so a menu opened from
653
+ * a bar is above the bands under it. Neither the panel nor its scroll area renders a focus ring,
654
+ * because the machine focuses the menu on opening and the highlight marks the row. A row reads
655
+ * the body role one size smaller, truncates, and sizes a leading icon to the icon size one size
656
+ * smaller. A row's mark renders its initial or glyph without a fill, in the muted ink. `inset`
657
+ * pads a row without a leading icon or mark by the gutter a leading icon takes, so its text
658
+ * starts where an icon row's text starts. The indicator mirrors in a right-to-left menu, so a
659
+ * submenu row's chevron points to the side the submenu opens on. The panel sets the palette, and
660
+ * the rows and the highlight inherit it. A critical row reads `error`. The recipe has no `effect`
661
+ * axis, because the highlight moves with the pointer and the arrow keys, and a glow would move
662
+ * with it.
663
+ */
664
+ /**
665
+ * Sizes the menu offers.
666
+ */
667
+ const SIZES$2 = [
668
+ "sm",
669
+ "md",
670
+ "lg"
671
+ ];
672
+ /**
673
+ * The recipe's class name, which the selectors across parts are built from.
674
+ *
675
+ * @remarks
676
+ * The binding writes one class per part and no part attribute. A selector names a part by the
677
+ * compiler's class, such as `menu__itemMark`, and the naming step renames it to the class the
678
+ * element carries, `menu__item-mark`.
679
+ */
680
+ const CLASS$2 = "menu";
681
+ /**
682
+ * Custom property a panel sets to its depth in the nest, which the panel adds to its z-index.
683
+ *
684
+ * @remarks
685
+ * When a caller portals a menu and its submenu to the document, neither is an ancestor of the
686
+ * other. Without the depth, mount order decides the stacking, and a submenu renders under its
687
+ * parent.
688
+ */
689
+ const MENU_DEPTH = "--menu-depth";
690
+ /**
691
+ * Attribute a panel sets when it opened from a row of another menu.
692
+ *
693
+ * @remarks
694
+ * A selector cannot compare a custom property's number, so the attribute marks a nested panel.
695
+ */
696
+ const NESTED = "data-nested";
697
+ /**
698
+ * Custom property the size axis sets to the gutter a leading icon takes, which `inset` reads.
699
+ */
700
+ const GUTTER$1 = "--menu-gutter";
701
+ /**
702
+ * Custom property the scroll area's root reads for the style of its focus ring.
703
+ */
704
+ const RING_STYLE = "--scroll-area-ring-style";
705
+ /**
706
+ * Selects a row whose first element is neither an icon nor a mark.
707
+ *
708
+ * @remarks
709
+ * The selector matches element children only, so the text before an icon does not count.
710
+ */
711
+ const UNMARKED = `&:not(:has(> svg:first-child, > .${CLASS$2}__itemMark:first-child))`;
712
+ /**
713
+ * Custom property each look sets to the panel's fill, which the arrow reads.
714
+ */
715
+ const SURFACE = "var(--menu-surface)";
716
+ /**
717
+ * Returns the side of a row's leading icon at a size: the icon size one size smaller.
718
+ */
719
+ function icon(size) {
720
+ return dense(`{sizes.icon.${below(size)}}`);
721
+ }
722
+ /**
723
+ * Returns the gutter a leading icon takes at a size: the row's inline padding, the icon and the
724
+ * gap, each multiplied by the density as the row multiplies them.
725
+ */
726
+ function gutter(size) {
727
+ return `calc(${dense(`{spacing.inset.${below(size)}}`)} + ${icon(size)} + ${dense(`{spacing.gap.${size}}`)})`;
728
+ }
729
+ /**
730
+ * Returns the margin that moves a submenu off its parent's panel, on the side it opened towards.
731
+ *
732
+ * @remarks
733
+ * The machine places a submenu against the row that opened it, and the row stops one padding
734
+ * short of the panel's edge, so the margin moves the submenu off its parent by that padding. The
735
+ * machine reports the side as left or right, and the margin is logical, so each side sets both
736
+ * inline margins and swaps them under `_rtl`.
737
+ * @param room - The rows' padding at the size.
738
+ */
739
+ function cleared(room) {
740
+ return {
741
+ [`&[${NESTED}][data-placement^=left]`]: {
742
+ _rtl: {
743
+ marginInlineEnd: "0",
744
+ marginInlineStart: room
745
+ },
746
+ marginInlineEnd: room,
747
+ marginInlineStart: "0"
748
+ },
749
+ [`&[${NESTED}][data-placement^=right]`]: {
750
+ _rtl: {
751
+ marginInlineEnd: room,
752
+ marginInlineStart: "0"
753
+ },
754
+ marginInlineEnd: "0",
755
+ marginInlineStart: room
756
+ }
757
+ };
758
+ }
759
+ /**
760
+ * Returns a row's styles at a size: its padding, the leading icon, the gap after a mark, the gutter
761
+ * and the body role one size smaller.
762
+ */
763
+ function rowOf(size) {
764
+ return {
765
+ textStyle: `body.${below(size)}`,
766
+ "& > svg": {
767
+ boxSize: icon(size),
768
+ flexShrink: "0"
769
+ },
770
+ gap: dense(`{spacing.gap.${size}}`),
771
+ [GUTTER$1]: gutter(size),
772
+ paddingBlock: dense(`{spacing.gap.${below(size)}}`),
773
+ paddingInline: dense(`{spacing.inset.${below(size)}}`)
774
+ };
775
+ }
776
+ /**
777
+ * Defines the menu recipe: the surface look at size `md` in the neutral palette, with the tint
778
+ * highlight, by default.
779
+ */
780
+ const recipe$7 = defineSlotRecipe({
781
+ base: {
782
+ arrow: {
783
+ "--arrow-background": SURFACE,
784
+ "--arrow-size": "sizes.icon.sm"
785
+ },
786
+ arrowTip: {
787
+ borderInlineStartWidth: "hairline",
788
+ borderTopWidth: "hairline"
789
+ },
790
+ content: {
791
+ ...motion("slide-fade.in", "slide-fade.out"),
792
+ display: "flex",
793
+ flexDirection: "column",
794
+ maxBlockSize: "var(--available-height)",
795
+ minInlineSize: "44",
796
+ outline: "0",
797
+ [RING_STYLE]: "none",
798
+ zIndex: `calc({zIndex.popover} + var(${MENU_DEPTH}, 0))`
799
+ },
800
+ contextTrigger: { cursor: "menuitem" },
801
+ indicator: {
802
+ _rtl: { scale: "-1 1" },
803
+ "& > svg": { boxSize: "100%" },
804
+ alignItems: "center",
805
+ display: "inline-flex",
806
+ flexShrink: "0",
807
+ justifyContent: "center",
808
+ marginInlineStart: "auto"
809
+ },
810
+ item: {
811
+ ...row(),
812
+ "&[data-tone=critical]": {
813
+ color: "colorPalette.fg",
814
+ colorPalette: "error"
815
+ },
816
+ borderRadius: "l1"
817
+ },
818
+ itemCommand: {
819
+ color: "fg.muted",
820
+ flexShrink: "0",
821
+ fontFamily: "inherit",
822
+ letterSpacing: "wide",
823
+ marginInlineStart: "auto"
824
+ },
825
+ itemDescription: {
826
+ ...truncate(),
827
+ textStyle: "caption",
828
+ color: "fg.subtle",
829
+ display: "block"
830
+ },
831
+ itemGroup: {
832
+ display: "flex",
833
+ flexDirection: "column"
834
+ },
835
+ itemGroupLabel: {
836
+ color: "fg.subtle",
837
+ fontWeight: "medium"
838
+ },
839
+ itemIndicator: {
840
+ "&[data-state=checked]": { visibility: "visible" },
841
+ alignItems: "center",
842
+ boxSizing: "content-box",
843
+ display: "inline-flex",
844
+ flexShrink: "0",
845
+ justifyContent: "center",
846
+ marginInlineStart: "auto",
847
+ order: "1",
848
+ visibility: "hidden"
849
+ },
850
+ itemLines: {
851
+ display: "flex",
852
+ flex: "1",
853
+ flexDirection: "column",
854
+ minInlineSize: "0"
855
+ },
856
+ itemMark: {
857
+ alignItems: "center",
858
+ color: "fg.muted",
859
+ display: "inline-flex",
860
+ flexShrink: "0",
861
+ fontWeight: "semibold",
862
+ justifyContent: "center",
863
+ lineHeight: "tight",
864
+ overflow: "clip"
865
+ },
866
+ itemText: {
867
+ ...truncate(),
868
+ flex: "1"
869
+ },
870
+ positioner: { position: "relative" },
871
+ root: { display: "contents" },
872
+ rows: {
873
+ display: "flex",
874
+ flexDirection: "column"
875
+ },
876
+ separator: {
877
+ ...divider(),
878
+ borderColor: "border.muted",
879
+ inlineSize: "auto"
880
+ },
881
+ trigger: { ...interactive() },
882
+ triggerItem: {
883
+ ...row(),
884
+ borderRadius: "l1"
885
+ },
886
+ viewport: { overscrollBehavior: "contain" }
887
+ },
888
+ className: CLASS$2,
889
+ defaultVariants: {
890
+ highlight: "tint",
891
+ palette: "neutral",
892
+ size: "md",
893
+ variant: "surface"
894
+ },
895
+ jsx: [/^Menu(\.\w+)?$/u],
896
+ slots: [
897
+ "root",
898
+ "trigger",
899
+ "contextTrigger",
900
+ "triggerItem",
901
+ "indicator",
902
+ "positioner",
903
+ "content",
904
+ "viewport",
905
+ "rows",
906
+ "itemGroup",
907
+ "itemGroupLabel",
908
+ "item",
909
+ "itemMark",
910
+ "itemLines",
911
+ "itemText",
912
+ "itemDescription",
913
+ "itemIndicator",
914
+ "itemCommand",
915
+ "separator",
916
+ "arrow",
917
+ "arrowTip"
918
+ ],
919
+ staticCss: [{ palette: [...PALETTES] }],
920
+ variants: {
921
+ /**
922
+ * Look of the highlighted row.
923
+ */
924
+ highlight: onSlots({
925
+ item: highlightVariants(),
926
+ triggerItem: highlightVariants()
927
+ }),
928
+ /**
929
+ * Whether a row without a leading icon or mark starts its text after the gutter an icon takes,
930
+ * for a menu where some rows lead with an icon.
931
+ */
932
+ inset: { true: {
933
+ item: { [UNMARKED]: { paddingInlineStart: `var(${GUTTER$1})` } },
934
+ triggerItem: { [UNMARKED]: { paddingInlineStart: `var(${GUTTER$1})` } }
935
+ } },
936
+ /**
937
+ * Palette of the rows and the highlight, set on the panel, which a portalled panel keeps.
938
+ */
939
+ palette: onSlot("content", paletteVariants()),
940
+ /**
941
+ * Size of the panel's padding, the rows, the marks and the keys.
942
+ */
943
+ size: onSlots({
944
+ /**
945
+ * The margin that moves a submenu off its parent's panel.
946
+ */
947
+ content: sizeVariants((size) => cleared(dense(`{spacing.gap.${below(size)}}`)), SIZES$2),
948
+ /**
949
+ * The indicator at the icon size one size smaller, the same square as a checked row's mark.
950
+ */
951
+ indicator: sizeVariants((size) => ({ boxSize: dense(`{sizes.icon.${below(size)}}`) }), SIZES$2),
952
+ item: sizeVariants((size) => rowOf(size), SIZES$2),
953
+ itemCommand: sizeVariants((size) => ({
954
+ textStyle: `body.${below(below(size))}`,
955
+ paddingInlineStart: dense(`{spacing.inset.${size}}`)
956
+ }), SIZES$2),
957
+ itemGroupLabel: sizeVariants((size) => ({
958
+ textStyle: `body.${below(below(size))}`,
959
+ paddingBlock: dense(`{spacing.gap.${below(below(size))}}`),
960
+ paddingInline: dense(`{spacing.inset.${below(size)}}`)
961
+ }), SIZES$2),
962
+ itemIndicator: sizeVariants((size) => ({
963
+ boxSize: dense(`{sizes.icon.${below(size)}}`),
964
+ paddingInlineStart: dense(`{spacing.gap.${size}}`)
965
+ }), SIZES$2),
966
+ itemMark: sizeVariants((size) => ({
967
+ boxSize: dense(`{sizes.tag.${size}}`),
968
+ fontSize: below(below(size))
969
+ }), SIZES$2),
970
+ /**
971
+ * The rows' padding inside the scroll area, which the separators' negative margins cross.
972
+ */
973
+ rows: sizeVariants((size) => ({ padding: dense(`{spacing.gap.${below(size)}}`) }), SIZES$2),
974
+ separator: sizeVariants((size) => ({
975
+ marginBlock: dense(`{spacing.gap.${below(size)}}`),
976
+ marginInline: `calc(-1 * ${dense(`{spacing.gap.${below(size)}}`)})`
977
+ }), SIZES$2),
978
+ triggerItem: sizeVariants((size) => rowOf(size), SIZES$2),
979
+ /**
980
+ * The scroll padding that keeps a revealed row the rows' padding from the edge.
981
+ */
982
+ viewport: sizeVariants((size) => ({ scrollPadding: dense(`{spacing.gap.${below(size)}}`) }), SIZES$2)
983
+ }),
984
+ /**
985
+ * Surface of the panel: the popover surface inside a hairline edge, the panel surface with a
986
+ * large shadow, or the glass layer style.
987
+ *
988
+ * @remarks
989
+ * The elevated panel has a transparent hairline edge, which forced colors paint in
990
+ * `CanvasText`, so the panel keeps its outline where the shadow is not drawn.
991
+ */
992
+ variant: {
993
+ elevated: {
994
+ arrowTip: { borderColor: SURFACE },
995
+ content: {
996
+ "--menu-surface": "colors.bg.panel",
997
+ background: SURFACE,
998
+ borderColor: "transparent",
999
+ borderRadius: "l2",
1000
+ borderStyle: "solid",
1001
+ borderWidth: "hairline",
1002
+ boxShadow: "lg"
1003
+ }
1004
+ },
1005
+ glass: {
1006
+ arrowTip: { borderColor: "border" },
1007
+ content: {
1008
+ layerStyle: "glass",
1009
+ "--menu-surface": "colors.bg.popover",
1010
+ borderRadius: "l2"
1011
+ }
1012
+ },
1013
+ surface: {
1014
+ arrowTip: { borderColor: "border" },
1015
+ content: {
1016
+ "--menu-surface": "colors.bg.popover",
1017
+ background: SURFACE,
1018
+ borderColor: "border",
1019
+ borderRadius: "l2",
1020
+ borderWidth: "hairline",
1021
+ boxShadow: "md"
1022
+ }
1023
+ }
1024
+ }
1025
+ }
1026
+ });
1027
+ //#endregion
1028
+ //#region src/menubar/recipe.ts
1029
+ /**
1030
+ * Styles the menubar's root, which measures its row, its bar of names, a name, and the trigger a
1031
+ * crowded bar folds into.
1032
+ *
1033
+ * @remarks
1034
+ * A name is drawn as a menu's row: no fill at rest, the palette's subtle role under the pointer,
1035
+ * and its muted role while its menu is open, the fill a highlighted row takes, with a row's
1036
+ * padding and text at the bar's size, so the names read as the rows they open. The root sets the
1037
+ * palette the names read, which the root passes to its menus as well. While the root marks its
1038
+ * row `data-crowded`, the bar is hidden and the fold trigger shows in its place. The panels are
1039
+ * the menu recipe's, so the bar states no panel. Under forced colors an open name fills with
1040
+ * `Highlight`, under the pointer too.
1041
+ */
1042
+ /**
1043
+ * Class name of the recipe, from which the binding writes each part's class.
1044
+ */
1045
+ const CLASS$1 = "menubar";
1046
+ /**
1047
+ * Sizes the bar offers, which its menus take too.
1048
+ */
1049
+ const SIZES$1 = [
1050
+ "sm",
1051
+ "md",
1052
+ "lg"
1053
+ ];
1054
+ /**
1055
+ * Selects the bar of a root that marks its row crowded.
1056
+ */
1057
+ const CROWDED = "[data-crowded] > &";
1058
+ /**
1059
+ * Selects the fold trigger of a root that marks its row crowded.
1060
+ *
1061
+ * @remarks
1062
+ * The fold trigger is a child of its menu's root, a `div` with `display: contents`, so the
1063
+ * crowded root is its grandparent.
1064
+ */
1065
+ const FOLDED = "[data-crowded] > * > &";
1066
+ /**
1067
+ * Forced colors of an open name: the system highlight, which the browser keeps.
1068
+ */
1069
+ const FORCED = { _highContrast: {
1070
+ background: "Highlight",
1071
+ color: "HighlightText",
1072
+ forcedColorAdjust: "none"
1073
+ } };
1074
+ /**
1075
+ * Returns the styles a name shares with the fold trigger, which draw it as a menu's row that fills
1076
+ * under the pointer and while its menu is open.
1077
+ *
1078
+ * @remarks
1079
+ * The compiler emits the hover rule after the forced-colors rule, so the open state's forced
1080
+ * colors are restated inside the hover rule, where the open selector raises their specificity.
1081
+ */
1082
+ function named() {
1083
+ return {
1084
+ ...interactive(),
1085
+ _hover: {
1086
+ _open: FORCED,
1087
+ background: "colorPalette.subtle"
1088
+ },
1089
+ _open: {
1090
+ ...FORCED,
1091
+ background: "colorPalette.muted"
1092
+ },
1093
+ alignItems: "center",
1094
+ appearance: "none",
1095
+ background: "transparent",
1096
+ borderRadius: "l1",
1097
+ borderWidth: "0",
1098
+ color: "fg",
1099
+ display: "inline-flex",
1100
+ flexShrink: "0",
1101
+ fontWeight: "medium",
1102
+ whiteSpace: "nowrap"
1103
+ };
1104
+ }
1105
+ /**
1106
+ * Returns the padding, gap and text of a menu's row at a size, and a leading icon one size smaller
1107
+ * on the icon scale, which a name and the fold trigger take.
1108
+ *
1109
+ * @param size - The size of the bar.
1110
+ */
1111
+ function sized(size) {
1112
+ return {
1113
+ textStyle: `body.${below(size)}`,
1114
+ "& > svg": {
1115
+ boxSize: dense(`{sizes.icon.${below(size)}}`),
1116
+ flexShrink: "0"
1117
+ },
1118
+ gap: dense(`{spacing.gap.${size}}`),
1119
+ paddingBlock: dense(`{spacing.gap.${below(size)}}`),
1120
+ paddingInline: dense(`{spacing.inset.${below(size)}}`)
1121
+ };
1122
+ }
1123
+ /**
1124
+ * Defines the menubar recipe, in the neutral palette at size `md` by default.
1125
+ */
1126
+ const recipe$6 = defineSlotRecipe({
1127
+ base: {
1128
+ bar: {
1129
+ alignItems: "center",
1130
+ [CROWDED]: { display: "none" }
1131
+ },
1132
+ fold: {
1133
+ ...named(),
1134
+ display: "none",
1135
+ [FOLDED]: { display: "inline-flex" }
1136
+ },
1137
+ root: {
1138
+ display: "flex",
1139
+ maxInlineSize: "full",
1140
+ minInlineSize: "0"
1141
+ },
1142
+ trigger: named()
1143
+ },
1144
+ className: CLASS$1,
1145
+ defaultVariants: {
1146
+ palette: "neutral",
1147
+ size: "md"
1148
+ },
1149
+ jsx: [/^Menubar(\.\w+)?$/u],
1150
+ slots: [
1151
+ "root",
1152
+ "bar",
1153
+ "trigger",
1154
+ "fold"
1155
+ ],
1156
+ staticCss: [{ palette: [...PALETTES] }],
1157
+ variants: {
1158
+ /**
1159
+ * Palette the names fill in under the pointer and while their menu is open, set on the root.
1160
+ */
1161
+ palette: onSlot("root", paletteVariants()),
1162
+ /**
1163
+ * Padding and text of the names and the fold trigger, and the gap between the names.
1164
+ */
1165
+ size: onSlots({
1166
+ bar: sizeVariants((size) => ({ gap: dense(`{spacing.gap.${below(size)}}`) }), SIZES$1),
1167
+ fold: sizeVariants(sized, SIZES$1),
1168
+ trigger: sizeVariants(sized, SIZES$1)
1169
+ })
1170
+ }
1171
+ });
1172
+ //#endregion
1173
+ //#region src/popover/recipe.ts
1174
+ /**
1175
+ * Recipe for the popover: a panel beside its trigger, with a heading, a description, a close
1176
+ * button and an arrow.
1177
+ *
1178
+ * @remarks
1179
+ * The machine has no root part. The recipe adds a root with `display: contents`, because the
1180
+ * trigger and the positioner are siblings and a slot recipe passes its variants from an element
1181
+ * above both. The machine writes the positioner's position inline, so the recipe sets no
1182
+ * position. The panel scales from the machine's `--transform-origin` and is at least as wide as
1183
+ * the trigger, `--reference-width`. The title and the description read the body role at the
1184
+ * panel's size, the title in the semibold weight, because a panel beside a control does not
1185
+ * start a section of the page. The weight is a compound over every size, because the size's
1186
+ * text style states the role's weight in the variants layer, which applies over the base.
1187
+ * The trigger takes a control's cursor, focus ring and disabled look, and no fill, edge or
1188
+ * padding, so a caller passes a button through `as`. The recipe has no `palette` axis, because
1189
+ * every look uses a neutral surface, and no `effect` axis, because a panel is not a control.
1190
+ */
1191
+ /**
1192
+ * Custom property the size axis sets to the room the title leaves for the close trigger.
1193
+ *
1194
+ * @remarks
1195
+ * The close trigger is positioned absolutely, so a long title ran under it. The size axis sets
1196
+ * the room once, and the title reads it as its inline-end padding.
1197
+ */
1198
+ const CLOSED = "--popover-closed";
1199
+ /**
1200
+ * Defines the popover recipe: the surface look at size `md` by default.
1201
+ */
1202
+ const recipe$5 = defineSlotRecipe({
1203
+ base: {
1204
+ arrow: {
1205
+ "--arrow-background": "var(--popover-surface)",
1206
+ "--arrow-size": "sizes.icon.sm"
1207
+ },
1208
+ arrowTip: {
1209
+ borderInlineStartWidth: "hairline",
1210
+ borderTopWidth: "hairline"
1211
+ },
1212
+ closeTrigger: {
1213
+ ...interactive(),
1214
+ _hover: { color: "fg" },
1215
+ alignItems: "center",
1216
+ borderRadius: "l1",
1217
+ color: "fg.muted",
1218
+ display: "inline-flex",
1219
+ insetBlockStart: "0",
1220
+ insetInlineEnd: "0",
1221
+ justifyContent: "center",
1222
+ position: "absolute"
1223
+ },
1224
+ content: {
1225
+ ...motion("scale-fade.in", "scale-fade.out"),
1226
+ _focusVisible: { focusVisibleRing: "outside" },
1227
+ display: "flex",
1228
+ flexDirection: "column",
1229
+ gap: dense("{spacing.gap.sm}"),
1230
+ minInlineSize: "var(--reference-width)",
1231
+ position: "relative",
1232
+ transformOrigin: "var(--transform-origin)",
1233
+ zIndex: "popover"
1234
+ },
1235
+ description: { color: "fg.muted" },
1236
+ indicator: {
1237
+ _motionReduce: { transitionDuration: "0s" },
1238
+ _open: { rotate: "180deg" },
1239
+ transitionDuration: "press"
1240
+ },
1241
+ positioner: { position: "relative" },
1242
+ root: { display: "contents" },
1243
+ title: { paddingInlineEnd: `var(${CLOSED})` },
1244
+ trigger: {
1245
+ ...interactive(),
1246
+ alignItems: "center",
1247
+ display: "inline-flex",
1248
+ gap: dense("{spacing.gap.xs}")
1249
+ }
1250
+ },
1251
+ className: "popover",
1252
+ compoundVariants: [{
1253
+ css: { title: { fontWeight: "semibold" } },
1254
+ name: "titled",
1255
+ size: [
1256
+ "xs",
1257
+ "sm",
1258
+ "md",
1259
+ "lg",
1260
+ "xl"
1261
+ ]
1262
+ }],
1263
+ defaultVariants: {
1264
+ size: "md",
1265
+ variant: "surface"
1266
+ },
1267
+ jsx: [/^Popover(\.\w+)?$/u],
1268
+ slots: [
1269
+ "root",
1270
+ "anchor",
1271
+ "trigger",
1272
+ "indicator",
1273
+ "positioner",
1274
+ "content",
1275
+ "title",
1276
+ "description",
1277
+ "closeTrigger",
1278
+ "arrow",
1279
+ "arrowTip"
1280
+ ],
1281
+ variants: {
1282
+ /**
1283
+ * Padding of the panel, text of the title and the description, and size of the close trigger.
1284
+ */
1285
+ size: onSlots({
1286
+ /**
1287
+ * Close trigger: a square two sizes smaller on the control scale, with a mark one size
1288
+ * smaller on the icon scale, inset from the panel's corner by the panel's padding.
1289
+ */
1290
+ closeTrigger: sizeVariants((size) => ({
1291
+ "& > svg": { boxSize: dense(`{sizes.icon.${below(size)}}`) },
1292
+ boxSize: dense(`{sizes.control.${below(below(size))}}`),
1293
+ insetBlockStart: dense(`{spacing.inset.${size}}`),
1294
+ insetInlineEnd: dense(`{spacing.inset.${size}}`)
1295
+ })),
1296
+ content: sizeVariants((size) => ({
1297
+ [CLOSED]: `calc(${dense(`{sizes.control.${below(below(size))}}`)} + ${dense(`{spacing.gap.${size}}`)})`,
1298
+ padding: dense(`{spacing.inset.${size}}`)
1299
+ })),
1300
+ description: textSizes("body"),
1301
+ title: textSizes("body")
1302
+ }),
1303
+ /**
1304
+ * Surface of the panel: the popover surface inside a hairline edge, the panel surface with a
1305
+ * large shadow, or the glass layer style.
1306
+ *
1307
+ * @remarks
1308
+ * The elevated panel has a transparent hairline edge, which forced colors paint in
1309
+ * `CanvasText`, so the panel keeps its outline where the shadow is not drawn.
1310
+ */
1311
+ variant: {
1312
+ elevated: {
1313
+ arrowTip: { borderColor: "var(--popover-surface)" },
1314
+ content: {
1315
+ "--popover-surface": "colors.bg.panel",
1316
+ background: "var(--popover-surface)",
1317
+ borderColor: "transparent",
1318
+ borderRadius: "l3",
1319
+ borderStyle: "solid",
1320
+ borderWidth: "hairline",
1321
+ boxShadow: "xl"
1322
+ }
1323
+ },
1324
+ glass: {
1325
+ arrowTip: { borderColor: "border" },
1326
+ content: {
1327
+ layerStyle: "glass",
1328
+ "--popover-surface": "colors.bg.popover",
1329
+ borderRadius: "l3"
1330
+ }
1331
+ },
1332
+ surface: {
1333
+ arrowTip: { borderColor: "border" },
1334
+ content: {
1335
+ "--popover-surface": "colors.bg.popover",
1336
+ background: "var(--popover-surface)",
1337
+ borderColor: "border",
1338
+ borderRadius: "l3",
1339
+ borderWidth: "hairline",
1340
+ boxShadow: "lg"
1341
+ }
1342
+ }
1343
+ }
1344
+ }
1345
+ });
1346
+ //#endregion
1347
+ //#region src/steps/recipe.ts
1348
+ /**
1349
+ * Recipe for the steps: a list of numbered discs with a title, the rules between them, the content
1350
+ * of the current step and the buttons that move between steps.
1351
+ *
1352
+ * @remarks
1353
+ * The machine writes `data-orientation` on the root, the list, the items, the triggers and the
1354
+ * rules, so a vertical flow is a column of steps beside the content and a horizontal flow is a
1355
+ * row above it. The machine marks a disc and a rule with `data-complete`, `data-current` or
1356
+ * `data-incomplete`, and the looks read those. The size writes the disc's side and the gap around
1357
+ * it as custom properties on the root, which the rules read to meet the discs. A rule is a
1358
+ * border, so forced colors paint it in `CanvasText`. The recipe has no `effect` axis, because
1359
+ * the discs change on every step and an effect would move with them.
1360
+ */
1361
+ /**
1362
+ * Sizes the recipe offers.
1363
+ */
1364
+ const SIZES = [
1365
+ "sm",
1366
+ "md",
1367
+ "lg"
1368
+ ];
1369
+ /**
1370
+ * Side of a disc, which the root sets per size.
1371
+ */
1372
+ const DISC = "var(--steps-disc)";
1373
+ /**
1374
+ * Room between a disc and the rule beside it, which the root sets per size.
1375
+ */
1376
+ const GUTTER = "var(--steps-gutter)";
1377
+ /**
1378
+ * Thickness of a rule and of an outlined disc's edge.
1379
+ */
1380
+ const THICKNESS = "{borderWidths.indicator}";
1381
+ /**
1382
+ * Selects an item of a list that is too narrow for its steps with the titles beside the discs.
1383
+ */
1384
+ const CROWDED_ITEM = "[data-crowded] > &";
1385
+ /**
1386
+ * Selects a trigger or a rule inside such a list.
1387
+ */
1388
+ const CROWDED_PART = "[data-crowded] &";
1389
+ /**
1390
+ * Selects an item while the list measures its steps at their natural width.
1391
+ */
1392
+ const MEASURED_ITEM = "[data-measuring] > &";
1393
+ /**
1394
+ * Lays an item out as a centred column of its disc over its words, for titles below the discs.
1395
+ */
1396
+ const BELOW_ITEM = {
1397
+ _last: { flex: "1 0 0" },
1398
+ flexDirection: "column",
1399
+ textAlign: "center"
1400
+ };
1401
+ /**
1402
+ * Runs a rule from the centre line of one disc to the next, one gutter clear of both, for titles
1403
+ * below the discs.
1404
+ */
1405
+ const BELOW_SEPARATOR = {
1406
+ insetBlockStart: `calc(${DISC} / 2 - ${THICKNESS} / 2)`,
1407
+ insetInlineEnd: `calc(-50% + ${DISC} / 2 + ${GUTTER})`,
1408
+ insetInlineStart: `calc(50% + ${DISC} / 2 + ${GUTTER})`,
1409
+ marginInlineEnd: "0",
1410
+ position: "absolute"
1411
+ };
1412
+ /**
1413
+ * Stacks a trigger's disc over its words, for titles below the discs.
1414
+ */
1415
+ const BELOW_TRIGGER = {
1416
+ flexDirection: "column",
1417
+ textAlign: "center"
1418
+ };
1419
+ /**
1420
+ * Fills a completed disc with `Highlight` under forced colors, so it is distinct from the outlined
1421
+ * discs around it.
1422
+ */
1423
+ const FORCED_COMPLETE = { _highContrast: {
1424
+ background: "Highlight",
1425
+ borderColor: "Highlight",
1426
+ color: "HighlightText",
1427
+ forcedColorAdjust: "none"
1428
+ } };
1429
+ /**
1430
+ * Rings the current disc with `Highlight` on `Canvas` under forced colors, so it is distinct from
1431
+ * the outlined discs of the later steps.
1432
+ */
1433
+ const FORCED_CURRENT = { _highContrast: {
1434
+ background: "Canvas",
1435
+ borderColor: "Highlight",
1436
+ borderStyle: "solid",
1437
+ borderWidth: THICKNESS,
1438
+ color: "CanvasText",
1439
+ forcedColorAdjust: "none"
1440
+ } };
1441
+ /**
1442
+ * Defines the steps recipe: solid discs at size `md` in the neutral palette with the titles beside
1443
+ * them, by default.
1444
+ */
1445
+ const recipe$4 = defineSlotRecipe({
1446
+ base: {
1447
+ content: {
1448
+ _vertical: { flex: "1" },
1449
+ minInlineSize: "0"
1450
+ },
1451
+ description: { color: "fg.muted" },
1452
+ indicator: {
1453
+ _highContrast: {
1454
+ borderColor: "CanvasText",
1455
+ borderStyle: "solid",
1456
+ borderWidth: "hairline"
1457
+ },
1458
+ alignItems: "center",
1459
+ blockSize: DISC,
1460
+ borderRadius: "full",
1461
+ display: "inline-flex",
1462
+ flexShrink: "0",
1463
+ fontVariantNumeric: "tabular-nums",
1464
+ fontWeight: "medium",
1465
+ inlineSize: DISC,
1466
+ justifyContent: "center",
1467
+ transitionDuration: "press",
1468
+ transitionProperty: "common",
1469
+ transitionTimingFunction: "press"
1470
+ },
1471
+ item: {
1472
+ _horizontal: {
1473
+ _last: { flex: "0 0 auto" },
1474
+ alignItems: "center",
1475
+ flex: "1 0 0",
1476
+ [MEASURED_ITEM]: { flex: "0 0 auto" }
1477
+ },
1478
+ _vertical: { alignItems: "flex-start" },
1479
+ display: "flex",
1480
+ gap: GUTTER,
1481
+ minInlineSize: "0",
1482
+ position: "relative"
1483
+ },
1484
+ list: {
1485
+ _horizontal: {
1486
+ alignItems: "center",
1487
+ flexDirection: "row"
1488
+ },
1489
+ _vertical: { flexDirection: "column" },
1490
+ display: "flex",
1491
+ listStyle: "none",
1492
+ margin: "0",
1493
+ padding: "0"
1494
+ },
1495
+ root: {
1496
+ _horizontal: { flexDirection: "column" },
1497
+ _vertical: { flexDirection: "row" },
1498
+ display: "flex",
1499
+ inlineSize: "full"
1500
+ },
1501
+ separator: {
1502
+ _horizontal: {
1503
+ borderBlockStartWidth: THICKNESS,
1504
+ flex: "1",
1505
+ marginInlineEnd: GUTTER,
1506
+ minInlineSize: GUTTER
1507
+ },
1508
+ _vertical: {
1509
+ borderInlineStartWidth: THICKNESS,
1510
+ insetBlockEnd: GUTTER,
1511
+ insetBlockStart: `calc(${DISC} + ${GUTTER})`,
1512
+ insetInlineStart: `calc(${DISC} / 2 - ${THICKNESS} / 2)`,
1513
+ position: "absolute"
1514
+ },
1515
+ borderColor: "border",
1516
+ borderRadius: "full"
1517
+ },
1518
+ status: { srOnly: true },
1519
+ title: {
1520
+ "&[data-incomplete]": { color: "fg.muted" },
1521
+ color: "fg",
1522
+ fontWeight: "medium"
1523
+ },
1524
+ trigger: {
1525
+ focusVisibleRing: "outside",
1526
+ _disabled: { cursor: "disabled" },
1527
+ alignItems: "center",
1528
+ borderRadius: "l2",
1529
+ cursor: "button",
1530
+ display: "flex",
1531
+ focusRingColor: "colorPalette.focusRing",
1532
+ gap: GUTTER,
1533
+ minInlineSize: "0",
1534
+ textAlign: "start",
1535
+ userSelect: "none"
1536
+ }
1537
+ },
1538
+ className: "steps",
1539
+ defaultVariants: {
1540
+ labelPlacement: "beside",
1541
+ palette: "neutral",
1542
+ size: "md",
1543
+ variant: "solid"
1544
+ },
1545
+ jsx: [/^Steps(\.\w+)?$/u],
1546
+ slots: [
1547
+ "root",
1548
+ "list",
1549
+ "item",
1550
+ "trigger",
1551
+ "indicator",
1552
+ "title",
1553
+ "status",
1554
+ "description",
1555
+ "separator",
1556
+ "content",
1557
+ "nextTrigger",
1558
+ "prevTrigger"
1559
+ ],
1560
+ staticCss: [{ palette: [...PALETTES] }],
1561
+ variants: {
1562
+ /**
1563
+ * Place of the titles in a horizontal flow: beside each disc, or centred below it.
1564
+ *
1565
+ * @remarks
1566
+ * Below, every item takes an equal share of the row and stacks its disc over its words in a
1567
+ * centred column, with a trigger or without one, and each rule runs from one disc to the next
1568
+ * across the gap between the items, one gutter clear of both. Beside falls back to below
1569
+ * while the list is too narrow for its steps at their natural width, which the list measures
1570
+ * and marks with `data-crowded`. A vertical flow keeps the titles beside the discs either
1571
+ * way.
1572
+ */
1573
+ labelPlacement: {
1574
+ below: {
1575
+ item: { _horizontal: BELOW_ITEM },
1576
+ separator: { _horizontal: BELOW_SEPARATOR },
1577
+ trigger: { _horizontal: BELOW_TRIGGER }
1578
+ },
1579
+ beside: {
1580
+ item: { _horizontal: { [CROWDED_ITEM]: BELOW_ITEM } },
1581
+ separator: { _horizontal: { [CROWDED_PART]: BELOW_SEPARATOR } },
1582
+ trigger: { _horizontal: { [CROWDED_PART]: BELOW_TRIGGER } }
1583
+ }
1584
+ },
1585
+ /**
1586
+ * Palette the discs and the completed rules read, set on the root.
1587
+ */
1588
+ palette: onSlot("root", paletteVariants()),
1589
+ /**
1590
+ * Size of the discs on the control scale, the titles, the descriptions and the room around
1591
+ * them.
1592
+ *
1593
+ * @remarks
1594
+ * The discs measure 36, 40 and 44px. An icon in a disc is on the icon scale one step below
1595
+ * the size. In a vertical flow every item but the last leaves two insets below its trigger,
1596
+ * so the rule under a disc with a title and a description is 21px long at `md`.
1597
+ */
1598
+ size: onSlots({
1599
+ description: sizeVariants((size) => ({ textStyle: `body.${below(size)}` }), SIZES),
1600
+ indicator: sizeVariants((size) => ({
1601
+ textStyle: `label.${size}`,
1602
+ "& > svg": { boxSize: dense(`{sizes.icon.${below(size)}}`) }
1603
+ }), SIZES),
1604
+ item: sizeVariants((size) => ({ _vertical: {
1605
+ _last: { paddingBlockEnd: "0" },
1606
+ paddingBlockEnd: dense(`calc({spacing.inset.${size}} * 2)`)
1607
+ } }), SIZES),
1608
+ root: sizeVariants((size) => ({
1609
+ "--steps-disc": dense(`{sizes.control.${size}}`),
1610
+ "--steps-gutter": dense(`{spacing.gap.${size}}`),
1611
+ gap: dense(`{spacing.inset.${size}}`)
1612
+ }), SIZES),
1613
+ title: sizeVariants((size) => ({ textStyle: `label.${size}` }), SIZES)
1614
+ }),
1615
+ /**
1616
+ * Look of the discs and of the rules between completed steps.
1617
+ *
1618
+ * @remarks
1619
+ * `solid` outlines a later step's disc, rings the current disc and fills a completed one with
1620
+ * the palette's solid color. `subtle` fills every disc from the palette: its subtle role for
1621
+ * a later step, its muted role for a completed one and its emphasized role for the current
1622
+ * one. The three roles step away from the page in both color modes, so the current disc is
1623
+ * the strongest by day and after dark.
1624
+ */
1625
+ variant: {
1626
+ solid: {
1627
+ indicator: {
1628
+ "&[data-complete]": {
1629
+ ...FORCED_COMPLETE,
1630
+ background: "colorPalette.solid",
1631
+ borderColor: "colorPalette.solid",
1632
+ borderStyle: "solid",
1633
+ borderWidth: THICKNESS,
1634
+ color: "colorPalette.contrast"
1635
+ },
1636
+ "&[data-current]": {
1637
+ ...FORCED_CURRENT,
1638
+ background: "colorPalette.subtle",
1639
+ borderColor: "colorPalette.solid",
1640
+ borderStyle: "solid",
1641
+ borderWidth: THICKNESS,
1642
+ color: "colorPalette.fg"
1643
+ },
1644
+ "&[data-incomplete]": {
1645
+ borderColor: "border",
1646
+ borderStyle: "solid",
1647
+ borderWidth: THICKNESS,
1648
+ color: "fg.muted"
1649
+ }
1650
+ },
1651
+ separator: { "&[data-complete]": {
1652
+ _highContrast: {
1653
+ borderColor: "Highlight",
1654
+ forcedColorAdjust: "none"
1655
+ },
1656
+ borderColor: "colorPalette.solid"
1657
+ } }
1658
+ },
1659
+ subtle: {
1660
+ indicator: {
1661
+ "&[data-complete]": {
1662
+ ...FORCED_COMPLETE,
1663
+ background: "colorPalette.muted",
1664
+ color: "colorPalette.fg"
1665
+ },
1666
+ "&[data-current]": {
1667
+ ...FORCED_CURRENT,
1668
+ background: "colorPalette.emphasized",
1669
+ color: "colorPalette.fg"
1670
+ },
1671
+ "&[data-incomplete]": {
1672
+ background: "colorPalette.subtle",
1673
+ color: "fg.muted"
1674
+ }
1675
+ },
1676
+ separator: { "&[data-complete]": {
1677
+ _highContrast: {
1678
+ borderColor: "Highlight",
1679
+ forcedColorAdjust: "none"
1680
+ },
1681
+ borderColor: "colorPalette.emphasized"
1682
+ } }
1683
+ }
1684
+ }
1685
+ }
1686
+ });
1687
+ //#endregion
1688
+ //#region src/tabs/recipe.ts
1689
+ /**
1690
+ * Recipe for the tabs: a list of tabs, the panel of each, and the indicator under the selected
1691
+ * tab.
1692
+ *
1693
+ * @remarks
1694
+ * The orientation is the machine's option, not an axis. The machine sets `data-orientation` on
1695
+ * every part, so the list turns into a column and the line indicator moves to the inline edge
1696
+ * without a second prop. The machine measures the selected tab into `--width` and `--height` and
1697
+ * positions the indicator, so the recipe sets its size and fill and never its position. Every
1698
+ * tab sits over the indicator, so a filled indicator does not cover the tab's text. The palette
1699
+ * is set on the root, and the line and subtle looks read it. The recipe has no `effect` axis,
1700
+ * because the indicator moves on every selection, and a glow would move with it.
1701
+ */
1702
+ /**
1703
+ * Sizes an indicator that fills the selected tab to the tab's measured width and height.
1704
+ *
1705
+ * @remarks
1706
+ * A line indicator takes one of the two and its own stroke for the other. A filled indicator
1707
+ * takes both. Without them it collapsed to nothing and left the selected tab unmarked.
1708
+ */
1709
+ const FILLED = {
1710
+ height: "var(--height)",
1711
+ width: "var(--width)"
1712
+ };
1713
+ /**
1714
+ * Places a line indicator under the selected tab, or at its inline start in a vertical list.
1715
+ */
1716
+ const BAR = {
1717
+ _horizontal: {
1718
+ bottom: "0",
1719
+ height: "{borderWidths.indicator}",
1720
+ width: "var(--width)"
1721
+ },
1722
+ _vertical: {
1723
+ height: "var(--height)",
1724
+ insetInlineStart: "0",
1725
+ width: "{borderWidths.indicator}"
1726
+ }
1727
+ };
1728
+ /**
1729
+ * Paints a bar in the system's selection colour under forced colors.
1730
+ *
1731
+ * @remarks
1732
+ * Forced colors replace every author background with `Canvas`, so a filled bar vanishes and the
1733
+ * selected tab keeps no mark. A system colour with `forcedColorAdjust: none` keeps the bar.
1734
+ */
1735
+ const FORCED_BAR = {
1736
+ background: "Highlight",
1737
+ forcedColorAdjust: "none"
1738
+ };
1739
+ /**
1740
+ * Defines the tabs recipe: a line of tabs at size `md` by default.
1741
+ */
1742
+ const recipe$3 = defineSlotRecipe({
1743
+ base: {
1744
+ closeTrigger: {
1745
+ _hover: {
1746
+ background: "colorPalette.muted",
1747
+ color: "fg"
1748
+ },
1749
+ "& > svg": {
1750
+ blockSize: "1em",
1751
+ inlineSize: "1em"
1752
+ },
1753
+ alignItems: "center",
1754
+ borderRadius: "l1",
1755
+ color: "fg.muted",
1756
+ display: "inline-flex",
1757
+ flexShrink: "0",
1758
+ justifyContent: "center",
1759
+ marginInline: "calc((1em - {sizes.6}) / 2)",
1760
+ minBlockSize: "6",
1761
+ minInlineSize: "6"
1762
+ },
1763
+ content: {
1764
+ _focusVisible: { focusVisibleRing: "outside" },
1765
+ outline: "none"
1766
+ },
1767
+ indicator: {
1768
+ borderRadius: "l1",
1769
+ pointerEvents: "none",
1770
+ zIndex: "0"
1771
+ },
1772
+ list: {
1773
+ _horizontal: { flexDirection: "row" },
1774
+ _vertical: { flexDirection: "column" },
1775
+ display: "flex",
1776
+ position: "relative"
1777
+ },
1778
+ root: {
1779
+ _horizontal: { flexDirection: "column" },
1780
+ _vertical: { flexDirection: "row" },
1781
+ display: "flex",
1782
+ width: "full"
1783
+ },
1784
+ trigger: {
1785
+ ...interactive(),
1786
+ _disabled: { cursor: "disabled" },
1787
+ _vertical: { justifyContent: "flex-start" },
1788
+ "& > svg": {
1789
+ blockSize: "1em",
1790
+ flexShrink: "0",
1791
+ inlineSize: "1em"
1792
+ },
1793
+ alignItems: "center",
1794
+ display: "inline-flex",
1795
+ justifyContent: "center",
1796
+ position: "relative",
1797
+ whiteSpace: "nowrap",
1798
+ zIndex: "1"
1799
+ }
1800
+ },
1801
+ className: "tabs",
1802
+ defaultVariants: {
1803
+ size: "md",
1804
+ variant: "line"
1805
+ },
1806
+ jsx: [/^Tabs(\.\w+)?$/u],
1807
+ slots: [
1808
+ "root",
1809
+ "list",
1810
+ "trigger",
1811
+ "closeTrigger",
1812
+ "content",
1813
+ "indicator"
1814
+ ],
1815
+ staticCss: [{ palette: [...PALETTES] }],
1816
+ variants: {
1817
+ /**
1818
+ * Whether the tabs share the list's width equally.
1819
+ */
1820
+ fitted: { true: { trigger: { flex: "1" } } },
1821
+ /**
1822
+ * Distribution of the tabs in a list wider than they are.
1823
+ */
1824
+ justify: onSlot("list", justifyVariants()),
1825
+ /**
1826
+ * Palette of the line indicator, the subtle indicator and the selected tab's text, set on the
1827
+ * root.
1828
+ */
1829
+ palette: onSlot("root", paletteVariants()),
1830
+ /**
1831
+ * Size of the tabs on the control scale and the panel's padding on the inset scale.
1832
+ */
1833
+ size: onSlots({
1834
+ content: insetSizes(),
1835
+ trigger: controlSizes()
1836
+ }),
1837
+ /**
1838
+ * Look of the list and the indicator of the selected tab.
1839
+ */
1840
+ variant: {
1841
+ enclosed: {
1842
+ indicator: {
1843
+ ...FILLED,
1844
+ background: "bg.panel",
1845
+ borderColor: "border",
1846
+ borderWidth: "hairline"
1847
+ },
1848
+ list: {
1849
+ background: "bg.muted",
1850
+ borderRadius: "l2",
1851
+ padding: dense("{spacing.inset.xs}")
1852
+ },
1853
+ trigger: {
1854
+ _selected: { color: "fg" },
1855
+ borderRadius: "l1",
1856
+ color: "fg.muted"
1857
+ }
1858
+ },
1859
+ line: {
1860
+ indicator: {
1861
+ ...BAR,
1862
+ _highContrast: FORCED_BAR,
1863
+ background: "colorPalette.solid"
1864
+ },
1865
+ list: {
1866
+ _horizontal: {
1867
+ borderBlockEndColor: "border",
1868
+ borderBlockEndWidth: "hairline"
1869
+ },
1870
+ _vertical: {
1871
+ borderInlineEndColor: "border",
1872
+ borderInlineEndWidth: "hairline"
1873
+ }
1874
+ },
1875
+ trigger: {
1876
+ _hover: { color: "fg" },
1877
+ _selected: { color: "colorPalette.fg" },
1878
+ color: "fg.muted"
1879
+ }
1880
+ },
1881
+ plain: {
1882
+ indicator: {
1883
+ ...BAR,
1884
+ _highContrast: {
1885
+ ...FORCED_BAR,
1886
+ display: "block"
1887
+ },
1888
+ display: "none"
1889
+ },
1890
+ trigger: {
1891
+ _selected: { color: "fg" },
1892
+ color: "fg.muted"
1893
+ }
1894
+ },
1895
+ subtle: {
1896
+ indicator: {
1897
+ ...FILLED,
1898
+ _highContrast: {
1899
+ outlineColor: "Highlight",
1900
+ outlineOffset: "calc({borderWidths.indicator} * -1)",
1901
+ outlineStyle: "solid",
1902
+ outlineWidth: "{borderWidths.indicator}"
1903
+ },
1904
+ background: "colorPalette.subtle",
1905
+ borderRadius: "l1"
1906
+ },
1907
+ trigger: {
1908
+ _selected: { color: "colorPalette.fg" },
1909
+ color: "fg.muted"
1910
+ }
1911
+ }
1912
+ }
1913
+ }
1914
+ });
1915
+ //#endregion
1916
+ //#region src/toggle-tip/recipe.ts
1917
+ /**
1918
+ * Recipe for the toggle tip: a short note that a press opens beside its trigger, with an arrow.
1919
+ *
1920
+ * @remarks
1921
+ * The toggle tip looks like the tooltip and opens like the popover, so the recipe repeats the
1922
+ * tooltip's looks and sizes and gives the trigger a control's cursor, focus ring and disabled
1923
+ * look. The machine has no root part. The recipe adds a root with `display: contents`, because
1924
+ * the trigger and the positioner are siblings and a slot recipe passes its variants from an
1925
+ * element above both. The machine writes the positioner's position inline, so the recipe sets no
1926
+ * position. The content takes the `tooltip` z-index, so a tip opened inside a popover renders
1927
+ * above it, and reads the label role. It wraps at 20rem, or at the room the machine measures on
1928
+ * its side of the trigger, `--available-width`, where that is less. A link inside the content
1929
+ * takes the content's ink and an underline, because the link ink measures 1.71:1 in light mode
1930
+ * and 1.38:1 in dark mode on the inverted surface of the ink theme. Each look sets
1931
+ * `--toggle-tip-surface` once, and the arrow tip reads it. The inverted look has a transparent
1932
+ * hairline edge, which forced colors paint in `CanvasText`. The recipe has no `palette` axis,
1933
+ * because both looks use neutral surfaces, and no `effect` axis, because a note is not a
1934
+ * control.
1935
+ */
1936
+ /**
1937
+ * Defines the toggle tip recipe: an inverted tip at size `md` by default.
1938
+ */
1939
+ const recipe$2 = defineSlotRecipe({
1940
+ base: {
1941
+ arrow: {
1942
+ "--arrow-background": "var(--toggle-tip-surface)",
1943
+ "--arrow-size": "sizes.icon.xs"
1944
+ },
1945
+ arrowTip: {
1946
+ borderInlineStartWidth: "hairline",
1947
+ borderTopWidth: "hairline"
1948
+ },
1949
+ content: {
1950
+ ...motion("scale-fade.in", "scale-fade.out"),
1951
+ "& a": {
1952
+ color: "inherit",
1953
+ textDecoration: "underline"
1954
+ },
1955
+ background: "var(--toggle-tip-surface)",
1956
+ borderRadius: "l2",
1957
+ boxShadow: "md",
1958
+ fontWeight: "medium",
1959
+ maxInlineSize: "min({sizes.xs}, var(--available-width, {sizes.xs}))",
1960
+ textWrap: "pretty",
1961
+ transformOrigin: "var(--transform-origin)",
1962
+ zIndex: "tooltip"
1963
+ },
1964
+ positioner: { position: "relative" },
1965
+ root: { display: "contents" },
1966
+ trigger: {
1967
+ ...interactive(),
1968
+ alignItems: "center",
1969
+ display: "inline-flex"
1970
+ }
1971
+ },
1972
+ className: "toggle-tip",
1973
+ defaultVariants: {
1974
+ size: "md",
1975
+ variant: "inverted"
1976
+ },
1977
+ jsx: [/^ToggleTip(\.\w+)?$/u],
1978
+ slots: [
1979
+ "root",
1980
+ "trigger",
1981
+ "positioner",
1982
+ "content",
1983
+ "arrow",
1984
+ "arrowTip"
1985
+ ],
1986
+ variants: {
1987
+ /**
1988
+ * Padding of the content on the inset scale and its text on the label role.
1989
+ */
1990
+ size: onSlot("content", sizeVariants((size) => ({
1991
+ textStyle: `label.${size}`,
1992
+ padding: dense(`{spacing.inset.${size}}`)
1993
+ }))),
1994
+ /**
1995
+ * Surface of the content: the inverted surface, or the popover surface inside a hairline edge.
1996
+ */
1997
+ variant: {
1998
+ inverted: {
1999
+ arrowTip: { borderColor: "var(--toggle-tip-surface)" },
2000
+ content: {
2001
+ "--toggle-tip-surface": "colors.bg.inverted",
2002
+ borderColor: "transparent",
2003
+ borderStyle: "solid",
2004
+ borderWidth: "hairline",
2005
+ color: "fg.inverted"
2006
+ }
2007
+ },
2008
+ surface: {
2009
+ arrowTip: { borderColor: "border" },
2010
+ content: {
2011
+ "--toggle-tip-surface": "colors.bg.popover",
2012
+ borderColor: "border",
2013
+ borderWidth: "hairline",
2014
+ color: "fg"
2015
+ }
2016
+ }
2017
+ }
2018
+ }
2019
+ });
2020
+ //#endregion
2021
+ //#region src/tooltip/recipe.ts
2022
+ /**
2023
+ * Recipe for the tooltip: a short label beside its trigger, with an arrow.
2024
+ *
2025
+ * @remarks
2026
+ * The machine has no root part. The recipe adds a root with `display: contents`, because the
2027
+ * trigger and the positioner are siblings and a slot recipe passes its variants from an element
2028
+ * above both. The machine measures the trigger and writes the positioner's position inline, so
2029
+ * the recipe sets no position. The content scales from the `--transform-origin` the machine sets.
2030
+ * The content takes the `tooltip` z-index. The machine writes `z-index: var(--z-index)` inline on
2031
+ * the positioner from the content's computed value, so a z-index on the positioner has no effect.
2032
+ * Each look sets `--tooltip-surface` once, and the arrow tip reads it. The content reads the
2033
+ * label role and wraps at `maxWidth: xs`. The trigger has no styles, because the caller passes
2034
+ * the control through `as`. The recipe has no `palette` axis, because both looks use neutral
2035
+ * surfaces, and no `effect` axis, because a tooltip is not a control.
2036
+ */
2037
+ /**
2038
+ * Defines the tooltip recipe: an inverted tooltip at size `md` by default.
2039
+ */
2040
+ const recipe$1 = defineSlotRecipe({
2041
+ base: {
2042
+ arrow: {
2043
+ "--arrow-background": "var(--tooltip-surface)",
2044
+ "--arrow-size": "sizes.icon.xs"
2045
+ },
2046
+ arrowTip: {
2047
+ borderInlineStartWidth: "hairline",
2048
+ borderTopWidth: "hairline"
2049
+ },
2050
+ content: {
2051
+ ...motion("scale-fade.in", "scale-fade.out"),
2052
+ background: "var(--tooltip-surface)",
2053
+ borderRadius: "l2",
2054
+ boxShadow: "md",
2055
+ fontWeight: "medium",
2056
+ maxWidth: "xs",
2057
+ textWrap: "pretty",
2058
+ transformOrigin: "var(--transform-origin)",
2059
+ zIndex: "tooltip"
2060
+ },
2061
+ positioner: { position: "relative" },
2062
+ root: { display: "contents" }
2063
+ },
2064
+ className: "tooltip",
2065
+ defaultVariants: {
2066
+ size: "md",
2067
+ variant: "inverted"
2068
+ },
2069
+ jsx: [/^Tooltip(\.\w+)?$/u],
2070
+ slots: [
2071
+ "root",
2072
+ "trigger",
2073
+ "positioner",
2074
+ "content",
2075
+ "arrow",
2076
+ "arrowTip"
2077
+ ],
2078
+ variants: {
2079
+ /**
2080
+ * Padding of the content on the inset scale and its text on the label role.
2081
+ */
2082
+ size: onSlot("content", sizeVariants((size) => ({
2083
+ textStyle: `label.${size}`,
2084
+ padding: dense(`{spacing.inset.${size}}`)
2085
+ }))),
2086
+ /**
2087
+ * Surface of the content: the inverted surface, or the popover surface inside a hairline edge.
2088
+ *
2089
+ * @remarks
2090
+ * The inverted content has a transparent hairline edge, which forced colors paint in
2091
+ * `CanvasText`, so the content keeps its outline where its fill turns into the page's.
2092
+ */
2093
+ variant: {
2094
+ inverted: {
2095
+ arrowTip: { borderColor: "var(--tooltip-surface)" },
2096
+ content: {
2097
+ "--tooltip-surface": "colors.bg.inverted",
2098
+ borderColor: "transparent",
2099
+ borderStyle: "solid",
2100
+ borderWidth: "hairline",
2101
+ color: "fg.inverted"
2102
+ }
2103
+ },
2104
+ surface: {
2105
+ arrowTip: { borderColor: "border" },
2106
+ content: {
2107
+ "--tooltip-surface": "colors.bg.popover",
2108
+ borderColor: "border",
2109
+ borderWidth: "hairline",
2110
+ color: "fg"
2111
+ }
2112
+ }
2113
+ }
2114
+ }
2115
+ });
2116
+ /**
2117
+ * Defines the truncate recipe.
2118
+ */
2119
+ const recipe = defineRecipe({
2120
+ base: {
2121
+ focusVisibleRing: "outside",
2122
+ lineClamp: "var(--truncate-lines, 1)",
2123
+ minInlineSize: "0",
2124
+ overflowWrap: "anywhere"
2125
+ },
2126
+ className: "truncate",
2127
+ jsx: ["Truncate"]
2128
+ });
2129
+ //#endregion
2130
+ export { recipe$4 as a, MENU_DEPTH as c, recipe$8 as d, recipe$9 as f, recipe$3 as i, NESTED as l, recipe$11 as m, recipe$1 as n, recipe$5 as o, recipe$10 as p, recipe$2 as r, recipe$6 as s, recipe as t, recipe$7 as u };