@adia-ai/web-components 0.8.38 → 0.8.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/MIGRATION.md +3 -2
  3. package/components/calendar-picker/calendar-picker.css +4 -1
  4. package/components/combobox/combobox.css +6 -0
  5. package/components/command/command.a2ui.json +3 -0
  6. package/components/command/command.class.js +28 -6
  7. package/components/command/command.css +14 -3
  8. package/components/command/command.yaml +5 -0
  9. package/components/date-range-picker/date-range-picker.css +6 -0
  10. package/components/datetime-picker/datetime-picker.css +4 -0
  11. package/components/drilldown/drilldown.a2ui.json +3 -1
  12. package/components/drilldown/drilldown.class.js +12 -4
  13. package/components/drilldown/drilldown.css +7 -8
  14. package/components/drilldown/drilldown.yaml +2 -0
  15. package/components/modal/modal.class.js +68 -4
  16. package/components/nav/nav.a2ui.json +5 -0
  17. package/components/nav/nav.class.js +24 -4
  18. package/components/nav/nav.d.ts +2 -0
  19. package/components/nav/nav.yaml +18 -0
  20. package/components/select/select.class.js +30 -12
  21. package/components/select/select.css +11 -2
  22. package/components/swatch/swatch.css +6 -4
  23. package/components/toggle-group/toggle-group.class.js +21 -11
  24. package/components/toggle-group/toggle-group.css +16 -8
  25. package/components/toggle-group/toggle-group.d.ts +6 -0
  26. package/components/toggle-group/toggle-group.yaml +10 -0
  27. package/components/toggle-group/toggle-option.a2ui.json +5 -0
  28. package/components/toggle-group/toggle-option.yaml +18 -2
  29. package/custom-elements.json +95 -75
  30. package/dist/theme-provider.min.js +3 -3
  31. package/dist/web-components.min.css +1 -1
  32. package/dist/web-components.min.js +56 -53
  33. package/dist/web-components.sheet.js +1 -1
  34. package/package.json +1 -1
