contain-css-svelte 1.2.3 → 1.4.0

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.
Files changed (50) hide show
  1. package/dist/Card.svelte +216 -288
  2. package/dist/controls/Button.svelte +39 -1
  3. package/dist/controls/ButtonLink.svelte +39 -1
  4. package/dist/controls/Checkbox.svelte +38 -0
  5. package/dist/controls/CircleButton.svelte +39 -1
  6. package/dist/controls/Input.svelte +39 -1
  7. package/dist/controls/RadioButton.svelte +38 -0
  8. package/dist/controls/Select.svelte +38 -0
  9. package/dist/controls/Slider.svelte +39 -1
  10. package/dist/controls/TabItem.svelte +39 -1
  11. package/dist/controls/Toggle.svelte +39 -1
  12. package/dist/dropdowns/DropdownMenu.svelte +39 -1
  13. package/dist/index.d.ts +2 -1
  14. package/dist/index.js +2 -1
  15. package/dist/layout/Accordion.svelte +39 -1
  16. package/dist/layout/Bar.svelte +49 -1
  17. package/dist/layout/ColumnContainer.svelte +39 -1
  18. package/dist/layout/Columns.svelte +39 -1
  19. package/dist/layout/Container.svelte +217 -292
  20. package/dist/layout/DataList.svelte +39 -1
  21. package/dist/layout/DataListItem.svelte +217 -292
  22. package/dist/layout/Fieldset.svelte +39 -1
  23. package/dist/layout/Form.svelte +217 -292
  24. package/dist/layout/FormItem.svelte +39 -1
  25. package/dist/layout/GridLayout.svelte +38 -0
  26. package/dist/layout/Hero.svelte +217 -292
  27. package/dist/layout/Inline.svelte +45 -1
  28. package/dist/layout/MenuList.svelte +39 -1
  29. package/dist/layout/Page.svelte +44 -2
  30. package/dist/layout/RowContainer.svelte +39 -1
  31. package/dist/layout/Sidebar.svelte +757 -65
  32. package/dist/layout/Sidebar.svelte.d.ts +58 -1
  33. package/dist/layout/SidebarContainer.svelte +198 -0
  34. package/dist/layout/SidebarContainer.svelte.d.ts +20 -0
  35. package/dist/layout/SplitPane.svelte +39 -1
  36. package/dist/layout/Stack.svelte +45 -1
  37. package/dist/layout/TabBar.svelte +39 -1
  38. package/dist/layout/Table.svelte +469 -443
  39. package/dist/layout/Tile.svelte +216 -291
  40. package/dist/misc/CodeInner.svelte +39 -1
  41. package/dist/misc/Progress.svelte +217 -292
  42. package/dist/misc/Tag.svelte +39 -1
  43. package/dist/overlays/Dialog.svelte +217 -292
  44. package/dist/overlays/Tooltip.svelte +267 -349
  45. package/dist/sass/_responsive.scss +15 -0
  46. package/dist/sass/_typography.scss +203 -191
  47. package/dist/typography/TextLayout.svelte +494 -292
  48. package/dist/vars/defaults.css +258 -0
  49. package/dist/vars/typography-context.css +271 -0
  50. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">import { elementProps } from "../util";
2
2
  import { COLOR_VARS, TYPOGRAPHY_VARS, } from "../styleProps";
3
- let { left, right, children, expandLabel = "Expand sidebar", collapseLabel = "Collapse sidebar", class: className, ...restProps } = $props();
3
+ let { left, right, children, overlay = false, sticky = false, sheetButton = "tab", open = $bindable(undefined), expandLabel = "Expand sidebar", collapseLabel = "Collapse sidebar", class: className, ...restProps } = $props();
4
4
  /* The shorthands sidebar's own CSS backs, one group per mixin it
5
5
  includes. The Props type is derived from this same array, so what the
6
6
  component accepts and what it emits cannot drift apart. */
@@ -12,14 +12,24 @@ const el = $derived(elementProps(restProps, "sidebar", [
12
12
  ...SIDEBAR_VARS,
13
13
  "width",
14
14
  ]));
15
- let expandedHamburger = $state(false);
16
- let expandedBar = $state(true);
15
+ /* One piece of state, two layouts. The wide rail and the compact sheet
16
+ disagree about what "untouched" should look like -- the rail starts open,
17
+ the sheet starts closed -- so `undefined` stands for "this layout's
18
+ default" and each layout resolves it its own way. The moment anything
19
+ (either built-in toggle, or a caller through `bind:open`) writes a
20
+ boolean, both layouts read that same boolean, which is what keeps
21
+ programmatic control and the internal toggles in sync. */
22
+ const expandedBar = $derived(open ?? true);
23
+ const expandedHamburger = $derived(open ?? false);
17
24
  </script>
18
25
 
19
26
  <aside
20
27
  class={["sidebar", className]}
21
28
  class:right
22
29
  class:left
30
+ class:overlay
31
+ class:sticky
32
+ class:button-sheet-toggle={sheetButton === "button"}
23
33
  class:expandedHamburger
24
34
  class:expandedBar
25
35
  {...el}
@@ -28,25 +38,53 @@ let expandedBar = $state(true);
28
38
  class:expander={!expandedHamburger}
29
39
  class:close={expandedHamburger}
30
40
  aria-label={expandedHamburger ? collapseLabel : expandLabel}
41
+ aria-expanded={expandedHamburger}
31
42
  data-audit-action="toggle-sidebar-sheet"
32
- onclick={() => (expandedHamburger = !expandedHamburger)}
43
+ onclick={() => (open = !expandedHamburger)}
33
44
  ></button>
34
45
  <div class="content">
35
46
  {@render children?.()}
36
47
  </div>
37
48
  <label class="edge-bar">
38
49
  <button
39
- onclick={() => (expandedBar = !expandedBar)}
50
+ onclick={() => (open = !expandedBar)}
40
51
  class="expander"
41
52
  class:expander={!expandedBar}
42
53
  class:close={expandedBar}
43
- aria-label={expandedBar ? "Collapse sidebar" : "Expand sidebar"}
54
+ aria-label={expandedBar ? collapseLabel : expandLabel}
55
+ aria-expanded={expandedBar}
44
56
  data-audit-action="toggle-sidebar-rail"
45
57
  ></button>
46
58
  </label>
47
59
  </aside>
48
60
 
49
61
  <style>@charset "UTF-8";
