mnfst 0.5.157 → 0.5.159

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  @layer components {
4
4
 
5
- /* Field shell — inherits input tokens so it reads as a normal field */
5
+ /* Wrapper */
6
6
  :where(.combobox):not(.unstyle) {
7
7
  display: flex;
8
8
  flex-wrap: wrap;
@@ -40,16 +40,8 @@
40
40
  pointer-events: none
41
41
  }
42
42
 
43
- /* Inner typing surface. The :not(.unstyle) lifts specificity above
44
- manifest.input.css, which sorts after this file in the bundled CSS and
45
- would otherwise re-apply its own width/background/hover to the editor. */
43
+ /* Inner typing surface */
46
44
  &> :where(input:not([type=hidden]), textarea):not(.unstyle) {
47
- /* flex-basis (not min-width) sets the wrap threshold: the editor fills
48
- the rest of the row, and once less than 7rem is left it wraps to its
49
- own line. min-width:0 is REQUIRED — a flex item's default min-width:auto
50
- is its content size, and WebKit enforces it by crushing the
51
- flex-shrink:0 chips to an ellipsis. Resetting it to 0 lets the editor
52
- shrink/wrap instead, so the chips keep their room. */
53
45
  flex: 1 1 7rem;
54
46
  min-width: 0;
55
47
  width: auto;
@@ -75,7 +67,7 @@
75
67
  /* Chips */
76
68
  :where(.combobox-chip):not(.unstyle) {
77
69
  display: inline-flex;
78
- flex: 0 0 auto; /* hold content width; don't grow or shrink — overflow:hidden below otherwise lets flex squeeze chips to an ellipsis (notably Safari) */
70
+ flex: 0 0 auto;
79
71
  align-items: center;
80
72
  gap: var(--spacing, 0.25rem);
81
73
  max-width: 100%;
@@ -117,6 +109,12 @@
117
109
  }
118
110
  }
119
111
 
112
+ /* Non-removable */
113
+ &[data-locked] {
114
+ padding-inline-end: calc(var(--spacing, 0.25rem) * 2);
115
+ cursor: default
116
+ }
117
+
120
118
  /* Failed validation */
121
119
  &[aria-invalid="true"] {
122
120
  color: var(--color-negative-inverse, oklch(44.4% 0.177 26.899));
@@ -124,9 +122,8 @@
124
122
  }
125
123
  }
126
124
 
127
- /* Trigger (editor:none button mode) — transparent; the shell is the field.
128
- Fills the row like the input editor, with a select-style caret. */
129
- :where(.combobox) > button:not(.unstyle) {
125
+ /* Button trigger */
126
+ :where(.combobox)>button:not(.unstyle) {
130
127
  flex: 1 1 auto;
131
128
  align-self: stretch;
132
129
  min-width: 7rem;
@@ -155,10 +152,7 @@
155
152
  }
156
153
  }
157
154
 
158
- /* Chip-less field (single input, textarea, or button trigger): the editor IS
159
- the field — it fills the whole wrapper and carries the padding, so the click
160
- target, caret and text selection align with the field edges instead of
161
- floating inside the wrapper's padding. */
155
+ /* Chip-less */
162
156
  :where(.combobox):has(> :where(input:not([type=hidden]), textarea, button):not(.unstyle)):not(:has(.combobox-chip)) {
163
157
  padding: 0;
164
158
 
@@ -169,15 +163,10 @@
169
163
  }
170
164
  }
171
165
 
172
- /* Listbox — base look comes from menu[popover]; these are combobox-only extras.
173
- Selectors deliberately AVOID :where() on the option part so they out-specify
174
- dropdown.css's `menu li { display: inline-flex }` (0,1,0), which sorts after
175
- this file in the bundle and would otherwise keep filtered options visible. */
166
+ /* Listbox */
176
167
  :where(menu[role=listbox]):not(.unstyle) {
177
168
 
178
- /* Active descendant — only shown while the keyboard is driving (data-kbd).
179
- On a mouse-opened menu the hover state alone highlights, so the first
180
- option isn't left with a persistent background. */
169
+ /* Active descendant */
181
170
  &[data-kbd] [role=option][aria-current="true"] {
182
171
  color: var(--color-field-inverse, oklch(43.9% 0 0));
183
172
  background-color: var(--color-field-surface, color-mix(in oklch, oklch(20.5% 0 0) 10%, transparent))
@@ -81,6 +81,7 @@ function initializeComboboxPlugin() {
81
81
  value: li.dataset.value != null ? li.dataset.value : li.textContent.trim(),
82
82
  label: li.dataset.label || li.textContent.trim(),
83
83
  pattern: li.dataset.pattern || null,
84
+ locked: li.hasAttribute('data-locked'),
84
85
  html: li.innerHTML
85
86
  }));
