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,184 +1,235 @@
1
1
  <script lang="ts">/* No attribute pass-through, deliberately: the sticky variant renders the
2
- visible tables AND a visually-hidden clone of them used to measure column
3
- widths. Spreading a caller's `id` would emit it twice, which is invalid.
4
- Set attributes on the <table> markup you pass in as `thead`/`tbody`. */
2
+ header twice -- once as the visible sticky <table>, once collapsed inside
3
+ the body table so the browser sizes the columns from it. Spreading a
4
+ caller's `id` would emit it twice, which is invalid. Set attributes on the
5
+ <table> markup you pass in as `thead`/`tbody`. */
5
6
  let { sticky = false, column_widths = null, thead, tbody, children, } = $props();
6
7
  // svelte-ignore state_referenced_locally
7
8
  let columns = $state(column_widths || []);
8
- /* Code for syncing column widths for scrolling table solution */
9
- let headClone = $state(null);
10
- let bodyClone = $state(null);
9
+ /* Column-width syncing for the sticky-header layout.
10
+
11
+ The sticky header and the scrolling body have to look like one table, but
12
+ a single <table> cannot do it: `position: sticky` on a <thead> loses its
13
+ borders under `border-collapse: collapse` (verified in Chromium, Firefox
14
+ and WebKit), and this library's whole border system is built on collapsed
15
+ borders. So the header is its own <table>, sized from a measured colgroup.
16
+
17
+ The trick that keeps that honest: the body table renders the REAL <thead>
18
+ too, at `visibility: collapse`. That keeps the header's full contribution
19
+ to column widths while costing zero vertical space -- so the browser sizes
20
+ the columns from header and body together, in one ordinary layout pass.
21
+ There is nothing to blend, nothing measured against the wrong containing
22
+ block, and no second copy of the body.
23
+
24
+ `visibility: collapse` also drops that copy out of the accessibility tree
25
+ and out of the tab order in all three engines, so its duplicated controls
26
+ are inert without needing the `inert` attribute. Note the consequence: the
27
+ body table has no exposed column headers -- the visible sticky header is
28
+ the accessible one. That matches the previous behavior, but it is a real
29
+ gap, and only a genuine single-table layout would close it. */
30
+ let bodyTable = $state(null);
31
+ let headTable = $state(null);
32
+ let columnCount = $state(0);
33
+ /* See trimHeadOverhang(): a fixed-layout table's collapsed outer borders
34
+ make it render a little wider than the width we ask for. */
35
+ let overhang = $state(0);
11
36
  let resizeObserver = $state(null);
12
37
  let tableWidth = $state(null);
13
38
  import { onDestroy } from "svelte";
14
39
  onDestroy(() => {
15
- if (resizeObserver) {
16
- if (headClone)
17
- resizeObserver.unobserve(headClone);
18
- if (bodyClone)
19
- resizeObserver.unobserve(bodyClone);
20
- resizeObserver.disconnect();
21
- resizeObserver = null;
22
- }
40
+ resizeObserver?.disconnect();
41
+ resizeObserver = null;
23
42
  });
