contain-css-svelte 1.2.3 → 1.3.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 (47) hide show
  1. package/dist/Card.svelte +12 -0
  2. package/dist/controls/Button.svelte +12 -0
  3. package/dist/controls/ButtonLink.svelte +12 -0
  4. package/dist/controls/Checkbox.svelte +12 -0
  5. package/dist/controls/CircleButton.svelte +12 -0
  6. package/dist/controls/Input.svelte +12 -0
  7. package/dist/controls/RadioButton.svelte +12 -0
  8. package/dist/controls/Select.svelte +12 -0
  9. package/dist/controls/Slider.svelte +12 -0
  10. package/dist/controls/TabItem.svelte +12 -0
  11. package/dist/controls/Toggle.svelte +12 -0
  12. package/dist/dropdowns/DropdownMenu.svelte +12 -0
  13. package/dist/index.d.ts +2 -1
  14. package/dist/index.js +2 -1
  15. package/dist/layout/Accordion.svelte +12 -0
  16. package/dist/layout/Bar.svelte +12 -0
  17. package/dist/layout/ColumnContainer.svelte +12 -0
  18. package/dist/layout/Columns.svelte +12 -0
  19. package/dist/layout/Container.svelte +12 -0
  20. package/dist/layout/DataList.svelte +12 -0
  21. package/dist/layout/DataListItem.svelte +12 -0
  22. package/dist/layout/Fieldset.svelte +12 -0
  23. package/dist/layout/Form.svelte +12 -0
  24. package/dist/layout/FormItem.svelte +12 -0
  25. package/dist/layout/GridLayout.svelte +12 -0
  26. package/dist/layout/Hero.svelte +12 -0
  27. package/dist/layout/Inline.svelte +12 -0
  28. package/dist/layout/MenuList.svelte +12 -0
  29. package/dist/layout/Page.svelte +17 -1
  30. package/dist/layout/RowContainer.svelte +12 -0
  31. package/dist/layout/Sidebar.svelte +516 -57
  32. package/dist/layout/Sidebar.svelte.d.ts +24 -1
  33. package/dist/layout/SidebarContainer.svelte +172 -0
  34. package/dist/layout/SidebarContainer.svelte.d.ts +20 -0
  35. package/dist/layout/SplitPane.svelte +12 -0
  36. package/dist/layout/Stack.svelte +12 -0
  37. package/dist/layout/TabBar.svelte +12 -0
  38. package/dist/layout/Table.svelte +233 -151
  39. package/dist/layout/Tile.svelte +12 -0
  40. package/dist/misc/CodeInner.svelte +12 -0
  41. package/dist/misc/Progress.svelte +12 -0
  42. package/dist/misc/Tag.svelte +12 -0
  43. package/dist/overlays/Dialog.svelte +12 -0
  44. package/dist/overlays/Tooltip.svelte +12 -0
  45. package/dist/sass/_responsive.scss +15 -0
  46. package/dist/typography/TextLayout.svelte +12 -0
  47. 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, 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,22 @@ 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
23
31
  class:expandedHamburger
24
32
  class:expandedBar
25
33
  {...el}
@@ -28,19 +36,21 @@ let expandedBar = $state(true);
28
36
  class:expander={!expandedHamburger}
29
37
  class:close={expandedHamburger}
30
38
  aria-label={expandedHamburger ? collapseLabel : expandLabel}
39
+ aria-expanded={expandedHamburger}
31
40
  data-audit-action="toggle-sidebar-sheet"
32
- onclick={() => (expandedHamburger = !expandedHamburger)}
41
+ onclick={() => (open = !expandedHamburger)}
33
42
  ></button>
34
43
  <div class="content">
35
44
  {@render children?.()}
36
45
  </div>
37
46
  <label class="edge-bar">
38
47
  <button
39
- onclick={() => (expandedBar = !expandedBar)}
48
+ onclick={() => (open = !expandedBar)}
40
49
  class="expander"
41
50
  class:expander={!expandedBar}
42
51
  class:close={expandedBar}
43
- aria-label={expandedBar ? "Collapse sidebar" : "Expand sidebar"}
52
+ aria-label={expandedBar ? collapseLabel : expandLabel}
53
+ aria-expanded={expandedBar}
44
54
  data-audit-action="toggle-sidebar-rail"
45
55
  ></button>
46
56
  </label>
