contain-css-svelte 1.2.2 → 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.
- package/dist/Card.svelte +12 -0
- package/dist/controls/Button.svelte +12 -0
- package/dist/controls/ButtonLink.svelte +12 -0
- package/dist/controls/Checkbox.svelte +12 -0
- package/dist/controls/CircleButton.svelte +12 -0
- package/dist/controls/Input.svelte +12 -0
- package/dist/controls/RadioButton.svelte +12 -0
- package/dist/controls/Select.svelte +12 -0
- package/dist/controls/Slider.svelte +12 -0
- package/dist/controls/TabItem.svelte +12 -0
- package/dist/controls/Toggle.svelte +12 -0
- package/dist/dropdowns/DropdownMenu.svelte +12 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/layout/Accordion.svelte +12 -0
- package/dist/layout/Bar.svelte +12 -0
- package/dist/layout/ColumnContainer.svelte +12 -0
- package/dist/layout/Columns.svelte +12 -0
- package/dist/layout/Container.svelte +12 -0
- package/dist/layout/DataList.svelte +12 -0
- package/dist/layout/DataListItem.svelte +12 -0
- package/dist/layout/Fieldset.svelte +12 -0
- package/dist/layout/Form.svelte +12 -0
- package/dist/layout/FormItem.svelte +12 -0
- package/dist/layout/GridLayout.svelte +12 -0
- package/dist/layout/Hero.svelte +12 -0
- package/dist/layout/Inline.svelte +12 -0
- package/dist/layout/MenuList.svelte +12 -0
- package/dist/layout/Page.svelte +17 -1
- package/dist/layout/RowContainer.svelte +12 -0
- package/dist/layout/Sidebar.svelte +516 -57
- package/dist/layout/Sidebar.svelte.d.ts +24 -1
- package/dist/layout/SidebarContainer.svelte +172 -0
- package/dist/layout/SidebarContainer.svelte.d.ts +20 -0
- package/dist/layout/SplitPane.svelte +12 -0
- package/dist/layout/Stack.svelte +12 -0
- package/dist/layout/TabBar.svelte +12 -0
- package/dist/layout/Table.svelte +233 -151
- package/dist/layout/Tile.svelte +12 -0
- package/dist/misc/CodeInner.svelte +12 -0
- package/dist/misc/Progress.svelte +12 -0
- package/dist/misc/Tag.svelte +12 -0
- package/dist/overlays/Dialog.svelte +12 -0
- package/dist/overlays/Tooltip.svelte +20 -2
- package/dist/overlays/Tooltip.svelte.d.ts +1 -0
- package/dist/sass/_responsive.scss +15 -0
- package/dist/typography/TextLayout.svelte +12 -0
- package/package.json +1 -1
package/dist/layout/Table.svelte
CHANGED
|
@@ -1,184 +1,235 @@
|
|
|
1
1
|
<script lang="ts">/* No attribute pass-through, deliberately: the sticky variant renders the
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
/*
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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 (!
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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 (
|
|
107
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
style:
|
|
198
|
+
bind:this={headTable}
|
|
199
|
+
style:width={tableWidth ? `${tableWidth}px` : null}
|
|
200
|
+
style:max-width={tableWidth ? `${tableWidth}px` : null}
|
|
149
201
|
>
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
.
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
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 */
|
package/dist/layout/Tile.svelte
CHANGED
|
@@ -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.
|
package/dist/misc/Tag.svelte
CHANGED
|
@@ -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.
|
|
@@ -4,7 +4,7 @@ import { COLOR_VARS, PADDING_VARS, RADIUS_VARS, TYPOGRAPHY_CONTAINER_VARS, } fro
|
|
|
4
4
|
let tooltipDiv = $state();
|
|
5
5
|
let targetDiv = $state();
|
|
6
6
|
let tooltipMeasurementDiv = $state();
|
|
7
|
-
let { tooltipText = "", vertical = "bottom", horizontal = "right", children, tooltip, block = false, class: className, ...restProps } = $props();
|
|
7
|
+
let { tooltipText = "", vertical = "bottom", horizontal = "right", children, tooltip, block = false, tooltipDisabled = false, class: className, ...restProps } = $props();
|
|
8
8
|
/* The wrapper is the element a caller is pointing at -- the tooltip itself
|
|
9
9
|
is a popover the component owns and positions. */
|
|
10
10
|
/* The shorthands this component's own CSS backs, one group per mixin
|
|
@@ -37,6 +37,10 @@ function hidePopover() {
|
|
|
37
37
|
unwatchViewport();
|
|
38
38
|
tooltipDiv?.togglePopover(false);
|
|
39
39
|
}
|
|
40
|
+
$effect(() => {
|
|
41
|
+
if (tooltipDisabled)
|
|
42
|
+
hidePopover();
|
|
43
|
+
});
|
|
40
44
|
/**
|
|
41
45
|
* Find the rect to anchor the tooltip to.
|
|
42
46
|
*
|
|
@@ -203,13 +207,15 @@ onDestroy(() => {
|
|
|
203
207
|
unwatchViewport();
|
|
204
208
|
});
|
|
205
209
|
async function showPopover() {
|
|
210
|
+
if (tooltipDisabled)
|
|
211
|
+
return;
|
|
206
212
|
wantsShow = true;
|
|
207
213
|
if (!hasRendered) {
|
|
208
214
|
hasRendered = true;
|
|
209
215
|
// Wait for the content to exist before measuring it — positioning reads
|
|
210
216
|
// the measurement element's height/width to decide flip direction.
|
|
211
217
|
await tick();
|
|
212
|
-
if (!wantsShow)
|
|
218
|
+
if (!wantsShow || tooltipDisabled)
|
|
213
219
|
return;
|
|
214
220
|
}
|
|
215
221
|
if (!positionTooltip())
|
|
@@ -301,6 +307,18 @@ async function showPopover() {
|
|
|
301
307
|
day this mixin got added to a new component. Callers opt in with an actual
|
|
302
308
|
value. (Container does not use this mixin: it wants --gap/auto as its
|
|
303
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. */
|
|
304
322
|
/* Warning: because we define a fallback
|
|
305
323
|
media query, the media query can override the container
|
|
306
324
|
if we stack a bunch of these in a row and aren't thoughtful about the order.
|
|
@@ -8,6 +8,7 @@ declare const Tooltip: import("svelte").Component<ContainProps<HTMLAttributes<HT
|
|
|
8
8
|
children?: import("svelte").Snippet;
|
|
9
9
|
tooltip?: import("svelte").Snippet;
|
|
10
10
|
block?: boolean;
|
|
11
|
+
tooltipDisabled?: boolean;
|
|
11
12
|
}, StyleProps<readonly ["bg", "fg", "padding", "borderRadius", "fontFamily", "fontSize", "fontWeight", "lineHeight", "letterSpacing", "textAlign", "textTransform", "textDecoration", "fontVariant", "lineWidth"]>>, {}, "">;
|
|
12
13
|
type Tooltip = ReturnType<typeof Tooltip>;
|
|
13
14
|
export default Tooltip;
|
|
@@ -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.
|