43
+ /* How many columns the table has. Pure colspan arithmetic over every row --
44
+ no widths involved, so this is exact even when every row carries a colspan.
45
+ The widest row wins, which also covers rows shortened by a rowspan above. */
46
+ function countColumns(table) {
47
+ let max = 0;
48
+ for (const row of Array.from(table.rows)) {
49
+ let n = 0;
50
+ for (const cell of Array.from(row.cells))
51
+ n += cell.colSpan || 1;
52
+ if (n > max)
53
+ max = n;
54
+ }
55
+ return max;
56
+ }
57
+ /* Read the column widths off the ruler row: one empty, borderless, zero-
58
+ height cell per column, sitting in the real table. Because it is built to
59
+ have exactly one cell per column, there is no "find a row without a
60
+ colspan" guesswork and no dividing a spanned cell evenly and hoping. */
61
+ function measureRuler(table) {
62
+ const row = table.querySelector(".column-ruler > tr");
63
+ if (!row)
64
+ return [];
65
+ return Array.from(row.children).map((cell) => cell.getBoundingClientRect().width);
66
+ }
67
+ /* The table's own width is the truth; the per-cell widths only give the
68
+ proportions. They don't add up to it, because `border-collapse: collapse`
69
+ makes adjacent cells share a border that each of them reports in full, so
70
+ the sum overshoots by about one border per interior boundary. Left alone,
71
+ that makes the header table wider than the body it sits on. The overcount
72
+ is a constant per boundary rather than a percentage, so spread it evenly. */
73
+ function fitToTableWidth(widths, width) {
74
+ if (!widths.length || !width)
75
+ return widths;
76
+ const total = widths.reduce((sum, w) => sum + w, 0);
77
+ const delta = total - width;
78
+ if (Math.abs(delta) < 0.5)
79
+ return widths;
80
+ const perColumn = delta / widths.length;
81
+ return widths.map((w) => Math.max(0, w - perColumn));
82
+ }
24
83
  function syncColumnWidths() {
25
- if (!headClone || !bodyClone)
84
+ if (!bodyTable)
85
+ return;
86
+ /* First pass sizes the ruler. Rendering it makes this effect run again,
87
+ and the second pass does the measuring. */
88
+ const count = countColumns(bodyTable);
89
+ if (count !== columnCount) {
90
+ columnCount = count;
91
+ corrections = 0;
26
92
  return;
27
- const getFlattenedCellWidths = (row) => {
28
- if (!row)
29
- return [];
30
- const cells = Array.from(row.children);
31
- const colWidths = [];
32
- let colIndex = 0;
33
- for (const cell of cells) {
34
- const colspan = parseInt(cell.getAttribute("colspan") || "1", 10);
35
- const width = cell.offsetWidth / colspan;
36
- for (let j = 0; j < colspan; j++) {
37
- colWidths[colIndex++] = width;
38
- }
39
- }
40
- return colWidths;
41
- };
42
- const getTotalCols = (row) => {
43
- if (!row)
44
- return 0;
45
- return Array.from(row.children).reduce((acc, cell) => acc + parseInt(cell.getAttribute("colspan") || "1", 10), 0);
46
- };
47
- const hasNoColspan = (row) => !Array.from(row.children).some((cell) => cell.hasAttribute("colspan"));
48
- const findBestRow = (table) => {
49
- // Fast path: first row covers the vast majority of real tables
50
- const firstRow = table.querySelector("tbody tr, tr");
51
- if (firstRow && hasNoColspan(firstRow))
52
- return firstRow;
53
- // Slow path: single pass — return on first no-colspan row found,
54
- // while simultaneously tracking the least-colspan row as a fallback.
55
- let best = null;
56
- let bestSpanCount = Infinity;
57
- for (const row of table.querySelectorAll("tr")) {
58
- if (hasNoColspan(row))
59
- return row;
60
- const spanCount = getTotalCols(row);
61
- if (spanCount < bestSpanCount) {
62
- bestSpanCount = spanCount;
63
- best = row;
64
- }
65
- }
66
- return best;
67
- };
68
- // Step 1: pick best candidate rows
69
- let headRow = findBestRow(headClone);
70
- let bodyRow = findBestRow(bodyClone);
71
- // Step 2: if their total col counts don't match, try to find a pair that does
72
- if (headRow && bodyRow && getTotalCols(headRow) !== getTotalCols(bodyRow)) {
73
- const headRows = Array.from(headClone.querySelectorAll("tr"));
74
- const bodyRows = Array.from(bodyClone.querySelectorAll("tr"));
75
- outer: for (const hr of headRows) {
76
- const hCols = getTotalCols(hr);
77
- for (const br of bodyRows) {
78
- if (hCols === getTotalCols(br)) {
79
- headRow = hr;
80
- bodyRow = br;
81
- break outer;
82
- }
83
- }
84
- }
85
- }
86
- // Step 3: flatten widths and compute max
87
- const headWidths = getFlattenedCellWidths(headRow);
88
- const bodyWidths = getFlattenedCellWidths(bodyRow);
89
- const numCols = Math.max(headWidths.length, bodyWidths.length);
90
- const maxColWidths = [];
91
- for (let i = 0; i < numCols; i++) {
92
- maxColWidths[i] = Math.max(headWidths[i] || 0, bodyWidths[i] || 0);
93
93
  }
94
- columns = maxColWidths;
95
- fitTableToColGroup(columns);
94
+ const widths = measureRuler(bodyTable);
95
+ if (!widths.length)
96
+ return;
97
+ const width = bodyTable.getBoundingClientRect().width;
98
+ columns = fitToTableWidth(widths, width - overhang);
99
+ tableWidth = width - overhang;
96
100
  }
97
- function fitTableToColGroup(columns) {
98
- let totalWidth = 0;
99
- for (let c of columns) {
100
- totalWidth += c;
101
- }
102
- tableWidth = totalWidth;
101
+ /* The header table is `table-layout: fixed` with `box-sizing: border-box`,
102
+ and under `border-collapse: collapse` its outer borders are added on top
103
+ of the column widths -- so it renders a pixel or two wider than the width
104
+ we set, and its right edge overhangs the body. The amount isn't derivable
105
+ from the computed border widths (it differs per table), so measure it once
106
+ and fold it back in. One correction converges; the guard stops it from
107
+ hunting if a layout ever refuses to settle. */
108
+ let corrections = 0;
109
+ function trimHeadOverhang() {
110
+ if (!headTable || !bodyTable || !columns.length)
111
+ return;
112
+ const delta = headTable.getBoundingClientRect().width -
113
+ bodyTable.getBoundingClientRect().width;
114
+ if (Math.abs(delta) < 0.5 || corrections > 3)
115
+ return;
116
+ corrections += 1;
117
+ overhang += delta;
118
+ syncColumnWidths();
103
119
  }
104
- let hasInitialized = $state(false);
105
120
  $effect(() => {
106
- if (thead && sticky && !hasInitialized) {
107
- setupWidths();
108
- }
121
+ if (column_widths || !sticky || !thead || !bodyTable)
122
+ return;
123
+ // Read columnCount so this re-runs once the ruler row has rendered.
124
+ columnCount;
125
+ syncColumnWidths();
126
+ observeColumns();
109
127
  });
110
- function setupWidths() {
111
- if (column_widths)
128
+ $effect(() => {
129
+ columns;
130
+ trimHeadOverhang();
131
+ });
132
+ /* Keep columns in sync as the table changes underneath us.
133
+
134
+ Watching the table itself is not enough, and this is the subtle one:
135
+ reordering columns leaves the table exactly the same width, so a
136
+ ResizeObserver on the table never fires -- yet every column width has
137
+ moved and the header's colgroup is now stale. (Consumers have had to
138
+ paper over this by remounting the component on a column-set key.)
139
+
140
+ So observe the ruler cells: one per column, they ARE the quantity being
141
+ copied into the colgroup. Reorder them and they swap widths, which fires;
142
+ resize the container and they all change, which fires. Swap two columns of
143
+ identical width and nothing fires -- correctly, because the colgroup would
144
+ be unchanged. ResizeObserver reports them despite `visibility: collapse`,
145
+ verified in Chromium, Firefox and WebKit.
146
+
147
+ Debounced via rAF so a burst of Svelte updates costs one sync per frame. */
148
+ let observedCount = -1;
149
+ let observedRuler = null;
150
+ function observeColumns() {
151
+ if (!bodyTable)
112
152
  return;
113
- // If no column widths provided, reset columns to empty
114
- columns = [];
115
- if (thead && sticky && headClone && bodyClone) {
116
- syncColumnWidths();
117
- // Set up ResizeObserver to keep columns in sync when content or size changes.
118
- // Debounce via rAF so rapid Svelte updates only trigger one sync per frame.
119
- if (!resizeObserver &&
120
- typeof window !== "undefined" &&
121
- window.ResizeObserver) {
122
- let rafPending = false;
123
- resizeObserver = new ResizeObserver(() => {
124
- if (!rafPending) {
125
- rafPending = true;
126
- requestAnimationFrame(() => {
127
- rafPending = false;
128
- syncColumnWidths();
129
- });
130
- }
153
+ if (typeof window === "undefined" || !window.ResizeObserver)
154
+ return;
155
+ const cells = Array.from(bodyTable.querySelectorAll(".column-ruler > tr > td"));
156
+ // The ruler is rendered a tick after the column count is known, so the
157
+ // first call through here finds nothing. Bail without recording anything,
158
+ // and the effect that set the count will bring us straight back.
159
+ if (!cells.length)
160
+ return;
161
+ // Re-attach only when the ruler actually changed, or the observation
162
+ // callback that firing `observe()` produces would re-enter and loop.
163
+ if (observedRuler === cells[0] && observedCount === cells.length)
164
+ return;
165
+ observedRuler = cells[0];
166
+ observedCount = cells.length;
167
+ if (!resizeObserver) {
168
+ let rafPending = false;
169
+ resizeObserver = new ResizeObserver(() => {
170
+ if (rafPending)
171
+ return;
172
+ rafPending = true;
173
+ requestAnimationFrame(() => {
174
+ rafPending = false;
175
+ /* A fresh layout, so the overhang may be re-measured. Correcting
176
+ only ever touches the header table, never the observed ruler, so
177
+ this cannot re-enter. */
178
+ corrections = 0;
179
+ syncColumnWidths();
131
180
  });
132
- if (headClone)
133
- resizeObserver.observe(headClone);
134
- if (bodyClone)
135
- resizeObserver.observe(bodyClone);
136
- }
137
- hasInitialized = true;
181
+ });
138
182
  }
183
+ resizeObserver.disconnect();
184
+ resizeObserver.observe(bodyTable);
185
+ for (const cell of cells)
186
+ resizeObserver.observe(cell);
139
187
  }
140
188
  </script>
141
189
 
142
190
  {#if sticky}
143
191
  {#if thead}
144
192
  <div class="scrolling-table">
193
+ <!-- The visible, interactive header. Sticky as a whole <table>, because
194
+ a sticky <thead> loses its borders under border-collapse: collapse.
195
+ Its colgroup is copied from the real table's measured columns. -->
145
196
  <table
146
197
  class="fixed-table-head"
147
- style:width="{tableWidth}px"
148
- style:max-width="{tableWidth}px"
198
+ bind:this={headTable}
199
+ style:width={tableWidth ? `${tableWidth}px` : null}
200
+ style:max-width={tableWidth ? `${tableWidth}px` : null}
149
201
  >
150
- <colgroup>
151
- {#each columns as width}
152
- <col style="width: {width}px" />
153
- {/each}
154
- </colgroup>
202
+ {#if columns.length}
203
+ <colgroup>
204
+ {#each columns as width}
205
+ <col style="width: {width}px" />
206
+ {/each}
207
+ </colgroup>
208
+ {/if}
155
209
  {@render thead?.()}
156
210
  </table>
157
- <table
158
- class="scrolling-table-body"
159
- style:width="{tableWidth}px"
160
- style:max-width="{tableWidth}px"
161
- >
162
- <colgroup>
163
- {#each columns as width}
164
- <col style="width: {width}px" />
165
- {/each}
166
- </colgroup>
167
- {@render tbody?.()}
168
- <!-- Table body content -->
169
- {@render children?.()}
170
- <!-- default slot for additional content -->
171
- </table>
172
- </div>
173
- <div class="visually-hidden">
174
- <table class="fixed-table-head" bind:this={headClone}>
211
+
212
+ <!-- The real table. It carries the header as well as the body, so the
213
+ browser sizes the columns from both at once; the header copy is
214
+ collapsed in CSS, keeping its width contribution at zero height. -->
215
+ <table class="scrolling-table-body" bind:this={bodyTable}>
175
216
  {@render thead?.()}
176
- </table>
177
- <table class="scrolling-table-body" bind:this={bodyClone}>
178
217
  {@render tbody?.()}
179
218
  <!-- Table body content -->
180
219
  {@render children?.()}
181
220
  <!-- default slot for additional content -->
221
+ {#if columnCount}
222
+ <!-- The ruler: one cell per column, so column widths can be read
223
+ directly instead of guessed at from whichever row looks safe.
224
+ Collapsed, so it costs no height and contributes no width. -->
225
+ <tbody class="column-ruler" aria-hidden="true">
226
+ <tr>
227
+ {#each { length: columnCount } as _}
228
+ <td></td>
229
+ {/each}
230
+ </tr>
231
+ </tbody>
232
+ {/if}
182
233
  </table>
183
234
  </div>
184
235
  {:else}
@@ -209,6 +260,18 @@ function setupWidths() {
209
260
  day this mixin got added to a new component. Callers opt in with an actual
210
261
  value. (Container does not use this mixin: it wants --gap/auto as its
211
262
  defaults, which do not fit the "0 unless asked" contract here.) */
263
+ /* The width at or below which a Sidebar swaps its grab-bar rail for the sheet
264
+ affordance -- a floating button that opens a panel over the content.
265
+
266
+ Page has to agree with Sidebar about this, because Page is what floats the
267
+ aside out of flow and reserves the strip of padding the sheet button sits
268
+ in. They used to disagree: Sidebar switched at 512px and Page at 600px,
269
+ which left a band between 513 and 600 where Page had already lifted the
270
+ aside out of the layout while the Sidebar inside it was still showing a
271
+ rail -- a grab bar floating on top of the content, which is the affordance
272
+ we deliberately avoid everywhere else.
273
+
274
+ One definition, used by both, so they cannot drift again. */
212
275
  /* Warning: because we define a fallback
213
276
  media query, the media query can override the container
214
277
  if we stack a bunch of these in a row and aren't thoughtful about the order.
@@ -1108,11 +1171,30 @@ table :global(tr > th:first-child:has(~ td)) {
1108
1171
  border-top: none;
1109
1172
  }
1110
1173
 
1111
- .visually-hidden {
1112
- visibility: hidden;
1113
- /* opacity: 0.5; */
1114
- pointer-events: none;
1115
- position: fixed;
1174
+ /* The body table's own copy of the header. `visibility: collapse` is the
1175
+ one primitive that keeps a row group's contribution to column widths while
1176
+ reclaiming all of its vertical space -- so the browser solves the columns
1177
+ from header and body in a single pass, and the visible sticky header just
1178
+ copies the answer. Verified in Chromium, Firefox and WebKit, under
1179
+ `border-collapse: collapse` as well as `separate`.
1180
+
1181
+ `visibility: hidden` will NOT do: it keeps the header's full height and
1182
+ you get a blank gap. `display: none` is worse -- no layout, nothing to
1183
+ measure, and the columns size from the body alone. */
1184
+ .scrolling-table-body > :global(thead) {
1185
+ visibility: collapse;
1186
+ }
1187
+
1188
+ /* One cell per column, so widths can be read off directly. Collapsed for
1189
+ zero height, and stripped of padding and borders so it can never be the
1190
+ thing that decides a column's minimum width. */
1191
+ .column-ruler {
1192
+ visibility: collapse;
1193
+ }
1194
+
1195
+ .column-ruler > tr > td {
1196
+ padding: 0;
1197
+ border: 0;
1116
1198
  }
1117
1199
 
1118
1200
  /* Interactive affordances: target rows/cells with tabindex for keyboard accessibility */
@@ -79,6 +79,18 @@ const className = $derived(props.class);
79
79
  day this mixin got added to a new component. Callers opt in with an actual
80
80
  value. (Container does not use this mixin: it wants --gap/auto as its
81
81
  defaults, which do not fit the "0 unless asked" contract here.) */
82
+ /* The width at or below which a Sidebar swaps its grab-bar rail for the sheet
83
+ affordance -- a floating button that opens a panel over the content.
84
+
85
+ Page has to agree with Sidebar about this, because Page is what floats the
86
+ aside out of flow and reserves the strip of padding the sheet button sits
87
+ in. They used to disagree: Sidebar switched at 512px and Page at 600px,
88
+ which left a band between 513 and 600 where Page had already lifted the
89
+ aside out of the layout while the Sidebar inside it was still showing a
90
+ rail -- a grab bar floating on top of the content, which is the affordance
91
+ we deliberately avoid everywhere else.
92
+
93
+ One definition, used by both, so they cannot drift again. */
82
94
  /* Warning: because we define a fallback
83
95
  media query, the media query can override the container
84
96
  if we stack a bunch of these in a row and aren't thoughtful about the order.
@@ -24,6 +24,18 @@ let highlightedCode = $derived.by(() => {
24
24
  day this mixin got added to a new component. Callers opt in with an actual
25
25
  value. (Container does not use this mixin: it wants --gap/auto as its
26
26
  defaults, which do not fit the "0 unless asked" contract here.) */
27
+ /* The width at or below which a Sidebar swaps its grab-bar rail for the sheet
28
+ affordance -- a floating button that opens a panel over the content.
29
+
30
+ Page has to agree with Sidebar about this, because Page is what floats the
31
+ aside out of flow and reserves the strip of padding the sheet button sits
32
+ in. They used to disagree: Sidebar switched at 512px and Page at 600px,
33
+ which left a band between 513 and 600 where Page had already lifted the
34
+ aside out of the layout while the Sidebar inside it was still showing a
35
+ rail -- a grab bar floating on top of the content, which is the affordance
36
+ we deliberately avoid everywhere else.
37
+
38
+ One definition, used by both, so they cannot drift again. */
27
39
  /* Warning: because we define a fallback
28
40
  media query, the media query can override the container
29
41
  if we stack a bunch of these in a row and aren't thoughtful about the order.
@@ -71,6 +71,18 @@ let halfFull = $derived(fillFraction > 0.5);
71
71
  day this mixin got added to a new component. Callers opt in with an actual
72
72
  value. (Container does not use this mixin: it wants --gap/auto as its
73
73
  defaults, which do not fit the "0 unless asked" contract here.) */
74
+ /* The width at or below which a Sidebar swaps its grab-bar rail for the sheet
75
+ affordance -- a floating button that opens a panel over the content.
76
+
77
+ Page has to agree with Sidebar about this, because Page is what floats the
78
+ aside out of flow and reserves the strip of padding the sheet button sits
79
+ in. They used to disagree: Sidebar switched at 512px and Page at 600px,
80
+ which left a band between 513 and 600 where Page had already lifted the
81
+ aside out of the layout while the Sidebar inside it was still showing a
82
+ rail -- a grab bar floating on top of the content, which is the affordance
83
+ we deliberately avoid everywhere else.
84
+
85
+ One definition, used by both, so they cannot drift again. */
74
86
  /* Warning: because we define a fallback
75
87
  media query, the media query can override the container
76
88
  if we stack a bunch of these in a row and aren't thoughtful about the order.
@@ -38,6 +38,18 @@ const el = $derived(elementProps(restProps, "tag", TAG_VARS));
38
38
  day this mixin got added to a new component. Callers opt in with an actual
39
39
  value. (Container does not use this mixin: it wants --gap/auto as its
40
40
  defaults, which do not fit the "0 unless asked" contract here.) */
41
+ /* The width at or below which a Sidebar swaps its grab-bar rail for the sheet
42
+ affordance -- a floating button that opens a panel over the content.
43
+
44
+ Page has to agree with Sidebar about this, because Page is what floats the
45
+ aside out of flow and reserves the strip of padding the sheet button sits
46
+ in. They used to disagree: Sidebar switched at 512px and Page at 600px,
47
+ which left a band between 513 and 600 where Page had already lifted the
48
+ aside out of the layout while the Sidebar inside it was still showing a
49
+ rail -- a grab bar floating on top of the content, which is the affordance
50
+ we deliberately avoid everywhere else.
51
+
52
+ One definition, used by both, so they cannot drift again. */
41
53
  /* Warning: because we define a fallback
42
54
  media query, the media query can override the container
43
55
  if we stack a bunch of these in a row and aren't thoughtful about the order.
@@ -85,6 +85,18 @@ let ref;
85
85
  day this mixin got added to a new component. Callers opt in with an actual
86
86
  value. (Container does not use this mixin: it wants --gap/auto as its
87
87
  defaults, which do not fit the "0 unless asked" contract here.) */
88
+ /* The width at or below which a Sidebar swaps its grab-bar rail for the sheet
89
+ affordance -- a floating button that opens a panel over the content.
90
+
91
+ Page has to agree with Sidebar about this, because Page is what floats the
92
+ aside out of flow and reserves the strip of padding the sheet button sits
93
+ in. They used to disagree: Sidebar switched at 512px and Page at 600px,
94
+ which left a band between 513 and 600 where Page had already lifted the
95
+ aside out of the layout while the Sidebar inside it was still showing a
96
+ rail -- a grab bar floating on top of the content, which is the affordance
97
+ we deliberately avoid everywhere else.
98
+
99
+ One definition, used by both, so they cannot drift again. */
88
100
  /* Warning: because we define a fallback
89
101
  media query, the media query can override the container
90
102
  if we stack a bunch of these in a row and aren't thoughtful about the order.
@@ -307,6 +307,18 @@ async function showPopover() {
307
307
  day this mixin got added to a new component. Callers opt in with an actual
308
308
  value. (Container does not use this mixin: it wants --gap/auto as its
309
309
  defaults, which do not fit the "0 unless asked" contract here.) */
310
+ /* The width at or below which a Sidebar swaps its grab-bar rail for the sheet
311
+ affordance -- a floating button that opens a panel over the content.
312
+
313
+ Page has to agree with Sidebar about this, because Page is what floats the
314
+ aside out of flow and reserves the strip of padding the sheet button sits
315
+ in. They used to disagree: Sidebar switched at 512px and Page at 600px,
316
+ which left a band between 513 and 600 where Page had already lifted the
317
+ aside out of the layout while the Sidebar inside it was still showing a
318
+ rail -- a grab bar floating on top of the content, which is the affordance
319
+ we deliberately avoid everywhere else.
320
+
321
+ One definition, used by both, so they cannot drift again. */
310
322
  /* Warning: because we define a fallback
311
323
  media query, the media query can override the container
312
324
  if we stack a bunch of these in a row and aren't thoughtful about the order.
@@ -1,3 +1,18 @@
1
+ /* The width at or below which a Sidebar swaps its grab-bar rail for the sheet
2
+ affordance -- a floating button that opens a panel over the content.
3
+
4
+ Page has to agree with Sidebar about this, because Page is what floats the
5
+ aside out of flow and reserves the strip of padding the sheet button sits
6
+ in. They used to disagree: Sidebar switched at 512px and Page at 600px,
7
+ which left a band between 513 and 600 where Page had already lifted the
8
+ aside out of the layout while the Sidebar inside it was still showing a
9
+ rail -- a grab bar floating on top of the content, which is the affordance
10
+ we deliberately avoid everywhere else.
11
+
12
+ One definition, used by both, so they cannot drift again. */
13
+ $sidebar-compact-max: 512px;
14
+ $sidebar-wide-min: 513px;
15
+
1
16
 
2
17
  /* Warning: because we define a fallback
3
18
  media query, the media query can override the container
@@ -15,6 +15,18 @@ const el = $derived(elementProps(restProps, "text-layout", ["marginBlock", "marg
15
15
  day this mixin got added to a new component. Callers opt in with an actual
16
16
  value. (Container does not use this mixin: it wants --gap/auto as its
17
17
  defaults, which do not fit the "0 unless asked" contract here.) */
18
+ /* The width at or below which a Sidebar swaps its grab-bar rail for the sheet
19
+ affordance -- a floating button that opens a panel over the content.
20
+
21
+ Page has to agree with Sidebar about this, because Page is what floats the
22
+ aside out of flow and reserves the strip of padding the sheet button sits
23
+ in. They used to disagree: Sidebar switched at 512px and Page at 600px,
24
+ which left a band between 513 and 600 where Page had already lifted the
25
+ aside out of the layout while the Sidebar inside it was still showing a
26
+ rail -- a grab bar floating on top of the content, which is the affordance
27
+ we deliberately avoid everywhere else.
28
+
29
+ One definition, used by both, so they cannot drift again. */
18
30
  /* Warning: because we define a fallback
19
31
  media query, the media query can override the container
20
32
  if we stack a bunch of these in a row and aren't thoughtful about the order.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contain-css-svelte",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "license": "MIT",
5
5
  "scripts": {
6
6
  "capture:review": "node scripts/capture-review.mjs",