@@ -55,6 +65,18 @@ let expandedBar = $state(true);
55
65
  day this mixin got added to a new component. Callers opt in with an actual
56
66
  value. (Container does not use this mixin: it wants --gap/auto as its
57
67
  defaults, which do not fit the "0 unless asked" contract here.) */
68
+ /* The width at or below which a Sidebar swaps its grab-bar rail for the sheet
69
+ affordance -- a floating button that opens a panel over the content.
70
+
71
+ Page has to agree with Sidebar about this, because Page is what floats the
72
+ aside out of flow and reserves the strip of padding the sheet button sits
73
+ in. They used to disagree: Sidebar switched at 512px and Page at 600px,
74
+ which left a band between 513 and 600 where Page had already lifted the
75
+ aside out of the layout while the Sidebar inside it was still showing a
76
+ rail -- a grab bar floating on top of the content, which is the affordance
77
+ we deliberately avoid everywhere else.
78
+
79
+ One definition, used by both, so they cannot drift again. */
58
80
  /* Warning: because we define a fallback
59
81
  media query, the media query can override the container
60
82
  if we stack a bunch of these in a row and aren't thoughtful about the order.
@@ -294,38 +316,392 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
294
316
  }
295
317
 
296
318
  /* Expander doo-dad */
319
+ /* The sheet affordance: a floating panel you summon with a button and
320
+ dismiss when you are done with it.
321
+
322
+ Used by the compact layout, and by `overlay` at any width. Overlay wants
323
+ this rather than the rail: a panel that starts open on top of the content
324
+ is nonsense, and a grab-bar rail floating over the content is a strange
325
+ thing to ask anyone to grab. A button that opens a sheet is the
326
+ affordance people already know.
327
+
328
+ It keys on .expandedHamburger, which resolves `undefined` to false -- so
329
+ a sheet starts shut, where the rail starts open. That falls out of the
330
+ shared `open` state for free. */
297
331
  /* Responsive sidebar... */
