contain-css-svelte 1.1.10 → 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.
@@ -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);
@@ -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>
@@ -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,6 @@
1
1
  {
2
2
  "name": "contain-css-svelte",
3
- "version": "1.1.10",
3
+ "version": "1.1.11",
4
4
  "license": "MIT",
5
5
  "scripts": {
6
6
  "capture:review": "node scripts/capture-review.mjs",