contain-css-svelte 1.4.1 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/Card.svelte +25 -0
  2. package/dist/controls/Button.svelte +43 -18
  3. package/dist/controls/ButtonLink.svelte +31 -92
  4. package/dist/controls/Checkbox.svelte +34 -4
  5. package/dist/controls/CircleButton.svelte +36 -95
  6. package/dist/controls/Input.svelte +25 -0
  7. package/dist/controls/RadioButton.svelte +27 -2
  8. package/dist/controls/Select.svelte +25 -0
  9. package/dist/controls/Slider.svelte +25 -0
  10. package/dist/controls/TabItem.svelte +25 -0
  11. package/dist/controls/Toggle.svelte +27 -2
  12. package/dist/dropdowns/DropdownMenu.svelte +60 -6
  13. package/dist/layout/Accordion.svelte +28 -3
  14. package/dist/layout/Bar.svelte +25 -0
  15. package/dist/layout/ColumnContainer.svelte +25 -0
  16. package/dist/layout/Columns.svelte +25 -0
  17. package/dist/layout/Container.svelte +25 -0
  18. package/dist/layout/DataList.svelte +25 -0
  19. package/dist/layout/DataListItem.svelte +28 -3
  20. package/dist/layout/Fieldset.svelte +25 -0
  21. package/dist/layout/Form.svelte +25 -0
  22. package/dist/layout/FormItem.svelte +25 -0
  23. package/dist/layout/GridLayout.svelte +25 -0
  24. package/dist/layout/Hero.svelte +35 -0
  25. package/dist/layout/Inline.svelte +25 -0
  26. package/dist/layout/MenuList.svelte +36 -11
  27. package/dist/layout/Page.svelte +25 -0
  28. package/dist/layout/RowContainer.svelte +25 -0
  29. package/dist/layout/Sidebar.svelte +31 -6
  30. package/dist/layout/SidebarContainer.svelte +25 -0
  31. package/dist/layout/SplitPane.svelte +25 -0
  32. package/dist/layout/Stack.svelte +25 -0
  33. package/dist/layout/TabBar.svelte +25 -0
  34. package/dist/layout/Table.svelte +75 -10
  35. package/dist/layout/Tile.svelte +28 -3
  36. package/dist/misc/CodeInner.svelte +49 -22
  37. package/dist/misc/Progress.svelte +25 -0
  38. package/dist/misc/Tag.svelte +25 -0
  39. package/dist/overlays/Dialog.svelte +58 -0
  40. package/dist/overlays/Tooltip.svelte +57 -1
  41. package/dist/sass/_affordances.scss +10 -4
  42. package/dist/sass/_typography.scss +53 -0
  43. package/dist/typography/TextLayout.svelte +25 -0
  44. package/dist/vars/affordances.css +9 -4
  45. package/dist/vars/code.css +12 -0
  46. package/dist/vars/colors.css +39 -18
  47. package/dist/vars/defaults.css +60 -22
  48. package/dist/vars/themes/dark.css +3 -3
  49. package/dist/vars/themes/light.css +34 -31
  50. package/package.json +1 -1
