contain-css-svelte 1.4.3 → 1.6.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 (57) hide show
  1. package/dist/Card.svelte +57 -3
  2. package/dist/controls/Button.svelte +32 -0
  3. package/dist/controls/ButtonLink.svelte +32 -0
  4. package/dist/controls/Checkbox.svelte +36 -0
  5. package/dist/controls/CircleButton.svelte +32 -0
  6. package/dist/controls/GroupedRadioItem.svelte +551 -0
  7. package/dist/controls/GroupedRadioItem.svelte.d.ts +36 -0
  8. package/dist/controls/GroupedToggle.svelte +147 -0
  9. package/dist/controls/GroupedToggle.svelte.d.ts +9 -0
  10. package/dist/controls/GroupedToggleItem.svelte +452 -0
  11. package/dist/controls/GroupedToggleItem.svelte.d.ts +31 -0
  12. package/dist/controls/Input.svelte +32 -0
  13. package/dist/controls/RadioButton.svelte +36 -1
  14. package/dist/controls/Select.svelte +80 -2
  15. package/dist/controls/Slider.svelte +32 -0
  16. package/dist/controls/TabItem.svelte +36 -0
  17. package/dist/controls/Toggle.svelte +32 -0
  18. package/dist/dropdowns/DropdownMenu.svelte +53 -1
  19. package/dist/index.d.ts +4 -0
  20. package/dist/index.js +4 -0
  21. package/dist/layout/Accordion.svelte +32 -0
  22. package/dist/layout/Bar.svelte +50 -0
  23. package/dist/layout/ColumnContainer.svelte +32 -0
  24. package/dist/layout/Columns.svelte +32 -0
  25. package/dist/layout/Container.svelte +48 -2
  26. package/dist/layout/DataList.svelte +33 -1
  27. package/dist/layout/DataList.svelte.d.ts +7 -0
  28. package/dist/layout/DataListItem.svelte +56 -2
  29. package/dist/layout/Fieldset.svelte +32 -0
  30. package/dist/layout/Form.svelte +32 -0
  31. package/dist/layout/FormItem.svelte +46 -0
  32. package/dist/layout/GridLayout.svelte +59 -0
  33. package/dist/layout/Hero.svelte +42 -0
  34. package/dist/layout/Inline.svelte +37 -1
  35. package/dist/layout/MenuList.svelte +32 -0
  36. package/dist/layout/Page.svelte +137 -10
  37. package/dist/layout/Page.svelte.d.ts +17 -0
  38. package/dist/layout/RowContainer.svelte +32 -0
  39. package/dist/layout/Sidebar.svelte +104 -56
  40. package/dist/layout/SidebarContainer.svelte +32 -0
  41. package/dist/layout/SplitPane.svelte +32 -0
  42. package/dist/layout/Stack.svelte +32 -0
  43. package/dist/layout/TabBar.svelte +127 -8
  44. package/dist/layout/TabBar.svelte.d.ts +16 -0
  45. package/dist/layout/Table.svelte +60 -9
  46. package/dist/layout/Tile.svelte +89 -6
  47. package/dist/misc/CodeInner.svelte +32 -0
  48. package/dist/misc/Progress.svelte +32 -0
  49. package/dist/misc/Tag.svelte +32 -0
  50. package/dist/overlays/Dialog.svelte +32 -0
  51. package/dist/overlays/Tooltip.svelte +32 -0
  52. package/dist/sass/_responsive.scss +43 -0
  53. package/dist/typography/TextLayout.svelte +47 -0
  54. package/dist/vars/defaults.css +4 -1
  55. package/dist/vars/layout.css +4 -1
  56. package/dist/vars/responsive-typography.css +4 -1
  57. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">import { elementProps } from "../util";
2
2
  import { COLOR_VARS, TYPOGRAPHY_VARS, } from "../styleProps";
3
3
  import { onDestroy, onMount } from "svelte";
4
- const { right = false, sticky = false, hideSidebar = false, hideHeader = false, hideFooter = false, onStickyChange = () => { }, header, footer, sidebar, children, class: className, ...restProps } = $props();
4
+ const { right = false, sticky = false, hideSidebar = false, hideHeader = false, hideFooter = false, compactSidebar, onStickyChange = () => { }, header, footer, sidebar, children, class: className, ...restProps } = $props();
5
5
  /* The shorthands page's own CSS backs, one group per mixin it
6
6
  includes. The Props type is derived from this same array, so what the
7
7
  component accepts and what it emits cannot drift apart. */
