contain-css-svelte 1.1.9 → 1.1.11

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Tom Hinkle
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,58 +1,138 @@
1
- # create-svelte
1
+ # ContainCSS for Svelte
2
2
 
3
- Everything you need to build a Svelte library, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
3
+ **A small Svelte component library that gets out of your way.** Every component is
4
+ styled through CSS custom properties, sized by container queries, and built on
5
+ native HTML elements — so you can theme it from the outside, drop it into any
6
+ layout, and keep the accessibility you get for free from `<dialog>`, `<details>`,
7
+ and `popover`.
4
8
 
5
- Read more about creating a library [in the docs](https://kit.svelte.dev/docs/packaging).
9
+ ### 📖 [Browse the docs & live demos ](https://thinkle.github.io/svelte-contain-css/)
6
10
 
7
- ## Creating a project
11
+ [![npm](https://img.shields.io/npm/v/contain-css-svelte.svg)](https://www.npmjs.com/package/contain-css-svelte)
8
12
 
9
- If you're seeing this, you've probably already done this step. Congrats!
13
+ ---
10
14
 
11
- ```bash
12
- # create a new project in the current directory
13
- npm create svelte@latest
15
+ ## Which version do I want?
14
16
 
15
- # create a new project in my-app
16
- npm create svelte@latest my-app
17
- ```
17
+ | | Branch | npm version | Svelte | Docs |
18
+ | --- | --- | --- | --- | --- |
19
+ | **👉 Start here** | [`svelte5`](https://github.com/thinkle/svelte-contain-css/tree/svelte5) | `1.x` | Svelte 5 | [svelte5 docs](https://thinkle.github.io/svelte-contain-css/svelte5/) |
20
+ | Legacy | [`main`](https://github.com/thinkle/svelte-contain-css/tree/main) | `0.0.x` | Svelte 3 & 4 | [legacy docs](https://thinkle.github.io/svelte-contain-css/legacy/) |
18
21
 
19
- ## Developing
22
+ **`svelte5` is the active development branch** and where all new work lands.
23
+ `main` is frozen for existing Svelte 4 and earlier projects; it gets fixes
24
+ backported when they're worth it, but no new features. If you're starting
25
+ something new, use `svelte5` / `1.x`.
20
26
 
21
- Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
27
+ ## Install
22
28
 
23
29
  ```bash
24
- npm run dev
30
+ npm install contain-css-svelte
31
+ ```
25
32
 
26
- # or start the server and open the app in a new browser tab
27
- npm run dev -- --open
33
+ Import the default variables once, at the root of your app:
34
+
35
+ ```svelte
36
+ <!-- +layout.svelte or App.svelte -->
37
+ <script>
38
+ import "contain-css-svelte/vars/defaults.css";
39
+ </script>
28
40
  ```
29
41
 
30
- Everything inside `src/lib` is part of your library, everything inside `src/routes` can be used as a showcase or preview app.
42
+ Then use components anywhere:
43
+
44
+ ```svelte
45
+ <script>
46
+ import { Container, Button, Card } from "contain-css-svelte";
47
+ </script>
31
48
 
32
- ## Building
49
+ <Container>
50
+ <Card>
51
+ <h2>Hello</h2>
52
+ <Button primary>Do the thing</Button>
53
+ </Card>
54
+ </Container>
55
+ ```
33
56
 
34
- To build your library:
57
+ ## The idea
35
58
 
36
- ```bash
37
- npm run package
59
+ **1. Style props are CSS variables, not inline styles.**
60
+
61
+ ```svelte
62
+ <Button bg="green" fg="white" height="64px">Wow</Button>
63
+ <!-- becomes style="--button-bg: green; --button-fg: white; --button-height: 64px" -->
38
64
  ```
39
65
 
40
- To create a production version of your showcase app:
66
+ Because they're variables rather than hard-coded styles, the cascade still
67
+ works — which means you can theme a whole subtree from above:
41
68
 
42
- ```bash
43
- npm run build
69
+ ```svelte
70
+ <div style="--primary-bg: darkblue; --primary-fg: white; --border-radius: 0">
71
+ <Button primary>Themed</Button>
72
+ <Button primary>Also themed</Button>
73
+ </div>
74
+ ```
75
+
76
+ ...or in plain CSS, with no build step and no wrapper components:
77
+
78
+ ```css
79
+ .my-app {
80
+ --bg: #1a1a1a;
81
+ --fg: #fff;
82
+ --primary-bg: #4a90d9;
83
+ --font-family: "Inter", sans-serif;
84
+ }
44
85
  ```
45
86
 
46
- You can preview the production build with `npm run preview`.
87
+ **2. Components respond to their container, not the viewport.** A `Card` in a
88
+ narrow sidebar lays itself out like a narrow card, whether or not the window is
89
+ wide. No breakpoint bookkeeping.
90
+
91
+ **3. Native elements underneath.** `Dialog` is a real `<dialog>`, `Accordion` is
92
+ `<details>`, `Tooltip` uses the `popover` API. Focus trapping, escape-to-close,
93
+ and screen-reader semantics come from the platform.
94
+
95
+ ## What's in the box
96
+
97
+ - **Controls** — `Button`, `ButtonLink`, `MiniButton`, `Input`, `Select`,
98
+ `Checkbox`, `RadioButton`, `Toggle`, `Slider`
99
+ - **Layout** — `Container`, `Page`, `Row`/`Column`/`Columns`, `Stack`, `Inline`,
100
+ `GridLayout`, `SplitPane`, `Sidebar`, `Bar`, `TabBar`, `Table`, `Tile`,
101
+ `DataList`, `Hero`, `Accordion`, `Form`/`FormItem`/`Fieldset`
102
+ - **Overlays** — `Dialog`, `Tooltip`, `DropdownMenu`
103
+ - **Misc** — `Card`, `Code`, `Progress`, `Tag`, `TextLayout`, `ResponsiveText`
47
104
 
48
- > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
105
+ Plus a set of ready-made themes (`light`, `dark`, `material`, `bootstrap`,
106
+ `retro`, `purple`, `forest`, `canyon`, and several typography-only themes) under
107
+ `contain-css-svelte/themes/`.
49
108
 
50
- ## Publishing
109
+ Every component and every CSS variable is documented with a live, editable demo
110
+ in [the docs site](https://thinkle.github.io/svelte-contain-css/svelte5/).
51
111
 
52
- Go into the `package.json` and give your package the desired name through the `"name"` option. Also consider adding a `"license"` field and point it to a `LICENSE` file which you can create from a template (one popular option is the [MIT license](https://opensource.org/license/mit/)).
112
+ ## Using this with an AI assistant
53
113
 
54
- To publish your library to [npm](https://www.npmjs.com):
114
+ [`AGENTS-EXTERNAL.md`](AGENTS-EXTERNAL.md) is a complete reference written for
115
+ coding agents — component list, prop-to-variable conventions, and the full CSS
116
+ variable table. Point your assistant at it (or drop it in your project) and it
117
+ will write idiomatic ContainCSS code.
118
+
119
+ ## Developing the library
55
120
 
56
121
  ```bash
57
- npm publish
122
+ npm install
123
+ npm run dev # docs site + live demos at localhost:5173
124
+ npm run check # svelte-check
125
+ npm run test:e2e # playwright
126
+ npm run package # build dist/
58
127
  ```
128
+
129
+ The docs site lives in `src/routes`; the library itself is `src/lib`. Pushing to
130
+ `svelte5` or `main` rebuilds and publishes both doc versions to GitHub Pages via
131
+ [`.github/workflows/deploy-docs.yml`](.github/workflows/deploy-docs.yml).
132
+
133
+ See [AGENTS-INTERNAL.md](AGENTS-INTERNAL.md) for library-development conventions
134
+ and [BACKPORT.md](BACKPORT.md) for moving fixes to the legacy branch.
135
+
136
+ ## License
137
+
138
+ MIT © [Tom Hinkle](https://tomhinkle.net)
@@ -1,8 +1,36 @@
1
1
  <script lang="ts">let { children, value, ...restProps } = $props();
2
2
  let template = $state();
3
- // Extract text content from rendered children
4
- let textContent = $derived(template ? template.textContent ?? "" : "");
5
- let htmlContent = $derived(template ? template.innerHTML : "");
3
+ // Extracted from the rendered children, in the DOM.
4
+ let textContent = $state("");
5
+ let htmlContent = $state("");
6
+ function syncFromTemplate() {
7
+ if (!template)
8
+ return;
9
+ textContent = template.textContent ?? "";
10
+ htmlContent = template.innerHTML;
11
+ }
12
+ /*
13
+ These have to be read back out of the DOM, and a $derived would only
14
+ recompute when `template` itself changed -- which it never does once the
15
+ element is bound. So rendering different content into the snippet (renaming
16
+ a label, say) left the <option> showing whatever it was first given.
17
+
18
+ Watching the template covers that: any change to the rendered children
19
+ re-extracts the html and text.
20
+ */
21
+ $effect(() => {
22
+ if (!template)
23
+ return;
24
+ syncFromTemplate();
25
+ const observer = new MutationObserver(syncFromTemplate);
26
+ observer.observe(template, {
27
+ childList: true,
28
+ subtree: true,
29
+ characterData: true,
30
+ attributes: true,
31
+ });
32
+ return () => observer.disconnect();
33
+ });
6
34
  export {};
7
35
  </script>
8
36
 
@@ -8,16 +8,25 @@ let targetWidth = $state("");
8
8
  let optionButtons = $state([]);
9
9
  onMount(() => {
10
10
  tick().then(() => updateOptions());
11
- // Observe changes in the select element
12
- observer = new MutationObserver((mutations) => {
13
- mutations.forEach((mutation) => {
14
- if (mutation.type === "childList") {
15
- updateOptions();
16
- }
17
- });
18
- });
11
+ /*
12
+ Watch the options for any change, not just being added or removed. An
13
+ <Option> whose content is rewritten in place -- a label being renamed --
14
+ updates its own data-html without touching the child list, and a
15
+ childList-only observer never hears about it, so the dropdown kept
16
+ rendering the snapshot it took on mount.
17
+
18
+ Coalesced into one pass per microtask: a single re-render can produce a
19
+ burst of mutations, and updateOptions() measures layout.
20
+ */
21
+ observer = new MutationObserver(() => scheduleUpdateOptions());
19
22
  if (selectElement) {
20
- observer.observe(selectElement, { childList: true });
23
+ observer.observe(selectElement, {
24
+ childList: true,
25
+ subtree: true,
26
+ characterData: true,
27
+ attributes: true,
28
+ attributeFilter: ["data-html", "value", "label", "selected"],
29
+ });
21
30
  }
22
31
  // Observe size changes in option buttons
23
32
  resizeObserver = new ResizeObserver(() => updateTargetWidth());
@@ -29,6 +38,16 @@ onMount(() => {
29
38
  });
30
39
  let options = $state([]);
31
40
  let activeOption = $state(null);
41
+ let updateQueued = false;
42
+ function scheduleUpdateOptions() {
43
+ if (updateQueued)
44
+ return;
45
+ updateQueued = true;
46
+ queueMicrotask(() => {
47
+ updateQueued = false;
48
+ updateOptions();
49
+ });
50
+ }
32
51
  function updateOptions() {
33
52
  if (!selectElement) {
34
53
  return;
@@ -73,8 +73,7 @@ const style = $derived(injectVars(restProps, "tab", ["bg", "fg", "padding", "wid
73
73
  .tab > :global(button):focus-visible,
74
74
  .tab > :global(div > button):focus-visible {
75
75
  outline: var(--focus-color, -webkit-focus-ring-color) auto 1px;
76
- outline-offset: var(--focus-outline-offset, 2px);
77
- box-shadow: var(--focus-ring-box-shadow, 0 0 0 3px var(--focus-shadow-color, rgba(100, 150, 250, 0.5)));
76
+ outline-offset: var(--focus-inset-outline-offset, -3px);
78
77
  }
79
78
 
80
79
  .tab > :global(button),
package/dist/cssprops.js CHANGED
@@ -126,6 +126,7 @@ const components = [
126
126
  ...[
127
127
  "button",
128
128
  "mini-button",
129
+ "menu",
129
130
  "menu-trigger",
130
131
  "menu-item",
131
132
  "dropdown-menu",
@@ -56,28 +56,41 @@ function dismissPopover(_e) {
56
56
  }
57
57
  function handleToggle(event) {
58
58
  isOpen = event.newState === "open";
59
+ if (!isOpen)
60
+ clearSearch();
61
+ }
62
+ let searchString = $state("");
63
+ let clearTimer;
64
+ function clearSearch() {
65
+ clearTimeout(clearTimer);
66
+ clearTimer = undefined;
67
+ searchString = "";
68
+ }
69
+ const timeoutAfterMS = 2500; // 2.5 seconds seems more humane
70
+ function scheduleSearchClear() {
71
+ clearTimeout(clearTimer);
72
+ clearTimer = setTimeout(clearSearch, timeoutAfterMS);
59
73
  }
60
- let searchString = "";
61
- let lastPress;
62
74
  function handleKeystroke(event) {
63
75
  if (event.key == "Backspace" && searchString) {
64
76
  searchString = searchString.slice(0, -1);
77
+ scheduleSearchClear();
65
78
  }
66
79
  else if (event.key.length == 1) {
67
80
  if (searchString || event.key != " ") {
68
81
  searchString += event.key;
69
82
  maybeFocusMatch(searchString);
83
+ scheduleSearchClear();
70
84
  }
71
85
  }
72
86
  else {
73
- searchString = "";
87
+ clearSearch();
74
88
  if (event.key === "Escape") {
75
89
  popoverDiv?.hidePopover();
76
90
  }
77
91
  else if (event.key === "ArrowDown" || event.key === "ArrowUp") {
78
92
  event.preventDefault(); // Prevent default to stop scrolling the page
79
93
  navigateMenu(event.key);
80
- searchString = "";
81
94
  }
82
95
  }
83
96
  }
@@ -158,6 +171,9 @@ let popoverDiv = $state();
158
171
  style:left="{dropdownLeft}px"
159
172
  style:max-height="{dropdownMaxHeight}px"
160
173
  >
174
+ {#if searchString}
175
+ <div class="search-hint" aria-hidden="true">{searchString}</div>
176
+ {/if}
161
177
  <!-- svelte-ignore a11y_no_static_element_interactions -->
162
178
  <div
163
179
  class="dropdown-content"
@@ -418,6 +434,65 @@ button {
418
434
  overflow: hidden;
419
435
  }
420
436
 
437
+ .search-hint {
438
+ position: absolute;
439
+ top: var(--search-hint-offset, 4px);
440
+ right: var(--search-hint-offset, 4px);
441
+ z-index: 2;
442
+ pointer-events: none;
443
+ max-width: calc(100% - 2 * var(--search-hint-offset, 4px));
444
+ --link-bg: var(--search-hint-link-bg, var(--tag-link-bg, var(--secondary-link-bg, inherit)));
445
+ --link-fg: var(--search-hint-link-fg, var(--tag-link-fg, var(--secondary-link-fg, inherit)));
446
+ --_bg-mix-color: var(--search-hint-bg-mix-color, var(--tag-bg-mix-color, var(--secondary-bg-mix-color, var(--bg-mix-color, transparent))));
447
+ --_bg-mix-amount: var(--search-hint-bg-mix-amount, var(--tag-bg-mix-amount, var(--secondary-bg-mix-amount, var(--bg-mix-amount, 0%))));
448
+ --_fg-mix-color: var(--search-hint-fg-mix-color, var(--tag-fg-mix-color, var(--secondary-fg-mix-color, var(--fg-mix-color, transparent))));
449
+ --_fg-mix-amount: var(--search-hint-fg-mix-amount, var(--tag-fg-mix-amount, var(--secondary-fg-mix-amount, var(--fg-mix-amount, 0%))));
450
+ --_bg-base: var(--search-hint-bg, var(--tag-bg, var(--secondary-bg, var(--bg, unset))));
451
+ --_fg-base: var(--search-hint-fg, var(--tag-fg, var(--secondary-fg, var(--fg, unset))));
452
+ --_background-color: color-mix(
453
+ in srgb,
454
+ var(--_bg-base),
455
+ var(--_bg-mix-color) var(--_bg-mix-amount)
456
+ );
457
+ --_color: color-mix(in srgb, var(--_fg-base), var(--_fg-mix-color) var(--_fg-mix-amount));
458
+ background-color: var(--_background-color);
459
+ color: var(--_color);
460
+ font-family: var(--search-hint-font-family, var(--tag-font-family, inherit));
461
+ text-transform: var(--search-hint-text-transform, var(--tag-text-transform, inherit));
462
+ text-decoration: var(--search-hint-text-decoration, var(--tag-text-decoration, inherit));
463
+ --_font-size: var(--search-hint-font-size, var(--tag-font-size, inherit));
464
+ font-size: var(--_font-size);
465
+ font-weight: var(--search-hint-font-weight, var(--tag-font-weight, inherit));
466
+ line-height: var(--search-hint-line-height, var(--tag-line-height, inherit));
467
+ letter-spacing: var(--search-hint-letter-spacing, var(--tag-letter-spacing, inherit));
468
+ text-indent: var(--search-hint-indent, var(--tag-indent, inherit));
469
+ font-variant: var(--search-hint-font-variant, var(--tag-font-variant, inherit));
470
+ text-align: var(--search-hint-text-align, var(--tag-text-align, inherit));
471
+ box-sizing: border-box;
472
+ --_padding: var(--search-hint-padding, var(--tag-padding, var(--padding, 4px)));
473
+ padding: var(--_padding);
474
+ border: var(--search-hint-border, var(--tag-border, var(--border, inherit)));
475
+ border-width: var(--search-hint-border-width, var(--tag-border-width, var(--__missing-border-width)));
476
+ border-style: var(--search-hint-border-style, var(--tag-border-style, var(--__missing-border-style)));
477
+ border-color: var(--search-hint-border-color, var(--tag-border-color, var(--__missing-border-color)));
478
+ border-top: var(--search-hint-border-top, var(--tag-border-top, var(--border-top, var(--search-hint-border, var(--tag-border, var(--border, none))))));
479
+ border-right: var(--search-hint-border-right, var(--tag-border-right, var(--border-right, var(--search-hint-border, var(--tag-border, var(--border, none))))));
480
+ border-bottom: var(--search-hint-border-bottom, var(--tag-border-bottom, var(--border-bottom, var(--search-hint-border, var(--tag-border, var(--border, none))))));
481
+ border-left: var(--search-hint-border-left, var(--tag-border-left, var(--border-left, var(--search-hint-border, var(--tag-border, var(--border, none))))));
482
+ border-radius: var(--search-hint-border-radius, var(--tag-border-radius, var(--border-radius, none)));
483
+ font-size: var(--search-hint-font-size, var(--font-size-small, 0.75rem));
484
+ padding: var(--search-hint-padding, 0.2em 0.55em);
485
+ font-variant-numeric: tabular-nums;
486
+ white-space: nowrap;
487
+ overflow: hidden;
488
+ text-overflow: ellipsis;
489
+ border-radius: var(--search-hint-radius, var(--border-radius, 4px));
490
+ --_box-shadow:
491
+ var(--search-hint-shadow-distance, var(--dropdown-shadow-distance, var(--shadow-distance, var(--space)))) var(--search-hint-shadow-distance, var(--dropdown-shadow-distance, var(--shadow-distance, var(--space)))) var(--search-hint-shadow-blur, var(--dropdown-shadow-blur, var(--shadow-blur, var(--space)))) var(--search-hint-shadow-color, var(--dropdown-shadow-color, var(--shadow-color, rgba(127, 127, 127, 0.4))));
492
+ box-shadow: var(--_box-shadow);
493
+ opacity: var(--search-hint-opacity, 0.95);
494
+ }
495
+
421
496
  .dropdown-content :global(button),
422
497
  .dropdown-content :global(a) {
423
498
  white-space: var(--dropdown-wrap-mode, wrap);
package/dist/index.d.ts CHANGED
@@ -23,6 +23,7 @@ import Form from "./layout/Form.svelte";
23
23
  import FormItem from "./layout/FormItem.svelte";
24
24
  import FormProvider from "./layout/FormProvider.svelte";
25
25
  import GridLayout from "./layout/GridLayout.svelte";
26
+ import GridRow from "./layout/GridRow.svelte";
26
27
  import Hero from "./layout/Hero.svelte";
27
28
  import Inline from "./layout/Inline.svelte";
28
29
  import MenuList from "./layout/MenuList.svelte";
@@ -48,7 +49,7 @@ export { Tag };
48
49
  export { Hero };
49
50
  export { TabItem };
50
51
  export { Button, ButtonLink, Checkbox, Input, Slider, Toggle, RadioButton, MiniButton, Select, Option, };
51
- export { Bar, ColumnContainer, Container, GridLayout, Inline, MenuList, Page, ResponsiveText, RowContainer, Stack, TabBar, };
52
+ export { Bar, ColumnContainer, Container, GridLayout, GridRow, Inline, MenuList, Page, ResponsiveText, RowContainer, Stack, TabBar, };
52
53
  export { TextLayout, Code, Accordion };
53
54
  export { Card, Tile, Form, FormItem, FormProvider, Fieldset };
54
55
  export { DataList, DataListItem };
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ import Form from "./layout/Form.svelte";
24
24
  import FormItem from "./layout/FormItem.svelte";
25
25
  import FormProvider from "./layout/FormProvider.svelte";
26
26
  import GridLayout from "./layout/GridLayout.svelte";
27
+ import GridRow from "./layout/GridRow.svelte";
27
28
  import Hero from "./layout/Hero.svelte";
28
29
  import Inline from "./layout/Inline.svelte";
29
30
  import MenuList from "./layout/MenuList.svelte";
@@ -48,7 +49,7 @@ export { Tag };
48
49
  export { Hero };
49
50
  export { TabItem };
50
51
  export { Button, ButtonLink, Checkbox, Input, Slider, Toggle, RadioButton, MiniButton, Select, Option, };
51
- export { Bar, ColumnContainer, Container, GridLayout, Inline, MenuList, Page, ResponsiveText, RowContainer, Stack, TabBar, };
52
+ export { Bar, ColumnContainer, Container, GridLayout, GridRow, Inline, MenuList, Page, ResponsiveText, RowContainer, Stack, TabBar, };
52
53
  export { TextLayout, Code, Accordion };
53
54
  export { Card, Tile, Form, FormItem, FormProvider, Fieldset };
54
55
  export { DataList, DataListItem };
@@ -11,7 +11,8 @@ export {};
11
11
  {@render children?.()}
12
12
  </div>
13
13
 
14
- <style>/* Warning: because we define a fallback
14
+ <style>@charset "UTF-8";
15
+ /* Warning: because we define a fallback
15
16
  media query, the media query can override the container
16
17
  if we stack a bunch of these in a row and aren't thoughtful about the order.
17
18
  Put min-width queries *after* max-width queries so that smaller
@@ -20,10 +21,58 @@ export {};
20
21
  /* Convenience groupings */
21
22
  .grid-layout {
22
23
  display: grid;
23
- grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--grid-layout-item-width, var(--item-width, 250px))), 1fr));
24
+ /* Tracks sized to the item, not `1fr`.
25
+
26
+ `1fr` splits the leftover width between the tracks, which turns it into
27
+ gutter *between* the items while the edges of the run stay tight -- three
28
+ cards in a 900px container end up sprung apart and pushed left. That is
29
+ rarely what anyone means by declaring an item width: they mean "lay these
30
+ out at this size", and the leftover belongs at the edges.
31
+
32
+ Sizing the track to the item gives exactly the `gap` that was asked for
33
+ between items, and hands the remainder to `justify-content: center` below,
34
+ which centres the whole run. That declaration has been here all along and
35
+ could never fire, because `1fr` tracks always consume the full width.
36
+
37
+ `min(100%, …)` keeps a track from outgrowing a narrow screen.
38
+
39
+ `auto-fit`, not `auto-fill`, and the difference is the whole ballgame for
40
+ centring. Both lay down as many tracks as fit the container; `auto-fill`
41
+ keeps the empty ones at full width, so three cards in a wide container
42
+ leave `justify-content` no slack to work with and pack against the left.
43
+ `auto-fit` collapses the tracks nothing landed in, so the run shrinks to
44
+ the items and centres. Three 200px items in a 1400px container: 72px left
45
+ and 696px right under `auto-fill`, 384px either side under `auto-fit`.
46
+
47
+ One caveat worth knowing, because it is not a bug we can fix here: an item
48
+ spanning `1 / -1` -- a `GridRow`, a heading -- occupies every track, so
49
+ there is nothing left for `auto-fit` to collapse and the run goes back to
50
+ full width and left-packed. A grid with full-width rows in it is a
51
+ full-width grid. Headings kept outside the grid, one grid per section,
52
+ keep the centring. */
53
+ grid-template-columns: repeat(auto-fit, min(100%, var(--grid-layout-item-width, var(--item-width, 250px))));
24
54
  gap: var(--grid-layout-gap, var(--gap, 8px));
25
- justify-content: var(--grid-justify-content, center);
26
- place-content: var(--grid-place-content, center);
55
+ /* Where the run of tracks sits in the grid box. Both axes as longhands,
56
+ because `place-content` is a shorthand and was declared after
57
+ `justify-content` here -- so it quietly overwrote it and
58
+ `--grid-justify-content` never did anything at all. Only
59
+ `--grid-place-content` worked, and it moves both axes at once.
60
+
61
+ Named `--grid-layout-*` like everything else, with the old spellings kept
62
+ as fallbacks so anything already setting them keeps working -- and
63
+ `--grid-justify-content` starts working for the first time. */
64
+ justify-content: var(--grid-layout-justify-content, var(--justify-content, var(--grid-justify-content, var(--grid-place-content, center))));
65
+ align-content: var(--grid-layout-align-content, var(--align-content, var(--grid-place-content, center)));
66
+ /* Where an item sits *inside* its track -- a separate question from where
67
+ the tracks sit in the grid, which is `justify-content` above.
68
+
69
+ Mostly moot now that a track is sized to the item: there is usually no
70
+ room inside a track to sit anywhere. It still matters when a child is
71
+ narrower than the track it was handed, which happens whenever a component
72
+ carries its own width -- a `Tile` with a `--tile-width` smaller than the
73
+ grid's `--item-width`. The default stays `stretch` so anything that can
74
+ use its whole track still gets it. */
75
+ justify-items: var(--grid-layout-justify-items, var(--justify-items, stretch));
27
76
  box-sizing: border-box;
28
77
  --_padding: var(--grid-layout-padding, var(--padding, 4px));
29
78
  padding: var(--_padding);
@@ -38,10 +87,56 @@ export {};
38
87
  border-radius: var(--grid-layout-border-radius, var(--border-radius, none));
39
88
  }
40
89
 
90
+ /* Escape hatch for the things in a grid that are not items in the run: a
91
+ section heading, or a note explaining why the grid is empty. Both want the
92
+ whole row rather than one item-width column with tiles beside them.
93
+
94
+ Declared here so it travels with the component, and matched as a descendant
95
+ rather than a child on purpose -- a component handed a CSS custom property
96
+ is wrapped in a `<svelte-css-wrapper>`, so it is a grandchild of the grid
97
+ even though it is still the grid item. `grid-column` is inert on anything
98
+ that is not a grid item, so the looser match costs nothing. */
99
+ .grid-layout :global(.grid-full-row) {
100
+ grid-column: 1/-1;
101
+ /* Spanning is only half of it. How an item fills the tracks it spans is
102
+ `justify-items` on the grid, which a tile grid sets to `center` -- and a
103
+ centred block with no width of its own shrinks to fit its content, so the
104
+ row it spans renders as a short centred blob rather than a full-width
105
+ heading. Saying `stretch` here makes the class mean the same thing in
106
+ every grid, whatever the container prefers for ordinary items.
107
+
108
+ A consumer spanning their own item -- `grid-column: span 2` -- owns this
109
+ decision themselves and will want `justify-self` alongside it. */
110
+ justify-self: var(--grid-row-justify, var(--justify, stretch));
111
+ /* And the typography has to be let go of too, which is the part that took a
112
+ while to see. A heading inside a typographic container gets a readable
113
+ measure and auto side margins -- `max-width: var(--line-width)`, 42rem by
114
+ default, with `margin-inline: auto` -- so it sits as a centred column of
115
+ text. Sensible for prose, wrong for a band that is supposed to run the
116
+ width of a grid.
117
+
118
+ Worse, an auto inline margin makes `justify-self: stretch` do nothing at
119
+ all: the spec only stretches a box whose inline margins are both
120
+ non-auto, so the heading fell back to shrink-to-fit. "Sections" measured
121
+ 74px in a 1064px row, and computed style reports the *used* margin as
122
+ `0px`, so it does not look like margins are the culprit until you set them
123
+ and watch the width jump. */
124
+ max-width: none;
125
+ margin-inline: 0;
126
+ }
127
+
41
128
  .card-grid {
42
129
  --item-width: var(--card-width, 400px);
43
130
  }
44
131
 
45
132
  .tile-grid {
46
- --item-width: var(--tile-width, 200px);
133
+ /* Has to be the width a Tile actually defaults to, not a round number near
134
+ it. Tile says `calc(var(--space-lg) * 24)`, which is 192px; this said
135
+ 200px, so every tile sat 4px inside its own track and a full-width row
136
+ did not line up with the tiles under it. */
137
+ --item-width: var(--tile-width, calc(var(--space-lg) * 24));
138
+ /* The track now matches `--item-width`, so this only bites when a Tile is
139
+ narrower than the track it was handed -- a `--tile-width` set smaller than
140
+ the grid's item width, say. */
141
+ --justify-items: center;
47
142
  }</style>
@@ -0,0 +1,23 @@
1
+ <script lang="ts">import { injectVars } from "../util";
2
+ let { justify = null, center = false, children, ...restProps } = $props();
3
+ const resolvedJustify = $derived(justify ?? (center ? "center" : null));
4
+ const inlineStyle = $derived(restProps.style);
5
+ const elementProps = $derived.by(() => {
6
+ const { style: _, ...rest } = restProps;
7
+ return rest;
8
+ });
9
+ const style = $derived(injectVars({ justify: resolvedJustify, ...elementProps }, "grid-row", ["justify"]) +
10
+ (inlineStyle ?? ""));
11
+ </script>
12
+
13
+ <div class="grid-full-row grid-row" {...elementProps} {style}>
14
+ {@render children?.()}
15
+ </div>
16
+
17
+ <style>.grid-row {
18
+ /* Spanning, stretching and the margin reset all live on `.grid-full-row`,
19
+ in GridLayout. What this adds is releasing the measure for the content:
20
+ without it a heading in here is clamped to `--line-width` and centred by
21
+ its own auto margins, so the band spans and the words inside it do not. */
22
+ --line-width: none;
23
+ }</style>
@@ -0,0 +1,24 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ /**
4
+ * A full-width band inside a GridLayout: a section heading, an explanation, a
5
+ * note saying why the grid is empty. Anything that belongs *in* the run of
6
+ * items without being one of them.
7
+ *
8
+ * Carries `.grid-full-row`, which is the same thing as a class for when you
9
+ * already have an element and would rather not nest a div inside it. The
10
+ * difference is `--line-width`: a heading or paragraph placed inside this
11
+ * component is meant to run the width of the band, so the readable measure
12
+ * that typographic containers impose is lifted for its contents too. Put the
13
+ * class on your own heading instead and the heading keeps that measure.
14
+ */
15
+ type Props = {
16
+ /** How the row sits in the span it covers: start, center, end, stretch. */
17
+ justify?: string | null;
18
+ /** Shorthand for `justify="center"`, as `Stack` takes `center`. */
19
+ center?: boolean;
20
+ children?: Snippet;
21
+ } & HTMLAttributes<HTMLDivElement>;
22
+ declare const GridRow: import("svelte").Component<Props, {}, "">;
23
+ type GridRow = ReturnType<typeof GridRow>;
24
+ export default GridRow;
@@ -139,9 +139,12 @@ const style = $derived(injectVars(restProps, "menu", [
139
139
  .menu :global(li.interactive):focus-visible,
140
140
  .menu :global(li[role="button"]):focus-visible,
141
141
  .menu :global(li[tabindex]:not([tabindex="-1"])):focus-visible {
142
+ filter: var(--menu-item-hover-filter, var(--hover-filter, brightness(1.05)));
143
+ transform: var(--menu-item-hover-transform, var(--hover-transform, none));
144
+ 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%)));
145
+ box-shadow: var(--menu-item-hover-box-shadow, var(--hover-box-shadow, var(--_box-shadow, none)));
142
146
  outline: var(--focus-color, -webkit-focus-ring-color) auto 1px;
143
- outline-offset: var(--focus-outline-offset, 2px);
144
- box-shadow: var(--focus-ring-box-shadow, 0 0 0 3px var(--focus-shadow-color, rgba(100, 150, 250, 0.5)));
147
+ outline-offset: var(--focus-inset-outline-offset, -3px);
145
148
  }
146
149
 
147
150
  .menu :global(li > .subheader) {
@@ -205,9 +208,12 @@ const style = $derived(injectVars(restProps, "menu", [
205
208
  }
206
209
 
207
210
  .menu :global(a):focus-visible, .menu :global(button):focus-visible, .menu :global(input[type="submit"]):focus-visible, .menu :global(.button):focus-visible {
211
+ filter: var(--menu-item-hover-filter, var(--hover-filter, brightness(1.05)));
212
+ transform: var(--menu-item-hover-transform, var(--hover-transform, none));
213
+ 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%)));
214
+ box-shadow: var(--menu-item-hover-box-shadow, var(--hover-box-shadow, var(--_box-shadow, none)));
208
215
  outline: var(--focus-color, -webkit-focus-ring-color) auto 1px;
209
- outline-offset: var(--focus-outline-offset, 2px);
210
- box-shadow: var(--focus-ring-box-shadow, 0 0 0 3px var(--focus-shadow-color, rgba(100, 150, 250, 0.5)));
216
+ outline-offset: var(--focus-inset-outline-offset, -3px);
211
217
  }
212
218
 
213
219
  .menu :global(a), .menu :global(button), .menu :global(input[type="submit"]), .menu :global(.button) {
@@ -1,5 +1,4 @@
1
- <script lang="ts">import { run } from "svelte/legacy";
2
- let { sticky = false, column_widths = null, thead, tbody, children, } = $props();
1
+ <script lang="ts">let { sticky = false, column_widths = null, thead, tbody, children, } = $props();
3
2
  // svelte-ignore state_referenced_locally
4
3
  let columns = $state(column_widths || []);
5
4
  /* Code for syncing column widths for scrolling table solution */
@@ -205,6 +204,10 @@ function setupWidths() {
205
204
  container queries don't get their styles overridden by large media queries.
206
205
  */
207
206
  /* Convenience groupings */
207
+ table:has(colgroup) {
208
+ table-layout: fixed;
209
+ }
210
+
208
211
  table {
209
212
  --link-bg: var(--table-link-bg, var(--surface-link-bg, inherit));
210
213
  --link-fg: var(--table-link-fg, var(--surface-link-fg, inherit));
@@ -621,7 +624,7 @@ table :global(tr > th:first-child:has(~ td)) {
621
624
  position: sticky;
622
625
  top: 0;
623
626
  z-index: 1;
624
- background: var(--white);
627
+ background: var(--_background-color, var(--white, #fff));
625
628
  margin-inline-start: auto;
626
629
  margin-inline-end: auto;
627
630
  }
@@ -637,7 +640,7 @@ table :global(tr > th:first-child:has(~ td)) {
637
640
  }
638
641
 
639
642
  .veil {
640
- background-color: var(--white, #fff);
643
+ background-color: var(--_background-color, var(--white, #fff));
641
644
  position: sticky;
642
645
  top: -2em;
643
646
  height: 3em;
@@ -808,4 +811,17 @@ table :global(td[tabindex]):focus-visible {
808
811
  outline: var(--focus-color, -webkit-focus-ring-color) auto 1px;
809
812
  outline-offset: var(--focus-outline-offset, 2px);
810
813
  box-shadow: var(--focus-ring-box-shadow, 0 0 0 3px var(--focus-shadow-color, rgba(100, 150, 250, 0.5)));
814
+ }
815
+
816
+ /* Apply thick border to column 1 in BOTH header and body tables,
817
+ BUT ONLY IF the scrolling table contains row headers in its tbody */
818
+ .scrolling-table:has(.scrolling-table-body :global(tbody > tr > th:first-child)) .fixed-table-head :global(tr > th:first-child),
819
+ .scrolling-table:has(.scrolling-table-body :global(tbody > tr > th:first-child)) .scrolling-table-body :global(tbody > tr > th:first-child) {
820
+ border-right: var(--table-first-row-bottom-border, var(--table-thick-border, 3px) solid var(--table-first-row-border-color, var(--secondary-bg))) !important;
821
+ }
822
+
823
+ /* 2. Standard non-scrolling single <table> setup */
824
+ table:has(:global(tbody > tr > th:first-child)) :global(thead > tr > th:first-child),
825
+ table:has(:global(tbody > tr > th:first-child)) :global(tbody > tr > th:first-child) {
826
+ border-right: var(--table-first-row-bottom-border, var(--table-thick-border, 3px) solid var(--table-first-row-border-color, var(--secondary-bg)));
811
827
  }</style>
@@ -1,28 +1,54 @@
1
- <script lang="ts">let { checked = $bindable(false), ...props } = $props();
1
+ <script lang="ts">import { injectVars } from "../util";
2
+ let { checked = $bindable(false), ...props } = $props();
3
+ /* Props to CSS variables, the way Stack, Card, Tag and the rest do it, so a
4
+ Tile can be aimed with `<Tile center>` rather than only a raw variable. */
5
+ const styleProps = $derived.by(() => {
6
+ const { justify = null, align = null, center = false, bg = null, fg = null, padding = null, width = null, height = null, } = props;
7
+ return {
8
+ bg,
9
+ fg,
10
+ padding,
11
+ width,
12
+ height,
13
+ justify: justify ?? (center ? "center" : null),
14
+ align: align ?? (center ? "center" : null),
15
+ };
16
+ });
17
+ const style = $derived(injectVars(styleProps, "tile", [
18
+ "bg",
19
+ "fg",
20
+ "padding",
21
+ "width",
22
+ "height",
23
+ "justify",
24
+ "align",
25
+ ]) + (props.style ?? ""));
26
+ /** Everything that is ours rather than the element's. */
27
+ function stripOwnProps(value) {
28
+ const { selectable: _selectable, interactive: _interactive, children: _children, justify: _justify, align: _align, center: _center, bg: _bg, fg: _fg, padding: _padding, width: _width, height: _height, style: _style, ...rest } = value;
29
+ return rest;
30
+ }
2
31
  function getSelectableInputProps(value) {
3
- const { selectable: _selectable, interactive: _interactive, children: _children, ...inputProps } = value;
4
- return inputProps;
32
+ return stripOwnProps(value);
5
33
  }
6
34
  function getInteractiveButtonProps(value) {
7
- const { selectable: _selectable, interactive: _interactive, children: _children, ...buttonProps } = value;
8
- return buttonProps;
35
+ return stripOwnProps(value);
9
36
  }
10
- export {};
11
37
  </script>
12
38
 
13
39
  {#if props.selectable}
14
- <label class="tile">
40
+ <label class="tile" {style}>
15
41
  <div class="checkbox">
16
42
  <input type="checkbox" bind:checked {...getSelectableInputProps(props)} />
17
43
  </div>
18
44
  {@render props.children?.()}
19
45
  </label>
20
46
  {:else if props.interactive}
21
- <button class="tile" {...getInteractiveButtonProps(props)}>
47
+ <button class="tile" {style} {...getInteractiveButtonProps(props)}>
22
48
  {@render props.children?.()}
23
49
  </button>
24
50
  {:else}
25
- <div class="tile">
51
+ <div class="tile" {style} {...stripOwnProps(props as RenderProps)}>
26
52
  {@render props.children?.()}
27
53
  </div>
28
54
  {/if}
@@ -1,6 +1,18 @@
1
1
  import type { Snippet } from "svelte";
2
2
  import type { HTMLAttributes, HTMLButtonAttributes, HTMLInputAttributes } from "svelte/elements";
3
- type BaseProps = {
3
+ import type { BaseStyleProps } from "../types";
4
+ type BaseProps = BaseStyleProps & {
5
+ /** Distribution along the tile's own axis, which is vertical. */
6
+ justify?: string | null;
7
+ /** Cross-axis alignment, which is horizontal. Centred already. */
8
+ align?: string | null;
9
+ /**
10
+ * Shorthand for centring the tile's contents. Sets both axes, unlike
11
+ * `Stack`'s `center`, because a Tile already centres horizontally -- setting
12
+ * only the cross axis here would be a no-op and `<Tile center>` would look
13
+ * broken.
14
+ */
15
+ center?: boolean;
4
16
  children?: Snippet;
5
17
  };
6
18
  type StaticTileProps = BaseProps & HTMLAttributes<HTMLDivElement> & {
@@ -1,4 +1,5 @@
1
- <script lang="ts">import ReviewThemeSwitcher from "./ReviewThemeSwitcher.svelte";
1
+ <script lang="ts">import { base } from "$app/paths";
2
+ import ReviewThemeSwitcher from "./ReviewThemeSwitcher.svelte";
2
3
  import { reviewAppScenarios } from "./scenarios";
3
4
  let { data } = $props();
4
5
  const scenario = $derived(reviewAppScenarios[data.scenario]);
@@ -10,8 +11,8 @@ const scenario = $derived(reviewAppScenarios[data.scenario]);
10
11
 
11
12
  <div class="review-page-header">
12
13
  <div class="review-page-links">
13
- <a href={`/svelte-contain-css/review/${data.theme}/`}>{data.themeLabel} routes</a>
14
- <a href={`/svelte-contain-css/review/${data.theme}/app/`}>App scenarios</a>
14
+ <a href={`${base}/review/${data.theme}/`}>{data.themeLabel} routes</a>
15
+ <a href={`${base}/review/${data.theme}/app/`}>App scenarios</a>
15
16
  </div>
16
17
 
17
18
  <ReviewThemeSwitcher theme={data.theme} />
@@ -1,4 +1,5 @@
1
- <script lang="ts">import ReviewIndexShell from "./ReviewIndexShell.svelte";
1
+ <script lang="ts">import { base } from "$app/paths";
2
+ import ReviewIndexShell from "./ReviewIndexShell.svelte";
2
3
  import { reviewThemeLabels } from "./themes";
3
4
  </script>
4
5
 
@@ -13,7 +14,7 @@ import { reviewThemeLabels } from "./themes";
13
14
  <ul>
14
15
  {#each Object.entries(reviewThemeLabels) as [theme, label]}
15
16
  <li>
16
- <a href={`/svelte-contain-css/review/${theme}/`}>{label}</a>
17
+ <a href={`${base}/review/${theme}/`}>{label}</a>
17
18
  </li>
18
19
  {/each}
19
20
  </ul>
@@ -1,4 +1,5 @@
1
- <script lang="ts">import ReviewIndexShell from "./ReviewIndexShell.svelte";
1
+ <script lang="ts">import { base } from "$app/paths";
2
+ import ReviewIndexShell from "./ReviewIndexShell.svelte";
2
3
  import ReviewThemeSwitcher from "./ReviewThemeSwitcher.svelte";
3
4
  import { reviewAppScenarios } from "./scenarios";
4
5
  let { theme, themeLabel } = $props();
@@ -17,7 +18,7 @@ let { theme, themeLabel } = $props();
17
18
  <ul>
18
19
  {#each Object.entries(reviewAppScenarios) as [scenario, entry]}
19
20
  <li>
20
- <a href={`/svelte-contain-css/review/${theme}/app/${scenario}`}>
21
+ <a href={`${base}/review/${theme}/app/${scenario}`}>
21
22
  {entry.title}
22
23
  </a>
23
24
  </li>
@@ -1,4 +1,5 @@
1
- <script lang="ts">import ReviewIndexShell from "./ReviewIndexShell.svelte";
1
+ <script lang="ts">import { base } from "$app/paths";
2
+ import ReviewIndexShell from "./ReviewIndexShell.svelte";
2
3
  import ReviewThemeSwitcher from "./ReviewThemeSwitcher.svelte";
3
4
  import { reviewAppScenarios } from "./scenarios";
4
5
  let { theme, themeLabel } = $props();
@@ -18,7 +19,7 @@ let { theme, themeLabel } = $props();
18
19
  <h2>Groups</h2>
19
20
  <ul>
20
21
  <li>
21
- <a href={`/svelte-contain-css/review/${theme}/app/`}>App scenarios</a>
22
+ <a href={`${base}/review/${theme}/app/`}>App scenarios</a>
22
23
  </li>
23
24
  </ul>
24
25
  </section>
@@ -28,7 +29,7 @@ let { theme, themeLabel } = $props();
28
29
  <ul>
29
30
  {#each Object.entries(reviewAppScenarios) as [scenario, entry]}
30
31
  <li>
31
- <a href={`/svelte-contain-css/review/${theme}/app/${scenario}`}>
32
+ <a href={`${base}/review/${theme}/app/${scenario}`}>
32
33
  {entry.title}
33
34
  </a>
34
35
  </li>
@@ -1,4 +1,5 @@
1
- <script lang="ts">import { page } from "$app/state";
1
+ <script lang="ts">import { base } from "$app/paths";
2
+ import { page } from "$app/state";
2
3
  import { reviewThemeLabels, reviewThemeNames, } from "./themes";
3
4
  let { theme } = $props();
4
5
  const themeLinks = $derived.by(() => {
@@ -16,7 +17,7 @@ const themeLinks = $derived.by(() => {
16
17
  </script>
17
18
 
18
19
  <nav class="review-theme-switcher" aria-label="Review theme switcher">
19
- <a class="utility-link" href="/svelte-contain-css/review/">All Themes</a>
20
+ <a class="utility-link" href={`${base}/review/`}>All Themes</a>
20
21
 
21
22
  {#each themeLinks as item}
22
23
  <a
@@ -109,6 +109,33 @@
109
109
  }
110
110
  }
111
111
 
112
+ // Focus ring for controls living inside an overflow:hidden container
113
+ // (menus, tab bars). Drawn with a negative outline-offset so it sits inside
114
+ // the control and is not clipped by the container. Defined after `clickable`
115
+ // so `clickable-hover-affordance` is in scope.
116
+ @mixin focus-ring-inset {
117
+ outline: var(--focus-color, -webkit-focus-ring-color) auto 1px;
118
+ outline-offset: var(--focus-inset-outline-offset, -3px);
119
+ }
120
+
121
+ // Clip-safe focus indicator: just the inset ring, no fill change. For
122
+ // controls that already carry their own resting/active styling (tabs).
123
+ @mixin focusable-inset($prefixes...) {
124
+ &:focus-visible {
125
+ @include focus-ring-inset();
126
+ }
127
+ }
128
+
129
+ // As `focusable-inset`, plus the hover affordance for the fill, so keyboard
130
+ // focus reads the same as pointer hover. For controls with no resting
131
+ // visual weight of their own (menu items).
132
+ @mixin focusable-as-hover($prefixes...) {
133
+ &:focus-visible {
134
+ @include clickable-hover-affordance($prefixes...);
135
+ @include focus-ring-inset();
136
+ }
137
+ }
138
+
112
139
  @mixin custom-scrollbar($prefixes...) {
113
140
  overflow-y: auto;
114
141
 
@@ -20,8 +20,14 @@
20
20
  --menu-item-hover-filter-custom: ;
21
21
  --menu-item-hover-filter: var(--menu-item-hover-filter-brightness-hack,)
22
22
  var(--menu-item-hover-filter-custom,);
23
- /* Default menu items to the menu surface so row-wide filters read clearly. */
24
- --menu-item-bg: var(--menu-bg, var(--bg));
23
+ /* NB: do NOT default --menu-item-bg here. Declared at :root it would freeze
24
+ to var(--bg) (--menu-bg is not set yet at :root) and then win the
25
+ color-props(menu-item, menu, ...) chain ahead of a per-instance --menu-bg,
26
+ breaking overrides like <Menu --menu-bg="#111">. The chain already ends at
27
+ --bg, and color-props now computes an always-opaque --_background-color on
28
+ the element (evaluated where --menu-bg is live), which the hover/active
29
+ filter affordances derive from -- so the "give filters an opaque surface"
30
+ goal is met structurally without a frozen root default. */
25
31
  --menu-item-active-filter-custom: ;
26
32
  --menu-item-active-filter: var(--menu-item-active-filter-custom,);
27
33
  /* --button-hover-transform: var(var(--button-hover-transform));
@@ -36,7 +36,10 @@
36
36
  --surface-fg: var(--fg);
37
37
  --surface-link-fg: var(--link-fg, var(--primary-bg));
38
38
  --container-link-fg: var(--surface-link-fg);
39
- --menu-item-bg: var(--menu-bg);
39
+ /* --menu-item-bg intentionally not defaulted at :root -- see the note in
40
+ affordances.css. The color-props(menu-item, menu, button, control) chain
41
+ resolves it to --menu-bg / --bg at the element, keeping per-instance
42
+ --menu-bg overrides working. */
40
43
  --focus-color: var(--material-color-blue-a400);
41
44
  --tooltip-border: none;
42
45
  }
@@ -63,7 +63,10 @@
63
63
  --surface-fg: var(--fg);
64
64
  --surface-link-fg: var(--link-fg, var(--primary-bg));
65
65
  --container-link-fg: var(--surface-link-fg);
66
- --menu-item-bg: var(--menu-bg);
66
+ /* --menu-item-bg intentionally not defaulted at :root -- see the note in
67
+ affordances.css. The color-props(menu-item, menu, button, control) chain
68
+ resolves it to --menu-bg / --bg at the element, keeping per-instance
69
+ --menu-bg overrides working. */
67
70
  --focus-color: var(--material-color-blue-a400);
68
71
  --tooltip-border: none;
69
72
  }
@@ -424,8 +427,14 @@ a {
424
427
  --menu-item-hover-filter-custom: ;
425
428
  --menu-item-hover-filter: var(--menu-item-hover-filter-brightness-hack,)
426
429
  var(--menu-item-hover-filter-custom,);
427
- /* Default menu items to the menu surface so row-wide filters read clearly. */
428
- --menu-item-bg: var(--menu-bg, var(--bg));
430
+ /* NB: do NOT default --menu-item-bg here. Declared at :root it would freeze
431
+ to var(--bg) (--menu-bg is not set yet at :root) and then win the
432
+ color-props(menu-item, menu, ...) chain ahead of a per-instance --menu-bg,
433
+ breaking overrides like <Menu --menu-bg="#111">. The chain already ends at
434
+ --bg, and color-props now computes an always-opaque --_background-color on
435
+ the element (evaluated where --menu-bg is live), which the hover/active
436
+ filter affordances derive from -- so the "give filters an opaque surface"
437
+ goal is met structurally without a frozen root default. */
429
438
  --menu-item-active-filter-custom: ;
430
439
  --menu-item-active-filter: var(--menu-item-active-filter-custom,);
431
440
  /* --button-hover-transform: var(var(--button-hover-transform));
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "contain-css-svelte",
3
- "version": "1.1.9",
3
+ "version": "1.1.11",
4
+ "license": "MIT",
4
5
  "scripts": {
5
6
  "capture:review": "node scripts/capture-review.mjs",
6
7
  "capture:review:matrix": "node scripts/capture-review-matrix.mjs",