62
+ /* Per-level heading (h1-h6) box/typography properties, resolved to PRIVATE,
63
+ inheriting custom properties (--_h1-color, --_h2-font-size, etc) rather than
64
+ assigned directly to a matched h1-h6 selector. Nothing in this file matches
65
+ an actual heading element anymore -- see typography-context.css (vars/), which
66
+ is the single, global, :where()-scoped rule that actually reads these vars
67
+ and applies them to real elements. That split is what lets a component
68
+ nested arbitrarily deep (through <article>, <Tile>, whatever) still get the
69
+ nearest ancestor's heading styling: these are ordinary inherited custom
70
+ properties, so DOM proximity decides which component's values apply, not
71
+ selector specificity or stylesheet order. See AGENTS-INTERNAL.md. */
72
+ /* All six heading levels' context vars in one call -- what every
73
+ "typography container" component (Container, Card, Tile, TextLayout, ...)
74
+ includes on itself once, alongside the shared --_heading-* vars below. */
75
+ /* Shared prose-measure group: p, blockquote, dl, ul, ol. li/dt/dd don't get
76
+ their own vars -- they're never wider than their ul/ol/dl parent's own
77
+ max-width box, so they're already bounded for free by ordinary block
78
+ layout, not by anything selector-based reaching them individually. */
79
+ /* p specifically also gets the fuller typography-props-bare treatment
80
+ (paragraph-prefixed), which -- same as in the original selector-based
81
+ version -- is declared/consumed AFTER the shared prose vars above, so it
82
+ wins for whichever properties both groups set. */
83
+ /* The first paragraph in a typography context (p:first-of-type, or a p right
84
+ after a heading) gets its own, slightly different treatment -- larger lede
85
+ text, no top margin. Structural matching (:first-of-type, `h1 + p`) still
86
+ has to happen where the real element is, in the global consumer rule; only
87
+ the VALUES move to context vars here. */
50
88
  /* Logical, not top/bottom/left/right: margin-block/margin-inline already take
51
89
  one value (both sides equal) or two (start end), so there is no separate
52
90
  "which side" prop to remember and no 4-value shorthand order to get wrong.
@@ -55,6 +93,18 @@ let expandedBar = $state(true);
55
93
  day this mixin got added to a new component. Callers opt in with an actual
56
94
  value. (Container does not use this mixin: it wants --gap/auto as its
57
95
  defaults, which do not fit the "0 unless asked" contract here.) */
96
+ /* The width at or below which a Sidebar swaps its grab-bar rail for the sheet
97
+ affordance -- a floating button that opens a panel over the content.
98
+
99
+ Page has to agree with Sidebar about this, because Page is what floats the
100
+ aside out of flow and reserves the strip of padding the sheet button sits
101
+ in. They used to disagree: Sidebar switched at 512px and Page at 600px,
102
+ which left a band between 513 and 600 where Page had already lifted the
103
+ aside out of the layout while the Sidebar inside it was still showing a
104
+ rail -- a grab bar floating on top of the content, which is the affordance
105
+ we deliberately avoid everywhere else.
106
+
107
+ One definition, used by both, so they cannot drift again. */
58
108
  /* Warning: because we define a fallback
59
109
  media query, the media query can override the container
60
110
  if we stack a bunch of these in a row and aren't thoughtful about the order.
@@ -63,6 +113,9 @@ let expandedBar = $state(true);
63
113
  */
64
114
  /* Convenience groupings */
65
115
  aside {
116
+ /* Set only by `sheetButton="button"` (below); cleared here so it never
117
+ leaks in from an enclosing sidebar. */
118
+ --_sidebar-sheet-edge-override: initial;
66
119
  --link-bg: var(--sidebar-link-bg, var(--surface-link-bg, inherit));
67
120
  --link-fg: var(--sidebar-link-fg, var(--surface-link-fg, inherit));
68
121
  --_bg-mix-color: var(--sidebar-bg-mix-color, var(--surface-bg-mix-color, var(--bg-mix-color, transparent)));
@@ -294,38 +347,491 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
294
347
  }
295
348
 
296
349
  /* Expander doo-dad */
350
+ /* The sheet affordance: a floating panel you summon with a button and
351
+ dismiss when you are done with it.
352
+
353
+ Used by the compact layout, and by `overlay` at any width. Overlay wants
354
+ this rather than the rail: a panel that starts open on top of the content
355
+ is nonsense, and a grab-bar rail floating over the content is a strange
356
+ thing to ask anyone to grab. A button that opens a sheet is the
357
+ affordance people already know.
358
+
359
+ It keys on .expandedHamburger, which resolves `undefined` to false -- so
360
+ a sheet starts shut, where the rail starts open. That falls out of the
361
+ shared `open` state for free. */
297
362
  /* Responsive sidebar... */
