@stealthscale/component-screen 0.0.1 → 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.
@@ -1,4 +1,5 @@
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";
1
+ import { i as NARROW$1, n as FOLDING } from "./recipe-DRMsitrw.js";
2
+ import { CONTROL_INSET_END, CONTROL_INSET_START, axis, below, cornerVariants, defineSlotRecipe, dense, divider, interactive, motion, onSlot, onSlots, paletteVariants, sizeVariants, surface, truncate } from "@stealthscale/theme/authoring";
2
3
  //#region src/action-bar/recipe.ts
3
4
  /**
4
5
  * Styles the action bar, a panel fixed to the bottom of the window with the actions for what a
@@ -9,8 +10,9 @@ import { CONTROL_INSET_END, CONTROL_INSET_START, axis, below, cornerVariants, de
9
10
  * recipe passes its variants from an ancestor. The positioner spans the window inside the inset
10
11
  * and takes no pointer events, so the page around the bar remains pressable. The toolbar inside
11
12
  * 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,
13
+ * is narrower than the `sm` breakpoint, 640px, and while its controls need more room than the
14
+ * window leaves the bar. The minimum width makes it fold on the width of the window and not on
15
+ * the width of its folded content. The recipe has no `palette` axis,
14
16
  * because the bar reads the neutral `bg.panel`, and no `effect` axis, because the bar is not a
15
17
  * control.
16
18
  */