86
87
  }
@@ -88,11 +89,53 @@ function initializeComboboxPlugin() {
88
89
  value: o.value || o.textContent.trim(),
89
90
  label: o.textContent.trim() || o.value,
90
91
  pattern: o.getAttribute('data-pattern') || null,
92
+ locked: o.hasAttribute('data-locked'),
91
93
  html: null
92
94
  }));
93
95
  }
94
96
 
97
+ // Take ownership of x-model: capture the expression and strip the attribute so
98
+ // Alpine's own model directive never binds the editor. In chips mode the editor is
99
+ // a transient typing buffer — Alpine's x-model would dump the bound array into it
100
+ // and write partial typing back to the model. We read/write the model ourselves.
101
+ function captureModel(el) {
102
+ if (el.__cbModelExpr !== undefined) return;
103
+ let attr = null;
104
+ for (const a of Array.from(el.attributes)) {
105
+ if (a.name === 'x-model' || a.name.indexOf('x-model.') === 0) { attr = a.name; break; }
106
+ }
107
+ el.__cbModelExpr = attr ? el.getAttribute(attr) : null;
108
+ if (attr) el.removeAttribute(attr);
109
+ }
110
+
111
+ // NB: match by attribute-name prefix, not the `[x-combobox]` CSS selector — the
112
+ // directive is almost always written with modifiers (x-combobox.multiple.chips),
113
+ // a literal attribute name that `[x-combobox]` does NOT match.
114
+ function isComboboxEl(el) {
115
+ if (!el.attributes) return false;
116
+ for (const a of el.attributes) if (a.name === 'x-combobox' || a.name.indexOf('x-combobox.') === 0) return true;
117
+ return false;
118
+ }
119
+ function stripModels(root) {
120
+ if (!root || root.nodeType !== 1) return;
121
+ if (isComboboxEl(root)) captureModel(root);
122
+ const all = root.getElementsByTagName ? root.getElementsByTagName('*') : [];
123
+ for (let i = 0; i < all.length; i++) if (isComboboxEl(all[i])) captureModel(all[i]);
124
+ }
125
+
126
+ // Strip x-model before Alpine ever binds it. The initial static DOM is handled here
127
+ // (this runs on alpine:init, before the walk); subtrees mounted later — x-markdown,
128
+ // components — are handled by wrapping the public initTree they call to mount, so the
129
+ // attribute is gone before Alpine's model directive looks for it.
130
+ stripModels(document.body);
131
+ if (typeof Alpine.initTree === 'function' && !Alpine.__cbInitTreeWrapped) {
132
+ Alpine.__cbInitTreeWrapped = true;
133
+ const origInitTree = Alpine.initTree.bind(Alpine);
134
+ Alpine.initTree = (node, ...rest) => { stripModels(node); return origInitTree(node, ...rest); };
135
+ }
136
+
95
137
  Alpine.directive('combobox', (el, { modifiers, expression }, { cleanup }) => {
138
+ captureModel(el); // fallback for any path that bypasses the above
96
139
  // Build after the current tick so sibling sources (datalist/menu) exist.
97
140
  setTimeout(() => build(el, modifiers, expression || '', cleanup), 0);
98
141
  });