@@ -439,8 +439,14 @@ export class UISelect extends UIFormElement {
439
439
  : this.icon
440
440
  ? `<icon-ui slot="leading" data-select-leading name="${escapeHTML(this.icon)}"></icon-ui>`
441
441
  : '';
442
+ // No native <input> wrap (per ADR-0055 / ADR-0025): the searchable
443
+ // trigger is a `contenteditable="plaintext-only"` surface carrying
444
+ // `role="combobox"`, mirroring combobox-ui's editable-surface pattern.
445
+ // Placeholder text renders via the [data-empty]::before pseudo (see
446
+ // select.css), same mechanism as input-ui / combobox-ui.
447
+ const initialDisplayText = this.#displayText() === this.placeholder ? '' : this.#displayText();
442
448
  const displayMarkup = this.searchable
443
- ? `<input slot="display" type="text" role="combobox" aria-autocomplete="list" autocomplete="off" placeholder="${escapeHTML(this.placeholder || '')}" value="${escapeHTML(this.#displayText() === this.placeholder ? '' : this.#displayText())}" />`
449
+ ? `<span slot="display" contenteditable="plaintext-only" role="combobox" aria-autocomplete="list" tabindex="0" data-placeholder="${escapeHTML(this.placeholder || '')}"${initialDisplayText ? '' : ' data-empty'}>${escapeHTML(initialDisplayText)}</span>`
444
450
  : `<span slot="display">${escapeHTML(this.#displayText())}</span>`;
445
451
  // §184 (v0.5.5, FEEDBACK-08 §7): optional hint slot beneath the
446
452
  // trigger. Mirrors slider-ui's hint pattern + matches the
@@ -478,7 +484,7 @@ export class UISelect extends UIFormElement {
478
484
  this.#searchInput.removeEventListener('focus', this.#onSearchFocus);
479
485
  this.#searchInput.removeEventListener('click', this.#onSearchClick);
480
486
  }
481
- this.#searchInput = this.querySelector('input[slot="display"]');
487
+ this.#searchInput = this.querySelector('[slot="display"][contenteditable]');
482
488
  if (this.#searchInput) {
483
489
  this.#searchInput.addEventListener('input', this.#onSearchInput);
484
490
  this.#searchInput.addEventListener('focus', this.#onSearchFocus);
@@ -492,9 +498,13 @@ export class UISelect extends UIFormElement {
492
498
  const display = this.querySelector('[slot="display"]');
493
499
  if (display && !this.multiple) {
494
500
  // Single-select: keep the canonical text-rendering path.
495
- if (display.tagName === 'INPUT') {
496
- // Only update value when not actively editing (no active query)
497
- if (!this.#query) display.value = this.#displayText() === this.placeholder ? '' : this.#displayText();
501
+ if (display.hasAttribute('contenteditable')) {
502
+ // Only update text when not actively editing (no active query)
503
+ if (!this.#query) {
504
+ const text = this.#displayText() === this.placeholder ? '' : this.#displayText();
505
+ display.textContent = text;
506
+ display.toggleAttribute('data-empty', !text);
507
+ }
498
508
  } else {
499
509
  display.textContent = this.#displayText();
500
510
  }
@@ -502,10 +512,13 @@ export class UISelect extends UIFormElement {
502
512
  // Multi-select: display element holds the placeholder ONLY when
503
513
  // no chips are present (otherwise the chip row IS the display).
504
514
  const hasChips = this.#values().length > 0;
505
- if (display.tagName === 'INPUT') {
515
+ if (display.hasAttribute('contenteditable')) {
506
516
  // search input — keep placeholder; never inject the value text.
507
- display.placeholder = hasChips ? '' : (this.placeholder || '');
508
- if (!this.#query) display.value = '';
517
+ display.setAttribute('data-placeholder', hasChips ? '' : (this.placeholder || ''));
518
+ if (!this.#query) {
519
+ display.textContent = '';
520
+ display.toggleAttribute('data-empty', true);
521
+ }
509
522
  } else {
510
523
  display.textContent = hasChips ? '' : (this.placeholder || '');
511
524
  }
@@ -991,7 +1004,7 @@ export class UISelect extends UIFormElement {
991
1004
  // before it would start removing query characters.
992
1005
  if (e.key === 'Backspace' && this.multiple && !this.disabled && !this.readonly) {
993
1006
  const target = e.target;
994
- const fromInput = target && target.tagName === 'INPUT' && target.getAttribute('slot') === 'display';
1007
+ const fromInput = target && target.hasAttribute?.('contenteditable') && target.getAttribute('slot') === 'display';
995
1008
  const queryEmpty = !this.#query || this.#query.length === 0;
996
1009
  if (!fromInput || queryEmpty) {
997
1010
  if (this.#values().length > 0) {
@@ -1005,8 +1018,11 @@ export class UISelect extends UIFormElement {
1005
1018
  if (this.searchable && this.#query) {
1006
1019
  // First Escape: clear query
1007
1020
  this.#query = '';
1008
- const input = this.querySelector('input[slot="display"]');
1009
- if (input) input.value = '';
1021
+ const input = this.querySelector('[slot="display"][contenteditable]');
1022
+ if (input) {
1023
+ input.textContent = '';
1024
+ input.toggleAttribute('data-empty', true);
1025
+ }
1010
1026
  this.#applyFilter();
1011
1027
  return;
1012
1028
  }
@@ -1061,7 +1077,9 @@ export class UISelect extends UIFormElement {
1061
1077
  };
1062
1078
 
1063
1079
  #onSearchInput = (e) => {
1064
- this.#query = e.target.value || '';
1080
+ const text = e.target.textContent || '';
1081
+ this.#query = text;
1082
+ e.target.toggleAttribute('data-empty', !text);
1065
1083
  if (!this.open) this.open = true;
1066
1084
  this.#applyFilter();
1067
1085
  };
@@ -149,7 +149,7 @@
149
149
  white-space: nowrap;
150
150
  color: var(--select-fg);
151
151
  }
152
- input[slot="display"] {
152
+ [slot="display"][contenteditable] {
153
153
  border: none;
154
154
  outline: none;
155
155
  background: transparent;
@@ -159,9 +159,18 @@
159
159
  color: var(--select-fg);
160
160
  min-width: 0;
161
161
  width: 100%;
162
+ /* Positioning context for the [data-empty]::before placeholder pseudo
163
+ below — out-of-flow, same rationale as input-ui/combobox-ui: an
164
+ in-flow pseudo would render the caret after the placeholder text
165
+ instead of at content-start. */
166
+ position: relative;
162
167
  }
163
- input[slot="display"]::placeholder {
168
+ [slot="display"][contenteditable][data-empty]::before {
169
+ content: attr(data-placeholder);
164
170
  color: var(--select-placeholder-fg);
171
+ pointer-events: none;
172
+ position: absolute;
173
+ inset: 0;
165
174
  }
166
175
  :scope:not([value]) [slot="display"],
167
176
  :scope[value=""] [slot="display"] {
@@ -274,20 +274,22 @@
274
274
  z-index: 1;
275
275
  }
276
276
  :scope[shape="block"][auto-contrast] > [data-label][data-on-dark] {
277
- color: var(--a-chrome-light, #fafafa);
277
+ color: var(--a-chrome-light);
278
278
  }
279
279
  :scope[shape="block"][auto-contrast] > [data-label][data-on-light] {
280
- color: var(--a-chrome-dark, #111);
280
+ color: var(--a-chrome-dark);
281
281
  }
282
282
  /* Detail line follows the same auto-contrast choice. */
283
283
  :scope[shape="block"][auto-contrast] > [data-detail] {
284
284
  z-index: 1;
285
285
  }
286
286
  :scope[shape="block"][auto-contrast] > [data-label][data-on-dark] ~ [data-detail] {
287
- color: color-mix(in oklab, var(--a-chrome-light, #fafafa) 80%, transparent);
287
+ color: var(--a-chrome-light);
288
+ opacity: 0.8;
288
289
  }
289
290
  :scope[shape="block"][auto-contrast] > [data-label][data-on-light] ~ [data-detail] {
290
- color: color-mix(in oklab, var(--a-chrome-dark, #111) 70%, transparent);
291
+ color: var(--a-chrome-dark);
292
+ opacity: 0.7;
291
293
  }
292
294
 
293
295
  /* ═══════ Copy button — a stamped <button-ui variant="ghost" size="sm">
@@ -30,10 +30,15 @@ import { UIElement } from '../../core/element.js';
30
30
  // ── Toggle Option ──
31
31
  //
32
32
  // Parent-managed. Selection state flows through `toggle-group-ui`'s `value`
33
- // (and `single` mode flag). Items don't expose `checked` parent applies
34
- // `data-selected` on the DOM element when the item's `value` is part of the
35
- // group's comma-separated `value` list. Consistent with native `<option>`
36
- // inside `<select>`.
33
+ // (and `single` mode flag) the group's `value` stays the single source of
34
+ // truth. ADR-0056 (gh#1303): the child's current-state surface is the
35
+ // declared, reflected `selected` prop below, converging onto <segment-ui>'s
36
+ // mechanism (yaml → catalog → generative grammar) instead of a private
37
+ // stamp. `toggle-group-ui` also stamps the legacy `data-selected` attribute
38
+ // on the same element in parallel — DEPRECATED, kept only because external
39
+ // CSS may already target it as an undocumented hook; its removal rides the
40
+ // 0.9.0 breaking wave. Don't set `selected` directly on an option; drive
41
+ // the group's `value` instead.
37
42
 
38
43
  export class UIToggleOption extends UIElement {
39
44
  static properties = {
@@ -41,6 +46,7 @@ export class UIToggleOption extends UIElement {
41
46
  text: { type: String, default: '', reflect: true },
42
47
  icon: { type: String, default: '', reflect: true },
43
48
  disabled: { type: Boolean, default: false, reflect: true },
49
+ selected: { type: Boolean, default: false, reflect: true },
44
50
  };
45
51
 
46
52
  static template = () => null;
@@ -59,6 +65,7 @@ export class UIToggleOption extends UIElement {
59
65
  }
60
66
  if (this.disabled) this.setAttribute('aria-disabled', 'true');
61
67
  else this.removeAttribute('aria-disabled');
68
+ this.setAttribute('aria-pressed', this.selected ? 'true' : 'false');
62
69
  }
63
70
 
64
71
  #stamp() {
@@ -100,13 +107,16 @@ export class UIToggleGroup extends UIElement {
100
107
  const selected = this.#valueSet;
101
108
  for (const opt of this.querySelectorAll('toggle-option-ui')) {
102
109
  const optVal = opt.value || opt.getAttribute('value') || '';
103
- if (selected.has(optVal)) {
104
- opt.setAttribute('data-selected', '');
105
- opt.setAttribute('aria-pressed', 'true');
106
- } else {
107
- opt.removeAttribute('data-selected');
108
- opt.setAttribute('aria-pressed', 'false');
109
- }
110
+ const isSelected = selected.has(optVal);
111
+ // ADR-0056 (gh#1303): `selected` is the declared, reflected mechanism —
112
+ // converges onto <segment-ui>'s convention (opt's own render() stamps
113
+ // aria-pressed from it). `data-selected` is stamped alongside it only
114
+ // as a DEPRECATED parallel hook for pre-existing external CSS; it no
115
+ // longer drives styling (toggle-group.css) and its removal rides the
116
+ // 0.9.0 breaking wave.
117
+ opt.selected = isSelected;
118
+ if (isSelected) opt.setAttribute('data-selected', '');
119
+ else opt.removeAttribute('data-selected');
110
120
  }
111
121
  }
112
122
 
@@ -1,12 +1,23 @@
1
- /* Safari 17.x bug: `:scope:not(...):hover` inside `@scope` doesn't match
2
- the scope root. Plain selector outside works. The @scope is
3
- `(toggle-option-ui)` NOT `(toggle-group-ui)` — so the moved-out
4
- selector targets `toggle-option-ui:hover`. See docs/BROWSER-COMPAT.md §3a. */
1
+ /* Safari 17.x bug: `:scope:not(...):hover` (Flavor A) and `:scope[selected]`
2
+ (Flavor B attribute-removal restyle) both fail inside `@scope`.
3
+ Selectors moved out. The @scope is `(toggle-option-ui)` — NOT
4
+ `(toggle-group-ui)` so the moved-out selectors target
5
+ `toggle-option-ui:hover` / `toggle-option-ui[selected]`. See
6
+ docs/BROWSER-COMPAT.md §3a; mirrors segment.css. */
5
7
  toggle-option-ui:not([disabled]):hover {
6
8
  --toggle-option-bg: var(--toggle-option-bg-hover);
7
9
  --toggle-option-fg: var(--toggle-option-fg-hover);
8
10
  }
9
11
 
12
+ /* ADR-0056 (gh#1303): converged onto <segment-ui>'s declared, reflected
13
+ [selected] prop, set by <toggle-group-ui> (toggle-group.class.js). The
14
+ parent also stamps the legacy `data-selected` attribute in parallel —
15
+ DEPRECATED, no longer styled here; removal rides the 0.9.0 breaking wave. */
16
+ toggle-option-ui[selected] {
17
+ --toggle-option-bg: var(--toggle-option-selected-bg);
18
+ --toggle-option-fg: var(--toggle-option-selected-fg);
19
+ }
20
+
10
21
  @scope (toggle-group-ui) {
11
22
  :where(:scope) {
12
23
  /* ── Tokens ── */
@@ -94,10 +105,7 @@ toggle-option-ui:not([disabled]):hover {
94
105
  z-index: 1;
95
106
  }
96
107
 
97
- :scope[data-selected] {
98
- --toggle-option-bg: var(--toggle-option-selected-bg);
99
- --toggle-option-fg: var(--toggle-option-selected-fg);
100
- }
108
+ /* [selected] state rule moved outside @scope — see Safari 17.x bug note at top. */
101
109
 
102
110
  :scope[disabled] {
103
111
  --toggle-option-fg: var(--toggle-option-fg-disabled);
@@ -38,6 +38,12 @@ export class UIToggleOption extends UIElement {
38
38
  disabled: boolean;
39
39
  /** Optional Phosphor icon name; renders as a leading icon. */
40
40
  icon: string;
41
+ /** Whether this option is currently selected. Managed by the parent
42
+ <toggle-group-ui> container (ADR-0056) — don't set directly; the group's
43
+ `value` is the single source of truth. Converges onto <segment-ui>'s
44
+ declared reflected-prop mechanism (was previously a private
45
+ `data-selected` stamp with no declared API surface). */
46
+ selected: boolean;
41
47
  /** Visible label text. */
42
48
  text: string;
43
49
  /** Stable identifier matched against the parent group's `value`. */
@@ -1,5 +1,13 @@
1
1
  # Generated by scripts/migrate-yamls-to-v1.mjs — migrated to v1 contract.
2
2
  # Edit this file; run `npm run build:components` to regenerate a2ui.json.
3
+ #
4
+ # ADR-0056 (gh#1303): <toggle-option-ui>'s current-state surface converges
5
+ # onto <segment-ui>'s declared, reflected `[selected]` prop (see
6
+ # toggle-option.yaml). This container sets that prop, keeping `value` as the
7
+ # single source of truth. It also stamps the legacy `data-selected`
8
+ # attribute on each option in parallel — DEPRECATED (undocumented external
9
+ # CSS may target it); its removal rides the 0.9.0 breaking wave, not this
10
+ # change.
3
11
  $schema: ../../../../scripts/schemas/component.yaml.schema.json
4
12
  name: UIToggleGroup
5
13
  tag: toggle-group-ui
@@ -80,6 +88,8 @@ a2ui:
80
88
  reason: 'Selection model distinction.'
81
89
  - rule: 'Use for filter chips, multi-flag toggles, day-of-week pickers; for binary on/off use <switch-ui>.'
82
90
  reason: 'Capacity guidance.'
91
+ - rule: 'Sets the child <toggle-option-ui>''s declared [selected] prop to reflect the group value (ADR-0056) — do not author [selected] directly in generated markup; author the group''s value instead.'
92
+ reason: 'Parent-managed declared state — group value stays the single source of truth.'
83
93
  anti_patterns: []
84
94
  examples:
85
95
  - name: basic-toggle-group
@@ -26,6 +26,11 @@
26
26
  "type": "string",
27
27
  "default": ""
28
28
  },
29
+ "selected": {
30
+ "description": "Whether this option is currently selected. Managed by the parent\n<toggle-group-ui> container (ADR-0056) — don't set directly; the group's\n`value` is the single source of truth. Converges onto <segment-ui>'s\ndeclared reflected-prop mechanism (was previously a private\n`data-selected` stamp with no declared API surface).",
31
+ "type": "boolean",
32
+ "default": false
33
+ },
29
34
  "text": {
30
35
  "description": "Visible label text.",
31
36
  "type": "string",
@@ -3,6 +3,12 @@
3
3
  # §228 (v0.5.9): authored to close the §229 baseline class-export-undeclared
4
4
  # drift for UIToggleOption. Ships as a sibling class in toggle-group/toggle-group.class.js
5
5
  # + is registered alongside UIToggleGroup.
6
+ #
7
+ # ADR-0056 (gh#1303): `selected` converges onto <segment-ui>'s declared,
8
+ # parent-managed, reflected-prop convention. `<toggle-group-ui>` also stamps
9
+ # the legacy `data-selected` attribute in parallel — DEPRECATED, kept only
10
+ # because it was an undocumented hook external CSS may already target;
11
+ # removal rides the 0.9.0 breaking wave (see toggle-group.yaml + CHANGELOG).
6
12
 
7
13
  # Child component of <toggle-group-ui>. Surface only inside that parent.
8
14
  $schema: ../../../../scripts/schemas/component.yaml.schema.json
@@ -32,6 +38,16 @@ props:
32
38
  description: Disabled state — blocks pointer + keyboard activation.
33
39
  type: boolean
34
40
  default: false
41
+ selected:
42
+ description: |-
43
+ Whether this option is currently selected. Managed by the parent
44
+ <toggle-group-ui> container (ADR-0056) — don't set directly; the group's
45
+ `value` is the single source of truth. Converges onto <segment-ui>'s
46
+ declared reflected-prop mechanism (was previously a private
47
+ `data-selected` stamp with no declared API surface).
48
+ type: boolean
49
+ default: false
50
+ reflect: true
35
51
 
36
52
  keywords:
37
53
  - toggle-option
@@ -53,5 +69,5 @@ a2ui:
53
69
  reason: 'Multi-select cluster contract.'
54
70
  - rule: 'Different from <segment-ui> (which is single-select inside <segmented-ui>).'
55
71
  reason: 'Selection model boundary.'
56
- - rule: 'Active state controlled via the toggle-option''s own active attribute (independent of siblings).'
57
- reason: 'Parent does not own state in multi-select.'
72
+ - rule: 'Selected state exposed via its own declared [selected] reflected prop, set by the parent <toggle-group-ui> (ADR-0056) — mirrors <segment-ui>''s convention. Do not set [selected] directly; drive the group''s value instead.'
73
+ reason: 'Parent-managed declared state group value stays the single source of truth.'