@@ -37,7 +39,7 @@ const ALIGNED = {
37
39
  /**
38
40
  * Defines the action bar recipe, which places the bar in the middle of the bottom edge by default.
39
41
  */
40
- const recipe$8 = defineSlotRecipe({
42
+ const recipe$7 = defineSlotRecipe({
41
43
  base: {
42
44
  closeTrigger: { flexShrink: "0" },
43
45
  content: {
@@ -85,6 +87,98 @@ const recipe$8 = defineSlotRecipe({
85
87
  placement: onSlot("positioner", axis(PLACEMENTS, (placement) => ({ justifyContent: ALIGNED[placement] }))()) }
86
88
  });
87
89
  //#endregion
90
+ //#region src/resize/widths.ts
91
+ /**
92
+ * Attribute an element has while a drag inside it resizes a panel or a column.
93
+ */
94
+ const RESIZING = "data-resizing";
95
+ /**
96
+ * Returns the CSS length of a width a person resized an element to: the width, and never wider than
97
+ * its limit, so a width stored on a wide screen fits a narrow one.
98
+ *
99
+ * @param width - The width, in pixels.
100
+ * @param max - The widest width, in pixels. Half the window where left out.
101
+ * @returns The length, such as `min(320px, 50vw)`.
102
+ */
103
+ function lengthOf(width, max) {
104
+ const limit = max === void 0 ? "50vw" : `${String(max)}px`;
105
+ return `min(${String(width)}px, ${limit})`;
106
+ }
107
+ /**
108
+ * Returns a width held between the bounds.
109
+ *
110
+ * @param width - The width asked for.
111
+ * @param bounds - The narrowest and the widest width.
112
+ * @returns The width, raised to the narrowest or lowered to the widest.
113
+ */
114
+ function clamped(width, { max, min }) {
115
+ return Math.min(Math.max(width, min), max);
116
+ }
117
+ /**
118
+ * Returns 1 where the pointer moving to the physical right widens the element, and -1 where it
119
+ * narrows it.
120
+ *
121
+ * @param edge - Side of the page the element is on.
122
+ * @param rtl - Whether the page reads right to left.
123
+ */
124
+ function growthOf(edge, rtl) {
125
+ return edge === "start" === !rtl ? 1 : -1;
126
+ }
127
+ /**
128
+ * Returns the width a key moves the element to.
129
+ *
130
+ * @param key - The key pressed, as `KeyboardEvent.key` spells it.
131
+ * @param shift - Whether Shift is held.
132
+ * @param width - The element's width now.
133
+ * @param scale - The narrowest and the widest width, and the direction the width grows in.
134
+ * @returns The new width, or undefined for a key that resizes nothing.
135
+ */
136
+ function keyedWidth(key, shift, width, scale) {
137
+ const step = (shift ? 50 : 10) * scale.growth;
138
+ if (key === "ArrowRight") return clamped(width + step, scale);
139
+ if (key === "ArrowLeft") return clamped(width - step, scale);
140
+ if (key === "Home") return scale.min;
141
+ if (key === "End") return scale.max;
142
+ }
143
+ //#endregion
144
+ //#region src/resize/strip.ts
145
+ /**
146
+ * Styles the strip on the edge of a panel or a column that a person presses, drags or focuses.
147
+ *
148
+ * @remarks
149
+ * The strip is 24px wide and transparent, with a line down its middle under the pointer, on
150
+ * keyboard focus and while a drag inside its holder runs. A separator, the strip of an element a
151
+ * person resizes, takes the column resize cursor. The strip is hidden under a coarse pointer,
152
+ * where a finger presses the menu button instead. The module imports no React, because the theme
153
+ * plugin evaluates the recipes that read it in Node.
154
+ */
155
+ /**
156
+ * Styles the strip, with its placement left to the recipe that reads it.
157
+ */
158
+ const STRIP$3 = {
159
+ ...interactive(),
160
+ _after: {
161
+ background: "transparent",
162
+ content: "\"\"",
163
+ inlineSize: "{borderWidths.indicator}",
164
+ insetBlock: "0",
165
+ insetInlineStart: "calc(50% - {borderWidths.indicator} / 2)",
166
+ position: "absolute"
167
+ },
168
+ _focusVisible: { _after: { background: "colorPalette.focusRing" } },
169
+ _hover: { _after: { background: "border.emphasized" } },
170
+ _touch: { display: "none" },
171
+ "&[role=separator]": { cursor: "resizeColumn" },
172
+ [`[${RESIZING}] &`]: { _after: { background: "border.emphasized" } },
173
+ appearance: "none",
174
+ background: "transparent",
175
+ borderWidth: "0",
176
+ colorPalette: "neutral",
177
+ inlineSize: "6",
178
+ padding: "0",
179
+ zIndex: "docked"
180
+ };
181
+ //#endregion
88
182
  //#region src/app-shell/metrics.ts
89
183
  /**
90
184
  * Names the custom properties and attributes the app shell reads, and defines the style objects
@@ -115,10 +209,24 @@ const PANEL_RAIL = "--app-shell-panel-rail";
115
209
  */
116
210
  const STICKY_OFFSET$1 = "--app-shell-sticky-offset";
117
211
  /**
118
- * Custom property on the root with the height of every pinned bar.
212
+ * Custom property on the root with the height of every pinned bar, the bars in a pane included,
213
+ * which a page's sticky bands stick under.
119
214
  */
120
215
  const STICKY_TOP$1 = "--app-shell-sticky-top";
121
216
  /**
217
+ * Custom property on the root with the height of the pinned bars above the body, which a panel
218
+ * that sticks under them reads.
219
+ *
220
+ * @remarks
221
+ * A bar in a pane is beside the panels and not above them, so a panel sticks under the root's
222
+ * own bars alone.
223
+ */
224
+ const PANEL_TOP = "--app-shell-panel-top";
225
+ /**
226
+ * Custom property on the root with the height of the pinned bars in a pane.
227
+ */
228
+ const PANE_TOP = "--app-shell-pane-top";
229
+ /**
122
230
  * Custom property an ancestor sets to the height of the shell's window.
123
231
  *
124
232
  * @remarks
@@ -140,14 +248,15 @@ const WINDOW = `var(${WINDOW_HEIGHT}, 100dvh)`;
140
248
  */
141
249
  const SETTLED = "data-settled";
142
250
  /**
143
- * Styles the transition of a moving part, with no motion under reduced motion or before the root
144
- * has settled.
251
+ * Styles the transition of a moving part, with no motion under reduced motion, before the root has
252
+ * settled, or while a person drags a panel's width.
145
253
  */
146
254
  const MOVING = {
147
255
  _motionReduce: { transitionDuration: "0s" },
148
256
  transitionDuration: "move",
149
257
  transitionTimingFunction: "move",
150
- [`.${CLASS$5}__root:not([${SETTLED}]) &`]: { transitionDuration: "0s" }
258
+ [`.${CLASS$5}__root:not([${SETTLED}]) &`]: { transitionDuration: "0s" },
259
+ [`.${CLASS$5}__root[${RESIZING}] &`]: { transitionDuration: "0s" }
151
260
  };
152
261
  /**
153
262
  * Width of a panel over the page: its open width, and never closer than a rail's width to the far
@@ -236,16 +345,16 @@ const PINNED = {
236
345
  zIndex: "sticky"
237
346
  };
238
347
  /**
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.
348
+ * Styles a panel in the body while the window scrolls: stuck under the root's pinned bars and as
349
+ * tall as the height they leave.
241
350
  *
242
351
  * @remarks
243
352
  * The rule selects a panel beside the page only, because the window-scroll look is a variant and
244
353
  * applies over the base, where a sheet is fixed and a panel under the page is static.
245
354
  */
246
355
  const STUCK$1 = { ["&:not([data-overlaid], [data-stacked])"]: {
247
- blockSize: `calc(${WINDOW} - var(${STICKY_TOP$1}, 0px))`,
248
- insetBlockStart: `var(${STICKY_TOP$1}, 0px)`,
356
+ blockSize: `calc(${WINDOW} - var(${PANEL_TOP}, 0px))`,
357
+ insetBlockStart: `var(${PANEL_TOP}, 0px)`,
249
358
  position: "sticky"
250
359
  } };
251
360
  /**
@@ -294,38 +403,18 @@ const COLUMN = {
294
403
  }
295
404
  };
296
405
  /**
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.
406
+ * Styles the rail: the 24px strip centred on the edge between a panel and the page.
299
407
  *
300
408
  * @remarks
301
409
  * 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.
410
+ * and the page by 12px each and moves neither.
304
411
  */
305
412
  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" },
413
+ ...STRIP$3,
318
414
  alignSelf: "stretch",
319
- appearance: "none",
320
- background: "transparent",
321
- borderWidth: "0",
322
- colorPalette: "neutral",
323
415
  flexShrink: "0",
324
- inlineSize: "6",
325
416
  marginInline: "calc({sizes.6} / -2)",
326
- padding: "0",
327
- position: "relative",
328
- zIndex: "docked"
417
+ position: "relative"
329
418
  };
330
419
  //#endregion
331
420
  //#region src/app-shell/recipe.ts
@@ -334,29 +423,36 @@ const RAIL = {
334
423
  * and a panel on either side of the page in the body.
335
424
  *
336
425
  * @remarks
337
- * The recipe has eighteen slots. The body, the main region, a panel's content and a band that
426
+ * The recipe has nineteen slots. The body, the main region, a panel's content and a band that
338
427
  * scrolls are the primitives package's scroll areas, so every bar in the shell is the theme's:
339
428
  * `bodyViewport` and `row` are the body's viewport and content, `mainViewport` is the `main`
340
429
  * 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.
430
+ * of a band that scrolls. A pane is a column in the row between the panels, with bars above and
431
+ * below the main region, so the panels run the full height beside the bars. A part that is left
432
+ * out takes no room. Every part is a container: the recipe sets the layout, the scrolling, the
433
+ * motion, the hairlines between regions and the grounds, and the application styles the rest
434
+ * through a theme. The plain look fills no region, so the shell shows the ground it is placed
435
+ * on. The backdrop, a sheet and a pinned bar are filled, because the page is visible under each.
436
+ * A sheet is raised by a shadow. A panel in the body is a track whose width moves between its
437
+ * open and closed widths, and its content keeps the open width, so the track clips the content
438
+ * instead of reflowing it. A panel over the page is fixed to the window with a backdrop behind
439
+ * it. The open and closed widths are the theme's `sizes.sidebar`, `sizes.aside` and `sizes.rail`
440
+ * unless a panel states its own. The window's height is `100dvh` unless an ancestor sets
441
+ * `WINDOW_HEIGHT`, so a shell staged in a box of a fixed height fills the box. The header and
442
+ * the footer pad their content by the middle gap, the status bar pads its row of entries by the
443
+ * smallest gap above and below and the middle gap at the sides, and takes no room while every
444
+ * entry renders nothing. A section pads by the large gap. The recipe has no `palette` and no
445
+ * `effect` axis, because the parts are containers and the components inside them offer their
446
+ * own.
355
447
  */
356
448
  /**
357
449
  * Styles a plain, divided shell that scrolls its page.
450
+ *
451
+ * @remarks
452
+ * `staticCss` lists every value of `content`, `scroll` and `variant`. A product's frame states
453
+ * them as data, and the compiler emits a variant only where a source states it as a literal.
358
454
  */
359
- const recipe$7 = defineSlotRecipe({
455
+ const recipe$6 = defineSlotRecipe({
360
456
  base: {
361
457
  aside: {
362
458
  ...PANEL,
@@ -412,10 +508,12 @@ const recipe$7 = defineSlotRecipe({
412
508
  "&[data-sticky]": {
413
509
  ...PINNED,
414
510
  insetBlockStart: `var(${STICKY_OFFSET$1}, 0px)`
415
- }
511
+ },
512
+ [`.${CLASS$5}__pane > &`]: { "&[data-sticky]": { insetBlockStart: `calc(var(${PANEL_TOP}, 0px) + var(${STICKY_OFFSET$1}, 0px))` } }
416
513
  },
417
514
  main: {
418
515
  flex: "1",
516
+ minBlockSize: "0",
419
517
  minInlineSize: "0"
420
518
  },
421
519
  navbar: {
@@ -431,16 +529,28 @@ const recipe$7 = defineSlotRecipe({
431
529
  insetInlineStart: "0"
432
530
  }
433
531
  },
532
+ pane: {
533
+ display: "flex",
534
+ flex: "1",
535
+ flexDirection: "column",
536
+ minBlockSize: "0",
537
+ minInlineSize: "0"
538
+ },
434
539
  rail: RAIL,
435
540
  root: {
436
541
  display: "flex",
437
542
  flexDirection: "column",
438
- inlineSize: "100%"
543
+ inlineSize: "100%",
544
+ [STICKY_TOP$1]: `calc(var(${PANEL_TOP}, 0px) + var(${PANE_TOP}, 0px))`,
545
+ [`&[${RESIZING}]`]: {
546
+ cursor: "resizeColumn",
547
+ userSelect: "none"
548
+ }
439
549
  },
440
550
  row: {
441
551
  display: "flex",
442
552
  "&:has(> [data-stacked])": { flexWrap: "wrap" },
443
- [`&:has(> [data-stacked]) > .${CLASS$5}__main`]: { flexBasis: "100%" }
553
+ [`&:has(> [data-stacked]) > :is(.${CLASS$5}__main, .${CLASS$5}__pane)`]: { flexBasis: "100%" }
444
554
  },
445
555
  scroller: SCROLLER,
446
556
  section: SECTION,
@@ -452,12 +562,14 @@ const recipe$7 = defineSlotRecipe({
452
562
  flexWrap: "wrap",
453
563
  paddingBlock: dense("{spacing.gap.xs}"),
454
564
  paddingInline: dense("{spacing.gap.md}"),
455
- rowGap: dense("{spacing.gap.xs}")
565
+ rowGap: dense("{spacing.gap.xs}"),
566
+ "&:empty": { display: "none" }
456
567
  },
457
568
  trigger: { flexShrink: "0" }
458
569
  },
459
570
  className: CLASS$5,
460
571
  defaultVariants: {
572
+ content: "fluid",
461
573
  divided: true,
462
574
  scroll: "page",
463
575
  variant: "plain"
@@ -470,6 +582,7 @@ const recipe$7 = defineSlotRecipe({
470
582
  "bodyViewport",
471
583
  "row",
472
584
  "navbar",
585
+ "pane",
473
586
  "main",
474
587
  "mainViewport",
475
588
  "aside",
@@ -483,7 +596,27 @@ const recipe$7 = defineSlotRecipe({
483
596
  "rail",
484
597
  "backdrop"
485
598
  ],
599
+ staticCss: [{
600
+ content: ["constrained", "fluid"],
601
+ scroll: ["page", "window"],
602
+ variant: [
603
+ "floating",
604
+ "inset",
605
+ "plain"
606
+ ]
607
+ }],
486
608
  variants: {
609
+ /**
610
+ * Width of the page in the main region: the whole region, or at most the theme's wide page
611
+ * measure, centred in the region.
612
+ */
613
+ content: {
614
+ constrained: { column: { [`.${CLASS$5}__mainViewport > &`]: {
615
+ marginInline: "auto",
616
+ maxInlineSize: "{sizes.page.wide}"
617
+ } } },
618
+ fluid: { column: { [`.${CLASS$5}__mainViewport > &`]: { maxInlineSize: "none" } } }
619
+ },
487
620
  /**
488
621
  * Whether a hairline separates each bar and each panel from the page.
489
622
  *
@@ -529,7 +662,7 @@ const recipe$7 = defineSlotRecipe({
529
662
  [`&:has(> .${CLASS$5}__row > [data-stacked])`]: { overflow: "auto" },
530
663
  overflow: "visible"
531
664
  },
532
- mainViewport: { [`.${CLASS$5}__row:has(> [data-stacked]) > .${CLASS$5}__main > &`]: { overflow: "visible" } },
665
+ mainViewport: { [`.${CLASS$5}__row:has(> [data-stacked]) > :is(.${CLASS$5}__main, .${CLASS$5}__pane > .${CLASS$5}__main) > &`]: { overflow: "visible" } },
533
666
  root: { blockSize: WINDOW },
534
667
  row: {
535
668
  "&:has(> [data-stacked])": { blockSize: "auto" },
@@ -552,7 +685,9 @@ const recipe$7 = defineSlotRecipe({
552
685
  * `plain` fills nothing, so every region shows the ground the shell is placed on: the page's
553
686
  * `bg` in an application, or a staging box's own fill. A sidebar in a panel paints its own
554
687
  * 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.
688
+ * shadow, over a `bg.subtle` root, and gives it to the pane instead where the main region is
689
+ * in one, so the bars in the pane are on the card too. `floating` gives each panel's content
690
+ * `surface()` instead.
556
691
  */
557
692
  variant: {
558
693
  floating: {
@@ -565,7 +700,11 @@ const recipe$7 = defineSlotRecipe({
565
700
  root: { background: "bg.subtle" }
566
701
  },
567
702
  inset: {
568
- main: {
703
+ main: { [`&:not(.${CLASS$5}__pane > *)`]: {
704
+ ...surface(),
705
+ margin: dense("{spacing.gap.xs}")
706
+ } },
707
+ pane: {
569
708
  ...surface(),
570
709
  margin: dense("{spacing.gap.xs}")
571
710
  },
@@ -604,7 +743,7 @@ const CLASS$4 = "floating-panel";
604
743
  /**
605
744
  * Width of a resize trigger's strip, and the side of a corner's square.
606
745
  */
607
- const STRIP$1 = "{sizes.2}";
746
+ const STRIP$2 = "{sizes.2}";
608
747
  /**
609
748
  * Stacking level of the positioner: the banner level of the z-index scale plus the panel's place in
610
749
  * the stack of open panels, which the machine writes as `--z-index`.
@@ -617,15 +756,16 @@ const RING_OFFSET = "--scroll-area-ring-offset";
617
756
  /**
618
757
  * Defines the floating panel recipe.
619
758
  */
620
- const recipe$6 = defineSlotRecipe({
759
+ const recipe$5 = defineSlotRecipe({
621
760
  base: {
622
761
  body: {
623
- padding: dense("{spacing.inset.sm}"),
624
- textStyle: "body.sm"
762
+ textStyle: "body.sm",
763
+ padding: dense("{spacing.inset.sm}")
625
764
  },
626
765
  closeTrigger: { flexShrink: "0" },
627
766
  content: {
628
767
  ...motion("scale-fade.in", "scale-fade.out"),
768
+ focusVisibleRing: "outside",
629
769
  background: "bg.panel",
630
770
  borderColor: "transparent",
631
771
  borderRadius: "l3",
@@ -636,7 +776,6 @@ const recipe$6 = defineSlotRecipe({
636
776
  display: "flex",
637
777
  flexDirection: "column",
638
778
  focusRingColor: "colorPalette.focusRing",
639
- focusVisibleRing: "outside",
640
779
  position: "relative"
641
780
  },
642
781
  control: {
@@ -677,11 +816,11 @@ const recipe$6 = defineSlotRecipe({
677
816
  },
678
817
  positioner: { zIndex: LAYERED },
679
818
  resizeTrigger: {
680
- "&:is([data-axis=e], [data-axis=w])": { inlineSize: STRIP$1 },
681
- "&:is([data-axis=n], [data-axis=s])": { blockSize: STRIP$1 },
819
+ "&:is([data-axis=e], [data-axis=w])": { inlineSize: STRIP$2 },
820
+ "&:is([data-axis=n], [data-axis=s])": { blockSize: STRIP$2 },
682
821
  "&:is([data-axis=ne], [data-axis=nw], [data-axis=se], [data-axis=sw])": {
683
- blockSize: STRIP$1,
684
- inlineSize: STRIP$1
822
+ blockSize: STRIP$2,
823
+ inlineSize: STRIP$2
685
824
  },
686
825
  "&[data-disabled]": { display: "none" },
687
826
  zIndex: "docked"
@@ -690,8 +829,8 @@ const recipe$6 = defineSlotRecipe({
690
829
  scroller: {
691
830
  "&[hidden]": { display: "none" },
692
831
  flex: "1",
693
- marginBlockEnd: STRIP$1,
694
- marginInlineEnd: STRIP$1,
832
+ marginBlockEnd: STRIP$2,
833
+ marginInlineEnd: STRIP$2,
695
834
  minBlockSize: "0",
696
835
  [RING_OFFSET]: "calc({borderWidths.ring} * -1)"
697
836
  },
@@ -701,8 +840,8 @@ const recipe$6 = defineSlotRecipe({
701
840
  },
702
841
  title: {
703
842
  ...truncate(),
704
- fontWeight: "semibold",
705
- textStyle: "label.md"
843
+ textStyle: "label.md",
844
+ fontWeight: "semibold"
706
845
  },
707
846
  trigger: {
708
847
  ...interactive(),
@@ -730,58 +869,11 @@ const recipe$6 = defineSlotRecipe({
730
869
  ]
731
870
  });
732
871
  //#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
872
  //#region src/page/metrics.ts
782
873
  /**
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.
874
+ * Computes the rules the page recipe writes at each of its sizes: the padding and gaps of the
875
+ * bands, the tabs in the navigation band, the panel the tab picker opens and the title's heading
876
+ * role.
785
877
  *
786
878
  * @remarks
787
879
  * The module is separate from the recipe so the recipe is under the 300-line limit. It imports no
@@ -804,14 +896,35 @@ const TABS = "tabs";
804
896
  */
805
897
  const COMMAND = "command";
806
898
  /**
807
- * Selects the root of the tabs in the navigation band.
899
+ * Selects the strip of tabs in the navigation band: the tabs' root, or the `Page.Tabs` slot where
900
+ * the tabs' root contains the panels too and the band contains the list alone.
808
901
  */
809
- const TABS_IN_NAV = `& > .${TABS}__root`;
902
+ const STRIP$1 = `:is(.${TABS}__root, .${CLASS$3}__tabs)`;
903
+ /**
904
+ * Selects the strip of tabs as a child of the navigation band.
905
+ */
906
+ const TABS_IN_NAV = `& > ${STRIP$1}`;
810
907
  /**
811
908
  * Selects every tab of a strip in the navigation band.
812
909
  */
813
910
  const TAB_IN_NAV = `& .${TABS}__trigger`;
814
911
  /**
912
+ * Sizes the page offers, which a section in the page reads too.
913
+ */
914
+ const STEPS = [
915
+ "sm",
916
+ "md",
917
+ "lg"
918
+ ];
919
+ /**
920
+ * Selects a band followed by the navigation, which then has the hairline.
921
+ */
922
+ const BEFORE_NAV = `&:has(+ .${CLASS$3}__nav)`;
923
+ /**
924
+ * Selects a navigation band with a strip of tabs, which is flush with the band's hairline.
925
+ */
926
+ const WITH_TABS = `&:has(> ${STRIP$1})`;
927
+ /**
815
928
  * Tab height and inline inset per page size: a medium or large page's tabs are 48px tall and inset
816
929
  * by the `md` inset, a small page's 44px and the `sm` inset.
817
930
  */
@@ -877,18 +990,65 @@ const NARROW_TITLE = `.${CLASS$3}__root[data-narrow] > .${CLASS$3}__header > &`;
877
990
  */
878
991
  const TITLES = {
879
992
  lg: {
880
- [NARROW_TITLE]: { textStyle: "heading.lg" },
881
- textStyle: "heading.xl"
993
+ textStyle: "heading.xl",
994
+ [NARROW_TITLE]: { textStyle: "heading.lg" }
882
995
  },
883
996
  md: {
884
- [NARROW_TITLE]: { textStyle: "heading.md" },
885
- textStyle: "heading.lg"
997
+ textStyle: "heading.lg",
998
+ [NARROW_TITLE]: { textStyle: "heading.md" }
886
999
  },
887
1000
  sm: {
888
- [NARROW_TITLE]: { textStyle: "heading.sm" },
889
- textStyle: "heading.md"
1001
+ textStyle: "heading.md",
1002
+ [NARROW_TITLE]: { textStyle: "heading.sm" }
890
1003
  }
891
1004
  };
1005
+ /**
1006
+ * Rules the recipe's `size` axis writes on each slot it sizes, at the page's three sizes.
1007
+ */
1008
+ const SIZES = onSlots({
1009
+ actions: sizeVariants((size) => ({ gap: dense(`{spacing.gap.${size}}`) }), STEPS),
1010
+ aside: sizeVariants((size) => ({ paddingBlock: dense(`{spacing.inset.${size}}`) }), STEPS),
1011
+ banner: sizeVariants((size) => ({ paddingBlock: dense(`{spacing.inset.${size}}`) }), STEPS),
1012
+ body: sizeVariants((size) => ({ paddingBlock: dense(`{spacing.inset.${size}}`) }), STEPS),
1013
+ context: sizeVariants((size) => ({
1014
+ textStyle: `body.${below(size)}`,
1015
+ gap: dense(`{spacing.gap.${size}}`),
1016
+ marginBlockEnd: size === "lg" ? "{spacing.1}" : "{spacing.0.5}"
1017
+ }), STEPS),
1018
+ description: {
1019
+ lg: { textStyle: "body.md" },
1020
+ md: { textStyle: "body.md" },
1021
+ sm: { textStyle: "body.sm" }
1022
+ },
1023
+ footer: sizeVariants((size) => ({
1024
+ gap: dense(`{spacing.gap.${size}}`),
1025
+ paddingBlock: dense(`{spacing.inset.${size}}`)
1026
+ }), STEPS),
1027
+ header: sizeVariants((size) => ({
1028
+ [BEFORE_NAV]: { paddingBlockEnd: dense(`{spacing.gap.${size}}`) },
1029
+ paddingBlock: dense(`{spacing.inset.${size}}`)
1030
+ }), STEPS),
1031
+ meta: sizeVariants((size) => ({
1032
+ gap: dense(`{spacing.gap.${size}}`),
1033
+ marginInlineStart: dense(`{spacing.gap.${size}}`)
1034
+ }), STEPS),
1035
+ nav: sizeVariants((size) => ({
1036
+ ...tabbed(size),
1037
+ paddingBlock: dense(`{spacing.gap.${size}}`),
1038
+ [WITH_TABS]: { paddingBlock: "0" }
1039
+ }), STEPS),
1040
+ palette: sizeVariants(() => FLAT_PALETTE, STEPS),
1041
+ sidebar: sizeVariants((size) => ({ paddingBlock: dense(`{spacing.inset.${size}}`) }), STEPS),
1042
+ title: TITLES,
1043
+ toolbar: sizeVariants((size) => ({
1044
+ gap: dense(`{spacing.gap.${size}}`),
1045
+ paddingBlock: dense(`{spacing.gap.${size}}`)
1046
+ }), STEPS),
1047
+ trail: sizeVariants((size) => ({
1048
+ textStyle: `body.${below(size)}`,
1049
+ gap: dense(`{spacing.gap.${below(size)}}`)
1050
+ }), STEPS)
1051
+ });
892
1052
  //#endregion
893
1053
  //#region src/page/recipe.ts
894
1054
  /**
@@ -906,9 +1066,13 @@ const TITLES = {
906
1066
  * `folded` below the `sm` breakpoint. From the `lg` breakpoint of the window, a page with an
907
1067
  * aside becomes a grid with the aside beside the body, because a rail beside the text needs the
908
1068
  * 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.
1069
+ * `folds="hide"`. A page at least as wide as the `md` breakpoint, by its own measurement, becomes
1070
+ * a grid with its sidebar at the start of the body's row, and a narrower page renders the
1071
+ * sidebar's picker as a band above the body. A sticky aside keeps to the top of its row, under
1072
+ * the shell's sticky bars, and so does a sticky sidebar. The recipe has no `palette` axis,
1073
+ * because a page is layout on the page's surface, and no `effect` axis, because a page is not a
1074
+ * control. `staticCss` lists `folded`, because the root sets it from its measured width, so an
1075
+ * application's stylesheet compiler does not read the value.
912
1076
  */
913
1077
  /**
914
1078
  * Custom property the root sets to the room at the page's inline edges.
@@ -949,10 +1113,15 @@ const STICKY_OFFSET = "--page-sticky-offset";
949
1113
  */
950
1114
  const STICKY_TOP = "--page-sticky-top";
951
1115
  /**
952
- * Custom property a sticky aside takes, set to the height of the area that scrolls the page.
1116
+ * Custom property a sticky aside or sidebar takes, set to the height of the area that scrolls the
1117
+ * page.
953
1118
  */
954
1119
  const SCROLLPORT = "--page-scrollport";
955
1120
  /**
1121
+ * Custom property the sidebar sets to the width a person resized it to.
1122
+ */
1123
+ const SIDEBAR_SIZE = "--page-sidebar-size";
1124
+ /**
956
1125
  * Room a focus ring takes outside an element: its offset and its width.
957
1126
  */
958
1127
  const RING_ROOM = "calc(var(--focus-ring-offset, 0px) + var(--focus-ring-width, 0px))";
@@ -973,23 +1142,41 @@ const STUCK = {
973
1142
  zIndex: "sticky"
974
1143
  };
975
1144
  /**
1145
+ * Styles a column that sticks to the top of its row, under the shell's sticky bars and the page's
1146
+ * sticky bands, a gap below them.
1147
+ */
1148
+ const STICKY_COLUMN = {
1149
+ alignSelf: "start",
1150
+ insetBlockStart: `calc(var(${SHELL_TOP}, 0px) + var(${STICKY_TOP}, 0px) + {spacing.gap.xl})`,
1151
+ position: "sticky"
1152
+ };
1153
+ /**
1154
+ * Caps a sticky column at the height of the area that scrolls the page, less the sticky bars and
1155
+ * bands above it and a gap at each end, as a flex column that its scroll area fills.
1156
+ */
1157
+ const CAPPED_COLUMN = {
1158
+ display: "flex",
1159
+ flexDirection: "column",
1160
+ maxBlockSize: `calc(var(${SCROLLPORT}, 100dvh) - var(${SHELL_TOP}, 0px) - var(${STICKY_TOP}, 0px) - {spacing.gap.xl} * 2)`
1161
+ };
1162
+ /**
976
1163
  * Styles a row that centres its items, wraps and shrinks.
977
1164
  */
978
- const ROW$2 = {
1165
+ const ROW$1 = {
979
1166
  alignItems: "center",
980
1167
  display: "flex",
981
1168
  flexWrap: "wrap",
982
1169
  minInlineSize: "0"
983
1170
  };
984
1171
  /**
985
- * Selects a band followed by the navigation, which then has the hairline.
986
- */
987
- const BEFORE_NAV = `&:has(+ .${CLASS$3}__nav)`;
988
- /**
989
1172
  * Selects a root with an aside, which lays its bands out as a grid from the `lg` breakpoint.
990
1173
  */
991
1174
  const WITH_ASIDE = `&:has(> .${CLASS$3}__aside)`;
992
1175
  /**
1176
+ * Selects a root with a sidebar beside its body, which lays its bands out as a grid.
1177
+ */
1178
+ const WITH_SIDEBAR = `&:has(> .${CLASS$3}__sidebar[data-beside])`;
1179
+ /**
993
1180
  * Selects a toolbar in the navigation band.
994
1181
  */
995
1182
  const TOOLBAR_IN_NAV = `& > .toolbar__root`;
@@ -1004,38 +1191,47 @@ const STRIP_IN_NAV = `& .${CLASS$3}__tabs[data-orientation]`;
1004
1191
  * @remarks
1005
1192
  * The body's row takes the remaining height and every other row takes its content's height,
1006
1193
  * 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.
1194
+ * between its `auto` rows. The sidebar's row holds the picker a narrow page renders in the
1195
+ * sidebar's place, and takes no height without one.
1008
1196
  */
1009
1197
  const BESIDE = {
1010
1198
  columnGap: dense("{spacing.gap.xl}"),
1011
1199
  display: "grid",
1012
- gridTemplateAreas: "\"banner banner\" \"header header\" \"nav nav\" \"toolbar toolbar\" \"body aside\" \"footer footer\"",
1200
+ gridTemplateAreas: "\"banner banner\" \"header header\" \"nav nav\" \"toolbar toolbar\" \"sidebar sidebar\" \"body aside\" \"footer footer\"",
1013
1201
  gridTemplateColumns: "minmax(0, 1fr) auto",
1014
- gridTemplateRows: "auto auto auto auto 1fr auto"
1202
+ gridTemplateRows: "auto auto auto auto auto 1fr auto"
1015
1203
  };
1016
1204
  /**
1017
- * Sizes the page offers, which a section in the page reads too.
1205
+ * Styles the grid of a page with a sidebar beside its body: every band across, the sidebar at the
1206
+ * start, and an aside under the body.
1018
1207
  */
1019
- const STEPS = [
1020
- "sm",
1021
- "md",
1022
- "lg"
1023
- ];
1208
+ const SIDEBAR_BESIDE = {
1209
+ columnGap: dense("{spacing.gap.xl}"),
1210
+ display: "grid",
1211
+ gridTemplateAreas: "\"banner banner\" \"header header\" \"nav nav\" \"toolbar toolbar\" \"sidebar body\" \"sidebar aside\" \"footer footer\"",
1212
+ gridTemplateColumns: "auto minmax(0, 1fr)",
1213
+ gridTemplateRows: "auto auto auto auto 1fr auto auto"
1214
+ };
1024
1215
  /**
1025
- * Selects a navigation band with a strip of tabs, which is flush with the band's hairline.
1216
+ * Styles the grid of a page with a sidebar beside its body and an aside beside it from the `lg`
1217
+ * breakpoint: the sidebar, the body and the aside in one row.
1026
1218
  */
1027
- const WITH_TABS = `&:has(> .${TABS}__root)`;
1219
+ const SIDEBAR_AND_ASIDE = {
1220
+ gridTemplateAreas: "\"banner banner banner\" \"header header header\" \"nav nav nav\" \"toolbar toolbar toolbar\" \"sidebar body aside\" \"footer footer footer\"",
1221
+ gridTemplateColumns: "auto minmax(0, 1fr) auto",
1222
+ gridTemplateRows: "auto auto auto auto 1fr auto"
1223
+ };
1028
1224
  /**
1029
1225
  * Defines the page recipe: a full-width page at size `md` by default.
1030
1226
  */
1031
- const recipe$5 = defineSlotRecipe({
1227
+ const recipe$4 = defineSlotRecipe({
1032
1228
  base: {
1033
1229
  action: {
1034
1230
  ...FOLDING,
1035
1231
  flexShrink: "0"
1036
1232
  },
1037
1233
  actions: {
1038
- ...ROW$2,
1234
+ ...ROW$1,
1039
1235
  flexWrap: "nowrap",
1040
1236
  gridArea: "actions",
1041
1237
  justifySelf: "end",
@@ -1052,14 +1248,8 @@ const recipe$5 = defineSlotRecipe({
1052
1248
  aside: {
1053
1249
  "&[data-folds=hide]": { lgDown: { display: "none" } },
1054
1250
  "&[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"
1251
+ ...STICKY_COLUMN,
1252
+ lg: CAPPED_COLUMN
1063
1253
  },
1064
1254
  gridArea: "aside",
1065
1255
  lg: { paddingInlineStart: "0" },
@@ -1089,7 +1279,7 @@ const recipe$5 = defineSlotRecipe({
1089
1279
  minBlockSize: "0"
1090
1280
  },
1091
1281
  context: {
1092
- ...ROW$2,
1282
+ ...ROW$1,
1093
1283
  gridArea: "context",
1094
1284
  minInlineSize: "0"
1095
1285
  },
@@ -1107,7 +1297,7 @@ const recipe$5 = defineSlotRecipe({
1107
1297
  */
1108
1298
  footer: {
1109
1299
  ...BAND$1,
1110
- ...ROW$2,
1300
+ ...ROW$1,
1111
1301
  "&[data-sticky]": {
1112
1302
  ...STUCK,
1113
1303
  insetBlockEnd: "0",
@@ -1139,7 +1329,7 @@ const recipe$5 = defineSlotRecipe({
1139
1329
  marginInlineEnd: dense("{spacing.gap.lg}")
1140
1330
  },
1141
1331
  meta: {
1142
- ...ROW$2,
1332
+ ...ROW$1,
1143
1333
  gridArea: "meta"
1144
1334
  },
1145
1335
  nav: {
@@ -1172,14 +1362,64 @@ const recipe$5 = defineSlotRecipe({
1172
1362
  minInlineSize: "0"
1173
1363
  },
1174
1364
  root: {
1365
+ [`&[${RESIZING}]`]: {
1366
+ cursor: "resizeColumn",
1367
+ userSelect: "none"
1368
+ },
1175
1369
  display: "flex",
1176
1370
  flexDirection: "column",
1177
1371
  flexGrow: "1",
1178
1372
  inlineSize: "100%",
1179
1373
  minBlockSize: "100%",
1180
1374
  minInlineSize: "0",
1181
- [WITH_ASIDE]: { lg: BESIDE }
1375
+ [WITH_ASIDE]: { lg: BESIDE },
1376
+ [WITH_SIDEBAR]: {
1377
+ ...SIDEBAR_BESIDE,
1378
+ [WITH_ASIDE]: { lg: SIDEBAR_AND_ASIDE }
1379
+ }
1380
+ },
1381
+ /**
1382
+ * The sidebar: a column beside the body on a page as wide as the `md` breakpoint, and a band
1383
+ * with the picker on a narrower page.
1384
+ *
1385
+ * @remarks
1386
+ * Beside the body the column starts at the gutter, so the fills of its rows start where the
1387
+ * title starts. The column is as wide as the theme's sidebar unless a person resized it. A
1388
+ * sticky sidebar keeps to the top of its row and is at most as tall as the room under the
1389
+ * sticky bands, as a sticky aside is. The handle is centred on the column's end edge, in the
1390
+ * gap before the body.
1391
+ */
1392
+ sidebar: {
1393
+ ...BAND$1,
1394
+ "&[data-beside]": {
1395
+ "&[data-sticky]": {
1396
+ ...STICKY_COLUMN,
1397
+ ...CAPPED_COLUMN
1398
+ },
1399
+ inlineSize: `var(${SIDEBAR_SIZE}, {sizes.sidebar})`,
1400
+ paddingInlineEnd: "0",
1401
+ position: "relative"
1402
+ },
1403
+ gridArea: "sidebar"
1404
+ },
1405
+ /**
1406
+ * The padding of a sticky sidebar's scroll area, the room a focus ring takes at its edges.
1407
+ */
1408
+ sidebarContent: { padding: RING_ROOM },
1409
+ /**
1410
+ * The strip on a resizable sidebar's end edge, centred on the edge.
1411
+ */
1412
+ sidebarHandle: {
1413
+ ...STRIP$3,
1414
+ insetBlock: "0",
1415
+ insetInlineEnd: "calc({sizes.6} / -2)",
1416
+ position: "absolute"
1182
1417
  },
1418
+ /**
1419
+ * A sticky sidebar's scroll area, pulled out on every side by the room its content's padding
1420
+ * takes, so the content keeps its place.
1421
+ */
1422
+ sidebarScroller: { margin: `calc(${RING_ROOM} * -1)` },
1183
1423
  title: {
1184
1424
  gridArea: "title",
1185
1425
  minInlineSize: "0",
@@ -1187,7 +1427,7 @@ const recipe$5 = defineSlotRecipe({
1187
1427
  },
1188
1428
  toolbar: {
1189
1429
  ...BAND$1,
1190
- ...ROW$2,
1430
+ ...ROW$1,
1191
1431
  "&[data-sticky]": STUCK,
1192
1432
  gridArea: "toolbar"
1193
1433
  },
@@ -1250,6 +1490,10 @@ const recipe$5 = defineSlotRecipe({
1250
1490
  "picker",
1251
1491
  "palette",
1252
1492
  "toolbar",
1493
+ "sidebar",
1494
+ "sidebarScroller",
1495
+ "sidebarContent",
1496
+ "sidebarHandle",
1253
1497
  "body",
1254
1498
  "aside",
1255
1499
  "asideScroller",
@@ -1257,6 +1501,7 @@ const recipe$5 = defineSlotRecipe({
1257
1501
  "footer",
1258
1502
  "trail"
1259
1503
  ],
1504
+ staticCss: [{ folded: ["true"] }],
1260
1505
  variants: {
1261
1506
  /**
1262
1507
  * Position of the content when the measure is narrower than the page.
@@ -1333,49 +1578,7 @@ const recipe$5 = defineSlotRecipe({
1333
1578
  * The page offers three sizes, because larger steps belong to a display heading. A section in
1334
1579
  * the page reads this size too.
1335
1580
  */
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
- })
1581
+ size: SIZES
1379
1582
  }
1380
1583
  });
1381
1584
  //#endregion
@@ -1413,7 +1616,7 @@ const AFTER = `.${CLASS$2}__root + &`;
1413
1616
  /**
1414
1617
  * Selects a band of a narrow section.
1415
1618
  */
1416
- const NARROW$1 = `.${CLASS$2}__root[data-narrow] > &`;
1619
+ const NARROW = `.${CLASS$2}__root[data-narrow] > &`;
1417
1620
  /**
1418
1621
  * Selects the actions of a narrow section.
1419
1622
  */
@@ -1457,7 +1660,7 @@ const SPACED = {
1457
1660
  /**
1458
1661
  * Styles a row that centres its items, wraps and shrinks.
1459
1662
  */
1460
- const ROW$1 = {
1663
+ const ROW = {
1461
1664
  alignItems: "center",
1462
1665
  display: "flex",
1463
1666
  flexWrap: "wrap",
@@ -1466,14 +1669,14 @@ const ROW$1 = {
1466
1669
  /**
1467
1670
  * Defines the section recipe: a plain section at size `md` by default.
1468
1671
  */
1469
- const recipe$4 = defineSlotRecipe({
1672
+ const recipe$3 = defineSlotRecipe({
1470
1673
  base: {
1471
1674
  action: {
1472
1675
  ...FOLDING,
1473
1676
  flexShrink: "0"
1474
1677
  },
1475
1678
  actions: {
1476
- ...ROW$1,
1679
+ ...ROW,
1477
1680
  flexWrap: "nowrap",
1478
1681
  gridArea: "actions",
1479
1682
  justifySelf: "end",
@@ -1488,7 +1691,7 @@ const recipe$4 = defineSlotRecipe({
1488
1691
  minInlineSize: "0"
1489
1692
  },
1490
1693
  footer: {
1491
- ...ROW$1,
1694
+ ...ROW,
1492
1695
  "& > :last-child:not(:first-child)": { marginInlineStart: "auto" },
1493
1696
  justifyContent: "space-between"
1494
1697
  },
@@ -1599,7 +1802,7 @@ const recipe$4 = defineSlotRecipe({
1599
1802
  }
1600
1803
  },
1601
1804
  header: {
1602
- [NARROW$1]: { marginBlockEnd: `var(${BANDS})` },
1805
+ [NARROW]: { marginBlockEnd: `var(${BANDS})` },
1603
1806
  padding: "0"
1604
1807
  },
1605
1808
  root: {
@@ -1644,8 +1847,8 @@ const recipe$4 = defineSlotRecipe({
1644
1847
  "lg"
1645
1848
  ]),
1646
1849
  description: sizeVariants((size) => ({
1647
- marginBlockStart: size === "lg" ? "{spacing.1}" : "{spacing.0.5}",
1648
- textStyle: `body.${below(size)}`
1850
+ textStyle: `body.${below(size)}`,
1851
+ marginBlockStart: size === "lg" ? "{spacing.1}" : "{spacing.0.5}"
1649
1852
  }), [
1650
1853
  "sm",
1651
1854
  "md",
@@ -1722,7 +1925,7 @@ const recipe$4 = defineSlotRecipe({
1722
1925
  /**
1723
1926
  * Selects a part inside a sidebar collapsed to a rail.
1724
1927
  */
1725
- const ICONIC$1 = "[data-iconic] &";
1928
+ const ICONIC = "[data-iconic] &";
1726
1929
  /**
1727
1930
  * Selects the children of a band that fold to an icon by themselves: a search, a nav block, a
1728
1931
  * navigation list and a switcher.
@@ -1750,7 +1953,7 @@ const BANDED = {
1750
1953
  display: "flex",
1751
1954
  flexDirection: "column",
1752
1955
  flexShrink: "0",
1753
- [ICONIC$1]: {
1956
+ [ICONIC]: {
1754
1957
  alignItems: "center",
1755
1958
  [WORDS]: { srOnly: true }
1756
1959
  },
@@ -1810,16 +2013,21 @@ function squared(size) {
1810
2013
  */
1811
2014
  function banded(size) {
1812
2015
  return {
2016
+ textStyle: `label.${below(size)}`,
1813
2017
  gap: dense(`{spacing.gap.${below(size)}}`),
1814
2018
  [LINES]: { marginInline: marked(size) },
1815
- padding: dense(`{spacing.gap.${size}}`),
1816
- textStyle: `label.${below(size)}`
2019
+ padding: dense(`{spacing.gap.${size}}`)
1817
2020
  };
1818
2021
  }
1819
2022
  /**
1820
2023
  * Styles a plain sidebar at the middle size.
2024
+ *
2025
+ * @remarks
2026
+ * `staticCss` lists every size and every look. A sidebar in a panel over the page renders at `lg`
2027
+ * whatever its `size`, a product's frame states the size and the look as data, and the compiler
2028
+ * emits a variant only where a source states it as a literal.
1821
2029
  */
1822
- const recipe$3 = defineSlotRecipe({
2030
+ const recipe$2 = defineSlotRecipe({
1823
2031
  base: {
1824
2032
  content: {
1825
2033
  display: "flex",
@@ -1827,7 +2035,7 @@ const recipe$3 = defineSlotRecipe({
1827
2035
  },
1828
2036
  empty: {
1829
2037
  color: "fg.muted",
1830
- [ICONIC$1]: { display: "none" },
2038
+ [ICONIC]: { display: "none" },
1831
2039
  textAlign: "center"
1832
2040
  },
1833
2041
  footer: BANDED,
@@ -1855,7 +2063,7 @@ const recipe$3 = defineSlotRecipe({
1855
2063
  display: "flex",
1856
2064
  flexShrink: "0",
1857
2065
  gridColumn: "2 / 3",
1858
- [ICONIC$1]: { display: "none" },
2066
+ [ICONIC]: { display: "none" },
1859
2067
  justifyContent: "center",
1860
2068
  padding: "0"
1861
2069
  },
@@ -1866,18 +2074,18 @@ const recipe$3 = defineSlotRecipe({
1866
2074
  display: "flex",
1867
2075
  fontWeight: "medium",
1868
2076
  gridColumn: "1 / 2",
1869
- [ICONIC$1]: { srOnly: true },
2077
+ [ICONIC]: { srOnly: true },
1870
2078
  textTransform: "uppercase"
1871
2079
  },
1872
2080
  navLabel: {
1873
2081
  ...truncate(),
2082
+ textStyle: "label.xs",
1874
2083
  alignItems: "center",
1875
2084
  color: "fg.subtle",
1876
2085
  display: "flex",
1877
2086
  fontWeight: "medium",
1878
2087
  gridColumn: "1 / 2",
1879
- [ICONIC$1]: { srOnly: true },
1880
- textStyle: "label.xs",
2088
+ [ICONIC]: { srOnly: true },
1881
2089
  textTransform: "uppercase"
1882
2090
  },
1883
2091
  root: {
@@ -1892,7 +2100,7 @@ const recipe$3 = defineSlotRecipe({
1892
2100
  minBlockSize: "0"
1893
2101
  },
1894
2102
  search: {
1895
- [ICONIC$1]: {
2103
+ [ICONIC]: {
1896
2104
  display: "flex",
1897
2105
  justifyContent: "center"
1898
2106
  },
@@ -1921,6 +2129,19 @@ const recipe$3 = defineSlotRecipe({
1921
2129
  "empty",
1922
2130
  "separator"
1923
2131
  ],
2132
+ staticCss: [{
2133
+ size: [
2134
+ "sm",
2135
+ "md",
2136
+ "lg"
2137
+ ],
2138
+ variant: [
2139
+ "outline",
2140
+ "plain",
2141
+ "subtle",
2142
+ "surface"
2143
+ ]
2144
+ }],
1924
2145
  variants: {
1925
2146
  size: onSlots({
1926
2147
  /**
@@ -1938,8 +2159,8 @@ const recipe$3 = defineSlotRecipe({
1938
2159
  "lg"
1939
2160
  ]),
1940
2161
  empty: sizeVariants((size) => ({
1941
- padding: dense(`{spacing.inset.${size}}`),
1942
- textStyle: `body.${size}`
2162
+ textStyle: `body.${size}`,
2163
+ padding: dense(`{spacing.inset.${size}}`)
1943
2164
  }), [
1944
2165
  "sm",
1945
2166
  "md",
@@ -1978,10 +2199,10 @@ const recipe$3 = defineSlotRecipe({
1978
2199
  "lg"
1979
2200
  ]),
1980
2201
  navHeading: sizeVariants((size) => ({
2202
+ textStyle: "label.xs",
1981
2203
  marginBlockEnd: `calc(${dense(`{spacing.gap.${size}}`)} * -0.5)`,
1982
2204
  paddingBlock: dense("{spacing.gap.xs}"),
1983
- paddingInline: marked(size),
1984
- textStyle: "label.xs"
2205
+ paddingInline: marked(size)
1985
2206
  }), [
1986
2207
  "sm",
1987
2208
  "md",
@@ -2095,7 +2316,7 @@ const PILLED = `&:has(> .${CLASS$1}__resizeTriggerIndicator)`;
2095
2316
  /**
2096
2317
  * Defines the splitter recipe, in the primary palette by default.
2097
2318
  */
2098
- const recipe$2 = defineSlotRecipe({
2319
+ const recipe$1 = defineSlotRecipe({
2099
2320
  base: {
2100
2321
  panel: {
2101
2322
  display: "flex",
@@ -2104,6 +2325,7 @@ const recipe$2 = defineSlotRecipe({
2104
2325
  minInlineSize: "0"
2105
2326
  },
2106
2327
  resizeTrigger: {
2328
+ focusVisibleRing: "outside",
2107
2329
  _touch: {
2108
2330
  _before: {
2109
2331
  content: "\"\"",
@@ -2129,7 +2351,6 @@ const recipe$2 = defineSlotRecipe({
2129
2351
  marginBlock: `calc(${STRIP} * -0.5)`
2130
2352
  },
2131
2353
  focusRingColor: "colorPalette.focusRing",
2132
- focusVisibleRing: "outside",
2133
2354
  justifyItems: "center",
2134
2355
  [PILLED]: { _focusVisible: { outlineStyle: "none" } },
2135
2356
  position: "relative",
@@ -2228,224 +2449,6 @@ const recipe$2 = defineSlotRecipe({
2228
2449
  palette: onSlot("root", paletteVariants()) }
2229
2450
  });
2230
2451
  //#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
2452
  //#region src/toolbar/recipe.ts
2450
2453
  /**
2451
2454
  * Recipe for a toolbar: a row of controls with a start, a centre and an end band, and a search.
@@ -2460,9 +2463,11 @@ const recipe$1 = defineSlotRecipe({
2460
2463
  * percentage width gives a band no content width, so the search states an inline size. An opened
2461
2464
  * search covers the whole row, the padding and the edge of an outline or surface row included,
2462
2465
  * 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
+ * any surface. The title is semibold and leaves twice the gap after it, so the control after it
2467
+ * reads as the first of the row's controls and not as part of the title. A narrow row folds
2468
+ * those controls, and its title leaves the row's gap alone. The recipe has no
2469
+ * `palette` axis, because the controls in the row set their own palettes, and no `effect` axis,
2470
+ * because the row is not a control.
2466
2471
  */
2467
2472
  /**
2468
2473
  * Custom property the root sets to the gap between controls, which every band reads.
@@ -2548,6 +2553,13 @@ const recipe = defineSlotRecipe({
2548
2553
  start: {
2549
2554
  ...BAND,
2550
2555
  flexShrink: "0"
2556
+ },
2557
+ title: {
2558
+ ...BAND,
2559
+ [`&[${NARROW$1}]`]: { marginInlineEnd: "0" },
2560
+ flexShrink: "0",
2561
+ fontWeight: "semibold",
2562
+ marginInlineEnd: `calc(var(${GAP}) * 2)`
2551
2563
  }
2552
2564
  },
2553
2565
  className: CLASS,
@@ -2565,7 +2577,8 @@ const recipe = defineSlotRecipe({
2565
2577
  "action",
2566
2578
  "group",
2567
2579
  "separator",
2568
- "search"
2580
+ "search",
2581
+ "title"
2569
2582
  ],
2570
2583
  variants: {
2571
2584
  /**
@@ -2609,4 +2622,4 @@ const recipe = defineSlotRecipe({
2609
2622
  }
2610
2623
  });
2611
2624
  //#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 };
2625
+ export { lengthOf as C, keyedWidth as S, STICKY_OFFSET$1 as _, SCROLLPORT as a, clamped as b, STICKY_TOP as c, recipe$6 as d, PANEL_RAIL as f, SETTLED as g, PANE_TOP as h, recipe$3 as i, recipe$4 as l, PANEL_TOP as m, recipe$1 as n, SIDEBAR_SIZE as o, PANEL_SIZE as p, recipe$2 as r, STICKY_OFFSET as s, recipe as t, recipe$5 as u, WINDOW_HEIGHT as v, recipe$7 as w, growthOf as x, RESIZING as y };