298
363
  @container (min-width: 513px) {
299
- /* Aside is a relative container whose width
300
- will smoothly animate so our parent knows
301
- how to lay us out */
302
- aside {
364
+ /* The rail layout. Everything here is scoped away from `overlay`, which
365
+ uses the sheet affordance at every width instead. */
366
+ aside:not(.overlay) {
303
367
  transition: width var(--sidebar-transition);
304
368
  position: relative;
305
369
  height: 100%;
306
370
  width: var(--grab-bar-width);
307
371
  }
308
- aside.expandedBar {
372
+ aside:not(.overlay).expandedBar {
309
373
  width: calc(var(--sidebar-width) + var(--grab-bar-width));
310
374
  }
311
- aside .content {
375
+ aside:not(.overlay) .content {
312
376
  transition: transform var(--sidebar-transition);
313
377
  transform: translateX(-110%);
314
378
  }
315
- aside.right .content {
379
+ aside:not(.overlay).right .content {
316
380
  transform: translateX(100%);
317
381
  }
318
- /* Positioning */
319
- aside.expandedBar .content {
382
+ aside:not(.overlay).expandedBar .content {
320
383
  transform: translateX(0);
321
384
  position: absolute;
322
385
  top: 0;
323
386
  left: 0;
324
387
  width: var(--sidebar-width);
325
388
  }
326
- aside.expandedBar.right .content {
389
+ aside:not(.overlay).expandedBar.right .content {
390
+ left: auto;
391
+ right: 0;
392
+ }
393
+ aside.overlay {
394
+ /* The sheet and its button are absolutely positioned, so the sidebar has
395
+ to be their containing block. Without this they resolve against
396
+ whatever positioned ancestor happens to be up the tree -- inside <Page>
397
+ that is Page's own aside and it looks fine, but drop a Sidebar into a
398
+ Card or a Container and the button escapes to the top-left of the page,
399
+ over unrelated content.
400
+
401
+ `overflow` has to be released in the same breath. Every child here is
402
+ out of flow, so the aside computes to zero height -- harmless while it
403
+ was not a containing block, but the moment it becomes one the inherited
404
+ `overflow: hidden` clips the button and the sheet away to nothing.
405
+ Nothing needs clipping here: the shut sheet is hidden by opacity and
406
+ pointer-events, not by the box. */
407
+ /* Sized here rather than on the button, because the panel needs them too
408
+ -- see the padding it reserves below. */
409
+ --_sidebar-expander-width: max(
410
+ var(--sidebar-icon-width, 0.65rem),
411
+ var(--icon-size, 32px)
412
+ );
413
+ --_sidebar-expander-height: max(
414
+ var(--sidebar-icon-height, 1rem),
415
+ var(--icon-size, 32px)
416
+ );
417
+ /* The sheet button's corners. Its "edge" corners are the pair on the
418
+ side it attaches to -- the page edge while shut, the panel's inner edge
419
+ while open -- and default to 0, which is what makes it read as a tab
420
+ fastened to that edge. `sheetButton="button"` rounds them like the
421
+ other two, for a sheet that does not sit against an edge. */
422
+ --_sidebar-sheet-button-radius: var(--circle-button-radius, var(--mini-button-radius, var(--button-radius, var(--radius, 50%))));
423
+ --_sidebar-sheet-expand-edge-radius: var(
424
+ --sidebar-sheet-expand-edge-radius,
425
+ var(
426
+ --_sidebar-sheet-edge-override,
427
+ var(--sidebar-sheet-edge-radius, 0)
428
+ )
429
+ );
430
+ --_sidebar-sheet-collapse-edge-radius: var(
431
+ --sidebar-sheet-collapse-edge-radius,
432
+ var(
433
+ --_sidebar-sheet-edge-override,
434
+ var(--sidebar-sheet-edge-radius, 0)
435
+ )
436
+ );
437
+ position: relative;
438
+ overflow: visible;
439
+ background: transparent;
440
+ width: calc(var(--gap) + max(var(--sidebar-icon-width, 0.65rem), var(--icon-size, 32px)));
441
+ flex: 0 0 auto;
442
+ /* Every child is out of flow, so the aside has no content to hold it
443
+ open and the base `height: 100%` resolves against an auto-height
444
+ parent -- which is to say, nothing. Stretch to the row instead, so the
445
+ aside is as tall as whatever it sits beside, and the open sheet's own
446
+ `height: 100%` has something real to measure against. */
447
+ height: auto;
448
+ align-self: stretch;
449
+ }
450
+ aside.overlay .edge-bar {
451
+ display: none;
452
+ }
453
+ aside.overlay.right, aside.overlay.left {
454
+ border-left: none;
455
+ border-right: none;
456
+ }
457
+ aside.overlay > .content {
458
+ position: absolute;
459
+ top: 0;
460
+ left: 0;
461
+ width: var(--sidebar-width);
462
+ /* Above a sticky Table head (z-index 2), which a sheet beside a long table
463
+ otherwise slides under. */
464
+ z-index: var(--sidebar-overlay-z-index, 3);
465
+ transform: translateX(-100%);
466
+ opacity: 0;
467
+ background: transparent;
468
+ pointer-events: none;
469
+ transition: transform var(--sidebar-transition) ease-in-out, opacity var(--sidebar-transition) ease-in-out;
470
+ padding: var(--padding);
471
+ /* The close button floats over the panel's inner edge, so the panel has
472
+ to keep that strip clear or the first nav item sits underneath it.
473
+
474
+ Reserved on the inline axis, not the block one: a nav wants the full
475
+ height, and a blank band across the top of it to clear one button is
476
+ a poor trade. The cost is that the whole column is a button's width
477
+ narrower, even well below the button -- override
478
+ --sidebar-sheet-content-inset to 0 and set your own padding if you
479
+ would rather have the width back. */
480
+ padding-inline-end: var(--sidebar-sheet-content-inset, calc(var(--padding) + var(--_sidebar-expander-width)));
481
+ }
482
+ aside.overlay.left > .content {
483
+ border-right: var(--border-width) var(--border-style) var(--border-color);
484
+ }
485
+ aside.overlay {
486
+ /* A right-hand sheet slides out of the right edge, not the left -- and
487
+ its button lands on the panel's left edge, so the reserved strip
488
+ mirrors with it. */
489
+ }
490
+ aside.overlay.right > .content {
327
491
  left: auto;
328
492
  right: 0;
493
+ transform: translateX(100%);
494
+ border-left: var(--border-width) var(--border-style) var(--border-color);
495
+ padding-inline-end: var(--padding);
496
+ padding-inline-start: var(--sidebar-sheet-content-inset, calc(var(--padding) + var(--_sidebar-expander-width)));
497
+ }
498
+ aside.overlay.expandedHamburger > .content {
499
+ transform: translateX(0);
500
+ height: 100%;
501
+ opacity: 1;
502
+ pointer-events: all;
503
+ box-shadow: var(--sidebar-overlay-box-shadow, var(--overlay-box-shadow, 0 0 var(--space, 8px) rgba(127, 127, 127, 0.4)));
504
+ --link-bg: var(--sidebar-link-bg, var(--surface-link-bg, inherit));
505
+ --link-fg: var(--sidebar-link-fg, var(--surface-link-fg, inherit));
506
+ --_bg-mix-color: var(--sidebar-bg-mix-color, var(--surface-bg-mix-color, var(--bg-mix-color, transparent)));
507
+ --_bg-mix-amount: var(--sidebar-bg-mix-amount, var(--surface-bg-mix-amount, var(--bg-mix-amount, 0%)));
508
+ --_fg-mix-color: var(--sidebar-fg-mix-color, var(--surface-fg-mix-color, var(--fg-mix-color, transparent)));
509
+ --_fg-mix-amount: var(--sidebar-fg-mix-amount, var(--surface-fg-mix-amount, var(--fg-mix-amount, 0%)));
510
+ --_bg-base: var(--sidebar-bg, var(--surface-bg, var(--bg, unset)));
511
+ --_fg-base: var(--sidebar-fg, var(--surface-fg, var(--fg, unset)));
512
+ --_background-color: color-mix(
513
+ in srgb,
514
+ var(--_bg-base),
515
+ var(--_bg-mix-color) var(--_bg-mix-amount)
516
+ );
517
+ --_color: color-mix(
518
+ in srgb,
519
+ var(--_fg-base),
520
+ var(--_fg-mix-color) var(--_fg-mix-amount)
521
+ );
522
+ --muted-color: var(--muted-color-override, var(--_background-color));
523
+ --primary-text-fg: var(
524
+ --primary-text-fg-override,
525
+ color-mix(
526
+ in srgb,
527
+ var(--_color),
528
+ var(--primary-color)
529
+ var(--primary-text-amount, var(--text-amount, 50%))
530
+ )
531
+ );
532
+ --secondary-text-fg: var(
533
+ --secondary-text-fg-override,
534
+ color-mix(
535
+ in srgb,
536
+ var(--_color),
537
+ var(--secondary-color)
538
+ var(--secondary-text-amount, var(--text-amount, 50%))
539
+ )
540
+ );
541
+ --info-text-fg: var(
542
+ --info-text-fg-override,
543
+ color-mix(
544
+ in srgb,
545
+ var(--_color),
546
+ var(--info-color)
547
+ var(--info-text-amount, var(--text-amount, 50%))
548
+ )
549
+ );
550
+ --success-text-fg: var(
551
+ --success-text-fg-override,
552
+ color-mix(
553
+ in srgb,
554
+ var(--_color),
555
+ var(--success-color)
556
+ var(--success-text-amount, var(--text-amount, 50%))
557
+ )
558
+ );
559
+ --warning-text-fg: var(
560
+ --warning-text-fg-override,
561
+ color-mix(
562
+ in srgb,
563
+ var(--_color),
564
+ var(--warning-color)
565
+ var(--warning-text-amount, var(--text-amount, 50%))
566
+ )
567
+ );
568
+ --danger-text-fg: var(
569
+ --danger-text-fg-override,
570
+ color-mix(
571
+ in srgb,
572
+ var(--_color),
573
+ var(--danger-color)
574
+ var(--danger-text-amount, var(--text-amount, 50%))
575
+ )
576
+ );
577
+ --muted-text-fg: var(
578
+ --muted-text-fg-override,
579
+ color-mix(
580
+ in srgb,
581
+ var(--_color),
582
+ var(--muted-color)
583
+ var(--muted-text-amount, var(--text-amount, 50%))
584
+ )
585
+ );
586
+ background-color: var(--_background-color);
587
+ color: var(--_color);
588
+ }
589
+ aside.overlay > button {
590
+ transition: left var(--sidebar-transition);
591
+ transform: translateX(0);
592
+ z-index: calc(var(--sidebar-overlay-z-index, 3) + 1);
593
+ position: absolute;
594
+ opacity: 1;
595
+ pointer-events: all;
596
+ display: block;
597
+ top: var(--padding);
598
+ left: 0;
599
+ border-radius: var(--_sidebar-sheet-button-radius);
600
+ border-top-left-radius: var(--_sidebar-sheet-expand-edge-radius);
601
+ border-bottom-left-radius: var(--_sidebar-sheet-expand-edge-radius);
602
+ border: var(--circle-button-border, var(--mini-button-border));
603
+ width: var(--_sidebar-expander-width);
604
+ height: var(--_sidebar-expander-height);
605
+ --link-bg: var(--circle-button-link-bg, var(--mini-button-link-bg, var(--button-link-bg, var(--control-link-bg, var(--secondary-link-bg, inherit)))));
606
+ --link-fg: var(--circle-button-link-fg, var(--mini-button-link-fg, var(--button-link-fg, var(--control-link-fg, var(--secondary-link-fg, inherit)))));
607
+ --_bg-mix-color: var(--circle-button-bg-mix-color, var(--mini-button-bg-mix-color, var(--button-bg-mix-color, var(--control-bg-mix-color, var(--secondary-bg-mix-color, var(--bg-mix-color, transparent))))));
608
+ --_bg-mix-amount: var(--circle-button-bg-mix-amount, var(--mini-button-bg-mix-amount, var(--button-bg-mix-amount, var(--control-bg-mix-amount, var(--secondary-bg-mix-amount, var(--bg-mix-amount, 0%))))));
609
+ --_fg-mix-color: var(--circle-button-fg-mix-color, var(--mini-button-fg-mix-color, var(--button-fg-mix-color, var(--control-fg-mix-color, var(--secondary-fg-mix-color, var(--fg-mix-color, transparent))))));
610
+ --_fg-mix-amount: var(--circle-button-fg-mix-amount, var(--mini-button-fg-mix-amount, var(--button-fg-mix-amount, var(--control-fg-mix-amount, var(--secondary-fg-mix-amount, var(--fg-mix-amount, 0%))))));
611
+ --_bg-base: var(--circle-button-bg, var(--mini-button-bg, var(--button-bg, var(--control-bg, var(--secondary-bg, var(--bg, unset))))));
612
+ --_fg-base: var(--circle-button-fg, var(--mini-button-fg, var(--button-fg, var(--control-fg, var(--secondary-fg, var(--fg, unset))))));
613
+ --_background-color: color-mix(
614
+ in srgb,
615
+ var(--_bg-base),
616
+ var(--_bg-mix-color) var(--_bg-mix-amount)
617
+ );
618
+ --_color: color-mix(
619
+ in srgb,
620
+ var(--_fg-base),
621
+ var(--_fg-mix-color) var(--_fg-mix-amount)
622
+ );
623
+ --muted-color: var(--muted-color-override, var(--_background-color));
624
+ --primary-text-fg: var(
625
+ --primary-text-fg-override,
626
+ color-mix(
627
+ in srgb,
628
+ var(--_color),
629
+ var(--primary-color)
630
+ var(--primary-text-amount, var(--text-amount, 50%))
631
+ )
632
+ );
633
+ --secondary-text-fg: var(
634
+ --secondary-text-fg-override,
635
+ color-mix(
636
+ in srgb,
637
+ var(--_color),
638
+ var(--secondary-color)
639
+ var(--secondary-text-amount, var(--text-amount, 50%))
640
+ )
641
+ );
642
+ --info-text-fg: var(
643
+ --info-text-fg-override,
644
+ color-mix(
645
+ in srgb,
646
+ var(--_color),
647
+ var(--info-color)
648
+ var(--info-text-amount, var(--text-amount, 50%))
649
+ )
650
+ );
651
+ --success-text-fg: var(
652
+ --success-text-fg-override,
653
+ color-mix(
654
+ in srgb,
655
+ var(--_color),
656
+ var(--success-color)
657
+ var(--success-text-amount, var(--text-amount, 50%))
658
+ )
659
+ );
660
+ --warning-text-fg: var(
661
+ --warning-text-fg-override,
662
+ color-mix(
663
+ in srgb,
664
+ var(--_color),
665
+ var(--warning-color)
666
+ var(--warning-text-amount, var(--text-amount, 50%))
667
+ )
668
+ );
669
+ --danger-text-fg: var(
670
+ --danger-text-fg-override,
671
+ color-mix(
672
+ in srgb,
673
+ var(--_color),
674
+ var(--danger-color)
675
+ var(--danger-text-amount, var(--text-amount, 50%))
676
+ )
677
+ );
678
+ --muted-text-fg: var(
679
+ --muted-text-fg-override,
680
+ color-mix(
681
+ in srgb,
682
+ var(--_color),
683
+ var(--muted-color)
684
+ var(--muted-text-amount, var(--text-amount, 50%))
685
+ )
686
+ );
687
+ background-color: var(--_background-color);
688
+ color: var(--_color);
689
+ cursor: var(--circle-button-cursor, var(--mini-button-cursor, var(--button-cursor, var(--control-cursor, var(--cursor, pointer)))));
690
+ transition: filter var(--circle-button-transition, var(--mini-button-transition, var(--button-transition, var(--control-transition, var(--transition, 300ms))))), background-color var(--circle-button-transition, var(--mini-button-transition, var(--button-transition, var(--control-transition, var(--transition, 300ms))))), border-color var(--circle-button-transition, var(--mini-button-transition, var(--button-transition, var(--control-transition, var(--transition, 300ms))))), box-shadow var(--circle-button-transition, var(--mini-button-transition, var(--button-transition, var(--control-transition, var(--transition, 300ms))))), transform var(--circle-button-transition, var(--mini-button-transition, var(--button-transition, var(--control-transition, var(--transition, 300ms)))));
691
+ }
692
+ aside.overlay > button:hover {
693
+ filter: var(--circle-button-hover-filter, var(--mini-button-hover-filter, var(--button-hover-filter, var(--control-hover-filter, var(--hover-filter, brightness(1.05))))));
694
+ transform: var(--circle-button-hover-transform, var(--mini-button-hover-transform, var(--button-hover-transform, var(--control-hover-transform, var(--hover-transform, none)))));
695
+ color: var(--_color);
696
+ background-color: color-mix(in oklch, var(--_background-color) var(--hover-base-color-percentage, 90%), var(--hover-color-mix, white) calc(100% - var(--hover-base-color-percentage, 90%)));
697
+ box-shadow: var(--circle-button-hover-box-shadow, var(--mini-button-hover-box-shadow, var(--button-hover-box-shadow, var(--control-hover-box-shadow, var(--hover-box-shadow, var(--_box-shadow, none))))));
698
+ }
699
+ aside.overlay > button:active {
700
+ filter: var(--circle-button-active-filter, var(--mini-button-active-filter, var(--button-active-filter, var(--control-active-filter, var(--active-filter, brightness(0.9))))));
701
+ transform: var(--circle-button-active-transform, var(--mini-button-active-transform, var(--button-active-transform, var(--control-active-transform, var(--active-transform, none)))));
702
+ background-color: color-mix(in oklch, var(--_background-color) var(--active-base-color-percentage, 90%), var(--active-color-mix, black) calc(100% - var(--active-base-color-percentage, 90%)));
703
+ box-shadow: var(--circle-button-active-box-shadow, var(--mini-button-active-box-shadow, var(--button-active-box-shadow, var(--control-active-box-shadow, var(--active-box-shadow, var(--_box-shadow, none))))));
704
+ }
705
+ aside.overlay > button:disabled {
706
+ cursor: var(--circle-button-disabled-cursor, var(--mini-button-disabled-cursor, var(--button-disabled-cursor, var(--control-disabled-cursor, var(--disabled-cursor, not-allowed)))));
707
+ transform: var(--circle-button-disabled-transform, var(--mini-button-disabled-transform, var(--button-disabled-transform, var(--control-disabled-transform, var(--disabled-transform, none)))));
708
+ filter: var(--circle-button-disabled-filter, var(--mini-button-disabled-filter, var(--button-disabled-filter, var(--control-disabled-filter, var(--disabled-filter, grayscale(0.5))))));
709
+ background-color: color-mix(in oklch, var(--_background-color) var(--disabled-base-color-percentage, 50%), var(--disabled-color-mix, gray) calc(100% - var(--disabled-base-color-percentage, 50%)));
710
+ box-shadow: var(--circle-button-disabled-box-shadow, var(--mini-button-disabled-box-shadow, var(--button-disabled-box-shadow, var(--control-disabled-box-shadow, var(--disabled-box-shadow, var(--_box-shadow, none))))));
711
+ }
712
+ aside.overlay > button:focus-visible {
713
+ outline: var(--focus-color, -webkit-focus-ring-color) auto 1px;
714
+ outline-offset: var(--focus-outline-offset, 2px);
715
+ box-shadow: var(--focus-ring-box-shadow, 0 0 0 3px var(--focus-shadow-color, rgba(100, 150, 250, 0.5)));
716
+ }
717
+ aside.overlay > button::after {
718
+ color: var(--circle-button-fg, var(--mini-button-fg, currentColor));
719
+ filter: var(--sidebar-mobile-icon-filter, none);
720
+ }
721
+ aside.overlay > button.close {
722
+ /* Flush inside the panel's inner edge. Adding --padding here pushed the
723
+ button a padding's worth further in, which left it straddling the
724
+ edge -- part of it hanging outside the panel -- while the content
725
+ inset below still reserved the full button width, so the gap between
726
+ the button and the first nav item came out wider than intended.
727
+ Either the button pops out of the panel or it sits inside it; this
728
+ sits inside it, and the inset is then exactly button + one gutter. */
729
+ left: calc(var(--sidebar-width) - var(--_sidebar-expander-width));
730
+ border-radius: var(--_sidebar-sheet-button-radius);
731
+ border-top-right-radius: var(--_sidebar-sheet-collapse-edge-radius);
732
+ border-bottom-right-radius: var(--_sidebar-sheet-collapse-edge-radius);
733
+ }
734
+ aside.overlay {
735
+ /* Everything above anchors the button to the left, which is right for a
736
+ left-hand sidebar and nonsense for a right-hand one: the sheet slides
737
+ out of the right edge, so the button has to hug that edge and then step
738
+ *inwards* -- leftwards -- by the panel's width when it opens. Anchored
739
+ from the left it stepped the other way, off the far side of the page
740
+ entirely, where it could not even be clicked. The flattened corners
741
+ mirror too, so the button still reads as attached to its own edge. */
742
+ }
743
+ aside.overlay.right > button {
744
+ left: auto;
745
+ right: 0;
746
+ border-radius: var(--_sidebar-sheet-button-radius);
747
+ border-top-right-radius: var(--_sidebar-sheet-expand-edge-radius);
748
+ border-bottom-right-radius: var(--_sidebar-sheet-expand-edge-radius);
749
+ }
750
+ aside.overlay.right > button.close {
751
+ left: auto;
752
+ right: calc(var(--sidebar-width) - var(--_sidebar-expander-width));
753
+ border-radius: var(--_sidebar-sheet-button-radius);
754
+ border-top-left-radius: var(--_sidebar-sheet-collapse-edge-radius);
755
+ border-bottom-left-radius: var(--_sidebar-sheet-collapse-edge-radius);
756
+ }
757
+ aside.overlay {
758
+ /* `sticky`: the sheet follows the reader down the page instead of
759
+ stretching to the row.
760
+
761
+ Sticky needs the sheet back *in flow* -- an absolutely positioned box
762
+ cannot stick -- but the aside is only a button's width wide and the
763
+ row's full height, and that height is exactly the range the sheet
764
+ should be free to travel. So the aside becomes a one-cell grid as tall
765
+ as its row, and the sheet and its button share that cell, each sticky
766
+ within it.
767
+
768
+ Horizontal placement moves from `left`/`right` onto margins, because on
769
+ a sticky box `left` and `right` stop being offsets and become sticky
770
+ insets -- they would pin the button against a sideways-scrolling
771
+ ancestor rather than place it. The margins reproduce the absolute
772
+ geometry exactly: the aside is `--gap` plus one button wide, and a
773
+ sheet or button wider than that spills out of its cell on the side
774
+ that faces the content. */
775
+ }
776
+ aside.overlay.sticky {
777
+ --_sidebar-sheet-top: var(--sidebar-sheet-top, var(--padding));
778
+ --_sidebar-aside-width: calc(var(--gap) + var(--_sidebar-expander-width));
779
+ display: grid;
780
+ grid-template-columns: 100%;
781
+ align-content: stretch;
782
+ /* A shut sheet is still in flow now, and would otherwise hold the row
783
+ open to its own height. The aside's width is set explicitly, so
784
+ size containment only ever zeroes out the height it would have
785
+ contributed; the row still stretches it to full height. */
786
+ contain: size;
787
+ }
788
+ aside.overlay.sticky > .content, aside.overlay.sticky > button.expander, aside.overlay.sticky > button.close {
789
+ grid-area: 1/1;
790
+ position: sticky;
791
+ align-self: start;
792
+ justify-self: start;
793
+ left: auto;
794
+ right: auto;
795
+ }
796
+ aside.overlay.sticky > .content {
797
+ top: var(--_sidebar-sheet-top);
798
+ margin-left: 0;
799
+ height: auto;
800
+ /* The reserve is a budget for everything that is not the sheet, rather
801
+ than just the sticky offset: before the sheet sticks -- page scrolled
802
+ to the top -- its top edge is its row's, which may sit well below the
803
+ viewport top, and a sheet sized only from the offset would overflow
804
+ the viewport by exactly that much. Twice the top gives it an even
805
+ margin at both ends once it has stuck. */
806
+ max-height: var(--sidebar-sheet-max-height, calc(100dvh - var(--sidebar-sheet-reserve, calc(2 * var(--_sidebar-sheet-top)))));
807
+ overflow-y: auto;
808
+ /* A column, so a child with `min-height: 0` and its own overflow can
809
+ keep a header or footer in view and scroll only its middle. Children
810
+ that do not opt in keep their size, and the sheet scrolls as a
811
+ whole. */
812
+ display: flex;
813
+ flex-direction: column;
814
+ }
815
+ aside.overlay.sticky.expandedHamburger > .content {
816
+ height: auto;
817
+ }
818
+ aside.overlay.sticky.right > .content {
819
+ margin-left: calc(var(--_sidebar-aside-width) - var(--sidebar-width));
820
+ }
821
+ aside.overlay.sticky > button.expander, aside.overlay.sticky > button.close {
822
+ top: calc(var(--_sidebar-sheet-top) + var(--padding));
823
+ margin-top: var(--padding);
824
+ margin-left: 0;
825
+ transition: margin-left var(--sidebar-transition);
826
+ }
827
+ aside.overlay.sticky > button.close {
828
+ margin-left: calc(var(--sidebar-width) - var(--_sidebar-expander-width));
829
+ }
830
+ aside.overlay.sticky.right > button.expander {
831
+ margin-left: var(--gap);
832
+ }
833
+ aside.overlay.sticky.right > button.close {
834
+ margin-left: calc(var(--_sidebar-aside-width) - var(--sidebar-width));
329
835
  }
330
836
  .edge-bar {
331
837
  --link-bg: var(--grab-bar-link-bg, var(--sidebar-link-bg, var(--surface-link-bg, inherit)));
@@ -457,43 +963,123 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
457
963
  aside:has(.edge-bar:hover) .content {
458
964
  filter: var(--greyed-out-filter);
459
965
  }
460
- /* Hide hamburger expander when not in
461
- hamburger mode */
462
- button {
966
+ /* Hide the sheet button in rail mode */
967
+ aside:not(.overlay) > button {
463
968
  display: none;
464
969
  }
465
970
  }
466
971
  @container (max-width: 512px) {
467
- .edge-bar {
468
- display: none;
469
- }
470
- .sidebar {
972
+ aside {
973
+ /* The sheet and its button are absolutely positioned, so the sidebar has
974
+ to be their containing block. Without this they resolve against
975
+ whatever positioned ancestor happens to be up the tree -- inside <Page>
976
+ that is Page's own aside and it looks fine, but drop a Sidebar into a
977
+ Card or a Container and the button escapes to the top-left of the page,
978
+ over unrelated content.
979
+
980
+ `overflow` has to be released in the same breath. Every child here is
981
+ out of flow, so the aside computes to zero height -- harmless while it
982
+ was not a containing block, but the moment it becomes one the inherited
983
+ `overflow: hidden` clips the button and the sheet away to nothing.
984
+ Nothing needs clipping here: the shut sheet is hidden by opacity and
985
+ pointer-events, not by the box. */
986
+ /* Sized here rather than on the button, because the panel needs them too
987
+ -- see the padding it reserves below. */
988
+ --_sidebar-expander-width: max(
989
+ var(--sidebar-icon-width, 0.65rem),
990
+ var(--icon-size, 32px)
991
+ );
992
+ --_sidebar-expander-height: max(
993
+ var(--sidebar-icon-height, 1rem),
994
+ var(--icon-size, 32px)
995
+ );
996
+ /* The sheet button's corners. Its "edge" corners are the pair on the
997
+ side it attaches to -- the page edge while shut, the panel's inner edge
998
+ while open -- and default to 0, which is what makes it read as a tab
999
+ fastened to that edge. `sheetButton="button"` rounds them like the
1000
+ other two, for a sheet that does not sit against an edge. */
1001
+ --_sidebar-sheet-button-radius: var(--circle-button-radius, var(--mini-button-radius, var(--button-radius, var(--radius, 50%))));
1002
+ --_sidebar-sheet-expand-edge-radius: var(
1003
+ --sidebar-sheet-expand-edge-radius,
1004
+ var(
1005
+ --_sidebar-sheet-edge-override,
1006
+ var(--sidebar-sheet-edge-radius, 0)
1007
+ )
1008
+ );
1009
+ --_sidebar-sheet-collapse-edge-radius: var(
1010
+ --sidebar-sheet-collapse-edge-radius,
1011
+ var(
1012
+ --_sidebar-sheet-edge-override,
1013
+ var(--sidebar-sheet-edge-radius, 0)
1014
+ )
1015
+ );
1016
+ position: relative;
1017
+ overflow: visible;
471
1018
  background: transparent;
1019
+ width: calc(var(--gap) + max(var(--sidebar-icon-width, 0.65rem), var(--icon-size, 32px)));
1020
+ flex: 0 0 auto;
1021
+ /* Every child is out of flow, so the aside has no content to hold it
1022
+ open and the base `height: 100%` resolves against an auto-height
1023
+ parent -- which is to say, nothing. Stretch to the row instead, so the
1024
+ aside is as tall as whatever it sits beside, and the open sheet's own
1025
+ `height: 100%` has something real to measure against. */
1026
+ height: auto;
1027
+ align-self: stretch;
1028
+ }
1029
+ aside .edge-bar {
1030
+ display: none;
472
1031
  }
473
- .right,
474
- .left {
1032
+ aside.right, aside.left {
475
1033
  border-left: none;
476
1034
  border-right: none;
477
1035
  }
478
1036
  aside > .content {
1037
+ position: absolute;
1038
+ top: 0;
1039
+ left: 0;
1040
+ width: var(--sidebar-width);
1041
+ /* Above a sticky Table head (z-index 2), which a sheet beside a long table
1042
+ otherwise slides under. */
1043
+ z-index: var(--sidebar-overlay-z-index, 3);
479
1044
  transform: translateX(-100%);
480
1045
  opacity: 0;
481
1046
  background: transparent;
482
1047
  pointer-events: none;
483
- transition: transform var(--sidebar-transition) ease-in-out;
1048
+ transition: transform var(--sidebar-transition) ease-in-out, opacity var(--sidebar-transition) ease-in-out;
484
1049
  padding: var(--padding);
1050
+ /* The close button floats over the panel's inner edge, so the panel has
1051
+ to keep that strip clear or the first nav item sits underneath it.
1052
+
1053
+ Reserved on the inline axis, not the block one: a nav wants the full
1054
+ height, and a blank band across the top of it to clear one button is
1055
+ a poor trade. The cost is that the whole column is a button's width
1056
+ narrower, even well below the button -- override
1057
+ --sidebar-sheet-content-inset to 0 and set your own padding if you
1058
+ would rather have the width back. */
1059
+ padding-inline-end: var(--sidebar-sheet-content-inset, calc(var(--padding) + var(--_sidebar-expander-width)));
485
1060
  }
486
1061
  aside.left > .content {
487
1062
  border-right: var(--border-width) var(--border-style) var(--border-color);
488
1063
  }
1064
+ aside {
1065
+ /* A right-hand sheet slides out of the right edge, not the left -- and
1066
+ its button lands on the panel's left edge, so the reserved strip
1067
+ mirrors with it. */
1068
+ }
489
1069
  aside.right > .content {
1070
+ left: auto;
1071
+ right: 0;
1072
+ transform: translateX(100%);
490
1073
  border-left: var(--border-width) var(--border-style) var(--border-color);
1074
+ padding-inline-end: var(--padding);
1075
+ padding-inline-start: var(--sidebar-sheet-content-inset, calc(var(--padding) + var(--_sidebar-expander-width)));
491
1076
  }
492
1077
  aside.expandedHamburger > .content {
493
1078
  transform: translateX(0);
494
1079
  height: 100%;
495
1080
  opacity: 1;
496
1081
  pointer-events: all;
1082
+ box-shadow: var(--sidebar-overlay-box-shadow, var(--overlay-box-shadow, 0 0 var(--space, 8px) rgba(127, 127, 127, 0.4)));
497
1083
  --link-bg: var(--sidebar-link-bg, var(--surface-link-bg, inherit));
498
1084
  --link-fg: var(--sidebar-link-fg, var(--surface-link-fg, inherit));
499
1085
  --_bg-mix-color: var(--sidebar-bg-mix-color, var(--surface-bg-mix-color, var(--bg-mix-color, transparent)));
@@ -580,27 +1166,18 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
580
1166
  color: var(--_color);
581
1167
  }
582
1168
  aside > button {
583
- --_sidebar-expander-width: max(
584
- var(--sidebar-icon-width, 0.65rem),
585
- var(--icon-size, 32px)
586
- );
587
- --_sidebar-expander-height: max(
588
- var(--sidebar-icon-height, 1rem),
589
- var(--icon-size, 32px)
590
- );
591
1169
  transition: left var(--sidebar-transition);
592
1170
  transform: translateX(0);
593
- z-index: 3;
1171
+ z-index: calc(var(--sidebar-overlay-z-index, 3) + 1);
594
1172
  position: absolute;
595
1173
  opacity: 1;
596
1174
  pointer-events: all;
597
1175
  display: block;
598
- position: absolute;
599
1176
  top: var(--padding);
600
1177
  left: 0;
601
- border-radius: var(--circle-button-radius, var(--mini-button-radius, var(--button-radius, var(--radius, 50%))));
602
- border-top-left-radius: 0;
603
- border-bottom-left-radius: 0;
1178
+ border-radius: var(--_sidebar-sheet-button-radius);
1179
+ border-top-left-radius: var(--_sidebar-sheet-expand-edge-radius);
1180
+ border-bottom-left-radius: var(--_sidebar-sheet-expand-edge-radius);
604
1181
  border: var(--circle-button-border, var(--mini-button-border));
605
1182
  width: var(--_sidebar-expander-width);
606
1183
  height: var(--_sidebar-expander-height);
@@ -721,30 +1298,125 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
721
1298
  filter: var(--sidebar-mobile-icon-filter, none);
722
1299
  }
723
1300
  aside > button.close {
724
- left: calc(var(--sidebar-width) - var(--_sidebar-expander-width) + var(--padding));
725
- border-radius: var(--circle-button-radius, var(--mini-button-radius, var(--button-radius, var(--radius, 50%))));
726
- border-top-right-radius: 0;
727
- border-bottom-right-radius: 0;
728
- }
729
- /* aside > button:hover {
730
- background: var(--mini-button-hover-bg);
731
- color: var(--mini-button-hover-fg);
732
- } */
1301
+ /* Flush inside the panel's inner edge. Adding --padding here pushed the
1302
+ button a padding's worth further in, which left it straddling the
1303
+ edge -- part of it hanging outside the panel -- while the content
1304
+ inset below still reserved the full button width, so the gap between
1305
+ the button and the first nav item came out wider than intended.
1306
+ Either the button pops out of the panel or it sits inside it; this
1307
+ sits inside it, and the inset is then exactly button + one gutter. */
1308
+ left: calc(var(--sidebar-width) - var(--_sidebar-expander-width));
1309
+ border-radius: var(--_sidebar-sheet-button-radius);
1310
+ border-top-right-radius: var(--_sidebar-sheet-collapse-edge-radius);
1311
+ border-bottom-right-radius: var(--_sidebar-sheet-collapse-edge-radius);
1312
+ }
733
1313
  aside {
734
- width: calc(var(--gap) + max(var(--sidebar-icon-width, 0.65rem), var(--icon-size, 32px)));
735
- flex: 0 0 auto;
1314
+ /* Everything above anchors the button to the left, which is right for a
1315
+ left-hand sidebar and nonsense for a right-hand one: the sheet slides
1316
+ out of the right edge, so the button has to hug that edge and then step
1317
+ *inwards* -- leftwards -- by the panel's width when it opens. Anchored
1318
+ from the left it stepped the other way, off the far side of the page
1319
+ entirely, where it could not even be clicked. The flattened corners
1320
+ mirror too, so the button still reads as attached to its own edge. */
736
1321
  }
737
- aside .content {
738
- position: absolute;
739
- --top: calc(
740
- var(--padding) +
741
- max(var(--sidebar-icon-height, 1rem), var(--icon-size, 32px))
742
- );
743
- left: 0;
744
- width: var(--sidebar-width);
745
- z-index: 2;
1322
+ aside.right > button {
1323
+ left: auto;
1324
+ right: 0;
1325
+ border-radius: var(--_sidebar-sheet-button-radius);
1326
+ border-top-right-radius: var(--_sidebar-sheet-expand-edge-radius);
1327
+ border-bottom-right-radius: var(--_sidebar-sheet-expand-edge-radius);
1328
+ }
1329
+ aside.right > button.close {
1330
+ left: auto;
1331
+ right: calc(var(--sidebar-width) - var(--_sidebar-expander-width));
1332
+ border-radius: var(--_sidebar-sheet-button-radius);
1333
+ border-top-left-radius: var(--_sidebar-sheet-collapse-edge-radius);
1334
+ border-bottom-left-radius: var(--_sidebar-sheet-collapse-edge-radius);
1335
+ }
1336
+ aside {
1337
+ /* `sticky`: the sheet follows the reader down the page instead of
1338
+ stretching to the row.
1339
+
1340
+ Sticky needs the sheet back *in flow* -- an absolutely positioned box
1341
+ cannot stick -- but the aside is only a button's width wide and the
1342
+ row's full height, and that height is exactly the range the sheet
1343
+ should be free to travel. So the aside becomes a one-cell grid as tall
1344
+ as its row, and the sheet and its button share that cell, each sticky
1345
+ within it.
1346
+
1347
+ Horizontal placement moves from `left`/`right` onto margins, because on
1348
+ a sticky box `left` and `right` stop being offsets and become sticky
1349
+ insets -- they would pin the button against a sideways-scrolling
1350
+ ancestor rather than place it. The margins reproduce the absolute
1351
+ geometry exactly: the aside is `--gap` plus one button wide, and a
1352
+ sheet or button wider than that spills out of its cell on the side
1353
+ that faces the content. */
1354
+ }
1355
+ aside.sticky {
1356
+ --_sidebar-sheet-top: var(--sidebar-sheet-top, var(--padding));
1357
+ --_sidebar-aside-width: calc(var(--gap) + var(--_sidebar-expander-width));
1358
+ display: grid;
1359
+ grid-template-columns: 100%;
1360
+ align-content: stretch;
1361
+ /* A shut sheet is still in flow now, and would otherwise hold the row
1362
+ open to its own height. The aside's width is set explicitly, so
1363
+ size containment only ever zeroes out the height it would have
1364
+ contributed; the row still stretches it to full height. */
1365
+ contain: size;
1366
+ }
1367
+ aside.sticky > .content, aside.sticky > button.expander, aside.sticky > button.close {
1368
+ grid-area: 1/1;
1369
+ position: sticky;
1370
+ align-self: start;
1371
+ justify-self: start;
1372
+ left: auto;
1373
+ right: auto;
1374
+ }
1375
+ aside.sticky > .content {
1376
+ top: var(--_sidebar-sheet-top);
1377
+ margin-left: 0;
1378
+ height: auto;
1379
+ /* The reserve is a budget for everything that is not the sheet, rather
1380
+ than just the sticky offset: before the sheet sticks -- page scrolled
1381
+ to the top -- its top edge is its row's, which may sit well below the
1382
+ viewport top, and a sheet sized only from the offset would overflow
1383
+ the viewport by exactly that much. Twice the top gives it an even
1384
+ margin at both ends once it has stuck. */
1385
+ max-height: var(--sidebar-sheet-max-height, calc(100dvh - var(--sidebar-sheet-reserve, calc(2 * var(--_sidebar-sheet-top)))));
1386
+ overflow-y: auto;
1387
+ /* A column, so a child with `min-height: 0` and its own overflow can
1388
+ keep a header or footer in view and scroll only its middle. Children
1389
+ that do not opt in keep their size, and the sheet scrolls as a
1390
+ whole. */
1391
+ display: flex;
1392
+ flex-direction: column;
1393
+ }
1394
+ aside.sticky.expandedHamburger > .content {
1395
+ height: auto;
1396
+ }
1397
+ aside.sticky.right > .content {
1398
+ margin-left: calc(var(--_sidebar-aside-width) - var(--sidebar-width));
746
1399
  }
1400
+ aside.sticky > button.expander, aside.sticky > button.close {
1401
+ top: calc(var(--_sidebar-sheet-top) + var(--padding));
1402
+ margin-top: var(--padding);
1403
+ margin-left: 0;
1404
+ transition: margin-left var(--sidebar-transition);
1405
+ }
1406
+ aside.sticky > button.close {
1407
+ margin-left: calc(var(--sidebar-width) - var(--_sidebar-expander-width));
1408
+ }
1409
+ aside.sticky.right > button.expander {
1410
+ margin-left: var(--gap);
1411
+ }
1412
+ aside.sticky.right > button.close {
1413
+ margin-left: calc(var(--_sidebar-aside-width) - var(--sidebar-width));
1414
+ }
1415
+ }
1416
+ aside.button-sheet-toggle {
1417
+ --_sidebar-sheet-edge-override: var(--_sidebar-sheet-button-radius);
747
1418
  }
1419
+
748
1420
  button::after {
749
1421
  color: var(--sidebar-icon-fg, var(--fg, currentColor));
750
1422
  background-position: center;
@@ -758,14 +1430,34 @@ button::after {
758
1430
  line-height: 1;
759
1431
  }
760
1432
 
761
- button.expander::after {
762
- content: var(--sidebar-expand, "›");
763
- background-image: var(--sidebar-expand-image, none);
1433
+ /* The rail and the sheet are different affordances and want different
1434
+ glyphs. The rail slides a panel out from the edge it sits on, so a
1435
+ chevron pointing that way says what will happen. The sheet is summoned
1436
+ from nowhere by a free-floating button, where the menu glyph is what
1437
+ people already recognize -- and it closes with an x, not a back-chevron,
1438
+ because there is no edge to slide back into.
1439
+
1440
+ Both still fall through --sidebar-expand / --sidebar-collapse, so setting
1441
+ those restyles every button at once and themes that swap in SVGs keep
1442
+ working unchanged. */
1443
+ .edge-bar button.expander::after {
1444
+ content: var(--grab-bar-expand, var(--sidebar-expand, "›"));
1445
+ background-image: var(--grab-bar-expand-image, var(--sidebar-expand-image, none));
1446
+ }
1447
+
1448
+ .edge-bar button.close::after {
1449
+ content: var(--grab-bar-collapse, var(--sidebar-collapse, "‹"));
1450
+ background-image: var(--grab-bar-collapse-image, var(--sidebar-collapse-image, none));
1451
+ }
1452
+
1453
+ aside > button.expander::after {
1454
+ content: var(--sidebar-sheet-expand, var(--sidebar-expand, "☰"));
1455
+ background-image: var(--sidebar-sheet-expand-image, var(--sidebar-expand-image, none));
764
1456
  }
765
1457
 
766
- button.close::after {
767
- content: var(--sidebar-collapse, "‹");
768
- background-image: var(--sidebar-collapse-image, none);
1458
+ aside > button.close::after {
1459
+ content: var(--sidebar-sheet-collapse, var(--sidebar-collapse, "✕"));
1460
+ background-image: var(--sidebar-sheet-collapse-image, var(--sidebar-collapse-image, none));
769
1461
  }
770
1462
 
771
1463
  .right button::after {