298
332
  @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 {
333
+ /* The rail layout. Everything here is scoped away from `overlay`, which
334
+ uses the sheet affordance at every width instead. */
335
+ aside:not(.overlay) {
303
336
  transition: width var(--sidebar-transition);
304
337
  position: relative;
305
338
  height: 100%;
306
339
  width: var(--grab-bar-width);
307
340
  }
308
- aside.expandedBar {
341
+ aside:not(.overlay).expandedBar {
309
342
  width: calc(var(--sidebar-width) + var(--grab-bar-width));
310
343
  }
311
- aside .content {
344
+ aside:not(.overlay) .content {
312
345
  transition: transform var(--sidebar-transition);
313
346
  transform: translateX(-110%);
314
347
  }
315
- aside.right .content {
348
+ aside:not(.overlay).right .content {
316
349
  transform: translateX(100%);
317
350
  }
318
- /* Positioning */
319
- aside.expandedBar .content {
351
+ aside:not(.overlay).expandedBar .content {
320
352
  transform: translateX(0);
321
353
  position: absolute;
322
354
  top: 0;
323
355
  left: 0;
324
356
  width: var(--sidebar-width);
325
357
  }
326
- aside.expandedBar.right .content {
358
+ aside:not(.overlay).expandedBar.right .content {
359
+ left: auto;
360
+ right: 0;
361
+ }
362
+ aside.overlay {
363
+ /* The sheet and its button are absolutely positioned, so the sidebar has
364
+ to be their containing block. Without this they resolve against
365
+ whatever positioned ancestor happens to be up the tree -- inside <Page>
366
+ that is Page's own aside and it looks fine, but drop a Sidebar into a
367
+ Card or a Container and the button escapes to the top-left of the page,
368
+ over unrelated content.
369
+
370
+ `overflow` has to be released in the same breath. Every child here is
371
+ out of flow, so the aside computes to zero height -- harmless while it
372
+ was not a containing block, but the moment it becomes one the inherited
373
+ `overflow: hidden` clips the button and the sheet away to nothing.
374
+ Nothing needs clipping here: the shut sheet is hidden by opacity and
375
+ pointer-events, not by the box. */
376
+ /* Sized here rather than on the button, because the panel needs them too
377
+ -- see the padding it reserves below. */
378
+ --_sidebar-expander-width: max(
379
+ var(--sidebar-icon-width, 0.65rem),
380
+ var(--icon-size, 32px)
381
+ );
382
+ --_sidebar-expander-height: max(
383
+ var(--sidebar-icon-height, 1rem),
384
+ var(--icon-size, 32px)
385
+ );
386
+ position: relative;
387
+ overflow: visible;
388
+ background: transparent;
389
+ width: calc(var(--gap) + max(var(--sidebar-icon-width, 0.65rem), var(--icon-size, 32px)));
390
+ flex: 0 0 auto;
391
+ /* Every child is out of flow, so the aside has no content to hold it
392
+ open and the base `height: 100%` resolves against an auto-height
393
+ parent -- which is to say, nothing. Stretch to the row instead, so the
394
+ aside is as tall as whatever it sits beside, and the open sheet's own
395
+ `height: 100%` has something real to measure against. */
396
+ height: auto;
397
+ align-self: stretch;
398
+ }
399
+ aside.overlay .edge-bar {
400
+ display: none;
401
+ }
402
+ aside.overlay.right, aside.overlay.left {
403
+ border-left: none;
404
+ border-right: none;
405
+ }
406
+ aside.overlay > .content {
407
+ position: absolute;
408
+ top: 0;
409
+ left: 0;
410
+ width: var(--sidebar-width);
411
+ z-index: 2;
412
+ transform: translateX(-100%);
413
+ opacity: 0;
414
+ background: transparent;
415
+ pointer-events: none;
416
+ transition: transform var(--sidebar-transition) ease-in-out, opacity var(--sidebar-transition) ease-in-out;
417
+ padding: var(--padding);
418
+ /* The close button floats over the panel's inner edge, so the panel has
419
+ to keep that strip clear or the first nav item sits underneath it.
420
+
421
+ Reserved on the inline axis, not the block one: a nav wants the full
422
+ height, and a blank band across the top of it to clear one button is
423
+ a poor trade. The cost is that the whole column is a button's width
424
+ narrower, even well below the button -- override
425
+ --sidebar-sheet-content-inset to 0 and set your own padding if you
426
+ would rather have the width back. */
427
+ padding-inline-end: var(--sidebar-sheet-content-inset, calc(var(--padding) + var(--_sidebar-expander-width)));
428
+ }
429
+ aside.overlay.left > .content {
430
+ border-right: var(--border-width) var(--border-style) var(--border-color);
431
+ }
432
+ aside.overlay {
433
+ /* A right-hand sheet slides out of the right edge, not the left -- and
434
+ its button lands on the panel's left edge, so the reserved strip
435
+ mirrors with it. */
436
+ }
437
+ aside.overlay.right > .content {
327
438
  left: auto;
328
439
  right: 0;
440
+ transform: translateX(100%);
441
+ border-left: var(--border-width) var(--border-style) var(--border-color);
442
+ padding-inline-end: var(--padding);
443
+ padding-inline-start: var(--sidebar-sheet-content-inset, calc(var(--padding) + var(--_sidebar-expander-width)));
444
+ }
445
+ aside.overlay.expandedHamburger > .content {
446
+ transform: translateX(0);
447
+ height: 100%;
448
+ opacity: 1;
449
+ pointer-events: all;
450
+ box-shadow: var(--sidebar-overlay-box-shadow, var(--overlay-box-shadow, 0 0 var(--space, 8px) rgba(127, 127, 127, 0.4)));
451
+ --link-bg: var(--sidebar-link-bg, var(--surface-link-bg, inherit));
452
+ --link-fg: var(--sidebar-link-fg, var(--surface-link-fg, inherit));
453
+ --_bg-mix-color: var(--sidebar-bg-mix-color, var(--surface-bg-mix-color, var(--bg-mix-color, transparent)));
454
+ --_bg-mix-amount: var(--sidebar-bg-mix-amount, var(--surface-bg-mix-amount, var(--bg-mix-amount, 0%)));
455
+ --_fg-mix-color: var(--sidebar-fg-mix-color, var(--surface-fg-mix-color, var(--fg-mix-color, transparent)));
456
+ --_fg-mix-amount: var(--sidebar-fg-mix-amount, var(--surface-fg-mix-amount, var(--fg-mix-amount, 0%)));
457
+ --_bg-base: var(--sidebar-bg, var(--surface-bg, var(--bg, unset)));
458
+ --_fg-base: var(--sidebar-fg, var(--surface-fg, var(--fg, unset)));
459
+ --_background-color: color-mix(
460
+ in srgb,
461
+ var(--_bg-base),
462
+ var(--_bg-mix-color) var(--_bg-mix-amount)
463
+ );
464
+ --_color: color-mix(
465
+ in srgb,
466
+ var(--_fg-base),
467
+ var(--_fg-mix-color) var(--_fg-mix-amount)
468
+ );
469
+ --muted-color: var(--muted-color-override, var(--_background-color));
470
+ --primary-text-fg: var(
471
+ --primary-text-fg-override,
472
+ color-mix(
473
+ in srgb,
474
+ var(--_color),
475
+ var(--primary-color)
476
+ var(--primary-text-amount, var(--text-amount, 50%))
477
+ )
478
+ );
479
+ --secondary-text-fg: var(
480
+ --secondary-text-fg-override,
481
+ color-mix(
482
+ in srgb,
483
+ var(--_color),
484
+ var(--secondary-color)
485
+ var(--secondary-text-amount, var(--text-amount, 50%))
486
+ )
487
+ );
488
+ --info-text-fg: var(
489
+ --info-text-fg-override,
490
+ color-mix(
491
+ in srgb,
492
+ var(--_color),
493
+ var(--info-color)
494
+ var(--info-text-amount, var(--text-amount, 50%))
495
+ )
496
+ );
497
+ --success-text-fg: var(
498
+ --success-text-fg-override,
499
+ color-mix(
500
+ in srgb,
501
+ var(--_color),
502
+ var(--success-color)
503
+ var(--success-text-amount, var(--text-amount, 50%))
504
+ )
505
+ );
506
+ --warning-text-fg: var(
507
+ --warning-text-fg-override,
508
+ color-mix(
509
+ in srgb,
510
+ var(--_color),
511
+ var(--warning-color)
512
+ var(--warning-text-amount, var(--text-amount, 50%))
513
+ )
514
+ );
515
+ --danger-text-fg: var(
516
+ --danger-text-fg-override,
517
+ color-mix(
518
+ in srgb,
519
+ var(--_color),
520
+ var(--danger-color)
521
+ var(--danger-text-amount, var(--text-amount, 50%))
522
+ )
523
+ );
524
+ --muted-text-fg: var(
525
+ --muted-text-fg-override,
526
+ color-mix(
527
+ in srgb,
528
+ var(--_color),
529
+ var(--muted-color)
530
+ var(--muted-text-amount, var(--text-amount, 50%))
531
+ )
532
+ );
533
+ background-color: var(--_background-color);
534
+ color: var(--_color);
535
+ }
536
+ aside.overlay > button {
537
+ transition: left var(--sidebar-transition);
538
+ transform: translateX(0);
539
+ z-index: 3;
540
+ position: absolute;
541
+ opacity: 1;
542
+ pointer-events: all;
543
+ display: block;
544
+ top: var(--padding);
545
+ left: 0;
546
+ border-radius: var(--circle-button-radius, var(--mini-button-radius, var(--button-radius, var(--radius, 50%))));
547
+ border-top-left-radius: 0;
548
+ border-bottom-left-radius: 0;
549
+ border: var(--circle-button-border, var(--mini-button-border));
550
+ width: var(--_sidebar-expander-width);
551
+ height: var(--_sidebar-expander-height);
552
+ --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)))));
553
+ --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)))));
554
+ --_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))))));
555
+ --_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%))))));
556
+ --_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))))));
557
+ --_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%))))));
558
+ --_bg-base: var(--circle-button-bg, var(--mini-button-bg, var(--button-bg, var(--control-bg, var(--secondary-bg, var(--bg, unset))))));
559
+ --_fg-base: var(--circle-button-fg, var(--mini-button-fg, var(--button-fg, var(--control-fg, var(--secondary-fg, var(--fg, unset))))));
560
+ --_background-color: color-mix(
561
+ in srgb,
562
+ var(--_bg-base),
563
+ var(--_bg-mix-color) var(--_bg-mix-amount)
564
+ );
565
+ --_color: color-mix(
566
+ in srgb,
567
+ var(--_fg-base),
568
+ var(--_fg-mix-color) var(--_fg-mix-amount)
569
+ );
570
+ --muted-color: var(--muted-color-override, var(--_background-color));
571
+ --primary-text-fg: var(
572
+ --primary-text-fg-override,
573
+ color-mix(
574
+ in srgb,
575
+ var(--_color),
576
+ var(--primary-color)
577
+ var(--primary-text-amount, var(--text-amount, 50%))
578
+ )
579
+ );
580
+ --secondary-text-fg: var(
581
+ --secondary-text-fg-override,
582
+ color-mix(
583
+ in srgb,
584
+ var(--_color),
585
+ var(--secondary-color)
586
+ var(--secondary-text-amount, var(--text-amount, 50%))
587
+ )
588
+ );
589
+ --info-text-fg: var(
590
+ --info-text-fg-override,
591
+ color-mix(
592
+ in srgb,
593
+ var(--_color),
594
+ var(--info-color)
595
+ var(--info-text-amount, var(--text-amount, 50%))
596
+ )
597
+ );
598
+ --success-text-fg: var(
599
+ --success-text-fg-override,
600
+ color-mix(
601
+ in srgb,
602
+ var(--_color),
603
+ var(--success-color)
604
+ var(--success-text-amount, var(--text-amount, 50%))
605
+ )
606
+ );
607
+ --warning-text-fg: var(
608
+ --warning-text-fg-override,
609
+ color-mix(
610
+ in srgb,
611
+ var(--_color),
612
+ var(--warning-color)
613
+ var(--warning-text-amount, var(--text-amount, 50%))
614
+ )
615
+ );
616
+ --danger-text-fg: var(
617
+ --danger-text-fg-override,
618
+ color-mix(
619
+ in srgb,
620
+ var(--_color),
621
+ var(--danger-color)
622
+ var(--danger-text-amount, var(--text-amount, 50%))
623
+ )
624
+ );
625
+ --muted-text-fg: var(
626
+ --muted-text-fg-override,
627
+ color-mix(
628
+ in srgb,
629
+ var(--_color),
630
+ var(--muted-color)
631
+ var(--muted-text-amount, var(--text-amount, 50%))
632
+ )
633
+ );
634
+ background-color: var(--_background-color);
635
+ color: var(--_color);
636
+ cursor: var(--circle-button-cursor, var(--mini-button-cursor, var(--button-cursor, var(--control-cursor, var(--cursor, pointer)))));
637
+ 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)))));
638
+ }
639
+ aside.overlay > button:hover {
640
+ 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))))));
641
+ transform: var(--circle-button-hover-transform, var(--mini-button-hover-transform, var(--button-hover-transform, var(--control-hover-transform, var(--hover-transform, none)))));
642
+ color: var(--_color);
643
+ 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%)));
644
+ 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))))));
645
+ }
646
+ aside.overlay > button:active {
647
+ 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))))));
648
+ transform: var(--circle-button-active-transform, var(--mini-button-active-transform, var(--button-active-transform, var(--control-active-transform, var(--active-transform, none)))));
649
+ 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%)));
650
+ 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))))));
651
+ }
652
+ aside.overlay > button:disabled {
653
+ cursor: var(--circle-button-disabled-cursor, var(--mini-button-disabled-cursor, var(--button-disabled-cursor, var(--control-disabled-cursor, var(--disabled-cursor, not-allowed)))));
654
+ transform: var(--circle-button-disabled-transform, var(--mini-button-disabled-transform, var(--button-disabled-transform, var(--control-disabled-transform, var(--disabled-transform, none)))));
655
+ 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))))));
656
+ 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%)));
657
+ 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))))));
658
+ }
659
+ aside.overlay > button:focus-visible {
660
+ outline: var(--focus-color, -webkit-focus-ring-color) auto 1px;
661
+ outline-offset: var(--focus-outline-offset, 2px);
662
+ box-shadow: var(--focus-ring-box-shadow, 0 0 0 3px var(--focus-shadow-color, rgba(100, 150, 250, 0.5)));
663
+ }
664
+ aside.overlay > button::after {
665
+ color: var(--circle-button-fg, var(--mini-button-fg, currentColor));
666
+ filter: var(--sidebar-mobile-icon-filter, none);
667
+ }
668
+ aside.overlay > button.close {
669
+ /* Flush inside the panel's inner edge. Adding --padding here pushed the
670
+ button a padding's worth further in, which left it straddling the
671
+ edge -- part of it hanging outside the panel -- while the content
672
+ inset below still reserved the full button width, so the gap between
673
+ the button and the first nav item came out wider than intended.
674
+ Either the button pops out of the panel or it sits inside it; this
675
+ sits inside it, and the inset is then exactly button + one gutter. */
676
+ left: calc(var(--sidebar-width) - var(--_sidebar-expander-width));
677
+ border-radius: var(--circle-button-radius, var(--mini-button-radius, var(--button-radius, var(--radius, 50%))));
678
+ border-top-right-radius: 0;
679
+ border-bottom-right-radius: 0;
680
+ }
681
+ aside.overlay {
682
+ /* Everything above anchors the button to the left, which is right for a
683
+ left-hand sidebar and nonsense for a right-hand one: the sheet slides
684
+ out of the right edge, so the button has to hug that edge and then step
685
+ *inwards* -- leftwards -- by the panel's width when it opens. Anchored
686
+ from the left it stepped the other way, off the far side of the page
687
+ entirely, where it could not even be clicked. The flattened corners
688
+ mirror too, so the button still reads as attached to its own edge. */
689
+ }
690
+ aside.overlay.right > button {
691
+ left: auto;
692
+ right: 0;
693
+ border-top-left-radius: var(--circle-button-radius, var(--mini-button-radius, var(--button-radius, var(--radius, 50%))));
694
+ border-bottom-left-radius: var(--circle-button-radius, var(--mini-button-radius, var(--button-radius, var(--radius, 50%))));
695
+ border-top-right-radius: 0;
696
+ border-bottom-right-radius: 0;
697
+ }
698
+ aside.overlay.right > button.close {
699
+ left: auto;
700
+ right: calc(var(--sidebar-width) - var(--_sidebar-expander-width));
701
+ border-top-right-radius: var(--circle-button-radius, var(--mini-button-radius, var(--button-radius, var(--radius, 50%))));
702
+ border-bottom-right-radius: var(--circle-button-radius, var(--mini-button-radius, var(--button-radius, var(--radius, 50%))));
703
+ border-top-left-radius: 0;
704
+ border-bottom-left-radius: 0;
329
705
  }
