@stealthscale/component-screen 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2612 @@
1
+ import { CONTROL_INSET_END, CONTROL_INSET_START, axis, below, cornerVariants, defineSlotRecipe, dense, divider, interactive, lookVariants, motion, onSlot, onSlots, paletteVariants, sizeVariants, surface, truncate } from "@stealthscale/theme/authoring";
2
+ //#region src/action-bar/recipe.ts
3
+ /**
4
+ * Styles the action bar, a panel fixed to the bottom of the window with the actions for what a
5
+ * person has selected.
6
+ *
7
+ * @remarks
8
+ * The root has `display: contents`, because the positioner is fixed to the window and a slot
9
+ * recipe passes its variants from an ancestor. The positioner spans the window inside the inset
10
+ * and takes no pointer events, so the page around the bar remains pressable. The toolbar inside
11
+ * the bar is as wide as the bar, which is at least `sizes.2xl`, 672px. The toolbar folds when it
12
+ * is narrower than the `sm` breakpoint, 640px. The minimum width makes it fold on the width of
13
+ * the window and not on the width of its folded content. The recipe has no `palette` axis,
14
+ * because the bar reads the neutral `bg.panel`, and no `effect` axis, because the bar is not a
15
+ * control.
16
+ */
17
+ /**
18
+ * Distance of the bar from the edges of the window.
19
+ */
20
+ const OFFSET = "{spacing.inset.lg}";
21
+ /**
22
+ * Places the bar may take along the bottom of the window, in the order a line reads them.
23
+ */
24
+ const PLACEMENTS = [
25
+ "bottom-start",
26
+ "bottom",
27
+ "bottom-end"
28
+ ];
29
+ /**
30
+ * Maps each placement to where the positioner puts the bar along its row.
31
+ */
32
+ const ALIGNED = {
33
+ bottom: "center",
34
+ "bottom-end": "flex-end",
35
+ "bottom-start": "flex-start"
36
+ };
37
+ /**
38
+ * Defines the action bar recipe, which places the bar in the middle of the bottom edge by default.
39
+ */
40
+ const recipe$8 = defineSlotRecipe({
41
+ base: {
42
+ closeTrigger: { flexShrink: "0" },
43
+ content: {
44
+ ...motion("sheet.bottom.in", "sheet.bottom.out"),
45
+ background: "bg.panel",
46
+ borderColor: "transparent",
47
+ borderRadius: "l3",
48
+ borderStyle: "solid",
49
+ borderWidth: "hairline",
50
+ boxShadow: "lg",
51
+ color: "fg",
52
+ inlineSize: "fit-content",
53
+ maxInlineSize: "full",
54
+ minInlineSize: "min(100%, {sizes.2xl})",
55
+ padding: dense("{spacing.inset.xs}"),
56
+ pointerEvents: "auto"
57
+ },
58
+ positioner: {
59
+ display: "flex",
60
+ insetBlockEnd: `calc({spacing.safe.bottom} + ${OFFSET})`,
61
+ insetInline: "0",
62
+ paddingInline: OFFSET,
63
+ pointerEvents: "none",
64
+ position: "fixed",
65
+ zIndex: "banner"
66
+ },
67
+ root: { display: "contents" }
68
+ },
69
+ className: "action-bar",
70
+ defaultVariants: { placement: "bottom" },
71
+ jsx: [/^ActionBar(\.\w+)?$/u],
72
+ slots: [
73
+ "root",
74
+ "positioner",
75
+ "content",
76
+ "closeTrigger"
77
+ ],
78
+ variants: {
79
+ /**
80
+ * Place of the bar along the bottom of the window: at the start, in the middle or at the end.
81
+ *
82
+ * @remarks
83
+ * Start and end follow the writing direction.
84
+ */
85
+ placement: onSlot("positioner", axis(PLACEMENTS, (placement) => ({ justifyContent: ALIGNED[placement] }))()) }
86
+ });
87
+ //#endregion
88
+ //#region src/app-shell/metrics.ts
89
+ /**
90
+ * Names the custom properties and attributes the app shell reads, and defines the style objects
91
+ * its recipe is written from.
92
+ *
93
+ * @remarks
94
+ * The module keeps the recipe under the 300-line limit. It imports no React, because the theme
95
+ * plugin evaluates the recipe in Node.
96
+ */
97
+ /**
98
+ * The recipe's class name, used to build the selectors from one part to another.
99
+ *
100
+ * @remarks
101
+ * The binding writes one class per part, such as `app-shell__main`, and no part attribute, so a
102
+ * selector across parts targets the class built from this constant.
103
+ */
104
+ const CLASS$5 = "app-shell";
105
+ /**
106
+ * Custom property with the width a panel opens to.
107
+ */
108
+ const PANEL_SIZE = "--app-shell-panel-size";
109
+ /**
110
+ * Custom property with the width a panel closes to.
111
+ */
112
+ const PANEL_RAIL = "--app-shell-panel-rail";
113
+ /**
114
+ * Custom property with the height of the pinned bars above a pinned bar.
115
+ */
116
+ const STICKY_OFFSET$1 = "--app-shell-sticky-offset";
117
+ /**
118
+ * Custom property on the root with the height of every pinned bar.
119
+ */
120
+ const STICKY_TOP$1 = "--app-shell-sticky-top";
121
+ /**
122
+ * Custom property an ancestor sets to the height of the shell's window.
123
+ *
124
+ * @remarks
125
+ * A box that stages a shell at a fixed height sets it, and also contains the shell's fixed
126
+ * parts with `contain: layout`. Unset, the window is the viewport.
127
+ */
128
+ const WINDOW_HEIGHT = "--app-shell-window-height";
129
+ /**
130
+ * Height of the shell's window: the ancestor's `WINDOW_HEIGHT`, or the dynamic viewport height.
131
+ */
132
+ const WINDOW = `var(${WINDOW_HEIGHT}, 100dvh)`;
133
+ /**
134
+ * Attribute the root writes after its first paint.
135
+ *
136
+ * @remarks
137
+ * A panel measures the shell after its first render and may open, close or leave the body. No
138
+ * part transitions before the root has this attribute, so a panel takes its measured state
139
+ * without a slide on load.
140
+ */
141
+ const SETTLED = "data-settled";
142
+ /**
143
+ * Styles the transition of a moving part, with no motion under reduced motion or before the root
144
+ * has settled.
145
+ */
146
+ const MOVING = {
147
+ _motionReduce: { transitionDuration: "0s" },
148
+ transitionDuration: "move",
149
+ transitionTimingFunction: "move",
150
+ [`.${CLASS$5}__root:not([${SETTLED}]) &`]: { transitionDuration: "0s" }
151
+ };
152
+ /**
153
+ * Width of a panel over the page: its open width, and never closer than a rail's width to the far
154
+ * edge of the window.
155
+ *
156
+ * @remarks
157
+ * The percentage resolves against the sheet's containing block, which is the viewport, or a box
158
+ * with `contain: layout` that stages the shell.
159
+ */
160
+ const SHEET = `min(var(${PANEL_SIZE}), calc(100% - {sizes.rail}))`;
161
+ /**
162
+ * Styles a panel over the page: fixed to the window, a rail's width short of the far edge, and
163
+ * inside the safe area.
164
+ *
165
+ * @remarks
166
+ * The panel is fixed to the window, so one rule applies to both scroll modes. A closed sheet
167
+ * keeps its width and slides out. Its visibility changes with no duration: a closing sheet
168
+ * becomes hidden after a delay as long as the slide, and an opening sheet becomes visible at
169
+ * once. The shell moves focus into a sheet in the commit that opens it, and a browser does not
170
+ * focus a hidden element. A sheet is a raised surface over the backdrop: the panel ground and the
171
+ * `lg` shadow. A sidebar inside it paints its own ground over the panel's.
172
+ */
173
+ const OVERLAID = {
174
+ _motionReduce: { transitionDelay: "0s" },
175
+ background: "bg.panel",
176
+ blockSize: WINDOW,
177
+ boxShadow: "lg",
178
+ inlineSize: SHEET,
179
+ insetBlock: "0",
180
+ paddingBlockEnd: "safe.bottom",
181
+ paddingBlockStart: "safe.top",
182
+ position: "fixed",
183
+ transitionDelay: "0s",
184
+ transitionDuration: "{durations.move}, 0s",
185
+ transitionProperty: "translate, visibility",
186
+ zIndex: "modal"
187
+ };
188
+ /**
189
+ * Styles a closed sheet on either side: it keeps its open width, and the visibility changes after
190
+ * the slide has ended.
191
+ *
192
+ * @remarks
193
+ * The width is written again here, because the panel's closed width is an equally specific rule
194
+ * that the compiler emits after the sheet's own.
195
+ */
196
+ const SLID_OUT = {
197
+ inlineSize: SHEET,
198
+ transitionDelay: "0s, {durations.moderate}"
199
+ };
200
+ /**
201
+ * Styles both panels: a track between the open and closed widths.
202
+ *
203
+ * @remarks
204
+ * A panel closed to nothing becomes hidden after its transition, so a border the application
205
+ * gives it is not visible. The shell sets the visibility, and the application sets the border.
206
+ */
207
+ const PANEL = {
208
+ ...MOVING,
209
+ display: "flex",
210
+ flexShrink: "0",
211
+ inlineSize: `var(${PANEL_SIZE})`,
212
+ overflow: "hidden",
213
+ [PANEL_RAIL]: "0px",
214
+ position: "relative",
215
+ transitionProperty: "inline-size, visibility",
216
+ "&[data-collapse=hide][data-state=closed]": { visibility: "hidden" },
217
+ "&[data-collapse=icons]": { [PANEL_RAIL]: "sizes.rail" },
218
+ "&[data-stacked]": {
219
+ flexBasis: "100%",
220
+ inlineSize: "100%",
221
+ order: "1",
222
+ position: "static"
223
+ },
224
+ "&[data-state=closed]": { inlineSize: `var(${PANEL_RAIL})` }
225
+ };
226
+ /**
227
+ * Styles a bar pinned to the window, filled with `bg.panel`.
228
+ *
229
+ * @remarks
230
+ * The page scrolls under a pinned bar, so the bar has a fill. The fill is `bg.panel`, so the bar
231
+ * reads as a layer over the page.
232
+ */
233
+ const PINNED = {
234
+ background: "bg.panel",
235
+ position: "sticky",
236
+ zIndex: "sticky"
237
+ };
238
+ /**
239
+ * Styles a panel in the body while the window scrolls: stuck under the pinned bars and as tall as
240
+ * the height they leave.
241
+ *
242
+ * @remarks
243
+ * The rule selects a panel beside the page only, because the window-scroll look is a variant and
244
+ * applies over the base, where a sheet is fixed and a panel under the page is static.
245
+ */
246
+ const STUCK$1 = { ["&:not([data-overlaid], [data-stacked])"]: {
247
+ blockSize: `calc(${WINDOW} - var(${STICKY_TOP$1}, 0px))`,
248
+ insetBlockStart: `var(${STICKY_TOP$1}, 0px)`,
249
+ position: "sticky"
250
+ } };
251
+ /**
252
+ * Styles a band of a panel or of the main region: a padded column, growing to the room left with
253
+ * `data-grows`.
254
+ *
255
+ * @remarks
256
+ * A band that scrolls is inside a scroll area whose root is the `scroller`, so the band pads what
257
+ * scrolls and the bar is at the scroller's edge.
258
+ */
259
+ const SECTION = {
260
+ display: "flex",
261
+ flexDirection: "column",
262
+ flexShrink: "0",
263
+ gap: dense("{spacing.gap.md}"),
264
+ minBlockSize: "0",
265
+ minInlineSize: "0",
266
+ padding: dense("{spacing.gap.lg}"),
267
+ "&[data-grows]": { flex: "1" }
268
+ };
269
+ /**
270
+ * Styles the root of a band that scrolls: it keeps its height, or grows to the room left with
271
+ * `data-grows`, and shrinks below its content, so its viewport scrolls.
272
+ */
273
+ const SCROLLER = {
274
+ flexShrink: "0",
275
+ minBlockSize: "0",
276
+ minInlineSize: "0",
277
+ "&[data-grows]": { flex: "1" }
278
+ };
279
+ /**
280
+ * Styles the content of a region's scroll area: a column at least as tall as the viewport.
281
+ *
282
+ * @remarks
283
+ * A page grows to the column's height, so a short page still fills the main region. A sidebar
284
+ * in a panel takes the column's height exactly and scrolls its own list, so the header and the
285
+ * footer of the sidebar remain in place.
286
+ */
287
+ const COLUMN = {
288
+ display: "flex",
289
+ flexDirection: "column",
290
+ minBlockSize: "100%",
291
+ "& > .sidebar__root": {
292
+ flex: "1 1 0",
293
+ minBlockSize: "0"
294
+ }
295
+ };
296
+ /**
297
+ * Styles the rail: a 24px strip centred on the edge between a panel and the page, with a line down
298
+ * its middle under the pointer and on keyboard focus.
299
+ *
300
+ * @remarks
301
+ * The negative margins take the strip's width back from the row, so the strip overlaps the panel
302
+ * and the page by 12px each and moves neither. The strip is hidden under a coarse pointer, where
303
+ * a finger presses the menu button instead.
304
+ */
305
+ const RAIL = {
306
+ ...interactive(),
307
+ _after: {
308
+ background: "transparent",
309
+ content: "\"\"",
310
+ inlineSize: "{borderWidths.indicator}",
311
+ insetBlock: "0",
312
+ insetInlineStart: "calc(50% - {borderWidths.indicator} / 2)",
313
+ position: "absolute"
314
+ },
315
+ _focusVisible: { _after: { background: "colorPalette.focusRing" } },
316
+ _hover: { _after: { background: "border.emphasized" } },
317
+ _touch: { display: "none" },
318
+ alignSelf: "stretch",
319
+ appearance: "none",
320
+ background: "transparent",
321
+ borderWidth: "0",
322
+ colorPalette: "neutral",
323
+ flexShrink: "0",
324
+ inlineSize: "6",
325
+ marginInline: "calc({sizes.6} / -2)",
326
+ padding: "0",
327
+ position: "relative",
328
+ zIndex: "docked"
329
+ };
330
+ //#endregion
331
+ //#region src/app-shell/recipe.ts
332
+ /**
333
+ * Declares the app shell's slot recipe, which lays out a column of bars with a body between them
334
+ * and a panel on either side of the page in the body.
335
+ *
336
+ * @remarks
337
+ * The recipe has eighteen slots. The body, the main region, a panel's content and a band that
338
+ * scrolls are the primitives package's scroll areas, so every bar in the shell is the theme's:
339
+ * `bodyViewport` and `row` are the body's viewport and content, `mainViewport` is the `main`
340
+ * element, `column` is the content of the main region and of a panel, and `scroller` is the root
341
+ * of a band that scrolls. A part that is left out takes no room. Every part is a container: the
342
+ * recipe sets the layout, the scrolling, the motion, the hairlines between regions and the
343
+ * grounds, and the application styles the rest through a theme. The plain look fills no region,
344
+ * so the shell shows the ground it is placed on. The backdrop, a sheet and a pinned bar are
345
+ * filled, because the page is visible under each. A sheet is raised by a shadow. A panel in the
346
+ * body is a track whose width moves between its open and closed widths, and its content keeps the
347
+ * open width, so the track clips the content instead of reflowing it. A panel over the page is
348
+ * fixed to the window with a backdrop behind it. The open and closed widths are the theme's
349
+ * `sizes.sidebar`, `sizes.aside` and `sizes.rail` unless a panel states its own. The window's
350
+ * height is `100dvh` unless an ancestor sets `WINDOW_HEIGHT`, so a shell staged in a box of a
351
+ * fixed height fills the box. The header and the footer pad their content by the middle gap, the
352
+ * status bar pads its row of entries by the smallest gap above and below and the middle gap at
353
+ * the sides, and a section pads by the large gap. The recipe has no `palette` and no `effect`
354
+ * axis, because the parts are containers and the components inside them offer their own.
355
+ */
356
+ /**
357
+ * Styles a plain, divided shell that scrolls its page.
358
+ */
359
+ const recipe$7 = defineSlotRecipe({
360
+ base: {
361
+ aside: {
362
+ ...PANEL,
363
+ [PANEL_SIZE]: "sizes.aside",
364
+ "&[data-overlaid]": {
365
+ ...OVERLAID,
366
+ "&[data-state=closed]": {
367
+ ...SLID_OUT,
368
+ _rtl: { translate: "-100% 0" },
369
+ translate: "100% 0"
370
+ },
371
+ insetInlineEnd: "0"
372
+ }
373
+ },
374
+ backdrop: {
375
+ ...MOVING,
376
+ background: "bg.inverted/40",
377
+ inset: "0",
378
+ opacity: "0",
379
+ pointerEvents: "none",
380
+ position: "fixed",
381
+ transitionProperty: "opacity",
382
+ zIndex: "overlay",
383
+ "&[data-state=open]": {
384
+ opacity: "1",
385
+ pointerEvents: "auto"
386
+ }
387
+ },
388
+ body: {
389
+ flex: "1",
390
+ minBlockSize: "0",
391
+ position: "relative"
392
+ },
393
+ column: COLUMN,
394
+ content: {
395
+ flexShrink: "0",
396
+ inlineSize: `var(${PANEL_SIZE})`,
397
+ minBlockSize: "0",
398
+ "[data-collapse=icons] > &, [data-overlaid] > &, [data-stacked] > &": { inlineSize: "100%" }
399
+ },
400
+ footer: {
401
+ flexShrink: "0",
402
+ padding: dense("{spacing.gap.md}"),
403
+ "&[data-sticky]": {
404
+ ...PINNED,
405
+ insetBlockEnd: "0",
406
+ paddingBlockEnd: `calc(${dense("{spacing.gap.md}")} + {spacing.safe.bottom})`
407
+ }
408
+ },
409
+ header: {
410
+ flexShrink: "0",
411
+ padding: dense("{spacing.gap.md}"),
412
+ "&[data-sticky]": {
413
+ ...PINNED,
414
+ insetBlockStart: `var(${STICKY_OFFSET$1}, 0px)`
415
+ }
416
+ },
417
+ main: {
418
+ flex: "1",
419
+ minInlineSize: "0"
420
+ },
421
+ navbar: {
422
+ ...PANEL,
423
+ [PANEL_SIZE]: "sizes.sidebar",
424
+ "&[data-overlaid]": {
425
+ ...OVERLAID,
426
+ "&[data-state=closed]": {
427
+ ...SLID_OUT,
428
+ _rtl: { translate: "100% 0" },
429
+ translate: "-100% 0"
430
+ },
431
+ insetInlineStart: "0"
432
+ }
433
+ },
434
+ rail: RAIL,
435
+ root: {
436
+ display: "flex",
437
+ flexDirection: "column",
438
+ inlineSize: "100%"
439
+ },
440
+ row: {
441
+ display: "flex",
442
+ "&:has(> [data-stacked])": { flexWrap: "wrap" },
443
+ [`&:has(> [data-stacked]) > .${CLASS$5}__main`]: { flexBasis: "100%" }
444
+ },
445
+ scroller: SCROLLER,
446
+ section: SECTION,
447
+ status: {
448
+ alignItems: "center",
449
+ columnGap: dense("{spacing.gap.md}"),
450
+ display: "flex",
451
+ flexShrink: "0",
452
+ flexWrap: "wrap",
453
+ paddingBlock: dense("{spacing.gap.xs}"),
454
+ paddingInline: dense("{spacing.gap.md}"),
455
+ rowGap: dense("{spacing.gap.xs}")
456
+ },
457
+ trigger: { flexShrink: "0" }
458
+ },
459
+ className: CLASS$5,
460
+ defaultVariants: {
461
+ divided: true,
462
+ scroll: "page",
463
+ variant: "plain"
464
+ },
465
+ jsx: [/^AppShell(\.\w+)?$/u],
466
+ slots: [
467
+ "root",
468
+ "header",
469
+ "body",
470
+ "bodyViewport",
471
+ "row",
472
+ "navbar",
473
+ "main",
474
+ "mainViewport",
475
+ "aside",
476
+ "footer",
477
+ "status",
478
+ "content",
479
+ "column",
480
+ "section",
481
+ "scroller",
482
+ "trigger",
483
+ "rail",
484
+ "backdrop"
485
+ ],
486
+ variants: {
487
+ /**
488
+ * Whether a hairline separates each bar and each panel from the page.
489
+ *
490
+ * @remarks
491
+ * The shell renders the lines, because only the shell knows where two of its regions meet. A
492
+ * sidebar inside a panel sets its ground and no line.
493
+ */
494
+ divided: { true: {
495
+ aside: {
496
+ borderColor: "border",
497
+ borderInlineStartWidth: "hairline"
498
+ },
499
+ footer: {
500
+ borderBlockStartWidth: "hairline",
501
+ borderColor: "border"
502
+ },
503
+ header: {
504
+ borderBlockEndWidth: "hairline",
505
+ borderColor: "border"
506
+ },
507
+ navbar: {
508
+ borderColor: "border",
509
+ borderInlineEndWidth: "hairline"
510
+ },
511
+ status: {
512
+ borderBlockStartWidth: "hairline",
513
+ borderColor: "border"
514
+ }
515
+ } },
516
+ /**
517
+ * What scrolls under the bars: the main region inside a shell the height of the window, or the
518
+ * window itself, with sticky bars pinned to it.
519
+ *
520
+ * @remarks
521
+ * The main region and the body are scroll areas, and this axis turns each viewport's
522
+ * scrolling on or off. While a panel has dropped under the page, the body scrolls the main
523
+ * region and the panel together, so a person scrolls down to the panel under the page. While
524
+ * the window scrolls, neither viewport scrolls, and the browser's own bar scrolls the page.
525
+ */
526
+ scroll: {
527
+ page: {
528
+ bodyViewport: {
529
+ [`&:has(> .${CLASS$5}__row > [data-stacked])`]: { overflow: "auto" },
530
+ overflow: "visible"
531
+ },
532
+ mainViewport: { [`.${CLASS$5}__row:has(> [data-stacked]) > .${CLASS$5}__main > &`]: { overflow: "visible" } },
533
+ root: { blockSize: WINDOW },
534
+ row: {
535
+ "&:has(> [data-stacked])": { blockSize: "auto" },
536
+ blockSize: "100%"
537
+ }
538
+ },
539
+ window: {
540
+ aside: STUCK$1,
541
+ bodyViewport: { overflow: "visible" },
542
+ mainViewport: { overflow: "visible" },
543
+ navbar: STUCK$1,
544
+ root: { minBlockSize: WINDOW },
545
+ row: { alignItems: "flex-start" }
546
+ }
547
+ },
548
+ /**
549
+ * How the page and the panels are set against the root's ground.
550
+ *
551
+ * @remarks
552
+ * `plain` fills nothing, so every region shows the ground the shell is placed on: the page's
553
+ * `bg` in an application, or a staging box's own fill. A sidebar in a panel paints its own
554
+ * ground. `inset` gives the main region `surface()`, the panel ground with a hairline and a
555
+ * shadow, over a `bg.subtle` root. `floating` gives each panel's content `surface()` instead.
556
+ */
557
+ variant: {
558
+ floating: {
559
+ aside: { padding: dense("{spacing.gap.xs}") },
560
+ content: {
561
+ ...surface(),
562
+ inlineSize: `calc(var(${PANEL_SIZE}) - {spacing.gap.md})`
563
+ },
564
+ navbar: { padding: dense("{spacing.gap.xs}") },
565
+ root: { background: "bg.subtle" }
566
+ },
567
+ inset: {
568
+ main: {
569
+ ...surface(),
570
+ margin: dense("{spacing.gap.xs}")
571
+ },
572
+ root: { background: "bg.subtle" }
573
+ },
574
+ plain: { root: { background: "transparent" } }
575
+ }
576
+ }
577
+ });
578
+ //#endregion
579
+ //#region src/floating-panel/recipe.ts
580
+ /**
581
+ * Recipe for a floating panel: a window over the page that a person drags by its header, resizes
582
+ * from its edges and corners, minimizes, maximizes and closes.
583
+ *
584
+ * @remarks
585
+ * The machine has no root part. The recipe adds a root with `display: contents`, because the
586
+ * trigger and the portalled positioner are siblings and a slot recipe passes its variants from an
587
+ * element above both. The machine fixes the positioner to the window and writes its place and
588
+ * size inline. The positioner stacks at `banner` plus the panel's place in the stack of open
589
+ * panels: above the page's sticky bands, and under a backdrop, a dialog, a popover, a toast and a
590
+ * tooltip. The panel has the dialog's surface, a transparent hairline edge that forced colors
591
+ * paint in `CanvasText`, and a ring outside it while it has keyboard focus. The resize triggers
592
+ * are strips `sizes.2` wide inside the panel's edges, and squares of that side at its corners.
593
+ * The panel does not clip its children, so a corner's square takes the pointer beyond the rounded
594
+ * corner. The machine clips a minimized panel to its header inline. The header pads its controls
595
+ * clear of the strips, and the body's scroll area ends clear of the end and bottom strips, so its
596
+ * bar takes the pointer. The recipe has no `palette` axis, because
597
+ * the panel reads the neutral `bg.panel`, and no `effect` axis, because a panel is not a
598
+ * control.
599
+ */
600
+ /**
601
+ * Class name of the recipe, which a selector across its parts writes.
602
+ */
603
+ const CLASS$4 = "floating-panel";
604
+ /**
605
+ * Width of a resize trigger's strip, and the side of a corner's square.
606
+ */
607
+ const STRIP$1 = "{sizes.2}";
608
+ /**
609
+ * Stacking level of the positioner: the banner level of the z-index scale plus the panel's place in
610
+ * the stack of open panels, which the machine writes as `--z-index`.
611
+ */
612
+ const LAYERED = "calc({zIndex.banner} + var(--z-index, 0))";
613
+ /**
614
+ * Custom property the primitives scroll area reads on its root for the offset of its focus ring.
615
+ */
616
+ const RING_OFFSET = "--scroll-area-ring-offset";
617
+ /**
618
+ * Defines the floating panel recipe.
619
+ */
620
+ const recipe$6 = defineSlotRecipe({
621
+ base: {
622
+ body: {
623
+ padding: dense("{spacing.inset.sm}"),
624
+ textStyle: "body.sm"
625
+ },
626
+ closeTrigger: { flexShrink: "0" },
627
+ content: {
628
+ ...motion("scale-fade.in", "scale-fade.out"),
629
+ background: "bg.panel",
630
+ borderColor: "transparent",
631
+ borderRadius: "l3",
632
+ borderStyle: "solid",
633
+ borderWidth: "hairline",
634
+ boxShadow: "lg",
635
+ color: "fg",
636
+ display: "flex",
637
+ flexDirection: "column",
638
+ focusRingColor: "colorPalette.focusRing",
639
+ focusVisibleRing: "outside",
640
+ position: "relative"
641
+ },
642
+ control: {
643
+ alignItems: "center",
644
+ display: "flex",
645
+ flexShrink: "0",
646
+ gap: dense("{spacing.gap.xs}")
647
+ },
648
+ dragTrigger: {
649
+ "&[data-disabled]": { cursor: "auto" },
650
+ "& > svg": {
651
+ boxSize: "icon.sm",
652
+ color: "fg.subtle",
653
+ flexShrink: "0"
654
+ },
655
+ [`.${CLASS$4}__header[data-dragging] > &`]: { cursor: "dragging" },
656
+ alignItems: "center",
657
+ alignSelf: "stretch",
658
+ cursor: "drag",
659
+ display: "flex",
660
+ flex: "1",
661
+ gap: dense("{spacing.gap.sm}"),
662
+ minInlineSize: "0",
663
+ touchAction: "none",
664
+ userSelect: "none"
665
+ },
666
+ header: {
667
+ alignItems: "center",
668
+ borderBlockEndColor: "border",
669
+ borderBlockEndStyle: "solid",
670
+ borderBlockEndWidth: "hairline",
671
+ display: "flex",
672
+ flexShrink: "0",
673
+ gap: dense("{spacing.gap.sm}"),
674
+ paddingBlock: dense("{spacing.inset.xs}"),
675
+ paddingInlineEnd: dense("{spacing.inset.xs}"),
676
+ paddingInlineStart: dense("{spacing.inset.sm}")
677
+ },
678
+ positioner: { zIndex: LAYERED },
679
+ resizeTrigger: {
680
+ "&:is([data-axis=e], [data-axis=w])": { inlineSize: STRIP$1 },
681
+ "&:is([data-axis=n], [data-axis=s])": { blockSize: STRIP$1 },
682
+ "&:is([data-axis=ne], [data-axis=nw], [data-axis=se], [data-axis=sw])": {
683
+ blockSize: STRIP$1,
684
+ inlineSize: STRIP$1
685
+ },
686
+ "&[data-disabled]": { display: "none" },
687
+ zIndex: "docked"
688
+ },
689
+ root: { display: "contents" },
690
+ scroller: {
691
+ "&[hidden]": { display: "none" },
692
+ flex: "1",
693
+ marginBlockEnd: STRIP$1,
694
+ marginInlineEnd: STRIP$1,
695
+ minBlockSize: "0",
696
+ [RING_OFFSET]: "calc({borderWidths.ring} * -1)"
697
+ },
698
+ stageTrigger: {
699
+ "&[hidden]": { display: "none" },
700
+ flexShrink: "0"
701
+ },
702
+ title: {
703
+ ...truncate(),
704
+ fontWeight: "semibold",
705
+ textStyle: "label.md"
706
+ },
707
+ trigger: {
708
+ ...interactive(),
709
+ alignItems: "center",
710
+ display: "inline-flex",
711
+ gap: dense("{spacing.gap.xs}")
712
+ }
713
+ },
714
+ className: CLASS$4,
715
+ jsx: [/^FloatingPanel(\.\w+)?$/u],
716
+ slots: [
717
+ "root",
718
+ "trigger",
719
+ "positioner",
720
+ "content",
721
+ "header",
722
+ "dragTrigger",
723
+ "title",
724
+ "control",
725
+ "stageTrigger",
726
+ "closeTrigger",
727
+ "scroller",
728
+ "body",
729
+ "resizeTrigger"
730
+ ]
731
+ });
732
+ //#endregion
733
+ //#region src/folding/folding.ts
734
+ /**
735
+ * Styles an action that folds to its icon while its row is narrow, for the page, the toolbar and
736
+ * the section.
737
+ *
738
+ * @remarks
739
+ * A secondary action shows its icon alone, and its words remain for a screen reader. A tertiary
740
+ * action renders nothing on a narrow row and runs from the row's menu, which the action registers
741
+ * with. The row measures its own width, so a row beside an open sidebar folds on its own width
742
+ * and the caller does not write a breakpoint.
743
+ */
744
+ /**
745
+ * Attribute an action sets on itself while its own row is narrow.
746
+ *
747
+ * @remarks
748
+ * The action reads the state from its row's context, so a narrow ancestor folds no action of a
749
+ * wider row inside it. A bar fixed to the window is wider than the page around it in the
750
+ * document.
751
+ */
752
+ const NARROW$2 = "data-narrow";
753
+ /**
754
+ * Attribute an action sets to its priority.
755
+ *
756
+ * @remarks
757
+ * The priority is an attribute and not an axis, because a slot recipe resolves its variants once,
758
+ * at the root, and each action in a row folds on its own priority.
759
+ */
760
+ const PRIORITY = "data-priority";
761
+ /**
762
+ * Styles an action by its priority while its row is narrow.
763
+ *
764
+ * @remarks
765
+ * Every action keeps its words on one line at every width, because a wrapped control would make
766
+ * the row taller. A secondary action hides its element children other than its icon with
767
+ * `srOnly`, so it keeps its accessible name, and renders as a square around its icon. Its words
768
+ * must be in an element, because a CSS selector cannot select a bare text node. A primary action
769
+ * is unchanged.
770
+ */
771
+ const FOLDING = {
772
+ [`&[${PRIORITY}=secondary]`]: { [`&[${NARROW$2}]`]: {
773
+ "& > :not(svg)": { srOnly: true },
774
+ aspectRatio: "square",
775
+ justifyContent: "center",
776
+ paddingInline: "0"
777
+ } },
778
+ whiteSpace: "nowrap"
779
+ };
780
+ //#endregion
781
+ //#region src/page/metrics.ts
782
+ /**
783
+ * Computes the rules the page recipe writes for the tabs in the navigation band, the panel the tab
784
+ * picker opens and the title's heading role.
785
+ *
786
+ * @remarks
787
+ * The module is separate from the recipe so the recipe is under the 300-line limit. It imports no
788
+ * React, because the theme plugin evaluates the recipe in Node.
789
+ */
790
+ /**
791
+ * Class name of the page's recipe, which a selector across bands reads.
792
+ *
793
+ * @remarks
794
+ * The binding writes one class per band, such as `page__nav`, and no attribute that names the
795
+ * band. A selector for another band builds that class from this constant.
796
+ */
797
+ const CLASS$3 = "page";
798
+ /**
799
+ * Class name of the tabs' recipe, whose root the navigation band shrinks to its tabs.
800
+ */
801
+ const TABS = "tabs";
802
+ /**
803
+ * Class name of the command palette's recipe, whose root the palette panel renders flat.
804
+ */
805
+ const COMMAND = "command";
806
+ /**
807
+ * Selects the root of the tabs in the navigation band.
808
+ */
809
+ const TABS_IN_NAV = `& > .${TABS}__root`;
810
+ /**
811
+ * Selects every tab of a strip in the navigation band.
812
+ */
813
+ const TAB_IN_NAV = `& .${TABS}__trigger`;
814
+ /**
815
+ * Tab height and inline inset per page size: a medium or large page's tabs are 48px tall and inset
816
+ * by the `md` inset, a small page's 44px and the `sm` inset.
817
+ */
818
+ const TABBED = {
819
+ lg: ["xl", "md"],
820
+ md: ["xl", "md"],
821
+ sm: ["lg", "sm"]
822
+ };
823
+ /**
824
+ * Returns the navigation band's rules for its tabs at a page size.
825
+ *
826
+ * @remarks
827
+ * The tabs are taller than a control of their text's size, so the band's other controls centre
828
+ * on them with room above and below. Each tab reads its inline inset from the control inset
829
+ * properties, and the strip starts one inset before the gutter, so the first tab's words start
830
+ * where the title starts. The recipe writes the rules in its size variant, because the tabs' own
831
+ * size variant sets the height in the same layer and these selectors outweigh it.
832
+ * @param size - The page's size.
833
+ */
834
+ function tabbed(size) {
835
+ const [height, inset] = TABBED[size];
836
+ const room = dense(`{spacing.inset.${inset}}`);
837
+ return {
838
+ [TAB_IN_NAV]: {
839
+ [CONTROL_INSET_END]: room,
840
+ [CONTROL_INSET_START]: room,
841
+ height: dense(`{sizes.control.${height}}`)
842
+ },
843
+ [TABS_IN_NAV]: { marginInlineStart: `calc(${room} * -1)` }
844
+ };
845
+ }
846
+ /**
847
+ * Styles the panel the tab picker opens as one box: the popover's edge and shadow around the
848
+ * command palette's rows, with neither the popover's padding nor the palette's own edge.
849
+ *
850
+ * @remarks
851
+ * The popover pads its content and the palette draws a raised panel, so without these rules the
852
+ * list renders as a box inside a box. The padding rule selects the machine's `data-state` too,
853
+ * because the popover's size variant sets the padding in the same layer.
854
+ */
855
+ const FLAT_PALETTE = {
856
+ "&[data-state]": { padding: "0" },
857
+ [`& > .${COMMAND}__root`]: {
858
+ borderRadius: "0",
859
+ borderWidth: "0",
860
+ boxShadow: "none",
861
+ maxBlockSize: "inherit"
862
+ },
863
+ maxBlockSize: "min(var(--available-height), {sizes.60})"
864
+ };
865
+ /**
866
+ * Selects the title of a narrow page.
867
+ */
868
+ const NARROW_TITLE = `.${CLASS$3}__root[data-narrow] > .${CLASS$3}__header > &`;
869
+ /**
870
+ * Heading role of the title at each size, one size larger than a section title's at that size, and
871
+ * one size smaller on a narrow page.
872
+ *
873
+ * @remarks
874
+ * The page title is the one heading above every section title on the page, so it reads a larger
875
+ * role than theirs. A narrow page gives the title's row less room, so the title reads one role
876
+ * smaller there.
877
+ */
878
+ const TITLES = {
879
+ lg: {
880
+ [NARROW_TITLE]: { textStyle: "heading.lg" },
881
+ textStyle: "heading.xl"
882
+ },
883
+ md: {
884
+ [NARROW_TITLE]: { textStyle: "heading.md" },
885
+ textStyle: "heading.lg"
886
+ },
887
+ sm: {
888
+ [NARROW_TITLE]: { textStyle: "heading.sm" },
889
+ textStyle: "heading.md"
890
+ }
891
+ };
892
+ //#endregion
893
+ //#region src/page/recipe.ts
894
+ /**
895
+ * Recipe for a page: a column of bands, from a banner and a header to a body and a footer.
896
+ *
897
+ * @remarks
898
+ * The root is a column at least as tall as its container, so the footer of a short page renders
899
+ * at the container's end. Every band runs edge to edge, so a sticky band's fill and hairline span
900
+ * the full width, and each band's content starts at the gutter. A page with a measure stops its
901
+ * content there, at the start or centred as `align` sets. The header is a grid of three rows, so
902
+ * the context, the leading mark, the title, the meta, the actions and the description are
903
+ * siblings placed by area name, and a part left out takes no room. The title's column shrinks,
904
+ * so its text wraps while the actions keep the end of its row. The root sets the gutter and the
905
+ * measure as custom properties that every band reads. The page measures its own width and sets
906
+ * `folded` below the `sm` breakpoint. From the `lg` breakpoint of the window, a page with an
907
+ * aside becomes a grid with the aside beside the body, because a rail beside the text needs the
908
+ * window's width. Below it the aside stacks under the body, or leaves the page with
909
+ * `folds="hide"`. A sticky aside keeps to the top of its row, under the shell's sticky bars. The
910
+ * recipe has no `palette` axis, because a page is layout on the page's surface, and no `effect`
911
+ * axis, because a page is not a control.
912
+ */
913
+ /**
914
+ * Custom property the root sets to the room at the page's inline edges.
915
+ */
916
+ const GUTTER = "--page-gutter";
917
+ /**
918
+ * Custom property the root sets to the width of the page's content.
919
+ */
920
+ const MEASURE = "--page-measure";
921
+ /**
922
+ * Custom property a band reads as its start padding before the measure.
923
+ *
924
+ * @remarks
925
+ * A band runs edge to edge, so its fill is full bleed, and its padding keeps the content in the
926
+ * measure. The `align` axis sets this property and every band reads it, so the header, the body
927
+ * and the footer share one column at either alignment.
928
+ */
929
+ const LEAD = "--page-lead";
930
+ /**
931
+ * Styles every band: edge to edge, with padding that keeps the content in the measure.
932
+ */
933
+ const BAND$1 = {
934
+ flexShrink: "0",
935
+ minInlineSize: "0",
936
+ paddingInlineEnd: `max(var(${GUTTER}), calc(100% - var(${MEASURE}, 100%) - var(${LEAD}, var(${GUTTER}))))`,
937
+ paddingInlineStart: `var(${LEAD}, var(${GUTTER}))`
938
+ };
939
+ /**
940
+ * Custom property the shell sets to the height of its sticky bars, under which a sticky band stops.
941
+ */
942
+ const SHELL_TOP = "--app-shell-sticky-top";
943
+ /**
944
+ * Custom property the root sets on each sticky band to the height of the sticky bands before it.
945
+ */
946
+ const STICKY_OFFSET = "--page-sticky-offset";
947
+ /**
948
+ * Custom property the root sets on itself to the height of every band that sticks to the top.
949
+ */
950
+ const STICKY_TOP = "--page-sticky-top";
951
+ /**
952
+ * Custom property a sticky aside takes, set to the height of the area that scrolls the page.
953
+ */
954
+ const SCROLLPORT = "--page-scrollport";
955
+ /**
956
+ * Room a focus ring takes outside an element: its offset and its width.
957
+ */
958
+ const RING_ROOM = "calc(var(--focus-ring-offset, 0px) + var(--focus-ring-width, 0px))";
959
+ /**
960
+ * Styles a sticky band: at the top, under the shell's sticky bars and the page's sticky bands
961
+ * before it, on the page's fill.
962
+ *
963
+ * @remarks
964
+ * A sticky band takes the page's fill, because the page scrolls under it. Its inset reads the
965
+ * shell's sticky height and the height of the sticky bands before it, because a sticky element
966
+ * with an `auto` inset does not stick and two bands at one inset cover each other. Its z-index is
967
+ * the `sticky` token the shell's bars use, so a band and a bar at one edge share one order.
968
+ */
969
+ const STUCK = {
970
+ background: "bg",
971
+ insetBlockStart: `calc(var(${SHELL_TOP}, 0px) + var(${STICKY_OFFSET}, 0px))`,
972
+ position: "sticky",
973
+ zIndex: "sticky"
974
+ };
975
+ /**
976
+ * Styles a row that centres its items, wraps and shrinks.
977
+ */
978
+ const ROW$2 = {
979
+ alignItems: "center",
980
+ display: "flex",
981
+ flexWrap: "wrap",
982
+ minInlineSize: "0"
983
+ };
984
+ /**
985
+ * Selects a band followed by the navigation, which then has the hairline.
986
+ */
987
+ const BEFORE_NAV = `&:has(+ .${CLASS$3}__nav)`;
988
+ /**
989
+ * Selects a root with an aside, which lays its bands out as a grid from the `lg` breakpoint.
990
+ */
991
+ const WITH_ASIDE = `&:has(> .${CLASS$3}__aside)`;
992
+ /**
993
+ * Selects a toolbar in the navigation band.
994
+ */
995
+ const TOOLBAR_IN_NAV = `& > .toolbar__root`;
996
+ /**
997
+ * Selects a strip of tabs in the navigation band, by the orientation the tabs' own line look
998
+ * selects too.
999
+ */
1000
+ const STRIP_IN_NAV = `& .${CLASS$3}__tabs[data-orientation]`;
1001
+ /**
1002
+ * Styles the grid of a page with an aside: every band across, and the body beside the aside.
1003
+ *
1004
+ * @remarks
1005
+ * The body's row takes the remaining height and every other row takes its content's height,
1006
+ * because the root is at least as tall as the shell's main region and a grid shares spare height
1007
+ * between its `auto` rows.
1008
+ */
1009
+ const BESIDE = {
1010
+ columnGap: dense("{spacing.gap.xl}"),
1011
+ display: "grid",
1012
+ gridTemplateAreas: "\"banner banner\" \"header header\" \"nav nav\" \"toolbar toolbar\" \"body aside\" \"footer footer\"",
1013
+ gridTemplateColumns: "minmax(0, 1fr) auto",
1014
+ gridTemplateRows: "auto auto auto auto 1fr auto"
1015
+ };
1016
+ /**
1017
+ * Sizes the page offers, which a section in the page reads too.
1018
+ */
1019
+ const STEPS = [
1020
+ "sm",
1021
+ "md",
1022
+ "lg"
1023
+ ];
1024
+ /**
1025
+ * Selects a navigation band with a strip of tabs, which is flush with the band's hairline.
1026
+ */
1027
+ const WITH_TABS = `&:has(> .${TABS}__root)`;
1028
+ /**
1029
+ * Defines the page recipe: a full-width page at size `md` by default.
1030
+ */
1031
+ const recipe$5 = defineSlotRecipe({
1032
+ base: {
1033
+ action: {
1034
+ ...FOLDING,
1035
+ flexShrink: "0"
1036
+ },
1037
+ actions: {
1038
+ ...ROW$2,
1039
+ flexWrap: "nowrap",
1040
+ gridArea: "actions",
1041
+ justifySelf: "end",
1042
+ paddingInlineStart: dense("{spacing.inset.xl}")
1043
+ },
1044
+ /**
1045
+ * The aside, beside the body from the `lg` breakpoint and under it below.
1046
+ *
1047
+ * @remarks
1048
+ * Beside the body a sticky aside is a column at most as tall as the area that scrolls the
1049
+ * page, less the sticky bars and bands above it and a gap at each end. Its scroll area fills
1050
+ * the column, so the end of a long aside remains reachable while it sticks.
1051
+ */
1052
+ aside: {
1053
+ "&[data-folds=hide]": { lgDown: { display: "none" } },
1054
+ "&[data-sticky]": {
1055
+ alignSelf: "start",
1056
+ insetBlockStart: `calc(var(${SHELL_TOP}, 0px) + var(${STICKY_TOP}, 0px) + {spacing.gap.xl})`,
1057
+ lg: {
1058
+ display: "flex",
1059
+ flexDirection: "column",
1060
+ maxBlockSize: `calc(var(${SCROLLPORT}, 100dvh) - var(${SHELL_TOP}, 0px) - var(${STICKY_TOP}, 0px) - {spacing.gap.xl} * 2)`
1061
+ },
1062
+ position: "sticky"
1063
+ },
1064
+ gridArea: "aside",
1065
+ lg: { paddingInlineStart: "0" },
1066
+ minInlineSize: "0",
1067
+ paddingInline: `var(${GUTTER})`
1068
+ },
1069
+ /**
1070
+ * The padding of a sticky aside's scroll area, the room a focus ring takes at its edges.
1071
+ */
1072
+ asideContent: { padding: RING_ROOM },
1073
+ /**
1074
+ * A sticky aside's scroll area, pulled out on every side by the room its content's padding
1075
+ * takes, so the content keeps its place.
1076
+ */
1077
+ asideScroller: { margin: `calc(${RING_ROOM} * -1)` },
1078
+ banner: {
1079
+ ...BAND$1,
1080
+ flexShrink: "0",
1081
+ gridArea: "banner"
1082
+ },
1083
+ body: {
1084
+ ...BAND$1,
1085
+ display: "flex",
1086
+ flex: "1",
1087
+ flexDirection: "column",
1088
+ gridArea: "body",
1089
+ minBlockSize: "0"
1090
+ },
1091
+ context: {
1092
+ ...ROW$2,
1093
+ gridArea: "context",
1094
+ minInlineSize: "0"
1095
+ },
1096
+ description: {
1097
+ color: "fg.subtle",
1098
+ gridArea: "description",
1099
+ maxInlineSize: "prose",
1100
+ minInlineSize: "0"
1101
+ },
1102
+ /**
1103
+ * Lays the footer out as a row that sticks to the bottom edge when told to.
1104
+ *
1105
+ * @remarks
1106
+ * A sticky footer resets the top inset the shared sticky rule sets.
1107
+ */
1108
+ footer: {
1109
+ ...BAND$1,
1110
+ ...ROW$2,
1111
+ "&[data-sticky]": {
1112
+ ...STUCK,
1113
+ insetBlockEnd: "0",
1114
+ insetBlockStart: "auto"
1115
+ },
1116
+ gridArea: "footer"
1117
+ },
1118
+ /**
1119
+ * Lays the header out in three rows: the context, the title's row and the description.
1120
+ *
1121
+ * @remarks
1122
+ * The rows have no gap. The context row keeps a small margin above the title's row, and the
1123
+ * line heights part the title from the description. The columns have no gap, because the
1124
+ * parts beside the title belong to its line. Every part names its area, because the grid
1125
+ * places a part without one in the first free cell.
1126
+ */
1127
+ header: {
1128
+ ...BAND$1,
1129
+ "&[data-sticky]": STUCK,
1130
+ alignItems: "center",
1131
+ display: "grid",
1132
+ gridArea: "header",
1133
+ gridTemplateAreas: "\"context context context context\" \"leading title meta actions\" \"description description description description\"",
1134
+ gridTemplateColumns: "auto auto minmax(0, 1fr) auto"
1135
+ },
1136
+ leading: {
1137
+ display: "flex",
1138
+ gridArea: "leading",
1139
+ marginInlineEnd: dense("{spacing.gap.lg}")
1140
+ },
1141
+ meta: {
1142
+ ...ROW$2,
1143
+ gridArea: "meta"
1144
+ },
1145
+ nav: {
1146
+ ...BAND$1,
1147
+ "&[data-sticky]": STUCK,
1148
+ alignItems: "center",
1149
+ display: "flex",
1150
+ flexWrap: "wrap",
1151
+ gap: dense("{spacing.gap.lg}"),
1152
+ gridArea: "nav",
1153
+ justifyContent: "space-between",
1154
+ [TABS_IN_NAV]: {
1155
+ flex: "0 1 auto",
1156
+ inlineSize: "auto",
1157
+ minInlineSize: "0"
1158
+ },
1159
+ [TOOLBAR_IN_NAV]: {
1160
+ flex: "0 1 auto",
1161
+ inlineSize: "auto"
1162
+ }
1163
+ },
1164
+ palette: {
1165
+ inlineSize: "var(--reference-width)",
1166
+ overflow: "clip"
1167
+ },
1168
+ picker: {
1169
+ "& > span": truncate(),
1170
+ flex: "1",
1171
+ justifyContent: "space-between",
1172
+ minInlineSize: "0"
1173
+ },
1174
+ root: {
1175
+ display: "flex",
1176
+ flexDirection: "column",
1177
+ flexGrow: "1",
1178
+ inlineSize: "100%",
1179
+ minBlockSize: "100%",
1180
+ minInlineSize: "0",
1181
+ [WITH_ASIDE]: { lg: BESIDE }
1182
+ },
1183
+ title: {
1184
+ gridArea: "title",
1185
+ minInlineSize: "0",
1186
+ overflowWrap: "anywhere"
1187
+ },
1188
+ toolbar: {
1189
+ ...BAND$1,
1190
+ ...ROW$2,
1191
+ "&[data-sticky]": STUCK,
1192
+ gridArea: "toolbar"
1193
+ },
1194
+ trail: {
1195
+ "& > svg": { _rtl: { transform: "scaleX(-1)" } },
1196
+ alignItems: "center",
1197
+ color: "fg.muted",
1198
+ display: "inline-flex",
1199
+ gridArea: "context",
1200
+ justifySelf: "start",
1201
+ minInlineSize: "0"
1202
+ }
1203
+ },
1204
+ className: CLASS$3,
1205
+ compoundVariants: [(
1206
+ /**
1207
+ * Moves the meta of a folded header onto its own row under the title.
1208
+ *
1209
+ * @remarks
1210
+ * The meta drops the start margin it has beside the title, so it starts where the title and
1211
+ * the description start, and keeps a small margin above and below. The actions keep the `sm`
1212
+ * inset from the title.
1213
+ */
1214
+ {
1215
+ css: {
1216
+ actions: { paddingInlineStart: dense("{spacing.inset.sm}") },
1217
+ header: {
1218
+ gridTemplateAreas: "\"context context context\" \"leading title actions\" \"meta meta meta\" \"description description description\"",
1219
+ gridTemplateColumns: "auto minmax(0, 1fr) auto"
1220
+ },
1221
+ meta: {
1222
+ marginBlock: dense("{spacing.gap.sm}"),
1223
+ marginInlineStart: "0"
1224
+ }
1225
+ },
1226
+ folded: true,
1227
+ name: "stacked"
1228
+ })],
1229
+ defaultVariants: {
1230
+ align: "start",
1231
+ divided: true,
1232
+ gutter: "xl",
1233
+ measure: "full",
1234
+ size: "md"
1235
+ },
1236
+ jsx: [/^Page(\.\w+)?$/u],
1237
+ slots: [
1238
+ "root",
1239
+ "banner",
1240
+ "header",
1241
+ "context",
1242
+ "leading",
1243
+ "title",
1244
+ "meta",
1245
+ "description",
1246
+ "actions",
1247
+ "action",
1248
+ "nav",
1249
+ "tabs",
1250
+ "picker",
1251
+ "palette",
1252
+ "toolbar",
1253
+ "body",
1254
+ "aside",
1255
+ "asideScroller",
1256
+ "asideContent",
1257
+ "footer",
1258
+ "trail"
1259
+ ],
1260
+ variants: {
1261
+ /**
1262
+ * Position of the content when the measure is narrower than the page.
1263
+ *
1264
+ * @remarks
1265
+ * The axis sets the start padding of every band. The root runs edge to edge, so a band's fill
1266
+ * is full bleed, and automatic margins on a full-width root would move nothing.
1267
+ */
1268
+ align: {
1269
+ start: { root: { [LEAD]: `var(${GUTTER})` } },
1270
+ center: { root: { [LEAD]: `max(var(${GUTTER}), calc((100% - var(${MEASURE}, 100%)) / 2))` } }
1271
+ },
1272
+ /**
1273
+ * Whether a hairline separates the bands.
1274
+ *
1275
+ * @remarks
1276
+ * A header followed by the navigation leaves the hairline to the navigation, and a strip of
1277
+ * tabs in the navigation drops its own line, so the strip has one line under it and none
1278
+ * above. The strip's rule outweighs the tabs' own line look, whose selector names the
1279
+ * orientation, because both are in the variants layer.
1280
+ */
1281
+ divided: { true: {
1282
+ footer: {
1283
+ ...divider("horizontal"),
1284
+ borderBlockStartWidth: "hairline"
1285
+ },
1286
+ header: {
1287
+ ...divider("horizontal"),
1288
+ [BEFORE_NAV]: { borderBlockEndWidth: "0" },
1289
+ borderBlockEndWidth: "hairline"
1290
+ },
1291
+ nav: {
1292
+ ...divider("horizontal"),
1293
+ borderBlockEndWidth: "hairline",
1294
+ [STRIP_IN_NAV]: { borderBlockEndWidth: "0" }
1295
+ },
1296
+ toolbar: {
1297
+ ...divider("horizontal"),
1298
+ borderBlockEndWidth: "hairline"
1299
+ }
1300
+ } },
1301
+ /**
1302
+ * Room at the page's inline edges, which every band reads.
1303
+ *
1304
+ * @remarks
1305
+ * The default is the `xl` inset. A folded page sets the `md` inset.
1306
+ */
1307
+ gutter: onSlots({ root: sizeVariants((size) => ({ [GUTTER]: `{spacing.inset.${size}}` })) }),
1308
+ /**
1309
+ * Width of the page's content.
1310
+ *
1311
+ * @remarks
1312
+ * The axis is not named `width`, because `width` is a style prop of a styled element.
1313
+ * `narrow` and `wide` read the theme's page sizes.
1314
+ */
1315
+ measure: {
1316
+ full: { root: { [MEASURE]: "100%" } },
1317
+ narrow: { root: { [MEASURE]: "{sizes.page.narrow}" } },
1318
+ wide: { root: { [MEASURE]: "{sizes.page.wide}" } }
1319
+ },
1320
+ /**
1321
+ * Whether the page is narrower than the `sm` breakpoint, which the page sets from its own
1322
+ * width.
1323
+ *
1324
+ * @remarks
1325
+ * A folded page has the `md` gutter. The axis is not named `narrow`, because `measure` has a
1326
+ * `narrow` value and two values of one name compile to one class.
1327
+ */
1328
+ folded: { true: { root: { [GUTTER]: "{spacing.inset.md}" } } },
1329
+ /**
1330
+ * Size of the title, the description, the actions and the bands' padding.
1331
+ *
1332
+ * @remarks
1333
+ * The page offers three sizes, because larger steps belong to a display heading. A section in
1334
+ * the page reads this size too.
1335
+ */
1336
+ size: onSlots({
1337
+ actions: sizeVariants((size) => ({ gap: dense(`{spacing.gap.${size}}`) }), STEPS),
1338
+ aside: sizeVariants((size) => ({ paddingBlock: dense(`{spacing.inset.${size}}`) }), STEPS),
1339
+ banner: sizeVariants((size) => ({ paddingBlock: dense(`{spacing.inset.${size}}`) }), STEPS),
1340
+ body: sizeVariants((size) => ({ paddingBlock: dense(`{spacing.inset.${size}}`) }), STEPS),
1341
+ context: sizeVariants((size) => ({
1342
+ gap: dense(`{spacing.gap.${size}}`),
1343
+ marginBlockEnd: size === "lg" ? "{spacing.1}" : "{spacing.0.5}",
1344
+ textStyle: `body.${below(size)}`
1345
+ }), STEPS),
1346
+ description: {
1347
+ lg: { textStyle: "body.md" },
1348
+ md: { textStyle: "body.md" },
1349
+ sm: { textStyle: "body.sm" }
1350
+ },
1351
+ footer: sizeVariants((size) => ({
1352
+ gap: dense(`{spacing.gap.${size}}`),
1353
+ paddingBlock: dense(`{spacing.inset.${size}}`)
1354
+ }), STEPS),
1355
+ header: sizeVariants((size) => ({
1356
+ [BEFORE_NAV]: { paddingBlockEnd: dense(`{spacing.gap.${size}}`) },
1357
+ paddingBlock: dense(`{spacing.inset.${size}}`)
1358
+ }), STEPS),
1359
+ meta: sizeVariants((size) => ({
1360
+ gap: dense(`{spacing.gap.${size}}`),
1361
+ marginInlineStart: dense(`{spacing.gap.${size}}`)
1362
+ }), STEPS),
1363
+ nav: sizeVariants((size) => ({
1364
+ ...tabbed(size),
1365
+ paddingBlock: dense(`{spacing.gap.${size}}`),
1366
+ [WITH_TABS]: { paddingBlock: "0" }
1367
+ }), STEPS),
1368
+ palette: sizeVariants(() => FLAT_PALETTE, STEPS),
1369
+ title: TITLES,
1370
+ toolbar: sizeVariants((size) => ({
1371
+ gap: dense(`{spacing.gap.${size}}`),
1372
+ paddingBlock: dense(`{spacing.gap.${size}}`)
1373
+ }), STEPS),
1374
+ trail: sizeVariants((size) => ({
1375
+ gap: dense(`{spacing.gap.${below(size)}}`),
1376
+ textStyle: `body.${below(size)}`
1377
+ }), STEPS)
1378
+ })
1379
+ }
1380
+ });
1381
+ //#endregion
1382
+ //#region src/section/recipe.ts
1383
+ /**
1384
+ * Recipe for a section of a page: a header over a body and a footer, plain or on a card.
1385
+ *
1386
+ * @remarks
1387
+ * The header is a grid of two rows, so the title, the actions and the description are siblings
1388
+ * placed by area name. The title's column shrinks, and the actions keep the end of the title's
1389
+ * row at every width. The root sets four custom properties per size: the gap between the bands,
1390
+ * the gap after the section before it, the card's inner room and the footer's block padding. A
1391
+ * card's bands pad themselves from them, so a body with `data-bleed` drops its padding while the
1392
+ * other bands keep theirs. A bleeding body overlaps the footer by one hairline, so a table's rule
1393
+ * under its last row and the footer's rule render as one line. The room is the inset of the
1394
+ * section's size, the inline padding of a table cell at that size, so a table in a bleeding body
1395
+ * starts its first column under the title. The description stops at the reading measure and
1396
+ * reads the body role one size smaller than the section. The title reads the heading role one
1397
+ * size smaller, so a section reads as part of its page. `annotated` moves the header into a
1398
+ * column beside the body and folds it back over the body while the root is `data-narrow`, which
1399
+ * the component measures. An annotated card raises the body and the footer as one card and leaves
1400
+ * the header on the page. A section scrolled to by its id stops one gap under the shell's sticky
1401
+ * bars and the page's sticky bands. The recipe has no `palette` axis, because a section is layout
1402
+ * on the page's surface and its content sets its own palettes, and no `effect` axis, because a
1403
+ * section is not a control.
1404
+ */
1405
+ /**
1406
+ * Class name of the recipe, which a selector across parts reads.
1407
+ */
1408
+ const CLASS$2 = "section";
1409
+ /**
1410
+ * Selects a section that follows another section.
1411
+ */
1412
+ const AFTER = `.${CLASS$2}__root + &`;
1413
+ /**
1414
+ * Selects a band of a narrow section.
1415
+ */
1416
+ const NARROW$1 = `.${CLASS$2}__root[data-narrow] > &`;
1417
+ /**
1418
+ * Selects the actions of a narrow section.
1419
+ */
1420
+ const NARROW_ACTIONS = `.${CLASS$2}__root[data-narrow] > .${CLASS$2}__header > &`;
1421
+ /**
1422
+ * Selects a band followed by the footer.
1423
+ */
1424
+ const BEFORE_FOOTER = `&:has(+ .${CLASS$2}__footer)`;
1425
+ /**
1426
+ * Custom property the root sets to the card's inner room, which every band's padding reads.
1427
+ */
1428
+ const ROOM = "--section-room";
1429
+ /**
1430
+ * Custom property the root sets to the gap between its bands.
1431
+ */
1432
+ const BANDS = "--section-bands";
1433
+ /**
1434
+ * Custom property the root sets to the gap after the section before it.
1435
+ */
1436
+ const APART = "--section-apart";
1437
+ /**
1438
+ * Custom property the root sets to the block padding of a card's footer.
1439
+ */
1440
+ const FOOTING = "--section-footing";
1441
+ /**
1442
+ * Maps each size to the gap two sizes larger, between the bands.
1443
+ */
1444
+ const AIRED = {
1445
+ lg: "2xl",
1446
+ md: "xl",
1447
+ sm: "lg"
1448
+ };
1449
+ /**
1450
+ * Maps each size to the gap three sizes larger, after the section before.
1451
+ */
1452
+ const SPACED = {
1453
+ lg: "3xl",
1454
+ md: "2xl",
1455
+ sm: "xl"
1456
+ };
1457
+ /**
1458
+ * Styles a row that centres its items, wraps and shrinks.
1459
+ */
1460
+ const ROW$1 = {
1461
+ alignItems: "center",
1462
+ display: "flex",
1463
+ flexWrap: "wrap",
1464
+ minInlineSize: "0"
1465
+ };
1466
+ /**
1467
+ * Defines the section recipe: a plain section at size `md` by default.
1468
+ */
1469
+ const recipe$4 = defineSlotRecipe({
1470
+ base: {
1471
+ action: {
1472
+ ...FOLDING,
1473
+ flexShrink: "0"
1474
+ },
1475
+ actions: {
1476
+ ...ROW$1,
1477
+ flexWrap: "nowrap",
1478
+ gridArea: "actions",
1479
+ justifySelf: "end",
1480
+ [NARROW_ACTIONS]: { paddingInlineStart: dense("{spacing.inset.sm}") },
1481
+ paddingInlineStart: dense("{spacing.inset.md}")
1482
+ },
1483
+ body: { minInlineSize: "0" },
1484
+ description: {
1485
+ color: "fg.subtle",
1486
+ gridArea: "description",
1487
+ maxInlineSize: "prose",
1488
+ minInlineSize: "0"
1489
+ },
1490
+ footer: {
1491
+ ...ROW$1,
1492
+ "& > :last-child:not(:first-child)": { marginInlineStart: "auto" },
1493
+ justifyContent: "space-between"
1494
+ },
1495
+ header: {
1496
+ alignItems: "center",
1497
+ display: "grid",
1498
+ gridTemplateAreas: "\"title actions\" \"description description\"",
1499
+ gridTemplateColumns: "minmax(0, 1fr) auto",
1500
+ minInlineSize: "0"
1501
+ },
1502
+ root: {
1503
+ display: "flex",
1504
+ flexDirection: "column",
1505
+ gap: `var(${BANDS})`,
1506
+ minInlineSize: "0",
1507
+ scrollMarginBlockStart: "calc(var(--app-shell-sticky-top, 0px) + var(--page-sticky-top, 0px) + {spacing.gap.lg})"
1508
+ },
1509
+ title: {
1510
+ gridArea: "title",
1511
+ minInlineSize: "0",
1512
+ overflowWrap: "anywhere"
1513
+ }
1514
+ },
1515
+ className: CLASS$2,
1516
+ compoundVariants: [
1517
+ {
1518
+ annotated: true,
1519
+ css: {
1520
+ actions: {
1521
+ justifySelf: "start",
1522
+ marginBlockStart: dense("{spacing.gap.md}"),
1523
+ paddingInlineStart: "0"
1524
+ },
1525
+ body: { gridArea: "body" },
1526
+ footer: { gridArea: "footer" },
1527
+ header: {
1528
+ alignItems: "flex-start",
1529
+ display: "flex",
1530
+ flexDirection: "column",
1531
+ gridArea: "header"
1532
+ },
1533
+ root: {
1534
+ "&[data-narrow]": {
1535
+ gridTemplateAreas: "\"header\" \"body\" \"footer\"",
1536
+ gridTemplateColumns: "minmax(0, 1fr)",
1537
+ gridTemplateRows: "none"
1538
+ },
1539
+ alignItems: "start",
1540
+ display: "grid",
1541
+ gridTemplateAreas: "\"header body\" \"header footer\"",
1542
+ gridTemplateColumns: "minmax({sizes.xs}, 1fr) minmax(0, 2fr)",
1543
+ gridTemplateRows: "auto 1fr"
1544
+ }
1545
+ },
1546
+ name: "aside"
1547
+ },
1548
+ {
1549
+ css: {
1550
+ body: {
1551
+ "&:first-child": { paddingBlockStart: `var(${ROOM})` },
1552
+ "&[data-bleed]": {
1553
+ "&:first-child": { borderBlockStartWidth: "0" },
1554
+ [BEFORE_FOOTER]: { marginBlockEnd: "calc({borderWidths.hairline} * -1)" },
1555
+ borderBlockStartWidth: "hairline",
1556
+ borderColor: "border",
1557
+ padding: "0"
1558
+ },
1559
+ padding: `var(${ROOM})`,
1560
+ paddingBlockStart: "0"
1561
+ },
1562
+ footer: {
1563
+ borderBlockStartWidth: "hairline",
1564
+ borderColor: "border",
1565
+ paddingBlock: `var(${FOOTING})`,
1566
+ paddingInline: `var(${ROOM})`
1567
+ },
1568
+ header: {
1569
+ "&:last-child": { paddingBlockEnd: `var(${ROOM})` },
1570
+ padding: `var(${ROOM})`,
1571
+ paddingBlockEnd: `var(${BANDS})`
1572
+ },
1573
+ root: {
1574
+ ...surface(),
1575
+ gap: "0"
1576
+ }
1577
+ },
1578
+ name: "raised",
1579
+ variant: "surface"
1580
+ },
1581
+ {
1582
+ annotated: true,
1583
+ css: {
1584
+ body: {
1585
+ ...surface(),
1586
+ [BEFORE_FOOTER]: {
1587
+ borderBlockEndWidth: "0",
1588
+ borderEndEndRadius: "0",
1589
+ borderEndStartRadius: "0"
1590
+ },
1591
+ overflow: "clip",
1592
+ paddingBlockStart: `var(${ROOM})`
1593
+ },
1594
+ footer: {
1595
+ ...surface(),
1596
+ [`.${CLASS$2}__body + &`]: {
1597
+ borderStartEndRadius: "0",
1598
+ borderStartStartRadius: "0"
1599
+ }
1600
+ },
1601
+ header: {
1602
+ [NARROW$1]: { marginBlockEnd: `var(${BANDS})` },
1603
+ padding: "0"
1604
+ },
1605
+ root: {
1606
+ background: "none",
1607
+ borderWidth: "0",
1608
+ boxShadow: "none",
1609
+ overflow: "visible",
1610
+ rowGap: "0"
1611
+ }
1612
+ },
1613
+ name: "beside",
1614
+ variant: "surface"
1615
+ }
1616
+ ],
1617
+ defaultVariants: {
1618
+ size: "md",
1619
+ variant: "plain"
1620
+ },
1621
+ jsx: [/^Section(\.\w+)?$/u],
1622
+ slots: [
1623
+ "root",
1624
+ "header",
1625
+ "title",
1626
+ "description",
1627
+ "actions",
1628
+ "action",
1629
+ "body",
1630
+ "footer"
1631
+ ],
1632
+ variants: {
1633
+ /**
1634
+ * Whether the header renders in a column beside the body.
1635
+ */
1636
+ annotated: { true: { root: { columnGap: dense("{spacing.gap.3xl}") } } },
1637
+ /**
1638
+ * Size of the title, the description, the gaps and the card's inner room.
1639
+ */
1640
+ size: onSlots({
1641
+ actions: sizeVariants((size) => ({ gap: dense(`{spacing.gap.${size}}`) }), [
1642
+ "sm",
1643
+ "md",
1644
+ "lg"
1645
+ ]),
1646
+ description: sizeVariants((size) => ({
1647
+ marginBlockStart: size === "lg" ? "{spacing.1}" : "{spacing.0.5}",
1648
+ textStyle: `body.${below(size)}`
1649
+ }), [
1650
+ "sm",
1651
+ "md",
1652
+ "lg"
1653
+ ]),
1654
+ footer: sizeVariants((size) => ({ gap: dense(`{spacing.gap.${size}}`) }), [
1655
+ "sm",
1656
+ "md",
1657
+ "lg"
1658
+ ]),
1659
+ root: sizeVariants((size) => ({
1660
+ [APART]: dense(`{spacing.gap.${SPACED[size]}}`),
1661
+ [BANDS]: dense(`{spacing.gap.${AIRED[size]}}`),
1662
+ [FOOTING]: dense(`{spacing.inset.${below(size)}}`),
1663
+ [ROOM]: dense(`{spacing.inset.${size}}`)
1664
+ }), [
1665
+ "sm",
1666
+ "md",
1667
+ "lg"
1668
+ ]),
1669
+ title: sizeVariants((size) => ({ textStyle: `heading.${below(size)}` }), [
1670
+ "sm",
1671
+ "md",
1672
+ "lg"
1673
+ ])
1674
+ }),
1675
+ /**
1676
+ * Look of the section: plain on the page, or raised as a card.
1677
+ *
1678
+ * @remarks
1679
+ * Both looks keep the size's gap after a section before them. A plain section after another
1680
+ * plain section renders a hairline in the middle of that gap. `surface` raises the section
1681
+ * as a card and clips its content to the card's corners, so a body with `data-bleed` keeps
1682
+ * them.
1683
+ */
1684
+ variant: {
1685
+ surface: { root: {
1686
+ [AFTER]: { marginBlockStart: `var(${APART})` },
1687
+ overflow: "clip"
1688
+ } },
1689
+ plain: { root: {
1690
+ "& + &": {
1691
+ borderBlockStartWidth: "hairline",
1692
+ borderColor: "border",
1693
+ paddingBlockStart: `var(${APART})`
1694
+ },
1695
+ [AFTER]: { marginBlockStart: `var(${APART})` }
1696
+ } }
1697
+ }
1698
+ }
1699
+ });
1700
+ //#endregion
1701
+ //#region src/sidebar/recipe.ts
1702
+ /**
1703
+ * Declares the sidebar's slot recipe, which lays out a fixed header, scrolling content and a fixed
1704
+ * footer in a column.
1705
+ *
1706
+ * @remarks
1707
+ * The recipe has twelve slots. The content scrolls in the primitives package's scroll area, whose
1708
+ * root is the `scroller` and fills the column, and the root does not scroll, so the header and
1709
+ * the footer remain in place however long the list of destinations is. The header and the footer
1710
+ * are columns padded like the content, so a search, a nav block or a switcher in a band is as
1711
+ * wide as the rows' fills, and a line of words in a band starts where the rows' icons start. A
1712
+ * nav block is a grid of two columns: the label and the headings take the first, the block's
1713
+ * control takes the second, and every other child spans both. A block's label is as tall as a
1714
+ * row, small, uppercase and in `fg.subtle`. A block whose rows a query matches none of is hidden,
1715
+ * unless it renders an empty message. On a rail the labels, the headings and the lines of words
1716
+ * in the bands are hidden visually and kept for screen readers, the empty message and the block's
1717
+ * control are removed, and the search centres its button. The app shell sets the sidebar's width,
1718
+ * and the root reads the shell's panel to render the rail. The recipe has no `palette` and no
1719
+ * `effect` axis: its looks are neutral grounds, and the navigation list inside it offers its own
1720
+ * palette and effect for the current row.
1721
+ */
1722
+ /**
1723
+ * Selects a part inside a sidebar collapsed to a rail.
1724
+ */
1725
+ const ICONIC$1 = "[data-iconic] &";
1726
+ /**
1727
+ * Selects the children of a band that fold to an icon by themselves: a search, a nav block, a
1728
+ * navigation list and a switcher.
1729
+ */
1730
+ const FOLDS = ".sidebar__search, .sidebar__nav, .nav-list__root, .switcher__root";
1731
+ /**
1732
+ * Selects the children of a band a caller writes around its words and marks: every child that does
1733
+ * not fold by itself.
1734
+ */
1735
+ const LINES = `& > :not(${FOLDS})`;
1736
+ /**
1737
+ * Selects the lines of a band that have no icon form: every line except a bare `svg`.
1738
+ */
1739
+ const WORDS = `& > :not(svg, ${FOLDS})`;
1740
+ /**
1741
+ * Styles the header and the footer as a column that keeps its height, and hides its words visually
1742
+ * on a rail.
1743
+ *
1744
+ * @remarks
1745
+ * On a rail only a bare `svg` and the parts that fold by themselves remain visible, so the rail
1746
+ * is as wide as its icons. The words remain in the accessibility tree, because the header's words
1747
+ * identify the workspace and the footer's words identify the signed-in person.
1748
+ */
1749
+ const BANDED = {
1750
+ display: "flex",
1751
+ flexDirection: "column",
1752
+ flexShrink: "0",
1753
+ [ICONIC$1]: {
1754
+ alignItems: "center",
1755
+ [WORDS]: { srOnly: true }
1756
+ },
1757
+ minInlineSize: "0"
1758
+ };
1759
+ /**
1760
+ * Maps each sidebar size to the height of a navigation list row at the same size: 24px, 32px and
1761
+ * 40px at the foundation's metrics.
1762
+ */
1763
+ const ROWS = {
1764
+ lg: "control.md",
1765
+ md: "control.xs",
1766
+ sm: "tag.md"
1767
+ };
1768
+ /**
1769
+ * Maps each sidebar size to the gap between the content's blocks, two gap sizes larger than the
1770
+ * content's padding: 12, 16 and 24px at the foundation's metrics.
1771
+ */
1772
+ const BLOCKS = {
1773
+ lg: "2xl",
1774
+ md: "xl",
1775
+ sm: "lg"
1776
+ };
1777
+ /**
1778
+ * Returns the inline inset of a navigation list row at the sidebar's size.
1779
+ *
1780
+ * @remarks
1781
+ * The navigation list insets a row by `spacing.inset` two sizes smaller than the row. The
1782
+ * labels, the headings, the block's control and the lines of words in the bands use the same
1783
+ * inset, so their text and icons start and end where the rows' do: 8, 8 and 12px at `sm`, `md`
1784
+ * and `lg`.
1785
+ * @param size - The sidebar's size.
1786
+ * @returns The inset, multiplied by the density.
1787
+ */
1788
+ function marked(size) {
1789
+ return dense(`{spacing.inset.${below(below(size))}}`);
1790
+ }
1791
+ /**
1792
+ * Returns the side of the block control's square: the tag size one size smaller than the sidebar,
1793
+ * and never under `sizes.6`.
1794
+ *
1795
+ * @remarks
1796
+ * The navigation list sizes the control and the count at the end of a row with the same
1797
+ * expression, so the block's control is in the rows' end column: 24px at every size.
1798
+ * @param size - The sidebar's size.
1799
+ * @returns The side of the square.
1800
+ */
1801
+ function squared(size) {
1802
+ return `max({sizes.6}, ${dense(`{sizes.tag.${below(size)}}`)})`;
1803
+ }
1804
+ /**
1805
+ * Returns the size styles of a band: the content's padding, the gap between its children, the rows'
1806
+ * inset on its lines of words and the text style of the rows' words one size up.
1807
+ *
1808
+ * @param size - The sidebar's size.
1809
+ * @returns The styles of the header or the footer at that size.
1810
+ */
1811
+ function banded(size) {
1812
+ return {
1813
+ gap: dense(`{spacing.gap.${below(size)}}`),
1814
+ [LINES]: { marginInline: marked(size) },
1815
+ padding: dense(`{spacing.gap.${size}}`),
1816
+ textStyle: `label.${below(size)}`
1817
+ };
1818
+ }
1819
+ /**
1820
+ * Styles a plain sidebar at the middle size.
1821
+ */
1822
+ const recipe$3 = defineSlotRecipe({
1823
+ base: {
1824
+ content: {
1825
+ display: "flex",
1826
+ flexDirection: "column"
1827
+ },
1828
+ empty: {
1829
+ color: "fg.muted",
1830
+ [ICONIC$1]: { display: "none" },
1831
+ textAlign: "center"
1832
+ },
1833
+ footer: BANDED,
1834
+ header: BANDED,
1835
+ nav: {
1836
+ alignItems: "center",
1837
+ display: "grid",
1838
+ gridTemplateColumns: "minmax(0, 1fr) auto",
1839
+ minInlineSize: "0",
1840
+ "&[data-unmatched]:not(:has(.sidebar__empty))": { display: "none" },
1841
+ "& > *": { gridColumn: "1 / -1" }
1842
+ },
1843
+ navAction: {
1844
+ ...interactive(),
1845
+ _active: { background: "colorPalette.emphasized" },
1846
+ _hover: {
1847
+ background: "colorPalette.muted",
1848
+ color: "fg"
1849
+ },
1850
+ alignItems: "center",
1851
+ appearance: "none",
1852
+ background: "transparent",
1853
+ borderRadius: "l1",
1854
+ color: "fg.muted",
1855
+ display: "flex",
1856
+ flexShrink: "0",
1857
+ gridColumn: "2 / 3",
1858
+ [ICONIC$1]: { display: "none" },
1859
+ justifyContent: "center",
1860
+ padding: "0"
1861
+ },
1862
+ navHeading: {
1863
+ ...truncate(),
1864
+ alignItems: "center",
1865
+ color: "fg.subtle",
1866
+ display: "flex",
1867
+ fontWeight: "medium",
1868
+ gridColumn: "1 / 2",
1869
+ [ICONIC$1]: { srOnly: true },
1870
+ textTransform: "uppercase"
1871
+ },
1872
+ navLabel: {
1873
+ ...truncate(),
1874
+ alignItems: "center",
1875
+ color: "fg.subtle",
1876
+ display: "flex",
1877
+ fontWeight: "medium",
1878
+ gridColumn: "1 / 2",
1879
+ [ICONIC$1]: { srOnly: true },
1880
+ textStyle: "label.xs",
1881
+ textTransform: "uppercase"
1882
+ },
1883
+ root: {
1884
+ blockSize: "100%",
1885
+ display: "flex",
1886
+ flexDirection: "column",
1887
+ minBlockSize: "0",
1888
+ minInlineSize: "0"
1889
+ },
1890
+ scroller: {
1891
+ flex: "1",
1892
+ minBlockSize: "0"
1893
+ },
1894
+ search: {
1895
+ [ICONIC$1]: {
1896
+ display: "flex",
1897
+ justifyContent: "center"
1898
+ },
1899
+ inlineSize: "full",
1900
+ minInlineSize: "0"
1901
+ },
1902
+ separator: divider("horizontal")
1903
+ },
1904
+ className: "sidebar",
1905
+ defaultVariants: {
1906
+ size: "md",
1907
+ variant: "plain"
1908
+ },
1909
+ jsx: [/^Sidebar(\.\w+)?$/u],
1910
+ slots: [
1911
+ "root",
1912
+ "header",
1913
+ "scroller",
1914
+ "content",
1915
+ "footer",
1916
+ "nav",
1917
+ "navLabel",
1918
+ "navHeading",
1919
+ "navAction",
1920
+ "search",
1921
+ "empty",
1922
+ "separator"
1923
+ ],
1924
+ variants: {
1925
+ size: onSlots({
1926
+ /**
1927
+ * The blocks are three to four times as far apart as the rows inside them, so each block
1928
+ * reads as its own group. A separator between two blocks sets no margin of its own, so the
1929
+ * gap on either side of it is the block gap.
1930
+ */
1931
+ content: sizeVariants((size) => ({
1932
+ "& > .sidebar__separator": { marginBlock: "0" },
1933
+ gap: dense(`{spacing.gap.${BLOCKS[size]}}`),
1934
+ padding: dense(`{spacing.gap.${size}}`)
1935
+ }), [
1936
+ "sm",
1937
+ "md",
1938
+ "lg"
1939
+ ]),
1940
+ empty: sizeVariants((size) => ({
1941
+ padding: dense(`{spacing.inset.${size}}`),
1942
+ textStyle: `body.${size}`
1943
+ }), [
1944
+ "sm",
1945
+ "md",
1946
+ "lg"
1947
+ ]),
1948
+ footer: sizeVariants(banded, [
1949
+ "sm",
1950
+ "md",
1951
+ "lg"
1952
+ ]),
1953
+ header: sizeVariants(banded, [
1954
+ "sm",
1955
+ "md",
1956
+ "lg"
1957
+ ]),
1958
+ /**
1959
+ * The label and the first row are as far apart as two rows.
1960
+ */
1961
+ nav: sizeVariants((size) => ({ gap: dense(`{spacing.gap.${below(below(size))}}`) }), [
1962
+ "sm",
1963
+ "md",
1964
+ "lg"
1965
+ ]),
1966
+ /**
1967
+ * The block's control is a square in the rows' end column, with the rows' end inset after
1968
+ * it and an icon the size of a row's leading icon.
1969
+ */
1970
+ navAction: sizeVariants((size) => ({
1971
+ "& > svg": { boxSize: dense(`{sizes.icon.${below(size)}}`) },
1972
+ blockSize: squared(size),
1973
+ marginInlineEnd: marked(size),
1974
+ minInlineSize: squared(size)
1975
+ }), [
1976
+ "sm",
1977
+ "md",
1978
+ "lg"
1979
+ ]),
1980
+ navHeading: sizeVariants((size) => ({
1981
+ marginBlockEnd: `calc(${dense(`{spacing.gap.${size}}`)} * -0.5)`,
1982
+ paddingBlock: dense("{spacing.gap.xs}"),
1983
+ paddingInline: marked(size),
1984
+ textStyle: "label.xs"
1985
+ }), [
1986
+ "sm",
1987
+ "md",
1988
+ "lg"
1989
+ ]),
1990
+ /**
1991
+ * The label is as tall as a row of the navigation list, so it reads as the list's first row.
1992
+ */
1993
+ navLabel: sizeVariants((size) => ({
1994
+ blockSize: `max({sizes.6}, ${dense(`{sizes.${ROWS[size]}}`)})`,
1995
+ paddingInline: marked(size)
1996
+ }), [
1997
+ "sm",
1998
+ "md",
1999
+ "lg"
2000
+ ]),
2001
+ /**
2002
+ * The search has zero inline padding, so the field is as wide as the rows under it.
2003
+ */
2004
+ search: sizeVariants(() => ({ paddingInline: "0" }), [
2005
+ "sm",
2006
+ "md",
2007
+ "lg"
2008
+ ]),
2009
+ separator: sizeVariants((size) => ({ marginBlock: dense(`{spacing.gap.${size}}`) }), [
2010
+ "sm",
2011
+ "md",
2012
+ "lg"
2013
+ ])
2014
+ }),
2015
+ /**
2016
+ * The ground and the edge of the sidebar.
2017
+ *
2018
+ * @remarks
2019
+ * `subtle` is a muted ground without an edge, for a sidebar inside an app shell panel. The
2020
+ * shell renders the hairline between the panel and the page.
2021
+ */
2022
+ variant: onSlot("root", {
2023
+ subtle: { background: "bg.subtle" },
2024
+ surface: {
2025
+ ...surface(),
2026
+ borderRadius: "0"
2027
+ },
2028
+ outline: {
2029
+ borderColor: "border",
2030
+ borderInlineEndWidth: "hairline"
2031
+ },
2032
+ plain: { background: "transparent" }
2033
+ })
2034
+ }
2035
+ });
2036
+ //#endregion
2037
+ //#region src/splitter/recipe.ts
2038
+ /**
2039
+ * Recipe for a splitter: panels in a row or a column, with a resize trigger between two panels.
2040
+ *
2041
+ * @remarks
2042
+ * The root is a flex row or column that fills its container: it grows in a flex container and is
2043
+ * as tall and as wide as any other container. The machine sizes each panel inline, and a panel is
2044
+ * a column its content can shrink in, so a scroll area inside it scrolls. The trigger is a strip
2045
+ * `sizes.2` across, pulled over both panels by half its width, so the panels share the root as if
2046
+ * it took no room. It stacks at `docked`, above a scroll area or other positioned content in the
2047
+ * panel after it, which would take the pointer off half the strip. Under a coarse pointer a
2048
+ * transparent pseudo-element widens the strip to `sizes.6`, the WCAG 2.5.8 target. The separator
2049
+ * is a hairline along the strip's middle, and the indicator a pill `sizes.6` long on its centre.
2050
+ * The line and the pill take the palette's solid while the pointer is over the strip, while it
2051
+ * has focus and while it drags. The focus ring is on the pill, because the strip's own box is
2052
+ * narrower than a ring, and on the strip of a trigger rendered without the pill. A disabled
2053
+ * trigger keeps the line and hides the pill. Under forced colours the line is `CanvasText`, an
2054
+ * active pill `Highlight` and the pill's ring `CanvasText`.
2055
+ */
2056
+ /**
2057
+ * Class name of the recipe, which a selector across its parts writes.
2058
+ */
2059
+ const CLASS$1 = "splitter";
2060
+ /**
2061
+ * Width of the trigger's strip across its line.
2062
+ */
2063
+ const STRIP = "{sizes.2}";
2064
+ /**
2065
+ * Width of the strip under a coarse pointer, the WCAG 2.5.8 target.
2066
+ */
2067
+ const TOUCH = "{sizes.6}";
2068
+ /**
2069
+ * Length of the indicator's pill along the line.
2070
+ */
2071
+ const PILL = "{sizes.6}";
2072
+ /**
2073
+ * Selects a part of a splitter whose panels sit in a row, where a trigger's line is vertical.
2074
+ */
2075
+ const ACROSS = "&[data-orientation=horizontal]";
2076
+ /**
2077
+ * Selects a part of a splitter whose panels sit in a column, where a trigger's line is horizontal.
2078
+ */
2079
+ const DOWN = "&[data-orientation=vertical]";
2080
+ /**
2081
+ * Selects a line or a pill while its trigger is under the pointer, has focus or drags.
2082
+ *
2083
+ * @remarks
2084
+ * `data-focus-visible` is the attribute a still scene stages focus with.
2085
+ */
2086
+ const ACTIVE = `.${CLASS$1}__resizeTrigger:is(:hover, [data-focus], [data-focus-visible], [data-dragging]):not([data-disabled]) > &`;
2087
+ /**
2088
+ * Selects the pill while its trigger has keyboard focus.
2089
+ */
2090
+ const RINGED = `.${CLASS$1}__resizeTrigger:is(:focus-visible, [data-focus-visible]) > &`;
2091
+ /**
2092
+ * Selects a trigger that renders the pill, which then takes the trigger's focus ring.
2093
+ */
2094
+ const PILLED = `&:has(> .${CLASS$1}__resizeTriggerIndicator)`;
2095
+ /**
2096
+ * Defines the splitter recipe, in the primary palette by default.
2097
+ */
2098
+ const recipe$2 = defineSlotRecipe({
2099
+ base: {
2100
+ panel: {
2101
+ display: "flex",
2102
+ flexDirection: "column",
2103
+ minBlockSize: "0",
2104
+ minInlineSize: "0"
2105
+ },
2106
+ resizeTrigger: {
2107
+ _touch: {
2108
+ _before: {
2109
+ content: "\"\"",
2110
+ position: "absolute"
2111
+ },
2112
+ [ACROSS]: { _before: {
2113
+ insetBlock: "0",
2114
+ insetInline: `calc((${TOUCH} - ${STRIP}) * -0.5)`
2115
+ } },
2116
+ [DOWN]: { _before: {
2117
+ insetBlock: `calc((${TOUCH} - ${STRIP}) * -0.5)`,
2118
+ insetInline: "0"
2119
+ } }
2120
+ },
2121
+ [ACROSS]: {
2122
+ inlineSize: STRIP,
2123
+ marginInline: `calc(${STRIP} * -0.5)`
2124
+ },
2125
+ alignItems: "center",
2126
+ display: "grid",
2127
+ [DOWN]: {
2128
+ blockSize: STRIP,
2129
+ marginBlock: `calc(${STRIP} * -0.5)`
2130
+ },
2131
+ focusRingColor: "colorPalette.focusRing",
2132
+ focusVisibleRing: "outside",
2133
+ justifyItems: "center",
2134
+ [PILLED]: { _focusVisible: { outlineStyle: "none" } },
2135
+ position: "relative",
2136
+ zIndex: "docked"
2137
+ },
2138
+ resizeTriggerIndicator: {
2139
+ _highContrast: {
2140
+ background: "Canvas",
2141
+ borderColor: "CanvasText",
2142
+ forcedColorAdjust: "none"
2143
+ },
2144
+ "&[data-disabled]": { visibility: "hidden" },
2145
+ [ACROSS]: {
2146
+ blockSize: PILL,
2147
+ inlineSize: STRIP
2148
+ },
2149
+ [ACTIVE]: {
2150
+ _highContrast: {
2151
+ background: "Highlight",
2152
+ borderColor: "Highlight"
2153
+ },
2154
+ background: "colorPalette.solid",
2155
+ borderColor: "colorPalette.solid"
2156
+ },
2157
+ background: "bg.panel",
2158
+ borderColor: "border.emphasized",
2159
+ borderRadius: "full",
2160
+ borderStyle: "solid",
2161
+ borderWidth: "hairline",
2162
+ boxShadow: "xs",
2163
+ [DOWN]: {
2164
+ blockSize: STRIP,
2165
+ inlineSize: PILL
2166
+ },
2167
+ gridArea: "1 / 1 / 2 / 2",
2168
+ position: "relative",
2169
+ [RINGED]: {
2170
+ _highContrast: { outlineColor: "CanvasText" },
2171
+ outlineColor: "colorPalette.focusRing",
2172
+ outlineOffset: "{spacing.ring}",
2173
+ outlineStyle: "solid",
2174
+ outlineWidth: "ring"
2175
+ },
2176
+ transitionDuration: "fast",
2177
+ transitionProperty: "common"
2178
+ },
2179
+ resizeTriggerSeparator: {
2180
+ _highContrast: {
2181
+ background: "CanvasText",
2182
+ forcedColorAdjust: "none"
2183
+ },
2184
+ [ACROSS]: {
2185
+ blockSize: "full",
2186
+ inlineSize: "{borderWidths.hairline}"
2187
+ },
2188
+ [ACTIVE]: {
2189
+ _highContrast: { background: "Highlight" },
2190
+ background: "colorPalette.solid"
2191
+ },
2192
+ background: "border",
2193
+ [DOWN]: {
2194
+ blockSize: "{borderWidths.hairline}",
2195
+ inlineSize: "full"
2196
+ },
2197
+ gridArea: "1 / 1 / 2 / 2",
2198
+ transitionDuration: "fast",
2199
+ transitionProperty: "common"
2200
+ },
2201
+ root: {
2202
+ [ACROSS]: { flexDirection: "row" },
2203
+ blockSize: "full",
2204
+ colorPalette: "primary",
2205
+ display: "flex",
2206
+ [DOWN]: { flexDirection: "column" },
2207
+ flex: "1",
2208
+ inlineSize: "full",
2209
+ minBlockSize: "0",
2210
+ minInlineSize: "0",
2211
+ overflow: "hidden"
2212
+ }
2213
+ },
2214
+ className: CLASS$1,
2215
+ jsx: [/^Splitter(\.\w+)?$/u],
2216
+ slots: [
2217
+ "root",
2218
+ "panel",
2219
+ "resizeTrigger",
2220
+ "resizeTriggerSeparator",
2221
+ "resizeTriggerIndicator"
2222
+ ],
2223
+ variants: {
2224
+ /**
2225
+ * Semantic palette of the line and the pill while the pointer is over a trigger, while it has
2226
+ * focus and while it drags, and of the focus ring. Without a value the root reads `primary`.
2227
+ */
2228
+ palette: onSlot("root", paletteVariants()) }
2229
+ });
2230
+ //#endregion
2231
+ //#region src/switcher/recipe.ts
2232
+ /**
2233
+ * Declares the switcher's slot recipe, which styles the control that shows the current workspace,
2234
+ * project or environment and opens the menu of the others.
2235
+ *
2236
+ * @remarks
2237
+ * The recipe has six slots. The root slot is the trigger, and the mark, the label, the name, the
2238
+ * detail and the indicator are inside it. `Switcher.Root` is the disclosure package's menu, so
2239
+ * the menu's recipe styles the panel and its rows. The trigger takes the button's looks and
2240
+ * palettes from the same layer styles, so an outlined switcher has the button's light edge. The
2241
+ * mark is a square in the palette's muted fill for an initial, an icon or an avatar. The name and
2242
+ * the detail truncate to keep the control at one height. A pair of chevrons marks a control that
2243
+ * switches and not a direction, so the indicator does not rotate while the menu is open. In a
2244
+ * sidebar closed to icons the control is its mark alone, square, and in a narrow toolbar a
2245
+ * switcher with a mark hides its name. The words remain in the accessible name in both.
2246
+ */
2247
+ /**
2248
+ * Selects a part of a switcher drawn as its mark alone, in a sidebar closed to icons.
2249
+ */
2250
+ const ICONIC = ".switcher__root[data-iconic] &";
2251
+ /**
2252
+ * Selects a part of a switcher with a mark in a toolbar too narrow for the name. A switcher
2253
+ * without a mark keeps its name, because the name is the only content that identifies it.
2254
+ */
2255
+ const NARROW = ".switcher__root[data-narrow]:has(.switcher__mark) &";
2256
+ /**
2257
+ * Custom property the size axis sets to the height of a button at the switcher's size.
2258
+ */
2259
+ const HEIGHT = "--switcher-height";
2260
+ /**
2261
+ * Styles a control that is one line as wide as its words: in a toolbar and on its own.
2262
+ *
2263
+ * @remarks
2264
+ * `inlineSize: fit` keeps the control as wide as its words, because a flex container fills its
2265
+ * line otherwise. The detail is hidden, because the control is one line tall. The control is at
2266
+ * least as tall as a button of its size, so it lines up with the buttons in a toolbar: 36px at
2267
+ * `sm` and 40px at `md`.
2268
+ */
2269
+ const INLINE = {
2270
+ detail: { display: "none" },
2271
+ root: {
2272
+ inlineSize: "fit",
2273
+ minBlockSize: `var(${HEIGHT})`
2274
+ }
2275
+ };
2276
+ /**
2277
+ * Styles the control in a sidebar: a row as wide as the column, and the mark alone on a rail.
2278
+ *
2279
+ * @remarks
2280
+ * On a rail the control has no padding and no edge, so it is the mark's square, as tall as the
2281
+ * rows under it: 32px at `md`.
2282
+ */
2283
+ const ROW = { root: {
2284
+ "&[data-iconic]": {
2285
+ alignSelf: "center",
2286
+ aspectRatio: "square",
2287
+ borderWidth: "0",
2288
+ inlineSize: "auto",
2289
+ justifyContent: "center",
2290
+ padding: "0"
2291
+ },
2292
+ inlineSize: "full"
2293
+ } };
2294
+ /**
2295
+ * Styles a ghost switcher at the middle size, alone.
2296
+ */
2297
+ const recipe$1 = defineSlotRecipe({
2298
+ base: {
2299
+ detail: {
2300
+ ...truncate(),
2301
+ color: "fg.subtle",
2302
+ textStyle: "caption"
2303
+ },
2304
+ indicator: {
2305
+ _open: { rotate: "0deg" },
2306
+ alignItems: "center",
2307
+ color: "fg.subtle",
2308
+ display: "inline-flex",
2309
+ flexShrink: "0",
2310
+ [ICONIC]: { display: "none" },
2311
+ justifyContent: "center",
2312
+ marginInlineStart: "auto",
2313
+ transitionDuration: "press",
2314
+ transitionProperty: "common",
2315
+ transitionTimingFunction: "press"
2316
+ },
2317
+ label: {
2318
+ display: "flex",
2319
+ flex: "1",
2320
+ flexDirection: "column",
2321
+ [ICONIC]: { srOnly: true },
2322
+ minInlineSize: "0",
2323
+ [NARROW]: { srOnly: true },
2324
+ textAlign: "start"
2325
+ },
2326
+ mark: {
2327
+ alignItems: "center",
2328
+ background: "colorPalette.muted",
2329
+ borderRadius: "l1",
2330
+ color: "colorPalette.fg",
2331
+ display: "inline-flex",
2332
+ flexShrink: "0",
2333
+ fontWeight: "semibold",
2334
+ justifyContent: "center",
2335
+ lineHeight: "tight",
2336
+ overflow: "clip"
2337
+ },
2338
+ name: {
2339
+ ...truncate(),
2340
+ fontWeight: "medium"
2341
+ },
2342
+ root: {
2343
+ ...interactive(),
2344
+ alignItems: "center",
2345
+ appearance: "none",
2346
+ borderColor: "transparent",
2347
+ borderWidth: "control",
2348
+ colorPalette: "neutral",
2349
+ display: "flex",
2350
+ minInlineSize: "0"
2351
+ }
2352
+ },
2353
+ className: "switcher",
2354
+ compoundVariants: [{
2355
+ css: { mark: {
2356
+ boxSize: dense("{sizes.icon.lg}"),
2357
+ fontSize: "xs"
2358
+ } },
2359
+ name: "marked",
2360
+ placement: ["alone", "toolbar"]
2361
+ }, (
2362
+ /**
2363
+ * Sets the detail and the indicator in the trigger's own ink on the solid look, where the
2364
+ * page's subtle ink is not read against the fill.
2365
+ */
2366
+ {
2367
+ css: {
2368
+ detail: { color: "inherit" },
2369
+ indicator: { color: "inherit" }
2370
+ },
2371
+ name: "inked",
2372
+ variant: "solid"
2373
+ })],
2374
+ defaultVariants: {
2375
+ placement: "alone",
2376
+ size: "md",
2377
+ variant: "ghost"
2378
+ },
2379
+ jsx: [/^Switcher(\.\w+)?$/u],
2380
+ slots: [
2381
+ "root",
2382
+ "mark",
2383
+ "label",
2384
+ "name",
2385
+ "detail",
2386
+ "indicator"
2387
+ ],
2388
+ variants: {
2389
+ /**
2390
+ * The palette of the look and the mark.
2391
+ */
2392
+ palette: onSlot("root", paletteVariants()),
2393
+ /**
2394
+ * Where the control is placed, which sets its width and its lines.
2395
+ *
2396
+ * @remarks
2397
+ * `Switcher.Root` reads the placement from the sidebar or the toolbar around it unless a
2398
+ * caller states one. The `marked` compound sizes the mark to `icon.lg` on one line, because
2399
+ * the size axis sizes it for a sidebar and a compound applies over an axis.
2400
+ */
2401
+ placement: {
2402
+ alone: INLINE,
2403
+ sidebar: ROW,
2404
+ toolbar: INLINE
2405
+ },
2406
+ size: onSlots({
2407
+ indicator: sizeVariants((size) => ({ boxSize: dense(`{sizes.icon.${below(size)}}`) }), [
2408
+ "sm",
2409
+ "md",
2410
+ "lg"
2411
+ ]),
2412
+ mark: sizeVariants((size) => ({
2413
+ boxSize: dense(`{sizes.control.${below(below(size))}}`),
2414
+ fontSize: below(size)
2415
+ }), [
2416
+ "sm",
2417
+ "md",
2418
+ "lg"
2419
+ ]),
2420
+ name: sizeVariants((size) => ({ textStyle: `body.${below(size)}` }), [
2421
+ "sm",
2422
+ "md",
2423
+ "lg"
2424
+ ]),
2425
+ root: sizeVariants((size) => ({
2426
+ borderRadius: "l2",
2427
+ gap: dense(`{spacing.gap.${size}}`),
2428
+ [HEIGHT]: dense(`{sizes.control.${size}}`),
2429
+ paddingBlock: dense(`{spacing.gap.${below(size)}}`),
2430
+ paddingInline: dense(`{spacing.gap.${size}}`),
2431
+ textStyle: `body.${below(size)}`
2432
+ }), [
2433
+ "sm",
2434
+ "md",
2435
+ "lg"
2436
+ ])
2437
+ }),
2438
+ /**
2439
+ * The button's six looks, each one of the theme's layer styles.
2440
+ *
2441
+ * @remarks
2442
+ * `ghost`, the default, has no fill at rest, so a switcher at the head of a sidebar shows the
2443
+ * sidebar's own ground and fills under a pointer.
2444
+ */
2445
+ variant: onSlot("root", lookVariants())
2446
+ }
2447
+ });
2448
+ //#endregion
2449
+ //#region src/toolbar/recipe.ts
2450
+ /**
2451
+ * Recipe for a toolbar: a row of controls with a start, a centre and an end band, and a search.
2452
+ *
2453
+ * @remarks
2454
+ * A band left out takes no room. The centre takes the room the other bands leave and truncates a
2455
+ * long title, because a toolbar on two lines moves everything under it. The root sets the gap as
2456
+ * `--toolbar-gap`, which every band reads: the gap token of the toolbar's size, 8px at `md`. The
2457
+ * layout package's `Group` joins controls, and the separator is the layout divider stretched to
2458
+ * the row's height. The search is 15rem wide and does not grow, because the centre takes the free
2459
+ * room. As a child of the row it shrinks down to zero before a band's controls do. A field's
2460
+ * percentage width gives a band no content width, so the search states an inline size. An opened
2461
+ * search covers the whole row, the padding and the edge of an outline or surface row included,
2462
+ * and the root hides its other children while it is open, so the row needs no fill of its own on
2463
+ * any surface. The recipe has no `palette`
2464
+ * axis, because the controls in the row set their own palettes, and no `effect` axis, because the
2465
+ * row is not a control.
2466
+ */
2467
+ /**
2468
+ * Custom property the root sets to the gap between controls, which every band reads.
2469
+ */
2470
+ const GAP = "--toolbar-gap";
2471
+ /**
2472
+ * Custom property the root sets to its own padding in the outline and surface looks.
2473
+ */
2474
+ const INSET = "--toolbar-inset";
2475
+ /**
2476
+ * Custom property the outline and surface looks set to the root's edge width, which an opened
2477
+ * search covers.
2478
+ */
2479
+ const EDGE = "--toolbar-edge";
2480
+ /**
2481
+ * Styles every band: a row of controls, centred on the cross axis.
2482
+ */
2483
+ const BAND = {
2484
+ alignItems: "center",
2485
+ display: "flex",
2486
+ gap: `var(${GAP})`,
2487
+ minInlineSize: "0"
2488
+ };
2489
+ /**
2490
+ * Class name of the recipe, which a selector across parts reads.
2491
+ */
2492
+ const CLASS = "toolbar";
2493
+ /**
2494
+ * Selects a root while its search is open.
2495
+ */
2496
+ const SEARCHING = `&:has(.${CLASS}__search[data-opened])`;
2497
+ /**
2498
+ * Defines the toolbar recipe: a plain row at size `md` with `l2` corners by default.
2499
+ */
2500
+ const recipe = defineSlotRecipe({
2501
+ base: {
2502
+ action: {
2503
+ ...FOLDING,
2504
+ flexShrink: "0"
2505
+ },
2506
+ center: {
2507
+ ...BAND,
2508
+ ...truncate(),
2509
+ "& > *": {
2510
+ minInlineSize: "0",
2511
+ overflow: "hidden",
2512
+ textOverflow: "ellipsis"
2513
+ },
2514
+ flex: "1",
2515
+ justifyContent: "center"
2516
+ },
2517
+ end: {
2518
+ ...BAND,
2519
+ flexShrink: "0",
2520
+ marginInlineStart: "auto"
2521
+ },
2522
+ group: { flexShrink: "0" },
2523
+ root: {
2524
+ ...BAND,
2525
+ inlineSize: "100%",
2526
+ position: "relative",
2527
+ [SEARCHING]: { [`& > :not(.${CLASS}__search)`]: { visibility: "hidden" } }
2528
+ },
2529
+ search: {
2530
+ "&[data-opened]": {
2531
+ "& > :first-child": { flex: "1" },
2532
+ alignItems: "stretch",
2533
+ display: "flex",
2534
+ inlineSize: "auto",
2535
+ inset: `calc(var(${EDGE}, 0px) * -1)`,
2536
+ position: "absolute",
2537
+ zIndex: "1"
2538
+ },
2539
+ "& > *": { minInlineSize: "0" },
2540
+ flex: "0 1 auto",
2541
+ inlineSize: "60",
2542
+ minInlineSize: "0"
2543
+ },
2544
+ separator: {
2545
+ alignSelf: "stretch",
2546
+ blockSize: "auto"
2547
+ },
2548
+ start: {
2549
+ ...BAND,
2550
+ flexShrink: "0"
2551
+ }
2552
+ },
2553
+ className: CLASS,
2554
+ defaultVariants: {
2555
+ radius: "l2",
2556
+ size: "md",
2557
+ variant: "plain"
2558
+ },
2559
+ jsx: [/^Toolbar(\.\w+)?$/u],
2560
+ slots: [
2561
+ "root",
2562
+ "start",
2563
+ "center",
2564
+ "end",
2565
+ "action",
2566
+ "group",
2567
+ "separator",
2568
+ "search"
2569
+ ],
2570
+ variants: {
2571
+ /**
2572
+ * Corner radius of an outlined or surface row.
2573
+ */
2574
+ radius: onSlot("root", cornerVariants([
2575
+ "l1",
2576
+ "l2",
2577
+ "l3"
2578
+ ])),
2579
+ /**
2580
+ * Size of the gap and of the separator's block margin.
2581
+ */
2582
+ size: onSlots({
2583
+ root: sizeVariants((size) => ({ [GAP]: `{spacing.gap.${size}}` })),
2584
+ separator: sizeVariants((size) => ({ marginBlock: dense(`{spacing.gap.${size}}`) }))
2585
+ }),
2586
+ /**
2587
+ * Look of the row.
2588
+ *
2589
+ * @remarks
2590
+ * `outline` and `surface` pad the row by its gap, so the controls start inside the row's
2591
+ * edge. `plain` has no edge and no padding.
2592
+ */
2593
+ variant: {
2594
+ surface: { root: {
2595
+ ...surface(),
2596
+ [EDGE]: "{borderWidths.hairline}",
2597
+ [INSET]: `var(${GAP})`,
2598
+ padding: `var(${INSET})`
2599
+ } },
2600
+ outline: { root: {
2601
+ borderColor: "border",
2602
+ borderWidth: "hairline",
2603
+ [EDGE]: "{borderWidths.hairline}",
2604
+ [INSET]: `var(${GAP})`,
2605
+ padding: `var(${INSET})`
2606
+ } },
2607
+ plain: { root: { background: "transparent" } }
2608
+ }
2609
+ }
2610
+ });
2611
+ //#endregion
2612
+ export { STICKY_OFFSET$1 as _, recipe$4 as a, recipe$8 as b, STICKY_TOP as c, PRIORITY as d, recipe$6 as f, SETTLED as g, PANEL_SIZE as h, recipe$3 as i, recipe$5 as l, PANEL_RAIL as m, recipe$1 as n, SCROLLPORT as o, recipe$7 as p, recipe$2 as r, STICKY_OFFSET as s, recipe as t, NARROW$2 as u, STICKY_TOP$1 as v, WINDOW_HEIGHT as y };