@@ -18,9 +18,17 @@ const el = $derived(elementProps(restProps, "page", [
18
18
  const hasSidebar = $derived(Boolean(sidebar) && !hideSidebar);
19
19
  const hasHeader = $derived(Boolean(header) && !hideHeader);
20
20
  const hasFooter = $derived(Boolean(footer) && !hideFooter);
21
+ const sheetSpace = $derived(compactSidebar === "header" || compactSidebar === undefined
22
+ ? hasHeader
23
+ ? "header"
24
+ : "gutter"
25
+ : compactSidebar);
21
26
  // Start "unfrozen" so i.e. scrolling on reload
22
27
  // or hash link works properly
23
28
  let freeze = $state(false);
29
+ /* Measured so the compact sheet button can be centred in the header row
30
+ (see the compact block in the styles). */
31
+ let headerHeight = $state(0);
24
32
  let pageElement;
25
33
  function handleScroll() {
26
34
  if (sticky) {
@@ -70,14 +78,18 @@ onMount(() => {
70
78
  class:hasHeader
71
79
  class:hasSidebar
72
80
  class:hasFooter
81
+ class:sheet-header={sheetSpace === "header"}
82
+ class:sheet-gutter={sheetSpace === "gutter"}
83
+ class:sheet-symmetric={sheetSpace === "symmetric"}
84
+ class:sheet-overlap={sheetSpace === "overlap"}
73
85
  bind:this={pageElement}
74
86
  {...el}
75
87
  >
76
- <header>
88
+ <header bind:clientHeight={headerHeight}>
77
89
  {#if hasHeader}{@render header?.()}{/if}
78
90
  </header>
79
91
  <div class="side-by-side">
80
- <div class="aside">
92
+ <div class="aside" style:--_page-header-height="{headerHeight}px">
81
93
  {#if hasSidebar}{@render sidebar?.()}{/if}
82
94
  </div>
83
95
  <div class="content">
@@ -160,6 +172,38 @@ onMount(() => {
160
172
  we deliberately avoid everywhere else.
161
173
 
162
174
  One definition, used by both, so they cannot drift again. */
175
+ /* The "small" size tier: a component whose container is this wide or less
176
+ is on a phone, or in a pane about that narrow, and sizes itself for it.
177
+
178
+ The convention, shared by every component that has a size worth changing
179
+ there (Card started it; Tile, GridLayout, Bar and Container follow):
180
+
181
+ --{component}-{property}-small
182
+
183
+ applies inside a small container. What wins there, in order:
184
+ 1. the `-small` variable, if you set it;
185
+ 2. otherwise the regular variable, if you set it -- a size you chose on
186
+ purpose is not overridden behind your back;
187
+ 3. otherwise the component's own small default, which for most
188
+ properties is simply its regular default.
189
+
190
+ Container queries can't read a custom property, so the breakpoint itself
191
+ is fixed here rather than themeable. 600px matches Card and SplitPane,
192
+ which already switched there. (The Sidebar's rail-to-sheet switch,
193
+ above, is a separate, layout-chrome decision and keeps its own value.) */
194
+ /* Styles for the small tier: the nearest container is narrow, OR the
195
+ viewport is.
196
+
197
+ The container query is the real answer -- a Tile in a 400px pane on a
198
+ desktop is small. But a container query only matches inside an ancestor
199
+ that *is* a container, and a component dropped straight into a page (no
200
+ Page, no Container around it) has none, so on a phone it would never
201
+ find out. The viewport query covers that: a narrow viewport means every
202
+ container in it is narrow too, so the two never disagree in the
203
+ direction that matters.
204
+
205
+ An element never queries itself, so on a component that is its own
206
+ container (Tile, Bar) this asks about the space it was given. */
163
207
  /* Warning: because we define a fallback
164
208
  media query, the media query can override the container
165
209
  if we stack a bunch of these in a row and aren't thoughtful about the order.
@@ -305,6 +349,12 @@ footer {
305
349
  justify-content: stretch;
306
350
  }
307
351
 
352
+ .page {
353
+ /* Containing block for the compact sidebar, which spans the whole page
354
+ (header included) once it has a header to sit in. */
355
+ position: relative;
356
+ }
357
+
308
358
  .page.sticky {
309
359
  position: sticky;
310
360
  top: 0;
@@ -315,6 +365,22 @@ footer {
315
365
  }
316
366
 
317
367
  .content {
368
+ /* The compact sidebar's reserved strip: at least the content's own
369
+ padding, at least a sheet button plus a gap. Declared here, where
370
+ --_padding (contentPadding) is set, so it resolves against it. */
371
+ --_page-gutter: max(
372
+ var(--_padding, var(--padding, 0px)),
373
+ var(
374
+ --sidebar-compact-side-padding,
375
+ calc(
376
+ max(
377
+ var(--sidebar-icon-width, 0.65rem),
378
+ var(--sidebar-icon-height, 1rem),
379
+ var(--icon-size, 32px)
380
+ ) + var(--gap, 8px)
381
+ )
382
+ )
383
+ );
318
384
  overflow-y: auto;
319
385
  scrollbar-width: thin;
320
386
  scrollbar-color: transparent transparent;
@@ -486,25 +552,86 @@ footer {
486
552
  .side-by-side {
487
553
  gap: 0;
488
554
  }
555
+ .page > div > .content {
556
+ min-width: 0;
557
+ }
558
+ /* The sidebar becomes a floating sheet, summoned by a button. The aside
559
+ holding it is a transparent strip laid over the content; only the
560
+ Sidebar's button and open sheet catch clicks, never the strip. What
561
+ room the content makes for the button is the `compactSidebar` mode,
562
+ below. */
489
563
  .hasSidebar > .side-by-side > .aside {
490
564
  position: absolute;
491
565
  inset-block: 0;
492
566
  inset-inline-start: 0;
493
567
  z-index: 3;
494
568
  display: flex;
495
- }
496
- .hasSidebar > .side-by-side > .content {
497
- padding-inline-start: max(var(--_padding, var(--padding, 0px)), var(--sidebar-compact-side-padding, calc(max(var(--sidebar-icon-width, 0.65rem), var(--sidebar-icon-height, 1rem), var(--icon-size, 32px)) + var(--gap, 8px))));
569
+ pointer-events: none;
498
570
  }
499
571
  .hasSidebar.right > .side-by-side > .aside {
500
572
  inset-inline-start: auto;
501
573
  inset-inline-end: 0;
502
574
  }
503
- .hasSidebar.right > .side-by-side > .content {
575
+ /* "gutter": a button-wide strip down the sidebar's side of the content. */
576
+ .hasSidebar.sheet-gutter:not(.right) > .side-by-side > .content {
577
+ padding-inline-start: var(--_page-gutter);
578
+ }
579
+ .hasSidebar.sheet-gutter.right > .side-by-side > .content {
504
580
  padding-inline-start: var(--_padding, var(--padding, 0px));
505
- padding-inline-end: max(var(--_padding, var(--padding, 0px)), var(--sidebar-compact-side-padding, calc(max(var(--sidebar-icon-width, 0.65rem), var(--sidebar-icon-height, 1rem), var(--icon-size, 32px)) + var(--gap, 8px))));
581
+ padding-inline-end: var(--_page-gutter);
506
582
  }
507
- .page > div > .content {
508
- min-width: 0;
583
+ /* "symmetric": the same strip on both sides, so the column stays
584
+ centred. */
585
+ .hasSidebar.sheet-symmetric > .side-by-side > .content {
586
+ padding-inline: var(--_page-gutter);
587
+ }
588
+ /* "overlap" and "header": the content keeps its ordinary, even padding.
589
+ Under "overlap" the button simply floats over the content's corner. */
590
+ .hasSidebar.sheet-overlap > .side-by-side > .content,
591
+ .hasSidebar.sheet-header > .side-by-side > .content {
592
+ padding-inline: var(--_padding, var(--padding, 0px));
593
+ }
594
+ /* "header": the button sits where a menu button is on every phone.
595
+ - The aside spans the whole page, header included, instead of just
596
+ the row beside the content, so the button can sit in the header row
597
+ and the open sheet covers the page top to bottom, like any drawer.
598
+ - The button is centred in the header's measured height.
599
+ - The header's content steps aside by the button's width. */
600
+ .hasSidebar.sheet-header > .side-by-side {
601
+ position: static;
602
+ }
603
+ .hasSidebar.sheet-header > .side-by-side > .aside {
604
+ --sidebar-sheet-button-top: calc(
605
+ (var(--_page-header-height, 0px) - var(--_page-sheet-button-size)) / 2
606
+ );
509
607
  }
608
+ /* It's the header's own content (a Bar, usually) that makes room, not
609
+ the header box: padding the header would leave a strip of page colour
610
+ beside a coloured bar. Children passed CSS variables arrive wrapped in
611
+ a display:contents <svelte-css-wrapper>, hence the second selector. */
612
+ .hasSidebar.sheet-header:not(.right) > header > :global(*:not(svelte-css-wrapper)),
613
+ .hasSidebar.sheet-header:not(.right) > header > :global(svelte-css-wrapper > *) {
614
+ padding-inline-start: var(--_page-header-inset);
615
+ }
616
+ .hasSidebar.sheet-header.right > header > :global(*:not(svelte-css-wrapper)),
617
+ .hasSidebar.sheet-header.right > header > :global(svelte-css-wrapper > *) {
618
+ padding-inline-end: var(--_page-header-inset);
619
+ }
620
+ }
621
+ .page {
622
+ /* Same size the Sidebar gives its sheet button. */
623
+ --_page-sheet-button-size: max(
624
+ var(--sidebar-icon-height, 1rem),
625
+ var(--icon-size, 32px)
626
+ );
627
+ --_page-header-inset: var(
628
+ --page-header-sidebar-inset,
629
+ calc(
630
+ max(
631
+ var(--sidebar-icon-width, 0.65rem),
632
+ var(--sidebar-icon-height, 1rem),
633
+ var(--icon-size, 32px)
634
+ ) + 2 * var(--gap, 8px)
635
+ )
636
+ );
510
637
  }</style>
@@ -8,6 +8,23 @@ declare const Page: import("svelte").Component<ContainProps<HTMLAttributes<HTMLE
8
8
  hideSidebar?: boolean;
9
9
  hideHeader?: boolean;
10
10
  hideFooter?: boolean;
11
+ /**
12
+ * Where the sidebar's menu button goes, and what room is made for it,
13
+ * once the page is narrow enough that the sidebar becomes a sheet:
14
+ *
15
+ * - `"header"` (default when there is a header): the button sits in
16
+ * the header row and the header's content steps aside for it. The
17
+ * page content keeps its ordinary, even padding.
18
+ * - `"gutter"` (default without a header): a button-wide strip is
19
+ * reserved down the sidebar's side of the content.
20
+ * - `"symmetric"`: the same strip on both sides, so the column stays
21
+ * centred at the cost of width on both.
22
+ * - `"overlap"`: no room is made; the button floats over the top
23
+ * corner of the content. Fine when that corner is empty.
24
+ *
25
+ * `"header"` with no header showing falls back to `"gutter"`.
26
+ */
27
+ compactSidebar?: "header" | "gutter" | "symmetric" | "overlap";
11
28
  onStickyChange?: (stuck: boolean) => void;
12
29
  header?: Snippet;
13
30
  footer?: Snippet;
@@ -92,6 +92,38 @@ const el = $derived(elementProps(restProps, "row-container", ROW_CONTAINER_VARS)
92
92
  we deliberately avoid everywhere else.
93
93
 
94
94
  One definition, used by both, so they cannot drift again. */
95
+ /* The "small" size tier: a component whose container is this wide or less
96
+ is on a phone, or in a pane about that narrow, and sizes itself for it.
97
+
98
+ The convention, shared by every component that has a size worth changing
99
+ there (Card started it; Tile, GridLayout, Bar and Container follow):
100
+
101
+ --{component}-{property}-small
102
+
103
+ applies inside a small container. What wins there, in order:
104
+ 1. the `-small` variable, if you set it;
105
+ 2. otherwise the regular variable, if you set it -- a size you chose on
106
+ purpose is not overridden behind your back;
107
+ 3. otherwise the component's own small default, which for most
108
+ properties is simply its regular default.
109
+
110
+ Container queries can't read a custom property, so the breakpoint itself
111
+ is fixed here rather than themeable. 600px matches Card and SplitPane,
112
+ which already switched there. (The Sidebar's rail-to-sheet switch,
113
+ above, is a separate, layout-chrome decision and keeps its own value.) */
114
+ /* Styles for the small tier: the nearest container is narrow, OR the
115
+ viewport is.
116
+
117
+ The container query is the real answer -- a Tile in a 400px pane on a
118
+ desktop is small. But a container query only matches inside an ancestor
119
+ that *is* a container, and a component dropped straight into a page (no
120
+ Page, no Container around it) has none, so on a phone it would never
121
+ find out. The viewport query covers that: a narrow viewport means every
122
+ container in it is narrow too, so the two never disagree in the
123
+ direction that matters.
124
+
125
+ An element never queries itself, so on a component that is its own
126
+ container (Tile, Bar) this asks about the space it was given. */
95
127
  /* Warning: because we define a fallback
96
128
  media query, the media query can override the container
97
129
  if we stack a bunch of these in a row and aren't thoughtful about the order.
@@ -130,6 +130,38 @@ const expandedHamburger = $derived(open ?? false);
130
130
  we deliberately avoid everywhere else.
131
131
 
132
132
  One definition, used by both, so they cannot drift again. */
133
+ /* The "small" size tier: a component whose container is this wide or less
134
+ is on a phone, or in a pane about that narrow, and sizes itself for it.
135
+
136
+ The convention, shared by every component that has a size worth changing
137
+ there (Card started it; Tile, GridLayout, Bar and Container follow):
138
+
139
+ --{component}-{property}-small
140
+
141
+ applies inside a small container. What wins there, in order:
142
+ 1. the `-small` variable, if you set it;
143
+ 2. otherwise the regular variable, if you set it -- a size you chose on
144
+ purpose is not overridden behind your back;
145
+ 3. otherwise the component's own small default, which for most
146
+ properties is simply its regular default.
147
+
148
+ Container queries can't read a custom property, so the breakpoint itself
149
+ is fixed here rather than themeable. 600px matches Card and SplitPane,
150
+ which already switched there. (The Sidebar's rail-to-sheet switch,
151
+ above, is a separate, layout-chrome decision and keeps its own value.) */
152
+ /* Styles for the small tier: the nearest container is narrow, OR the
153
+ viewport is.
154
+
155
+ The container query is the real answer -- a Tile in a 400px pane on a
156
+ desktop is small. But a container query only matches inside an ancestor
157
+ that *is* a container, and a component dropped straight into a page (no
158
+ Page, no Container around it) has none, so on a phone it would never
159
+ find out. The viewport query covers that: a narrow viewport means every
160
+ container in it is narrow too, so the two never disagree in the
161
+ direction that matters.
162
+
163
+ An element never queries itself, so on a component that is its own
164
+ container (Tile, Bar) this asks about the space it was given. */
133
165
  /* Warning: because we define a fallback
134
166
  media query, the media query can override the container
135
167
  if we stack a bunch of these in a row and aren't thoughtful about the order.
@@ -439,8 +471,14 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
439
471
  var(--sidebar-icon-height, 1rem),
440
472
  var(--icon-size, 32px)
441
473
  );
474
+ /* Where the open sheet's close button sits: just past the panel's outer
475
+ edge, but never off the screen. */
476
+ --_sidebar-close-offset: min(
477
+ var(--sidebar-width),
478
+ calc(100vw - var(--_sidebar-expander-width))
479
+ );
442
480
  /* The sheet button's corners. Its "edge" corners are the pair on the
443
- side it attaches to -- the page edge while shut, the panel's inner edge
481
+ side it attaches to -- the page edge while shut, the panel's outer edge
444
482
  while open -- and default to 0, which is what makes it read as a tab
445
483
  fastened to that edge. `sheetButton="button"` rounds them like the
446
484
  other two, for a sheet that does not sit against an edge. */
@@ -462,6 +500,12 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
462
500
  position: relative;
463
501
  overflow: visible;
464
502
  background: transparent;
503
+ /* The aside is only a transparent strip holding the button and the
504
+ sheet, and a host may let content run underneath it (Page does, once
505
+ it has moved the button into its header). The strip itself must not
506
+ swallow clicks meant for that content; the button and an open sheet
507
+ turn pointer events back on for themselves. */
508
+ pointer-events: none;
465
509
  width: calc(var(--gap) + max(var(--sidebar-icon-width, 0.65rem), var(--icon-size, 32px)));
466
510
  flex: 0 0 auto;
467
511
  /* Every child is out of flow, so the aside has no content to hold it
@@ -493,16 +537,13 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
493
537
  pointer-events: none;
494
538
  transition: transform var(--sidebar-transition) ease-in-out, opacity var(--sidebar-transition) ease-in-out;
495
539
  padding: var(--padding);
496
- /* The close button floats over the panel's inner edge, so the panel has
497
- to keep that strip clear or the first nav item sits underneath it.
498
-
499
- Reserved on the inline axis, not the block one: a nav wants the full
500
- height, and a blank band across the top of it to clear one button is
501
- a poor trade. The cost is that the whole column is a button's width
502
- narrower, even well below the button -- override
503
- --sidebar-sheet-content-inset to 0 and set your own padding if you
504
- would rather have the width back. */
505
- padding-inline-end: var(--sidebar-sheet-content-inset, calc(var(--padding) + var(--_sidebar-expander-width)));
540
+ /* The close button hangs off the panel's outer edge (below), so the
541
+ panel keeps its whole width. It used to sit inside that edge, which
542
+ meant reserving a button-wide strip down the full height of the
543
+ sheet -- a stripe beside every nav item, there only to clear one
544
+ button at the top. --sidebar-sheet-content-inset brings a reserve
545
+ back if you want one. */
546
+ padding-inline-end: var(--sidebar-sheet-content-inset, var(--padding));
506
547
  }
507
548
  aside.overlay.left > .content {
508
549
  border-right: var(--border-width) var(--border-style) var(--border-color);
@@ -518,7 +559,7 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
518
559
  transform: translateX(100%);
519
560
  border-left: var(--border-width) var(--border-style) var(--border-color);
520
561
  padding-inline-end: var(--padding);
521
- padding-inline-start: var(--sidebar-sheet-content-inset, calc(var(--padding) + var(--_sidebar-expander-width)));
562
+ padding-inline-start: var(--sidebar-sheet-content-inset, var(--padding));
522
563
  }
523
564
  aside.overlay.expandedHamburger > .content {
524
565
  transform: translateX(0);
@@ -619,7 +660,9 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
619
660
  opacity: 1;
620
661
  pointer-events: all;
621
662
  display: block;
622
- top: var(--padding);
663
+ /* A host can line the button up with something of its own -- Page
664
+ centres it in its header row. */
665
+ top: var(--sidebar-sheet-button-top, var(--padding));
623
666
  left: 0;
624
667
  border-radius: var(--_sidebar-sheet-button-radius);
625
668
  border-top-left-radius: var(--_sidebar-sheet-expand-edge-radius);
@@ -744,17 +787,14 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
744
787
  filter: var(--sidebar-mobile-icon-filter, none);
745
788
  }
746
789
  aside.overlay > button.close {
747
- /* Flush inside the panel's inner edge. Adding --padding here pushed the
748
- button a padding's worth further in, which left it straddling the
749
- edge -- part of it hanging outside the panel -- while the content
750
- inset below still reserved the full button width, so the gap between
751
- the button and the first nav item came out wider than intended.
752
- Either the button pops out of the panel or it sits inside it; this
753
- sits inside it, and the inset is then exactly button + one gutter. */
754
- left: calc(var(--sidebar-width) - var(--_sidebar-expander-width));
790
+ /* Just outside the open panel, fastened to its outer edge like a tab --
791
+ the same tab the open button was, now on the panel instead of the
792
+ page edge. Clamped so a sheet nearly as wide as the screen still
793
+ leaves its close button on screen. */
794
+ left: var(--_sidebar-close-offset);
755
795
  border-radius: var(--_sidebar-sheet-button-radius);
756
- border-top-right-radius: var(--_sidebar-sheet-collapse-edge-radius);
757
- border-bottom-right-radius: var(--_sidebar-sheet-collapse-edge-radius);
796
+ border-top-left-radius: var(--_sidebar-sheet-collapse-edge-radius);
797
+ border-bottom-left-radius: var(--_sidebar-sheet-collapse-edge-radius);
758
798
  }
759
799
  aside.overlay {
760
800
  /* Everything above anchors the button to the left, which is right for a
@@ -774,10 +814,10 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
774
814
  }
775
815
  aside.overlay.right > button.close {
776
816
  left: auto;
777
- right: calc(var(--sidebar-width) - var(--_sidebar-expander-width));
817
+ right: var(--_sidebar-close-offset);
778
818
  border-radius: var(--_sidebar-sheet-button-radius);
779
- border-top-left-radius: var(--_sidebar-sheet-collapse-edge-radius);
780
- border-bottom-left-radius: var(--_sidebar-sheet-collapse-edge-radius);
819
+ border-top-right-radius: var(--_sidebar-sheet-collapse-edge-radius);
820
+ border-bottom-right-radius: var(--_sidebar-sheet-collapse-edge-radius);
781
821
  }
782
822
  aside.overlay {
783
823
  /* `sticky`: the sheet follows the reader down the page instead of
@@ -850,13 +890,13 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
850
890
  transition: margin-left var(--sidebar-transition);
851
891
  }
852
892
  aside.overlay.sticky > button.close {
853
- margin-left: calc(var(--sidebar-width) - var(--_sidebar-expander-width));
893
+ margin-left: var(--_sidebar-close-offset);
854
894
  }
855
895
  aside.overlay.sticky.right > button.expander {
856
896
  margin-left: var(--gap);
857
897
  }
858
898
  aside.overlay.sticky.right > button.close {
859
- margin-left: calc(var(--_sidebar-aside-width) - var(--sidebar-width));
899
+ margin-left: calc(var(--_sidebar-aside-width) - var(--_sidebar-close-offset) - var(--_sidebar-expander-width));
860
900
  }
861
901
  .edge-bar {
862
902
  --link-bg: var(--grab-bar-link-bg, var(--sidebar-link-bg, var(--surface-link-bg, inherit)));
@@ -1018,8 +1058,14 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
1018
1058
  var(--sidebar-icon-height, 1rem),
1019
1059
  var(--icon-size, 32px)
1020
1060
  );
1061
+ /* Where the open sheet's close button sits: just past the panel's outer
1062
+ edge, but never off the screen. */
1063
+ --_sidebar-close-offset: min(
1064
+ var(--sidebar-width),
1065
+ calc(100vw - var(--_sidebar-expander-width))
1066
+ );
1021
1067
  /* The sheet button's corners. Its "edge" corners are the pair on the
1022
- side it attaches to -- the page edge while shut, the panel's inner edge
1068
+ side it attaches to -- the page edge while shut, the panel's outer edge
1023
1069
  while open -- and default to 0, which is what makes it read as a tab
1024
1070
  fastened to that edge. `sheetButton="button"` rounds them like the
1025
1071
  other two, for a sheet that does not sit against an edge. */
@@ -1041,6 +1087,12 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
1041
1087
  position: relative;
1042
1088
  overflow: visible;
1043
1089
  background: transparent;
1090
+ /* The aside is only a transparent strip holding the button and the
1091
+ sheet, and a host may let content run underneath it (Page does, once
1092
+ it has moved the button into its header). The strip itself must not
1093
+ swallow clicks meant for that content; the button and an open sheet
1094
+ turn pointer events back on for themselves. */
1095
+ pointer-events: none;
1044
1096
  width: calc(var(--gap) + max(var(--sidebar-icon-width, 0.65rem), var(--icon-size, 32px)));
1045
1097
  flex: 0 0 auto;
1046
1098
  /* Every child is out of flow, so the aside has no content to hold it
@@ -1072,16 +1124,13 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
1072
1124
  pointer-events: none;
1073
1125
  transition: transform var(--sidebar-transition) ease-in-out, opacity var(--sidebar-transition) ease-in-out;
1074
1126
  padding: var(--padding);
1075
- /* The close button floats over the panel's inner edge, so the panel has
1076
- to keep that strip clear or the first nav item sits underneath it.
1077
-
1078
- Reserved on the inline axis, not the block one: a nav wants the full
1079
- height, and a blank band across the top of it to clear one button is
1080
- a poor trade. The cost is that the whole column is a button's width
1081
- narrower, even well below the button -- override
1082
- --sidebar-sheet-content-inset to 0 and set your own padding if you
1083
- would rather have the width back. */
1084
- padding-inline-end: var(--sidebar-sheet-content-inset, calc(var(--padding) + var(--_sidebar-expander-width)));
1127
+ /* The close button hangs off the panel's outer edge (below), so the
1128
+ panel keeps its whole width. It used to sit inside that edge, which
1129
+ meant reserving a button-wide strip down the full height of the
1130
+ sheet -- a stripe beside every nav item, there only to clear one
1131
+ button at the top. --sidebar-sheet-content-inset brings a reserve
1132
+ back if you want one. */
1133
+ padding-inline-end: var(--sidebar-sheet-content-inset, var(--padding));
1085
1134
  }
1086
1135
  aside.left > .content {
1087
1136
  border-right: var(--border-width) var(--border-style) var(--border-color);
@@ -1097,7 +1146,7 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
1097
1146
  transform: translateX(100%);
1098
1147
  border-left: var(--border-width) var(--border-style) var(--border-color);
1099
1148
  padding-inline-end: var(--padding);
1100
- padding-inline-start: var(--sidebar-sheet-content-inset, calc(var(--padding) + var(--_sidebar-expander-width)));
1149
+ padding-inline-start: var(--sidebar-sheet-content-inset, var(--padding));
1101
1150
  }
1102
1151
  aside.expandedHamburger > .content {
1103
1152
  transform: translateX(0);
@@ -1198,7 +1247,9 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
1198
1247
  opacity: 1;
1199
1248
  pointer-events: all;
1200
1249
  display: block;
1201
- top: var(--padding);
1250
+ /* A host can line the button up with something of its own -- Page
1251
+ centres it in its header row. */
1252
+ top: var(--sidebar-sheet-button-top, var(--padding));
1202
1253
  left: 0;
1203
1254
  border-radius: var(--_sidebar-sheet-button-radius);
1204
1255
  border-top-left-radius: var(--_sidebar-sheet-expand-edge-radius);
@@ -1323,17 +1374,14 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
1323
1374
  filter: var(--sidebar-mobile-icon-filter, none);
1324
1375
  }
1325
1376
  aside > button.close {
1326
- /* Flush inside the panel's inner edge. Adding --padding here pushed the
1327
- button a padding's worth further in, which left it straddling the
1328
- edge -- part of it hanging outside the panel -- while the content
1329
- inset below still reserved the full button width, so the gap between
1330
- the button and the first nav item came out wider than intended.
1331
- Either the button pops out of the panel or it sits inside it; this
1332
- sits inside it, and the inset is then exactly button + one gutter. */
1333
- left: calc(var(--sidebar-width) - var(--_sidebar-expander-width));
1377
+ /* Just outside the open panel, fastened to its outer edge like a tab --
1378
+ the same tab the open button was, now on the panel instead of the
1379
+ page edge. Clamped so a sheet nearly as wide as the screen still
1380
+ leaves its close button on screen. */
1381
+ left: var(--_sidebar-close-offset);
1334
1382
  border-radius: var(--_sidebar-sheet-button-radius);
1335
- border-top-right-radius: var(--_sidebar-sheet-collapse-edge-radius);
1336
- border-bottom-right-radius: var(--_sidebar-sheet-collapse-edge-radius);
1383
+ border-top-left-radius: var(--_sidebar-sheet-collapse-edge-radius);
1384
+ border-bottom-left-radius: var(--_sidebar-sheet-collapse-edge-radius);
1337
1385
  }
1338
1386
  aside {
1339
1387
  /* Everything above anchors the button to the left, which is right for a
@@ -1353,10 +1401,10 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
1353
1401
  }
1354
1402
  aside.right > button.close {
1355
1403
  left: auto;
1356
- right: calc(var(--sidebar-width) - var(--_sidebar-expander-width));
1404
+ right: var(--_sidebar-close-offset);
1357
1405
  border-radius: var(--_sidebar-sheet-button-radius);
1358
- border-top-left-radius: var(--_sidebar-sheet-collapse-edge-radius);
1359
- border-bottom-left-radius: var(--_sidebar-sheet-collapse-edge-radius);
1406
+ border-top-right-radius: var(--_sidebar-sheet-collapse-edge-radius);
1407
+ border-bottom-right-radius: var(--_sidebar-sheet-collapse-edge-radius);
1360
1408
  }
1361
1409
  aside {
1362
1410
  /* `sticky`: the sheet follows the reader down the page instead of
@@ -1429,13 +1477,13 @@ aside .content:hover::-webkit-scrollbar-thumb:hover {
1429
1477
  transition: margin-left var(--sidebar-transition);
1430
1478
  }
1431
1479
  aside.sticky > button.close {
1432
- margin-left: calc(var(--sidebar-width) - var(--_sidebar-expander-width));
1480
+ margin-left: var(--_sidebar-close-offset);
1433
1481
  }
1434
1482
  aside.sticky.right > button.expander {
1435
1483
  margin-left: var(--gap);
1436
1484
  }
1437
1485
  aside.sticky.right > button.close {
1438
- margin-left: calc(var(--_sidebar-aside-width) - var(--sidebar-width));
1486
+ margin-left: calc(var(--_sidebar-aside-width) - var(--_sidebar-close-offset) - var(--_sidebar-expander-width));
1439
1487
  }
1440
1488
  }
1441
1489
  aside.button-sheet-toggle {
@@ -89,6 +89,38 @@ const el = $derived(elementProps(restProps, "sidebar-container", [
89
89
  we deliberately avoid everywhere else.
90
90
 
91
91
  One definition, used by both, so they cannot drift again. */
92
+ /* The "small" size tier: a component whose container is this wide or less
93
+ is on a phone, or in a pane about that narrow, and sizes itself for it.
94
+
95
+ The convention, shared by every component that has a size worth changing
96
+ there (Card started it; Tile, GridLayout, Bar and Container follow):
97
+
98
+ --{component}-{property}-small
99
+
100
+ applies inside a small container. What wins there, in order:
101
+ 1. the `-small` variable, if you set it;
102
+ 2. otherwise the regular variable, if you set it -- a size you chose on
103
+ purpose is not overridden behind your back;
104
+ 3. otherwise the component's own small default, which for most
105
+ properties is simply its regular default.
106
+
107
+ Container queries can't read a custom property, so the breakpoint itself
108
+ is fixed here rather than themeable. 600px matches Card and SplitPane,
109
+ which already switched there. (The Sidebar's rail-to-sheet switch,
110
+ above, is a separate, layout-chrome decision and keeps its own value.) */
111
+ /* Styles for the small tier: the nearest container is narrow, OR the
112
+ viewport is.
113
+
114
+ The container query is the real answer -- a Tile in a 400px pane on a
115
+ desktop is small. But a container query only matches inside an ancestor
116
+ that *is* a container, and a component dropped straight into a page (no
117
+ Page, no Container around it) has none, so on a phone it would never
118
+ find out. The viewport query covers that: a narrow viewport means every
119
+ container in it is narrow too, so the two never disagree in the
120
+ direction that matters.
121
+
122
+ An element never queries itself, so on a component that is its own
123
+ container (Tile, Bar) this asks about the space it was given. */
92
124
  /* Warning: because we define a fallback
93
125
  media query, the media query can override the container
94
126
  if we stack a bunch of these in a row and aren't thoughtful about the order.
@@ -187,6 +187,38 @@ onMount(() => {
187
187
  we deliberately avoid everywhere else.
188
188
 
189
189
  One definition, used by both, so they cannot drift again. */
190
+ /* The "small" size tier: a component whose container is this wide or less
191
+ is on a phone, or in a pane about that narrow, and sizes itself for it.
192
+
193
+ The convention, shared by every component that has a size worth changing
194
+ there (Card started it; Tile, GridLayout, Bar and Container follow):
195
+
196
+ --{component}-{property}-small
197
+
198
+ applies inside a small container. What wins there, in order:
199
+ 1. the `-small` variable, if you set it;
200
+ 2. otherwise the regular variable, if you set it -- a size you chose on
201
+ purpose is not overridden behind your back;
202
+ 3. otherwise the component's own small default, which for most
203
+ properties is simply its regular default.
204
+
205
+ Container queries can't read a custom property, so the breakpoint itself
206
+ is fixed here rather than themeable. 600px matches Card and SplitPane,
207
+ which already switched there. (The Sidebar's rail-to-sheet switch,
208
+ above, is a separate, layout-chrome decision and keeps its own value.) */
209
+ /* Styles for the small tier: the nearest container is narrow, OR the
210
+ viewport is.
211
+
212
+ The container query is the real answer -- a Tile in a 400px pane on a
213
+ desktop is small. But a container query only matches inside an ancestor
214
+ that *is* a container, and a component dropped straight into a page (no
215
+ Page, no Container around it) has none, so on a phone it would never
216
+ find out. The viewport query covers that: a narrow viewport means every
217
+ container in it is narrow too, so the two never disagree in the
218
+ direction that matters.
219
+
220
+ An element never queries itself, so on a component that is its own
221
+ container (Tile, Bar) this asks about the space it was given. */
190
222
  /* Warning: because we define a fallback
191
223
  media query, the media query can override the container
192
224
  if we stack a bunch of these in a row and aren't thoughtful about the order.