@@ -101,6 +144,25 @@ function initializeComboboxPlugin() {
101
144
  if (el.__mnfstCombobox) return;
102
145
  el.__mnfstCombobox = true;
103
146
 
147
+ // If a mount path bypassed the pre-emptive strip (x-if / x-for mount via Alpine's
148
+ // INTERNAL initTree, which our public-initTree wrapper can't see) and Alpine's
149
+ // native x-model already bound the editor, neutralize it here. Otherwise its
150
+ // value-sync would bleed the raw model into the editor ("a,b") and its input
151
+ // listener would write partial typing back to the model. We own read/write below,
152
+ // so make both native paths no-ops. captureModel still recovered the expression.
153
+ if (el._x_model) {
154
+ el._x_forceModelUpdate = function () { }; // kill the model→editor value-sync (no bleed)
155
+ // Remove Alpine's input/change listener that writes the editor back to the model
156
+ // (a local closure — overriding _x_model.set isn't enough). _x_removeModelListeners
157
+ // is Alpine's own removal hook for exactly this.
158
+ try {
159
+ const rm = el._x_removeModelListeners;
160
+ if (rm) Object.keys(rm).forEach(k => { try { rm[k](); } catch (_) { } });
161
+ } catch (_) { }
162
+ el._x_model.set = function () { }; // extra safety for any path that calls it
163
+ if (el.value) el.value = '';
164
+ }
165
+
104
166
  // Sweep generated menus left orphaned by a prior render — their controlling
105
167
  // editor is gone (a SPA x-markdown re-render) or never hydrated (duplicates
106
168
  // an old prerender baked into <body>). Either way, no connected editor points
@@ -198,6 +260,41 @@ function initializeComboboxPlugin() {
198
260
  let selected = adopt && seedSelected ? seedSelected : [];
199
261
  const isSelected = (v) => selected.some(s => String(s.value).toLowerCase() === String(v).toLowerCase());
200
262
  const atCap = () => multiple && selected.length >= max;
263
+ const labelFor = (v) => {
264
+ const o = options.find(o => String(o.value).toLowerCase() === String(v).toLowerCase());
265
+ return o ? o.label : String(v);
266
+ };
267
+
268
+ // ----- Locked values (non-removable chips) -----
269
+ // From a `locked: [...]` config list (re-evaluated reactively below) and/or a
270
+ // `data-locked` flag on individual options. Locked chips keep their × hidden and
271
+ // refuse removal while staying selected.
272
+ const lockedFromOptions = options.filter(o => o.locked).map(o => String(o.value).toLowerCase());
273
+ const computeLocked = (cfgObj) => {
274
+ const set = new Set(lockedFromOptions);
275
+ const raw = cfgObj && cfgObj.locked != null ? cfgObj.locked : cfg.locked;
276
+ const list = Array.isArray(raw) ? raw : (raw != null ? String(raw).split(',') : []);
277
+ list.forEach(v => set.add(String(v).trim().toLowerCase()));
278
+ return set;
279
+ };
280
+ let lockedSet = computeLocked();
281
+ const isLocked = (v) => lockedSet.has(String(v).toLowerCase());
282
+
283
+ // ----- Reactive model (x-model) — captured + stripped pre-walk, owned here -----
284
+ // The bound value may be a token array or a CSV string; we preserve whichever the
285
+ // author used (default array for .multiple). Chips render labels; the model carries
286
+ // values/tokens. Read/effect/set are wired near mount, once render() exists.
287
+ const modelExpr = el.__cbModelExpr || null;
288
+ let modelArrayShape = null; // null until first read; true = array, false = CSV
289
+ let modelSet = null;
290
+ const modelToValues = (v) => {
291
+ if (v == null || v === '') return [];
292
+ if (Array.isArray(v)) return v.map(x => (x && typeof x === 'object' && x.value != null) ? x.value : x).map(String);
293
+ if (typeof v === 'string') return v.split(/[,\n;]+/).map(s => s.trim()).filter(Boolean);
294
+ return [String(v)];
295
+ };
296
+ const sameList = (a, b) => a.length === b.length && a.every((x, i) => String(x).toLowerCase() === String(b[i]).toLowerCase());
297
+ function syncOut() { if (modelSet) modelSet(selected.map(s => s.value)); }
201
298
 
202
299
  // ----- Menu -----
203
300
  let menu = null, generatedMenu = null, optionEls = [], createEl = null, emptyEl = null, activeIndex = -1;
@@ -421,18 +518,21 @@ function initializeComboboxPlugin() {
421
518
  function refocus() { suppressOpen = true; el.focus(); setTimeout(() => { suppressOpen = false; }, 0); }
422
519
 
423
520
  function addValue(value, label) {
424
- if (!multiple) { selected = [{ value, label }]; render(); announce(label + ' selected'); return; }
521
+ if (!multiple) { selected = [{ value, label }]; render(); syncOut(); announce(label + ' selected'); return; }
425
522
  if (isSelected(value)) return;
426
523
  if (selected.length >= max) { announce('Maximum of ' + max + ' reached'); return; }
427
524
  selected.push({ value, label });
428
525
  render();
526
+ syncOut();
429
527
  announce(label + ' added');
430
528
  }
431
529
  function removeValue(value) {
530
+ if (isLocked(value)) return; // locked chips stay put
432
531
  const i = selected.findIndex(s => String(s.value).toLowerCase() === String(value).toLowerCase());
433
532
  if (i < 0) return;
434
533
  const [g] = selected.splice(i, 1);
435
534
  render();
535
+ syncOut();
436
536
  announce(g.label + ' removed');
437
537
  if (menu && !isAsync) filter();
438
538
  refocus();
@@ -444,13 +544,24 @@ function initializeComboboxPlugin() {
444
544
  chip.dataset.value = s.value;
445
545
  const label = document.createElement('span');
446
546
  label.textContent = s.label;
547
+ chip.appendChild(label);
548
+ applyLock(chip, s.value, s.label);
549
+ return chip;
550
+ }
551
+ // Add or drop the × to match the value's locked state. Re-run from render() so a
552
+ // reactive `locked` change toggles the affordance on existing chips too.
553
+ function applyLock(chip, value, label) {
554
+ const locked = isLocked(value);
555
+ chip.toggleAttribute('data-locked', locked);
556
+ const btn = chip.querySelector(':scope > button');
557
+ if (locked) { if (btn) btn.remove(); return; }
558
+ if (btn) return;
447
559
  const x = document.createElement('button');
448
560
  x.type = 'button';
449
- x.setAttribute('aria-label', 'Remove ' + s.label);
561
+ x.setAttribute('aria-label', 'Remove ' + (label || (chip.querySelector(':scope > span') || {}).textContent || value));
450
562
  x.textContent = '×';
451
- x.addEventListener('click', () => removeValue(s.value));
452
- chip.append(label, x);
453
- return chip;
563
+ x.addEventListener('click', () => removeValue(value));
564
+ chip.appendChild(x);
454
565
  }
455
566
  function hidden(value) {
456
567
  const h = document.createElement('input');
@@ -463,17 +574,27 @@ function initializeComboboxPlugin() {
463
574
 
464
575
  function render() {
465
576
  if (chips) {
466
- // Incremental: keep existing chip nodes, only add new and drop removed.
467
- // Recreating every chip re-inserts them next to the focused editor, which
468
- // collapses them to an ellipsis in WebKit; untouched nodes keep their width.
577
+ // Incremental: keep existing chip nodes, only add new / drop removed /
578
+ // refresh locked state. Recreating every chip re-inserts them next to the
579
+ // focused editor, which collapses them to an ellipsis in WebKit; untouched
580
+ // nodes keep their width.
581
+ const norm = v => String(v).toLowerCase();
469
582
  const have = new Map();
470
- wrap.querySelectorAll('.combobox-chip').forEach(c => have.set(String(c.dataset.value), c));
471
- have.forEach((node, val) => {
472
- if (!selected.some(s => String(s.value) === val)) node.remove();
473
- });
583
+ wrap.querySelectorAll('.combobox-chip').forEach(c => have.set(norm(c.dataset.value), c));
584
+ const want = selected.map(s => norm(s.value));
585
+ const wantSet = new Set(want);
586
+ have.forEach((node, key) => { if (!wantSet.has(key)) { node.remove(); have.delete(key); } });
474
587
  selected.forEach(s => {
475
- if (!have.has(String(s.value))) wrap.insertBefore(makeChip(s), el);
588
+ const key = norm(s.value);
589
+ const node = have.get(key);
590
+ if (!node) { const n = makeChip(s); have.set(key, n); wrap.insertBefore(n, el); }
591
+ else applyLock(node, s.value, s.label);
476
592
  });
593
+ // Reorder to match selection only when it actually differs — needless
594
+ // re-insertion collapses chips in WebKit while the editor is focused, so the
595
+ // common append path (order already matches) skips this.
596
+ const cur = Array.from(wrap.querySelectorAll('.combobox-chip')).map(c => norm(c.dataset.value));
597
+ if (!sameList(cur, want)) selected.forEach(s => wrap.insertBefore(have.get(norm(s.value)), el));
477
598
  // Hidden inputs are type=hidden (no layout), so a clean rebuild is harmless.
478
599
  wrap.querySelectorAll('input[data-cb]').forEach(n => n.remove());
479
600
  if (name) selected.forEach(s => wrap.appendChild(hidden(s.value)));
@@ -597,9 +718,61 @@ function initializeComboboxPlugin() {
597
718
  // a stale, sometimes-open duplicate. Authored/adopted menus go with their container.
598
719
  if (cleanup && generatedMenu) cleanup(() => { if (generatedMenu.isConnected) generatedMenu.remove(); });
599
720
 
600
- // ----- Seed initial chips from value="a, b" (fresh input triggers only;
601
- // an adopted wrapper already provided its selection above) -----
602
- if (!adopt && !editorNone && chips && el.value) {
721
+ // ----- Reactive model (x-model) -----
722
+ // Renders chips from the bound value on init and whenever it changes externally
723
+ // (e.g. switching which record is being edited); writes back on add/remove. The
724
+ // read runs inside an Alpine effect so $x / nested state stay reactive.
725
+ if (modelExpr) {
726
+ const read = Alpine.evaluateLater(el, modelExpr);
727
+ modelSet = (vals) => {
728
+ const out = !multiple ? (vals.length ? vals[0] : '')
729
+ : (modelArrayShape === false ? vals.join(',') : vals.slice());
730
+ try { Alpine.evaluate(el, `${modelExpr} = ${JSON.stringify(out)}`); } catch (_) { }
731
+ };
732
+ Alpine.effect(() => {
733
+ read(raw => {
734
+ if (modelArrayShape === null && raw != null && raw !== '') modelArrayShape = Array.isArray(raw);
735
+ const incoming = modelToValues(raw);
736
+ if (sameList(incoming, selected.map(s => s.value))) return; // unchanged / our own write-back
737
+ selected = incoming.map(v => ({ value: v, label: labelFor(v) }));
738
+ if (!multiple && selected.length > 1) selected = selected.slice(-1);
739
+ render();
740
+ });
741
+ });
742
+ }
743
+
744
+ // ----- Reactive locked list (config-object form re-evaluates, so authors can
745
+ // gate it on state, e.g. last-owner: locked: owners.length <= 1 ? ['owner'] : []) -----
746
+ if (expr.startsWith('{')) {
747
+ Alpine.effect(() => {
748
+ let c; try { c = Alpine.evaluate(el, expr); } catch (_) { return; }
749
+ const next = computeLocked(c && typeof c === 'object' && !Array.isArray(c) ? c : null);
750
+ if (next.size === lockedSet.size && [...next].every(v => lockedSet.has(v))) return;
751
+ lockedSet = next;
752
+ render();
753
+ });
754
+ }
755
+
756
+ // ----- Dynamic options: re-read when x-for / $x fills the source list after
757
+ // build, so the menu, chip labels, and data-locked flags stay current. -----
758
+ if (src) {
759
+ const reread = () => {
760
+ options = readOptions(src);
761
+ lockedFromOptions.length = 0;
762
+ options.filter(o => o.locked).forEach(o => lockedFromOptions.push(String(o.value).toLowerCase()));
763
+ lockedSet = computeLocked();
764
+ selected = selected.map(s => ({ value: s.value, label: labelFor(s.value) }));
765
+ if (generatedMenu) setOptions(options);
766
+ render();
767
+ };
768
+ const mo = new MutationObserver(reread);
769
+ mo.observe(src, { childList: true, subtree: true, characterData: true });
770
+ if (cleanup) cleanup(() => mo.disconnect());
771
+ }
772
+
773
+ // ----- Seed initial chips from value="a, b" — only when there's no x-model
774
+ // (x-model wins) and no adopted wrapper (which already seeded). -----
775
+ if (!modelExpr && !adopt && !editorNone && chips && el.value) {
603
776
  const seeds = el.value.split(/[,\n;]+/).map(s => s.trim()).filter(Boolean);
604
777
  el.value = '';
605
778
  seeds.forEach(s => commitText(s));
package/lib/manifest.css CHANGED
@@ -1460,7 +1460,7 @@
1460
1460
 
1461
1461
  @layer components {
1462
1462
 
1463
- /* Field shell — inherits input tokens so it reads as a normal field */
1463
+ /* Wrapper */
1464
1464
  :where(.combobox):not(.unstyle) {
1465
1465
  display: flex;
1466
1466
  flex-wrap: wrap;
@@ -1498,16 +1498,8 @@
1498
1498
  pointer-events: none
1499
1499
  }
1500
1500
 
1501
- /* Inner typing surface. The :not(.unstyle) lifts specificity above
1502
- manifest.input.css, which sorts after this file in the bundled CSS and
1503
- would otherwise re-apply its own width/background/hover to the editor. */
1501
+ /* Inner typing surface */
1504
1502
  &> :where(input:not([type=hidden]), textarea):not(.unstyle) {
1505
- /* flex-basis (not min-width) sets the wrap threshold: the editor fills
1506
- the rest of the row, and once less than 7rem is left it wraps to its
1507
- own line. min-width:0 is REQUIRED — a flex item's default min-width:auto
1508
- is its content size, and WebKit enforces it by crushing the
1509
- flex-shrink:0 chips to an ellipsis. Resetting it to 0 lets the editor
1510
- shrink/wrap instead, so the chips keep their room. */
1511
1503
  flex: 1 1 7rem;
1512
1504
  min-width: 0;
1513
1505
  width: auto;
@@ -1533,7 +1525,7 @@
1533
1525
  /* Chips */
1534
1526
  :where(.combobox-chip):not(.unstyle) {
1535
1527
  display: inline-flex;
1536
- flex: 0 0 auto; /* hold content width; don't grow or shrink — overflow:hidden below otherwise lets flex squeeze chips to an ellipsis (notably Safari) */
1528
+ flex: 0 0 auto;
1537
1529
  align-items: center;
1538
1530
  gap: var(--spacing, 0.25rem);
1539
1531
  max-width: 100%;
@@ -1575,6 +1567,12 @@
1575
1567
  }
1576
1568
  }
1577
1569
 
1570
+ /* Non-removable */
1571
+ &[data-locked] {
1572
+ padding-inline-end: calc(var(--spacing, 0.25rem) * 2);
1573
+ cursor: default
1574
+ }
1575
+
1578
1576
  /* Failed validation */
1579
1577
  &[aria-invalid="true"] {
1580
1578
  color: var(--color-negative-inverse, oklch(44.4% 0.177 26.899));
@@ -1582,9 +1580,8 @@
1582
1580
  }
1583
1581
  }
1584
1582
 
1585
- /* Trigger (editor:none button mode) — transparent; the shell is the field.
1586
- Fills the row like the input editor, with a select-style caret. */
1587
- :where(.combobox) > button:not(.unstyle) {
1583
+ /* Button trigger */
1584
+ :where(.combobox)>button:not(.unstyle) {
1588
1585
  flex: 1 1 auto;
1589
1586
  align-self: stretch;
1590
1587
  min-width: 7rem;
@@ -1613,10 +1610,7 @@
1613
1610
  }
1614
1611
  }
1615
1612
 
1616
- /* Chip-less field (single input, textarea, or button trigger): the editor IS
1617
- the field — it fills the whole wrapper and carries the padding, so the click
1618
- target, caret and text selection align with the field edges instead of
1619
- floating inside the wrapper's padding. */
1613
+ /* Chip-less */
1620
1614
  :where(.combobox):has(> :where(input:not([type=hidden]), textarea, button):not(.unstyle)):not(:has(.combobox-chip)) {
1621
1615
  padding: 0;
1622
1616
 
@@ -1627,15 +1621,10 @@
1627
1621
  }
1628
1622
  }
1629
1623
 
1630
- /* Listbox — base look comes from menu[popover]; these are combobox-only extras.
1631
- Selectors deliberately AVOID :where() on the option part so they out-specify
1632
- dropdown.css's `menu li { display: inline-flex }` (0,1,0), which sorts after
1633
- this file in the bundle and would otherwise keep filtered options visible. */
1624
+ /* Listbox */
1634
1625
  :where(menu[role=listbox]):not(.unstyle) {
1635
1626
 
1636
- /* Active descendant — only shown while the keyboard is driving (data-kbd).
1637
- On a mouse-opened menu the hover state alone highlights, so the first
1638
- option isn't left with a persistent background. */
1627
+ /* Active descendant */
1639
1628
  &[data-kbd] [role=option][aria-current="true"] {
1640
1629
  color: var(--color-field-inverse, oklch(43.9% 0 0));
1641
1630
  background-color: var(--color-field-surface, color-mix(in oklch, oklch(20.5% 0 0) 10%, transparent))
@@ -3861,8 +3850,13 @@
3861
3850
 
3862
3851
  @layer components {
3863
3852
 
3853
+ :where(.grid-table):not(.unstyle) {
3854
+ display: grid;
3855
+ }
3856
+
3864
3857
  :where(table, .grid-table):not(.unstyle) {
3865
3858
  table-layout: auto;
3859
+ align-items: start;
3866
3860
  width: 100%;
3867
3861
  max-width: 100%;
3868
3862
  overflow: hidden;
@@ -3896,7 +3890,6 @@
3896
3890
  font-size: 0.875rem;
3897
3891
  text-align: left;
3898
3892
  text-align: start;
3899
- overflow: hidden;
3900
3893
 
3901
3894
  /* Make elements within cell inline */
3902
3895
  &>*:not(template) {
@@ -3910,6 +3903,11 @@
3910
3903
  }
3911
3904
  }
3912
3905
 
3906
+ /* Native cells clip */
3907
+ :where(td, th) {
3908
+ overflow: hidden
3909
+ }
3910
+
3913
3911
  /* Footer row */
3914
3912
  :where(:not(:has(tfoot)) tbody tr:last-child, tfoot tr:last-child, .grid-footer > *) {
3915
3913
  border-bottom: 0
@@ -6,7 +6,7 @@
6
6
  "manifest.code.js": "sha384-BJSRJ7txA6fY660qX+SfEEBrEB0dOChHdHI4/9iNGz3rGqohEZdzs7qA3LuR1GJW",
7
7
  "manifest.color.js": "sha384-6Rv3LxyTcZNjrhtayQfqRdCx0uSZ4BiEbgEI98I62eTvp8Aw7LBIoNJ0Je1oktwL",
8
8
  "manifest.colorpicker.js": "sha384-Wqz0ZIbeIi7KarqqqSLsQk+7E/fMaKhb32hrq5/eWzX1yjqMrpPZKH8y+jZ3mfg+",
9
- "manifest.combobox.js": "sha384-IhAeScFTAva7bXM+L0/XQDpsSEiCkjNkXLT3/z/0gQeuvw0/vngKBY8jI62xi7H8",
9
+ "manifest.combobox.js": "sha384-1Wf4gcfBpct8PjDUScjBFnid7prfcQUZZOftruze2KPqGOVjqsOOWoG1rRDK7pkO",
10
10
  "manifest.components.js": "sha384-mzPFoM0vqL9dnTVLMN3OrmO+KCgSqGknM1fd7bM1xzYeCco5OaZi56IMR5RS5oad",
11
11
  "manifest.data.js": "sha384-bCYTYyAYNVkg5pSwGcoe07Dgf5B7JDN7GtOIQdS+BtrBStQwvjZtskiQ38Bncvrf",
12
12
  "manifest.datepicker.js": "sha384-NEb/H4vuR3CFtRcodHsm3jJjrcYW2JMpDlQKlgwTrzpMMTcDkFKYXzAYJD0gZ7Ov",
@@ -27,6 +27,6 @@
27
27
  "manifest.tooltips.js": "sha384-ADzAx9D0HWq2b46mvNG05iOwPmEWdiFZNpEOXONSbBxs4xj1B/bzNL7S3x2R9cS1",
28
28
  "manifest.url.parameters.js": "sha384-FIufiClqDx1rJpU/QUc9z/D43qClQ6Qm8rBahipbJl9BDHUvhrOsUDegmTWW7Tuf",
29
29
  "manifest.utilities.js": "sha384-HWyVkjQoDRlWFKDBQw4RQOYODkBcU72NHW6l1p4bhQv1RtN0/XtnjwIb+lQK6+zv",
30
- "manifest.virtual.js": "sha384-J2+MpRskVH39R2DmHUib2bItuGsvSLEhPM+83iznrdfQFMZ63Ea6xwLeCsG04jOl",
30
+ "manifest.virtual.js": "sha384-klEAlDCSGuzMZmO7NMSckKVeEJRR9UGWCesoPOOypX78EcPJ1/TUBAXz3MeSGx2s",
31
31
  "manifest.js": "sha384-zPXrym9jwpYVdg4TJ1XPQVxQhz7uezdDubaO/MZDvLeiTufor+6lNJsTG75cYPXm"
32
32
  }