@@ -1,4 +1,6 @@
1
- <script lang="ts">/* No attribute pass-through, deliberately: the sticky variant renders the
1
+ <script lang="ts">import { BROWSER, DEV } from "esm-env";
2
+ import { onDestroy } from "svelte";
3
+ /* No attribute pass-through, deliberately: the sticky variant renders the
2
4
  header twice -- once as the visible sticky <table>, once collapsed inside
3
5
  the body table so the browser sizes the columns from it. Spreading a
4
6
  caller's `id` would emit it twice, which is invalid. Set attributes on the
@@ -36,7 +38,6 @@ let columnCount = $state(0);
36
38
  let overhang = $state(0);
37
39
  let resizeObserver = $state(null);
38
40
  let tableWidth = $state(null);
39
- import { onDestroy } from "svelte";
40
41
  onDestroy(() => {
41
42
  resizeObserver?.disconnect();
42
43
  resizeObserver = null;
@@ -139,25 +140,60 @@ $effect(() => {
139
140
  accessibility tree, with no effect on layout.
140
141
 
141
142
  The thead is the caller's markup, rendered from their snippet, so it
142
- cannot carry the attributes in the template. Watch the table's direct
143
- children instead, in case the caller's snippet swaps its <thead> out. */
143
+ cannot carry the attributes in the template. Watch both tables' direct
144
+ children in case a snippet swaps its group out.
145
+
146
+ Older consumers sometimes supplied bare <tr>s in these named snippets.
147
+ Reparenting Svelte-owned nodes into a new <thead>/<tbody> is unsafe, but
148
+ sticky mode can still degrade safely: the visible table tells us how many
149
+ direct rows belong to the header, so mark the same leading rows in the
150
+ body table as its collapsed measuring copy. Bare tbody rows remain where
151
+ Svelte put them and visible. Warn in development so callers can restore
152
+ the semantic row groups without shipping a doubled header meanwhile. */
153
+ let warnedBareThead = false;
154
+ let warnedBareTbody = false;
144
155
  $effect(() => {
145
- if (!bodyTable)
156
+ if (!sticky || !headTable || !bodyTable)
146
157
  return;
147
- const table = bodyTable;
158
+ const directRows = (table) => Array.from(table.children).filter((child) => child.tagName === "TR");
148
159
  const mark = () => {
149
- for (const head of table.querySelectorAll(":scope > thead")) {
160
+ for (const head of bodyTable.querySelectorAll(":scope > thead")) {
150
161
  if (!head.hasAttribute("inert"))
151
162
  head.setAttribute("inert", "");
152
163
  if (head.getAttribute("aria-hidden") !== "true")
153
164
  head.setAttribute("aria-hidden", "true");
154
165
  }
166
+ const visibleBareHeadRows = directRows(headTable);
167
+ const bodyBareRows = directRows(bodyTable);
168
+ for (const row of bodyBareRows) {
169
+ row.removeAttribute("data-contain-implicit-table-head");
170
+ row.removeAttribute("inert");
171
+ row.removeAttribute("aria-hidden");
172
+ }
173
+ for (const row of bodyBareRows.slice(0, visibleBareHeadRows.length)) {
174
+ row.setAttribute("data-contain-implicit-table-head", "");
175
+ row.setAttribute("inert", "");
176
+ row.setAttribute("aria-hidden", "true");
177
+ }
178
+ if (BROWSER && DEV && visibleBareHeadRows.length && !warnedBareThead) {
179
+ warnedBareThead = true;
180
+ console.warn("<Table sticky>: the thead snippet must render a <thead> containing its rows. Bare <tr> children are tolerated for compatibility, but are not semantic table markup.");
181
+ }
182
+ if (BROWSER &&
183
+ DEV &&
184
+ tbody &&
185
+ bodyBareRows.length > visibleBareHeadRows.length &&
186
+ !warnedBareTbody) {
187
+ warnedBareTbody = true;
188
+ console.warn("<Table sticky>: the tbody snippet must render a <tbody> containing its rows. Bare <tr> children are tolerated for compatibility, but are not semantic table markup.");
189
+ }
155
190
  };
156
191
  mark();
157
192
  if (typeof MutationObserver === "undefined")
158
193
  return;
159
194
  const observer = new MutationObserver(mark);
160
- observer.observe(table, { childList: true });
195
+ observer.observe(headTable, { childList: true });
196
+ observer.observe(bodyTable, { childList: true });
161
197
  return () => observer.disconnect();
162
198
  });
163
199
  /* Keep columns in sync as the table changes underneath us.
@@ -309,6 +345,31 @@ function observeColumns() {
309
345
  text, no top margin. Structural matching (:first-of-type, `h1 + p`) still
310
346
  has to happen where the real element is, in the global consumer rule; only
311
347
  the VALUES move to context vars here. */
348
+ /* Text reset for an overlay's root element: a tooltip, popover menu or dialog
349
+ that sits in the DOM wherever its trigger was placed.
350
+
351
+ The top layer changes how an element paints and where it is positioned, not
352
+ what it inherits. A popover declared inside a `white-space: nowrap` table
353
+ cell is nowrap too, and one inside a bold, uppercase, centred header is
354
+ bold, uppercase and centred -- styles that belong to the trigger's context,
355
+ not to the overlay. typography-props() doesn't stop that: its chains end in
356
+ `unset`, which for an inherited property means "take the parent's value".
357
+
358
+ So this re-declares every inherited text property with a real default at
359
+ the end of the same variable chain. The vars still win (--tooltip-*, then
360
+ the category, then the global), and custom properties still inherit --
361
+ that is how theming reaches an overlay -- but when nothing is set, the
362
+ overlay gets its own defaults instead of its host's.
363
+
364
+ INCLUDE IT AFTER typography-props() / typography-container-props() in the
365
+ same rule: it has to beat their `unset` fallbacks by source order.
366
+
367
+ Deliberately left inheriting:
368
+ - direction, writing-mode: the document's script direction is the
369
+ overlay's too.
370
+ - visibility: a hidden subtree should not be able to show an overlay.
371
+ - font-family: every theme defines --font-family at :root, and there is no
372
+ neutral value to fall back to that would beat "the page's font". */
312
373
  /* Logical, not top/bottom/left/right: margin-block/margin-inline already take
313
374
  one value (both sides equal) or two (start end), so there is no separate
314
375
  "which side" prop to remember and no 4-value shorthand order to get wrong.
@@ -1129,6 +1190,10 @@ table :global(tr > th:first-child:has(~ td)) {
1129
1190
  visibility: collapse;
1130
1191
  }
1131
1192
 
1193
+ .scrolling-table-body > :global(tr[data-contain-implicit-table-head]) {
1194
+ visibility: collapse;
1195
+ }
1196
+
1132
1197
  /* One cell per column, so widths can be read off directly. Collapsed for
1133
1198
  zero height, and stripped of padding and borders so it can never be the
1134
1199
  thing that decides a column's minimum width. */
@@ -1171,7 +1236,7 @@ table :global(td[tabindex]:focus-visible) {
1171
1236
  filter: var(--hover-filter, brightness(1.05));
1172
1237
  transform: var(--hover-transform, none);
1173
1238
  color: var(--_color);
1174
- background-color: color-mix(in oklch, var(--_background-color) var(--hover-base-color-percentage, 90%), var(--hover-color-mix, white) calc(100% - var(--hover-base-color-percentage, 90%)));
1239
+ background-color: color-mix(in oklab, var(--_background-color) var(--hover-base-color-percentage, 90%), var(--hover-color-mix, var(--_color, white)) calc(100% - var(--hover-base-color-percentage, 90%)));
1175
1240
  box-shadow: var(--hover-box-shadow, var(--_box-shadow, none));
1176
1241
  }
1177
1242
 
@@ -1181,7 +1246,7 @@ table :global(th[tabindex]:active),
1181
1246
  table :global(td[tabindex]:active) {
1182
1247
  filter: var(--active-filter, brightness(0.9));
1183
1248
  transform: var(--active-transform, none);
1184
- background-color: color-mix(in oklch, var(--_background-color) var(--active-base-color-percentage, 90%), var(--active-color-mix, black) calc(100% - var(--active-base-color-percentage, 90%)));
1249
+ background-color: color-mix(in oklab, var(--_background-color) var(--active-base-color-percentage, 90%), var(--active-color-mix, black) calc(100% - var(--active-base-color-percentage, 90%)));
1185
1250
  box-shadow: var(--active-box-shadow, var(--_box-shadow, none));
1186
1251
  }
1187
1252
 
@@ -97,6 +97,31 @@ const className = $derived(props.class);
97
97
  text, no top margin. Structural matching (:first-of-type, `h1 + p`) still
98
98
  has to happen where the real element is, in the global consumer rule; only
99
99
  the VALUES move to context vars here. */
100
+ /* Text reset for an overlay's root element: a tooltip, popover menu or dialog
101
+ that sits in the DOM wherever its trigger was placed.
102
+
103
+ The top layer changes how an element paints and where it is positioned, not
104
+ what it inherits. A popover declared inside a `white-space: nowrap` table
105
+ cell is nowrap too, and one inside a bold, uppercase, centred header is
106
+ bold, uppercase and centred -- styles that belong to the trigger's context,
107
+ not to the overlay. typography-props() doesn't stop that: its chains end in
108
+ `unset`, which for an inherited property means "take the parent's value".
109
+
110
+ So this re-declares every inherited text property with a real default at
111
+ the end of the same variable chain. The vars still win (--tooltip-*, then
112
+ the category, then the global), and custom properties still inherit --
113
+ that is how theming reaches an overlay -- but when nothing is set, the
114
+ overlay gets its own defaults instead of its host's.
115
+
116
+ INCLUDE IT AFTER typography-props() / typography-container-props() in the
117
+ same rule: it has to beat their `unset` fallbacks by source order.
118
+
119
+ Deliberately left inheriting:
120
+ - direction, writing-mode: the document's script direction is the
121
+ overlay's too.
122
+ - visibility: a hidden subtree should not be able to show an overlay.
123
+ - font-family: every theme defines --font-family at :root, and there is no
124
+ neutral value to fall back to that would beat "the page's font". */
100
125
  /* Logical, not top/bottom/left/right: margin-block/margin-inline already take
101
126
  one value (both sides equal) or two (start end), so there is no separate
102
127
  "which side" prop to remember and no 4-value shorthand order to get wrong.
@@ -448,7 +473,7 @@ label.tile:hover {
448
473
  filter: var(--tile-hover-filter, var(--hover-filter, brightness(1.05)));
449
474
  transform: var(--tile-hover-transform, var(--hover-transform, none));
450
475
  color: var(--_color);
451
- background-color: color-mix(in oklch, var(--_background-color) var(--hover-base-color-percentage, 90%), var(--hover-color-mix, white) calc(100% - var(--hover-base-color-percentage, 90%)));
476
+ background-color: color-mix(in oklab, var(--_background-color) var(--hover-base-color-percentage, 90%), var(--hover-color-mix, var(--_color, white)) calc(100% - var(--hover-base-color-percentage, 90%)));
452
477
  box-shadow: var(--tile-hover-box-shadow, var(--hover-box-shadow, var(--_box-shadow, none)));
453
478
  }
454
479
 
@@ -456,7 +481,7 @@ button.tile:active,
456
481
  label.tile:active {
457
482
  filter: var(--tile-active-filter, var(--active-filter, brightness(0.9)));
458
483
  transform: var(--tile-active-transform, var(--active-transform, none));
459
- background-color: color-mix(in oklch, var(--_background-color) var(--active-base-color-percentage, 90%), var(--active-color-mix, black) calc(100% - var(--active-base-color-percentage, 90%)));
484
+ background-color: color-mix(in oklab, var(--_background-color) var(--active-base-color-percentage, 90%), var(--active-color-mix, black) calc(100% - var(--active-base-color-percentage, 90%)));
460
485
  box-shadow: var(--tile-active-box-shadow, var(--active-box-shadow, var(--_box-shadow, none)));
461
486
  }
462
487
 
@@ -465,7 +490,7 @@ label.tile:disabled {
465
490
  cursor: var(--tile-disabled-cursor, var(--disabled-cursor, not-allowed));
466
491
  transform: var(--tile-disabled-transform, var(--disabled-transform, none));
467
492
  filter: var(--tile-disabled-filter, var(--disabled-filter, grayscale(0.5)));
468
- background-color: color-mix(in oklch, var(--_background-color) var(--disabled-base-color-percentage, 50%), var(--disabled-color-mix, gray) calc(100% - var(--disabled-base-color-percentage, 50%)));
493
+ background-color: color-mix(in oklab, var(--_background-color) var(--disabled-base-color-percentage, 50%), var(--disabled-color-mix, gray) calc(100% - var(--disabled-base-color-percentage, 50%)));
469
494
  box-shadow: var(--tile-disabled-box-shadow, var(--disabled-box-shadow, var(--_box-shadow, none)));
470
495
  }
471
496
 
@@ -42,6 +42,31 @@ let highlightedCode = $derived.by(() => {
42
42
  text, no top margin. Structural matching (:first-of-type, `h1 + p`) still
43
43
  has to happen where the real element is, in the global consumer rule; only
44
44
  the VALUES move to context vars here. */
45
+ /* Text reset for an overlay's root element: a tooltip, popover menu or dialog
46
+ that sits in the DOM wherever its trigger was placed.
47
+
48
+ The top layer changes how an element paints and where it is positioned, not
49
+ what it inherits. A popover declared inside a `white-space: nowrap` table
50
+ cell is nowrap too, and one inside a bold, uppercase, centred header is
51
+ bold, uppercase and centred -- styles that belong to the trigger's context,
52
+ not to the overlay. typography-props() doesn't stop that: its chains end in
53
+ `unset`, which for an inherited property means "take the parent's value".
54
+
55
+ So this re-declares every inherited text property with a real default at
56
+ the end of the same variable chain. The vars still win (--tooltip-*, then
57
+ the category, then the global), and custom properties still inherit --
58
+ that is how theming reaches an overlay -- but when nothing is set, the
59
+ overlay gets its own defaults instead of its host's.
60
+
61
+ INCLUDE IT AFTER typography-props() / typography-container-props() in the
62
+ same rule: it has to beat their `unset` fallbacks by source order.
63
+
64
+ Deliberately left inheriting:
65
+ - direction, writing-mode: the document's script direction is the
66
+ overlay's too.
67
+ - visibility: a hidden subtree should not be able to show an overlay.
68
+ - font-family: every theme defines --font-family at :root, and there is no
69
+ neutral value to fall back to that would beat "the page's font". */
45
70
  /* Logical, not top/bottom/left/right: margin-block/margin-inline already take
46
71
  one value (both sides equal) or two (start end), so there is no separate
47
72
  "which side" prop to remember and no 4-value shorthand order to get wrong.
@@ -89,6 +114,30 @@ div > :global(pre) {
89
114
  .code :global(code[class*="language-"]),
90
115
  .code :global(pre[class*="language-"]) {
91
116
  tab-size: var(--code-tab-size, 2em);
117
+ font-family: var(--code-font-family, var(--surface-font-family, var(--font-family, unset)));
118
+ text-transform: var(--code-text-transform, var(--surface-text-transform, var(--text-transform, unset)));
119
+ text-decoration: var(--code-text-decoration, var(--surface-text-decoration, var(--text-decoration, unset)));
120
+ --_font-size: var(--code-font-size, var(--surface-font-size, var(--font-size, unset)));
121
+ font-size: var(--_font-size);
122
+ font-weight: var(--code-font-weight, var(--surface-font-weight, var(--font-weight, unset)));
123
+ line-height: var(--code-line-height, var(--surface-line-height, var(--line-height, unset)));
124
+ letter-spacing: var(--code-letter-spacing, var(--surface-letter-spacing, var(--letter-spacing, unset)));
125
+ --_margin-top: var(--code-margin, var(--surface-margin, var(--margin, unset)));
126
+ --_margin-top: var(--code-margin-top, var(--surface-margin-top, var(--margin-top, unset)));
127
+ margin-top: var(--_margin-top);
128
+ --_margin-bottom: var(--code-margin, var(--surface-margin, var(--margin, unset)));
129
+ --_margin-bottom: var(--code-margin-bottom, var(--surface-margin-bottom, var(--margin-bottom, unset)));
130
+ margin-bottom: var(--_margin-bottom);
131
+ text-indent: var(--code-indent, var(--surface-indent, var(--indent, unset)));
132
+ font-variant: var(--code-font-variant, var(--surface-font-variant, var(--font-variant, unset)));
133
+ text-align: var(--code-text-align, var(--surface-text-align, var(--text-align, unset)));
134
+ }
135
+
136
+ /* Colors live on the block only. Giving the <code> inside the <pre> its
137
+ own background paints line-by-line patches wherever the two resolve
138
+ differently (e.g. no --code-bg: the <pre> fell back to transparent while
139
+ the <code> took --surface-bg). */
140
+ .code :global(pre[class*="language-"]) {
92
141
  --link-bg: var(--code-link-bg, var(--surface-link-bg, inherit));
93
142
  --link-fg: var(--code-link-fg, var(--surface-link-fg, inherit));
94
143
  --_bg-mix-color: var(--code-bg-mix-color, var(--surface-bg-mix-color, var(--bg-mix-color, transparent)));
@@ -173,28 +222,6 @@ div > :global(pre) {
173
222
  );
174
223
  background-color: var(--_background-color);
175
224
  color: var(--_color);
176
- font-family: var(--code-font-family, var(--surface-font-family, var(--font-family, unset)));
177
- text-transform: var(--code-text-transform, var(--surface-text-transform, var(--text-transform, unset)));
178
- text-decoration: var(--code-text-decoration, var(--surface-text-decoration, var(--text-decoration, unset)));
179
- --_font-size: var(--code-font-size, var(--surface-font-size, var(--font-size, unset)));
180
- font-size: var(--_font-size);
181
- font-weight: var(--code-font-weight, var(--surface-font-weight, var(--font-weight, unset)));
182
- line-height: var(--code-line-height, var(--surface-line-height, var(--line-height, unset)));
183
- letter-spacing: var(--code-letter-spacing, var(--surface-letter-spacing, var(--letter-spacing, unset)));
184
- --_margin-top: var(--code-margin, var(--surface-margin, var(--margin, unset)));
185
- --_margin-top: var(--code-margin-top, var(--surface-margin-top, var(--margin-top, unset)));
186
- margin-top: var(--_margin-top);
187
- --_margin-bottom: var(--code-margin, var(--surface-margin, var(--margin, unset)));
188
- --_margin-bottom: var(--code-margin-bottom, var(--surface-margin-bottom, var(--margin-bottom, unset)));
189
- margin-bottom: var(--_margin-bottom);
190
- text-indent: var(--code-indent, var(--surface-indent, var(--indent, unset)));
191
- font-variant: var(--code-font-variant, var(--surface-font-variant, var(--font-variant, unset)));
192
- text-align: var(--code-text-align, var(--surface-text-align, var(--text-align, unset)));
193
- }
194
-
195
- .code :global(pre[class*="language-"]) {
196
- background: var(--code-bg);
197
- color: var(--code-fg);
198
225
  padding: 1em;
199
226
  border-radius: 0.3em;
200
227
  --_box-shadow:
@@ -89,6 +89,31 @@ let halfFull = $derived(fillFraction > 0.5);
89
89
  text, no top margin. Structural matching (:first-of-type, `h1 + p`) still
90
90
  has to happen where the real element is, in the global consumer rule; only
91
91
  the VALUES move to context vars here. */
92
+ /* Text reset for an overlay's root element: a tooltip, popover menu or dialog
93
+ that sits in the DOM wherever its trigger was placed.
94
+
95
+ The top layer changes how an element paints and where it is positioned, not
96
+ what it inherits. A popover declared inside a `white-space: nowrap` table
97
+ cell is nowrap too, and one inside a bold, uppercase, centred header is
98
+ bold, uppercase and centred -- styles that belong to the trigger's context,
99
+ not to the overlay. typography-props() doesn't stop that: its chains end in
100
+ `unset`, which for an inherited property means "take the parent's value".
101
+
102
+ So this re-declares every inherited text property with a real default at
103
+ the end of the same variable chain. The vars still win (--tooltip-*, then
104
+ the category, then the global), and custom properties still inherit --
105
+ that is how theming reaches an overlay -- but when nothing is set, the
106
+ overlay gets its own defaults instead of its host's.
107
+
108
+ INCLUDE IT AFTER typography-props() / typography-container-props() in the
109
+ same rule: it has to beat their `unset` fallbacks by source order.
110
+
111
+ Deliberately left inheriting:
112
+ - direction, writing-mode: the document's script direction is the
113
+ overlay's too.
114
+ - visibility: a hidden subtree should not be able to show an overlay.
115
+ - font-family: every theme defines --font-family at :root, and there is no
116
+ neutral value to fall back to that would beat "the page's font". */
92
117
  /* Logical, not top/bottom/left/right: margin-block/margin-inline already take
93
118
  one value (both sides equal) or two (start end), so there is no separate
94
119
  "which side" prop to remember and no 4-value shorthand order to get wrong.
@@ -56,6 +56,31 @@ const el = $derived(elementProps(restProps, "tag", TAG_VARS));
56
56
  text, no top margin. Structural matching (:first-of-type, `h1 + p`) still
57
57
  has to happen where the real element is, in the global consumer rule; only
58
58
  the VALUES move to context vars here. */
59
+ /* Text reset for an overlay's root element: a tooltip, popover menu or dialog
60
+ that sits in the DOM wherever its trigger was placed.
61
+
62
+ The top layer changes how an element paints and where it is positioned, not
63
+ what it inherits. A popover declared inside a `white-space: nowrap` table
64
+ cell is nowrap too, and one inside a bold, uppercase, centred header is
65
+ bold, uppercase and centred -- styles that belong to the trigger's context,
66
+ not to the overlay. typography-props() doesn't stop that: its chains end in
67
+ `unset`, which for an inherited property means "take the parent's value".
68
+
69
+ So this re-declares every inherited text property with a real default at
70
+ the end of the same variable chain. The vars still win (--tooltip-*, then
71
+ the category, then the global), and custom properties still inherit --
72
+ that is how theming reaches an overlay -- but when nothing is set, the
73
+ overlay gets its own defaults instead of its host's.
74
+
75
+ INCLUDE IT AFTER typography-props() / typography-container-props() in the
76
+ same rule: it has to beat their `unset` fallbacks by source order.
77
+
78
+ Deliberately left inheriting:
79
+ - direction, writing-mode: the document's script direction is the
80
+ overlay's too.
81
+ - visibility: a hidden subtree should not be able to show an overlay.
82
+ - font-family: every theme defines --font-family at :root, and there is no
83
+ neutral value to fall back to that would beat "the page's font". */
59
84
  /* Logical, not top/bottom/left/right: margin-block/margin-inline already take
60
85
  one value (both sides equal) or two (start end), so there is no separate
61
86
  "which side" prop to remember and no 4-value shorthand order to get wrong.
@@ -103,6 +103,31 @@ let ref;
103
103
  text, no top margin. Structural matching (:first-of-type, `h1 + p`) still
104
104
  has to happen where the real element is, in the global consumer rule; only
105
105
  the VALUES move to context vars here. */
106
+ /* Text reset for an overlay's root element: a tooltip, popover menu or dialog
107
+ that sits in the DOM wherever its trigger was placed.
108
+
109
+ The top layer changes how an element paints and where it is positioned, not
110
+ what it inherits. A popover declared inside a `white-space: nowrap` table
111
+ cell is nowrap too, and one inside a bold, uppercase, centred header is
112
+ bold, uppercase and centred -- styles that belong to the trigger's context,
113
+ not to the overlay. typography-props() doesn't stop that: its chains end in
114
+ `unset`, which for an inherited property means "take the parent's value".
115
+
116
+ So this re-declares every inherited text property with a real default at
117
+ the end of the same variable chain. The vars still win (--tooltip-*, then
118
+ the category, then the global), and custom properties still inherit --
119
+ that is how theming reaches an overlay -- but when nothing is set, the
120
+ overlay gets its own defaults instead of its host's.
121
+
122
+ INCLUDE IT AFTER typography-props() / typography-container-props() in the
123
+ same rule: it has to beat their `unset` fallbacks by source order.
124
+
125
+ Deliberately left inheriting:
126
+ - direction, writing-mode: the document's script direction is the
127
+ overlay's too.
128
+ - visibility: a hidden subtree should not be able to show an overlay.
129
+ - font-family: every theme defines --font-family at :root, and there is no
130
+ neutral value to fall back to that would beat "the page's font". */
106
131
  /* Logical, not top/bottom/left/right: margin-block/margin-inline already take
107
132
  one value (both sides equal) or two (start end), so there is no separate
108
133
  "which side" prop to remember and no 4-value shorthand order to get wrong.
@@ -442,9 +467,42 @@ dialog {
442
467
  --_h6-text-indent: var(--dialog-heading-6-indent, var(--top-heading-6-indent, var(--surface-heading-6-indent, var(--block-heading-6-indent, var(--dialog-heading-minor-indent, var(--top-heading-minor-indent, var(--surface-heading-minor-indent, var(--block-heading-minor-indent, var(--dialog-heading-indent, var(--top-heading-indent, var(--surface-heading-indent, var(--block-heading-indent, var(--heading-6-indent, var(--heading-minor-indent, var(--heading-indent, var(--indent, revert))))))))))))))));
443
468
  --_h6-font-variant: var(--dialog-heading-6-font-variant, var(--top-heading-6-font-variant, var(--surface-heading-6-font-variant, var(--block-heading-6-font-variant, var(--dialog-heading-minor-font-variant, var(--top-heading-minor-font-variant, var(--surface-heading-minor-font-variant, var(--block-heading-minor-font-variant, var(--dialog-heading-font-variant, var(--top-heading-font-variant, var(--surface-heading-font-variant, var(--block-heading-font-variant, var(--heading-6-font-variant, var(--heading-minor-font-variant, var(--heading-font-variant, var(--font-variant, revert))))))))))))))));
444
469
  --_h6-text-align: var(--dialog-heading-6-text-align, var(--top-heading-6-text-align, var(--surface-heading-6-text-align, var(--block-heading-6-text-align, var(--dialog-heading-minor-text-align, var(--top-heading-minor-text-align, var(--surface-heading-minor-text-align, var(--block-heading-minor-text-align, var(--dialog-heading-text-align, var(--top-heading-text-align, var(--surface-heading-text-align, var(--block-heading-text-align, var(--heading-6-text-align, var(--heading-minor-text-align, var(--heading-text-align, var(--text-align, revert))))))))))))))));
470
+ /* A <dialog> is a DOM child of wherever it was declared -- a table cell,
471
+ a bold header, a clickable card -- and would inherit its text styles
472
+ and cursor. Must follow typography-container-props. */
473
+ --_font-size: var(--dialog-font-size, var(--top-font-size, var(--surface-font-size, var(--block-font-size, var(--font-size, 1rem)))));
474
+ font-size: var(--_font-size);
475
+ font-weight: var(--dialog-font-weight, var(--top-font-weight, var(--surface-font-weight, var(--block-font-weight, var(--font-weight, normal)))));
476
+ font-style: var(--dialog-font-style, var(--top-font-style, var(--surface-font-style, var(--block-font-style, var(--font-style, normal)))));
477
+ font-variant: var(--dialog-font-variant, var(--top-font-variant, var(--surface-font-variant, var(--block-font-variant, var(--font-variant, normal)))));
478
+ font-feature-settings: normal;
479
+ text-transform: var(--dialog-text-transform, var(--top-text-transform, var(--surface-text-transform, var(--block-text-transform, var(--text-transform, none)))));
480
+ letter-spacing: var(--dialog-letter-spacing, var(--top-letter-spacing, var(--surface-letter-spacing, var(--block-letter-spacing, var(--letter-spacing, normal)))));
481
+ word-spacing: normal;
482
+ line-height: var(--dialog-line-height, var(--top-line-height, var(--surface-line-height, var(--block-line-height, var(--line-height, normal)))));
483
+ text-indent: var(--dialog-indent, var(--top-indent, var(--surface-indent, var(--block-indent, var(--indent, 0)))));
484
+ text-align: var(--dialog-text-align, var(--top-text-align, var(--surface-text-align, var(--block-text-align, var(--text-align, start)))));
485
+ text-shadow: none;
486
+ /* An overlay is capped by a max-width; its text has to wrap inside it. */
487
+ white-space: var(--dialog-white-space, var(--top-white-space, var(--surface-white-space, var(--block-white-space, var(--white-space, normal)))));
488
+ word-break: normal;
489
+ overflow-wrap: var(--dialog-overflow-wrap, var(--top-overflow-wrap, var(--surface-overflow-wrap, var(--block-overflow-wrap, var(--overflow-wrap, break-word)))));
490
+ hyphens: manual;
491
+ /* Not the trigger's pointer/text cursor, and never click-through. */
492
+ cursor: auto;
493
+ pointer-events: auto;
494
+ /* user-select isn't inherited, but `auto` copies a parent's `none`;
495
+ `text` is what `auto` resolves to in an ordinary context. */
496
+ -webkit-user-select: text;
497
+ user-select: text;
445
498
  --_box-shadow:
446
499
  var(--dialog-shadow-distance, var(--top-shadow-distance, var(--surface-shadow-distance, var(--block-shadow-distance, var(--shadow-distance, var(--space)))))) var(--dialog-shadow-distance, var(--top-shadow-distance, var(--surface-shadow-distance, var(--block-shadow-distance, var(--shadow-distance, var(--space)))))) var(--dialog-shadow-blur, var(--top-shadow-blur, var(--surface-shadow-blur, var(--block-shadow-blur, var(--shadow-blur, var(--space)))))) var(--dialog-shadow-color, var(--top-shadow-color, var(--surface-shadow-color, var(--block-shadow-color, var(--shadow-color, rgba(127, 127, 127, 0.4))))));
447
500
  box-shadow: var(--_box-shadow);
501
+ /* typography-props writes margin-top/bottom ending in `unset`, which
502
+ zeroes the UA's `margin: auto` and pins a modal to the top edge.
503
+ Restore auto so it centers vertically; only dialog-level vars move it. */
504
+ margin-top: var(--dialog-margin-top, auto);
505
+ margin-bottom: var(--dialog-margin-bottom, auto);
448
506
  min-width: min(100vw - var(--padding) * 2, var(--dialog-min-width, 400px));
449
507
  max-width: max(100vw - var(--padding) * 2, var(--dialog-max-width, 800px));
450
508
  min-height: min(100vh - var(--padding) * 2, var(--dialog-min-height, 300px));
@@ -16,7 +16,10 @@ const TOOLTIP_VARS = [
16
16
  ...TYPOGRAPHY_CONTAINER_VARS,
17
17
  ];
18
18
  /* The variables land on the wrapper and reach the popover by inheritance:
19
- the tooltip is a descendant of it, and custom properties inherit. */
19
+ the tooltip is a descendant of it, and custom properties inherit.
20
+ Ordinary inherited properties (white-space, font-weight, text-align...)
21
+ would reach it the same way from the host, which is why the popover's
22
+ CSS resets them -- see overlay-text-reset in _typography.scss. */
20
23
  const el = $derived(elementProps(restProps, "tooltip", TOOLTIP_VARS));
21
24
  // svelte-ignore state_referenced_locally
22
25
  let renderedVertical = $state(vertical);
@@ -320,6 +323,31 @@ async function showPopover() {
320
323
  text, no top margin. Structural matching (:first-of-type, `h1 + p`) still
321
324
  has to happen where the real element is, in the global consumer rule; only
322
325
  the VALUES move to context vars here. */
326
+ /* Text reset for an overlay's root element: a tooltip, popover menu or dialog
327
+ that sits in the DOM wherever its trigger was placed.
328
+
329
+ The top layer changes how an element paints and where it is positioned, not
330
+ what it inherits. A popover declared inside a `white-space: nowrap` table
331
+ cell is nowrap too, and one inside a bold, uppercase, centred header is
332
+ bold, uppercase and centred -- styles that belong to the trigger's context,
333
+ not to the overlay. typography-props() doesn't stop that: its chains end in
334
+ `unset`, which for an inherited property means "take the parent's value".
335
+
336
+ So this re-declares every inherited text property with a real default at
337
+ the end of the same variable chain. The vars still win (--tooltip-*, then
338
+ the category, then the global), and custom properties still inherit --
339
+ that is how theming reaches an overlay -- but when nothing is set, the
340
+ overlay gets its own defaults instead of its host's.
341
+
342
+ INCLUDE IT AFTER typography-props() / typography-container-props() in the
343
+ same rule: it has to beat their `unset` fallbacks by source order.
344
+
345
+ Deliberately left inheriting:
346
+ - direction, writing-mode: the document's script direction is the
347
+ overlay's too.
348
+ - visibility: a hidden subtree should not be able to show an overlay.
349
+ - font-family: every theme defines --font-family at :root, and there is no
350
+ neutral value to fall back to that would beat "the page's font". */
323
351
  /* Logical, not top/bottom/left/right: margin-block/margin-inline already take
324
352
  one value (both sides equal) or two (start end), so there is no separate
325
353
  "which side" prop to remember and no 4-value shorthand order to get wrong.
@@ -647,6 +675,34 @@ async function showPopover() {
647
675
  --_h6-text-indent: var(--tooltip-heading-6-indent, var(--ui-heading-6-indent, var(--tooltip-heading-minor-indent, var(--ui-heading-minor-indent, var(--tooltip-heading-indent, var(--ui-heading-indent, var(--heading-6-indent, var(--heading-minor-indent, var(--heading-indent, var(--indent, revert))))))))));
648
676
  --_h6-font-variant: var(--tooltip-heading-6-font-variant, var(--ui-heading-6-font-variant, var(--tooltip-heading-minor-font-variant, var(--ui-heading-minor-font-variant, var(--tooltip-heading-font-variant, var(--ui-heading-font-variant, var(--heading-6-font-variant, var(--heading-minor-font-variant, var(--heading-font-variant, var(--font-variant, revert))))))))));
649
677
  --_h6-text-align: var(--tooltip-heading-6-text-align, var(--ui-heading-6-text-align, var(--tooltip-heading-minor-text-align, var(--ui-heading-minor-text-align, var(--tooltip-heading-text-align, var(--ui-heading-text-align, var(--heading-6-text-align, var(--heading-minor-text-align, var(--heading-text-align, var(--text-align, revert))))))))));
678
+ /* The popover is a DOM child of wherever the Tooltip was placed, so
679
+ without this it inherits that spot's white-space, weight, alignment
680
+ and so on. Must follow typography-container-props. */
681
+ --_font-size: var(--tooltip-font-size, var(--ui-font-size, var(--font-size, 1rem)));
682
+ font-size: var(--_font-size);
683
+ font-weight: var(--tooltip-font-weight, var(--ui-font-weight, var(--font-weight, normal)));
684
+ font-style: var(--tooltip-font-style, var(--ui-font-style, var(--font-style, normal)));
685
+ font-variant: var(--tooltip-font-variant, var(--ui-font-variant, var(--font-variant, normal)));
686
+ font-feature-settings: normal;
687
+ text-transform: var(--tooltip-text-transform, var(--ui-text-transform, var(--text-transform, none)));
688
+ letter-spacing: var(--tooltip-letter-spacing, var(--ui-letter-spacing, var(--letter-spacing, normal)));
689
+ word-spacing: normal;
690
+ line-height: var(--tooltip-line-height, var(--ui-line-height, var(--line-height, normal)));
691
+ text-indent: var(--tooltip-indent, var(--ui-indent, var(--indent, 0)));
692
+ text-align: var(--tooltip-text-align, var(--ui-text-align, var(--text-align, start)));
693
+ text-shadow: none;
694
+ /* An overlay is capped by a max-width; its text has to wrap inside it. */
695
+ white-space: var(--tooltip-white-space, var(--ui-white-space, var(--white-space, normal)));
696
+ word-break: normal;
697
+ overflow-wrap: var(--tooltip-overflow-wrap, var(--ui-overflow-wrap, var(--overflow-wrap, break-word)));
698
+ hyphens: manual;
699
+ /* Not the trigger's pointer/text cursor, and never click-through. */
700
+ cursor: auto;
701
+ pointer-events: auto;
702
+ /* user-select isn't inherited, but `auto` copies a parent's `none`;
703
+ `text` is what `auto` resolves to in an ordinary context. */
704
+ -webkit-user-select: text;
705
+ user-select: text;
650
706
  }
651
707
 
652
708
  .tooltip-wrapper {
@@ -33,6 +33,12 @@
33
33
  fn.var-with-fallbacks(--transition, list.append($prefixes, 300ms)...);
34
34
  }
35
35
 
36
+ // Hover mixes toward --hover-color-mix, defaulting to the element's own
37
+ // text color: that darkens light controls and lightens dark ones whatever
38
+ // the theme's mode, so the hover can't vanish into the surface the way a
39
+ // fixed white did on light themes. Themes that set --hover-color-mix keep
40
+ // exactly the direction they chose. Mixing is done in oklab because oklch
41
+ // interpolates hue, which drifts when one side is a near-grey.
36
42
  @mixin clickable-hover-affordance($prefixes...) {
37
43
  filter: fn.var-with-fallbacks(
38
44
  --hover-filter,
@@ -44,9 +50,9 @@
44
50
  );
45
51
  color: var(--_color);
46
52
  background-color: color-mix(
47
- in oklch,
53
+ in oklab,
48
54
  var(--_background-color) var(--hover-base-color-percentage, 90%),
49
- var(--hover-color-mix, white)
55
+ var(--hover-color-mix, var(--_color, white))
50
56
  calc(100% - var(--hover-base-color-percentage, 90%))
51
57
  );
52
58
  box-shadow: fn.var-with-fallbacks(
@@ -65,7 +71,7 @@
65
71
  list.append($prefixes, none)...
66
72
  );
67
73
  background-color: color-mix(
68
- in oklch,
74
+ in oklab,
69
75
  var(--_background-color) var(--active-base-color-percentage, 90%),
70
76
  var(--active-color-mix, black)
71
77
  calc(100% - var(--active-base-color-percentage, 90%))
@@ -99,7 +105,7 @@
99
105
  list.append($prefixes, grayscale(0.5))...
100
106
  );
101
107
  background-color: color-mix(
102
- in oklch,
108
+ in oklab,
103
109
  var(--_background-color) var(--disabled-base-color-percentage, 50%),
104
110
  var(--disabled-color-mix, gray)
105
111
  calc(100% - var(--disabled-base-color-percentage, 50%))
@@ -293,3 +293,56 @@
293
293
  )};
294
294
  @include heading-context-vars($prefixes...);
295
295
  }
296
+
297
+ /* Text reset for an overlay's root element: a tooltip, popover menu or dialog
298
+ that sits in the DOM wherever its trigger was placed.
299
+
300
+ The top layer changes how an element paints and where it is positioned, not
301
+ what it inherits. A popover declared inside a `white-space: nowrap` table
302
+ cell is nowrap too, and one inside a bold, uppercase, centred header is
303
+ bold, uppercase and centred -- styles that belong to the trigger's context,
304
+ not to the overlay. typography-props() doesn't stop that: its chains end in
305
+ `unset`, which for an inherited property means "take the parent's value".
306
+
307
+ So this re-declares every inherited text property with a real default at
308
+ the end of the same variable chain. The vars still win (--tooltip-*, then
309
+ the category, then the global), and custom properties still inherit --
310
+ that is how theming reaches an overlay -- but when nothing is set, the
311
+ overlay gets its own defaults instead of its host's.
312
+
313
+ INCLUDE IT AFTER typography-props() / typography-container-props() in the
314
+ same rule: it has to beat their `unset` fallbacks by source order.
315
+
316
+ Deliberately left inheriting:
317
+ - direction, writing-mode: the document's script direction is the
318
+ overlay's too.
319
+ - visibility: a hidden subtree should not be able to show an overlay.
320
+ - font-family: every theme defines --font-family at :root, and there is no
321
+ neutral value to fall back to that would beat "the page's font". */
322
+ @mixin overlay-text-reset($prefixes...) {
323
+ --_font-size: #{fn.var-with-fallbacks(--font-size, list.append($prefixes, 1rem)...)};
324
+ font-size: var(--_font-size);
325
+ font-weight: fn.var-with-fallbacks(--font-weight, list.append($prefixes, normal)...);
326
+ font-style: fn.var-with-fallbacks(--font-style, list.append($prefixes, normal)...);
327
+ font-variant: fn.var-with-fallbacks(--font-variant, list.append($prefixes, normal)...);
328
+ font-feature-settings: normal;
329
+ text-transform: fn.var-with-fallbacks(--text-transform, list.append($prefixes, none)...);
330
+ letter-spacing: fn.var-with-fallbacks(--letter-spacing, list.append($prefixes, normal)...);
331
+ word-spacing: normal;
332
+ line-height: fn.var-with-fallbacks(--line-height, list.append($prefixes, normal)...);
333
+ text-indent: fn.var-with-fallbacks(--indent, list.append($prefixes, 0)...);
334
+ text-align: fn.var-with-fallbacks(--text-align, list.append($prefixes, start)...);
335
+ text-shadow: none;
336
+ /* An overlay is capped by a max-width; its text has to wrap inside it. */
337
+ white-space: fn.var-with-fallbacks(--white-space, list.append($prefixes, normal)...);
338
+ word-break: normal;
339
+ overflow-wrap: fn.var-with-fallbacks(--overflow-wrap, list.append($prefixes, break-word)...);
340
+ hyphens: manual;
341
+ /* Not the trigger's pointer/text cursor, and never click-through. */
342
+ cursor: auto;
343
+ pointer-events: auto;
344
+ /* user-select isn't inherited, but `auto` copies a parent's `none`;
345
+ `text` is what `auto` resolves to in an ordinary context. */
346
+ -webkit-user-select: text;
347
+ user-select: text;
348
+ }
@@ -33,6 +33,31 @@ const el = $derived(elementProps(restProps, "text-layout", ["marginBlock", "marg
33
33
  text, no top margin. Structural matching (:first-of-type, `h1 + p`) still
34
34
  has to happen where the real element is, in the global consumer rule; only
35
35
  the VALUES move to context vars here. */
36
+ /* Text reset for an overlay's root element: a tooltip, popover menu or dialog
37
+ that sits in the DOM wherever its trigger was placed.
38
+
39
+ The top layer changes how an element paints and where it is positioned, not
40
+ what it inherits. A popover declared inside a `white-space: nowrap` table
41
+ cell is nowrap too, and one inside a bold, uppercase, centred header is
42
+ bold, uppercase and centred -- styles that belong to the trigger's context,
43
+ not to the overlay. typography-props() doesn't stop that: its chains end in
44
+ `unset`, which for an inherited property means "take the parent's value".
45
+
46
+ So this re-declares every inherited text property with a real default at
47
+ the end of the same variable chain. The vars still win (--tooltip-*, then
48
+ the category, then the global), and custom properties still inherit --
49
+ that is how theming reaches an overlay -- but when nothing is set, the
50
+ overlay gets its own defaults instead of its host's.
51
+
52
+ INCLUDE IT AFTER typography-props() / typography-container-props() in the
53
+ same rule: it has to beat their `unset` fallbacks by source order.
54
+
55
+ Deliberately left inheriting:
56
+ - direction, writing-mode: the document's script direction is the
57
+ overlay's too.
58
+ - visibility: a hidden subtree should not be able to show an overlay.
59
+ - font-family: every theme defines --font-family at :root, and there is no
60
+ neutral value to fall back to that would beat "the page's font". */
36
61
  /* Logical, not top/bottom/left/right: margin-block/margin-inline already take
37
62
  one value (both sides equal) or two (start end), so there is no separate
38
63
  "which side" prop to remember and no 4-value shorthand order to get wrong.