330
706
  .edge-bar {
331
707
  --link-bg: var(--grab-bar-link-bg, var(--sidebar-link-bg, var(--surface-link-bg, inherit)));
@@ -457,43 +833,101 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
457
833
  aside:has(.edge-bar:hover) .content {
458
834
  filter: var(--greyed-out-filter);
459
835
  }
460
- /* Hide hamburger expander when not in
461
- hamburger mode */
462
- button {
836
+ /* Hide the sheet button in rail mode */
837
+ aside:not(.overlay) > button {
463
838
  display: none;
464
839
  }
465
840
  }
466
841
  @container (max-width: 512px) {
467
- .edge-bar {
468
- display: none;
469
- }
470
- .sidebar {
842
+ aside {
843
+ /* The sheet and its button are absolutely positioned, so the sidebar has
844
+ to be their containing block. Without this they resolve against
845
+ whatever positioned ancestor happens to be up the tree -- inside <Page>
846
+ that is Page's own aside and it looks fine, but drop a Sidebar into a
847
+ Card or a Container and the button escapes to the top-left of the page,
848
+ over unrelated content.
849
+
850
+ `overflow` has to be released in the same breath. Every child here is
851
+ out of flow, so the aside computes to zero height -- harmless while it
852
+ was not a containing block, but the moment it becomes one the inherited
853
+ `overflow: hidden` clips the button and the sheet away to nothing.
854
+ Nothing needs clipping here: the shut sheet is hidden by opacity and
855
+ pointer-events, not by the box. */
856
+ /* Sized here rather than on the button, because the panel needs them too
857
+ -- see the padding it reserves below. */
858
+ --_sidebar-expander-width: max(
859
+ var(--sidebar-icon-width, 0.65rem),
860
+ var(--icon-size, 32px)
861
+ );
862
+ --_sidebar-expander-height: max(
863
+ var(--sidebar-icon-height, 1rem),
864
+ var(--icon-size, 32px)
865
+ );
866
+ position: relative;
867
+ overflow: visible;
471
868
  background: transparent;
869
+ width: calc(var(--gap) + max(var(--sidebar-icon-width, 0.65rem), var(--icon-size, 32px)));
870
+ flex: 0 0 auto;
871
+ /* Every child is out of flow, so the aside has no content to hold it
872
+ open and the base `height: 100%` resolves against an auto-height
873
+ parent -- which is to say, nothing. Stretch to the row instead, so the
874
+ aside is as tall as whatever it sits beside, and the open sheet's own
875
+ `height: 100%` has something real to measure against. */
876
+ height: auto;
877
+ align-self: stretch;
878
+ }
879
+ aside .edge-bar {
880
+ display: none;
472
881
  }
473
- .right,
474
- .left {
882
+ aside.right, aside.left {
475
883
  border-left: none;
476
884
  border-right: none;
477
885
  }
478
886
  aside > .content {
887
+ position: absolute;
888
+ top: 0;
889
+ left: 0;
890
+ width: var(--sidebar-width);
891
+ z-index: 2;
479
892
  transform: translateX(-100%);
480
893
  opacity: 0;
481
894
  background: transparent;
482
895
  pointer-events: none;
483
- transition: transform var(--sidebar-transition) ease-in-out;
896
+ transition: transform var(--sidebar-transition) ease-in-out, opacity var(--sidebar-transition) ease-in-out;
484
897
  padding: var(--padding);
898
+ /* The close button floats over the panel's inner edge, so the panel has
899
+ to keep that strip clear or the first nav item sits underneath it.
900
+
901
+ Reserved on the inline axis, not the block one: a nav wants the full
902
+ height, and a blank band across the top of it to clear one button is
903
+ a poor trade. The cost is that the whole column is a button's width
904
+ narrower, even well below the button -- override
905
+ --sidebar-sheet-content-inset to 0 and set your own padding if you
906
+ would rather have the width back. */
907
+ padding-inline-end: var(--sidebar-sheet-content-inset, calc(var(--padding) + var(--_sidebar-expander-width)));
485
908
  }
486
909
  aside.left > .content {
487
910
  border-right: var(--border-width) var(--border-style) var(--border-color);
488
911
  }
912
+ aside {
913
+ /* A right-hand sheet slides out of the right edge, not the left -- and
914
+ its button lands on the panel's left edge, so the reserved strip
915
+ mirrors with it. */
916
+ }
489
917
  aside.right > .content {
918
+ left: auto;
919
+ right: 0;
920
+ transform: translateX(100%);
490
921
  border-left: var(--border-width) var(--border-style) var(--border-color);
922
+ padding-inline-end: var(--padding);
923
+ padding-inline-start: var(--sidebar-sheet-content-inset, calc(var(--padding) + var(--_sidebar-expander-width)));
491
924
  }
492
925
  aside.expandedHamburger > .content {
493
926
  transform: translateX(0);
494
927
  height: 100%;
495
928
  opacity: 1;
496
929
  pointer-events: all;
930
+ box-shadow: var(--sidebar-overlay-box-shadow, var(--overlay-box-shadow, 0 0 var(--space, 8px) rgba(127, 127, 127, 0.4)));
497
931
  --link-bg: var(--sidebar-link-bg, var(--surface-link-bg, inherit));
498
932
  --link-fg: var(--sidebar-link-fg, var(--surface-link-fg, inherit));
499
933
  --_bg-mix-color: var(--sidebar-bg-mix-color, var(--surface-bg-mix-color, var(--bg-mix-color, transparent)));
@@ -580,14 +1014,6 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
580
1014
  color: var(--_color);
581
1015
  }
582
1016
  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
1017
  transition: left var(--sidebar-transition);
592
1018
  transform: translateX(0);
593
1019
  z-index: 3;
@@ -595,7 +1021,6 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
595
1021
  opacity: 1;
596
1022
  pointer-events: all;
597
1023
  display: block;
598
- position: absolute;
599
1024
  top: var(--padding);
600
1025
  left: 0;
601
1026
  border-radius: var(--circle-button-radius, var(--mini-button-radius, var(--button-radius, var(--radius, 50%))));
@@ -721,28 +1146,42 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
721
1146
  filter: var(--sidebar-mobile-icon-filter, none);
722
1147
  }
723
1148
  aside > button.close {
724
- left: calc(var(--sidebar-width) - var(--_sidebar-expander-width) + var(--padding));
1149
+ /* Flush inside the panel's inner edge. Adding --padding here pushed the
1150
+ button a padding's worth further in, which left it straddling the
1151
+ edge -- part of it hanging outside the panel -- while the content
1152
+ inset below still reserved the full button width, so the gap between
1153
+ the button and the first nav item came out wider than intended.
1154
+ Either the button pops out of the panel or it sits inside it; this
1155
+ sits inside it, and the inset is then exactly button + one gutter. */
1156
+ left: calc(var(--sidebar-width) - var(--_sidebar-expander-width));
725
1157
  border-radius: var(--circle-button-radius, var(--mini-button-radius, var(--button-radius, var(--radius, 50%))));
726
1158
  border-top-right-radius: 0;
727
1159
  border-bottom-right-radius: 0;
728
1160
  }
729
- /* aside > button:hover {
730
- background: var(--mini-button-hover-bg);
731
- color: var(--mini-button-hover-fg);
732
- } */
733
1161
  aside {
734
- width: calc(var(--gap) + max(var(--sidebar-icon-width, 0.65rem), var(--icon-size, 32px)));
735
- flex: 0 0 auto;
1162
+ /* Everything above anchors the button to the left, which is right for a
1163
+ left-hand sidebar and nonsense for a right-hand one: the sheet slides
1164
+ out of the right edge, so the button has to hug that edge and then step
1165
+ *inwards* -- leftwards -- by the panel's width when it opens. Anchored
1166
+ from the left it stepped the other way, off the far side of the page
1167
+ entirely, where it could not even be clicked. The flattened corners
1168
+ mirror too, so the button still reads as attached to its own edge. */
736
1169
  }
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;
1170
+ aside.right > button {
1171
+ left: auto;
1172
+ right: 0;
1173
+ border-top-left-radius: var(--circle-button-radius, var(--mini-button-radius, var(--button-radius, var(--radius, 50%))));
1174
+ border-bottom-left-radius: var(--circle-button-radius, var(--mini-button-radius, var(--button-radius, var(--radius, 50%))));
1175
+ border-top-right-radius: 0;
1176
+ border-bottom-right-radius: 0;
1177
+ }
1178
+ aside.right > button.close {
1179
+ left: auto;
1180
+ right: calc(var(--sidebar-width) - var(--_sidebar-expander-width));
1181
+ border-top-right-radius: var(--circle-button-radius, var(--mini-button-radius, var(--button-radius, var(--radius, 50%))));
1182
+ border-bottom-right-radius: var(--circle-button-radius, var(--mini-button-radius, var(--button-radius, var(--radius, 50%))));
1183
+ border-top-left-radius: 0;
1184
+ border-bottom-left-radius: 0;
746
1185
  }
747
1186
  }
748
1187
  button::after {
@@ -758,14 +1197,34 @@ button::after {
758
1197
  line-height: 1;
759
1198
  }
760
1199
 
761
- button.expander::after {
762
- content: var(--sidebar-expand, "›");
763
- background-image: var(--sidebar-expand-image, none);
1200
+ /* The rail and the sheet are different affordances and want different
1201
+ glyphs. The rail slides a panel out from the edge it sits on, so a
1202
+ chevron pointing that way says what will happen. The sheet is summoned
1203
+ from nowhere by a free-floating button, where the menu glyph is what
1204
+ people already recognize -- and it closes with an x, not a back-chevron,
1205
+ because there is no edge to slide back into.
1206
+
1207
+ Both still fall through --sidebar-expand / --sidebar-collapse, so setting
1208
+ those restyles every button at once and themes that swap in SVGs keep
1209
+ working unchanged. */
1210
+ .edge-bar button.expander::after {
1211
+ content: var(--grab-bar-expand, var(--sidebar-expand, "›"));
1212
+ background-image: var(--grab-bar-expand-image, var(--sidebar-expand-image, none));
1213
+ }
1214
+
1215
+ .edge-bar button.close::after {
1216
+ content: var(--grab-bar-collapse, var(--sidebar-collapse, "‹"));
1217
+ background-image: var(--grab-bar-collapse-image, var(--sidebar-collapse-image, none));
1218
+ }
1219
+
1220
+ aside > button.expander::after {
1221
+ content: var(--sidebar-sheet-expand, var(--sidebar-expand, "☰"));
1222
+ background-image: var(--sidebar-sheet-expand-image, var(--sidebar-expand-image, none));
764
1223
  }
765
1224
 
766
- button.close::after {
767
- content: var(--sidebar-collapse, "‹");
768
- background-image: var(--sidebar-collapse-image, none);
1225
+ aside > button.close::after {
1226
+ content: var(--sidebar-sheet-collapse, var(--sidebar-collapse, "✕"));
1227
+ background-image: var(--sidebar-sheet-collapse-image, var(--sidebar-collapse-image, none));
769
1228
  }
770
1229
 
771
1230
  .right button::after {