@adia-ai/web-components 0.8.18 → 0.8.19

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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog — @adia-ai/web-components
2
2
 
3
+ ## [0.8.19] — 2026-07-27
4
+
5
+ ### Added
6
+ - **`select-ui[summary-label]` — multi-select trigger as one text line, not chips** (gh#442, ruled from the adiav2 admin dashboard filter bar). With the attribute set, a single selection shows its own label and N > 1 shows "Label (N)" ("Organizations (2)"); none shows the placeholder. The popover's checkbox rows, Backspace-removes-last, the comma-separated form value, `[select-all]`, and `[clearable]` semantics are unchanged, and chip mode stays the default when the attribute is absent (`[data-multi-chips]` is simply not set in summary mode, so the trigger keeps the plain single-line layout). Motivation: in a narrow filter-bar trigger, three selected organizations wrapped into a vertically ballooning chip stack; `max-chips` still renders chips and could not express the ruling. Entry re-homed from the root CHANGELOG (package-local change; PR #445 filed it there mid-flight while #452's re-home was landing).
7
+
8
+ ### Fixed
9
+ - **Row hover no longer goes near-black — `--a-bg-hover`/`--a-bg-active` (and the `--a-canvas-*` twins) move off the fill ladder onto the container alpha ladder** (`styles/colors/semantics/core.css`; operator directive 2026-07-27). The roles pointed at `neutral-hover`/`neutral-active` — the FILLED-BUTTON stops, opaque 650/700 — so all ten row/option/cell consumers (list, tree, select, command, calendar-grid, calendar-picker, date-range-picker, tag, agent-artifact) tinted toward black on hover, while the adjacent `--a-bg-selected` note already prescribed "hover = container-low". Now: hover = `container-low` (10%), active = `container` (20%, the selected/tonal tier). The fill ladder itself is untouched — filled buttons keep 600/650/700.
10
+ - **The unit suite can no longer touch real network** (`test-setup.js`) — happy-dom's `fetch` does real I/O against its default `http://localhost:3000` base URL, so test outcomes depended on which dev servers happened to be running (the v0.8.17 cut lost a gate run to an ECONNREFUSED flake in `onboarding-checklist.test.js`). A fast-rejecting guard now replaces `globalThis.fetch`; tests exercising fetch paths keep installing their own per-file mocks, which override it.
11
+
12
+ ### Maintenance
13
+ - **`components/` touched in this release window** (6 file(s), e.g. `select/select.a2ui.json`) — carried by the entries above.
14
+ - **`dist/` bundles rebuilt** in this cut's window (6 file(s)) — regenerated from the source changes described above, not independent edits.
15
+
3
16
  ## [0.8.18] — 2026-07-27
4
17
 
5
18
  ### Maintenance
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://adiaui.dev/a2ui/v0_9/components/Select.json",
4
4
  "title": "Select",
5
- "description": "Single- and multi-select dropdown primitive — the canonical AdiaUI\nselect control. Form-bearing via UIFormElement: [name], [value],\n[required], [disabled], fires `change`. Options via native\n<option> / <optgroup> children, programmatic `.options` array, or\nJSON [data-options] (hydrated by <editor-shell>'s wireSelects).\nUse for single-select with > 4 options; for ≤ 4 options use\n<segmented-ui> or <radio-ui>. Multi-select via [multiple]: trigger\nrenders <tag-ui> chips per selection, popover shows checkbox-style\noption list, Backspace removes the last chip, [select-all] adds\nbulk controls, [max-chips] caps the visible chip count with a\n\"+N more\" pill. Form value stays a comma-separated string under\n[name] for native <form> compatibility (parse with .split(',')).\n",
5
+ "description": "Single- and multi-select dropdown primitive — the canonical AdiaUI\nselect control. Form-bearing via UIFormElement: [name], [value],\n[required], [disabled], fires `change`. Options via native\n<option> / <optgroup> children, programmatic `.options` array, or\nJSON [data-options] (hydrated by <editor-shell>'s wireSelects).\nUse for single-select with > 4 options; for ≤ 4 options use\n<segmented-ui> or <radio-ui>. Multi-select via [multiple]: trigger\nrenders <tag-ui> chips per selection, popover shows checkbox-style\noption list, Backspace removes the last chip, [select-all] adds\nbulk controls, [max-chips] caps the visible chip count with a\n\"+N more\" pill, and [summary-label] swaps the chip trigger for one\ntext line — the single selection's label, or \"Label (N)\" for more.\nForm value stays a comma-separated string under\n[name] for native <form> compatibility (parse with .split(',')).\n",
6
6
  "type": "object",
7
7
  "allOf": [
8
8
  {
@@ -150,6 +150,11 @@
150
150
  ],
151
151
  "default": "md"
152
152
  },
153
+ "summary-label": {
154
+ "description": "Multi-select only (gh#442). Replaces the chip trigger with one\ntext line: a single selection shows its own label; N > 1 shows\n\"<summary-label> (N)\" — e.g. \"Organizations (2)\"; none shows the\nplaceholder. Popover checkbox rows, Backspace-removes-last, the\ncomma-separated form value, [select-all], and [clearable] are\nunchanged. Absent (default) = tag-ui chip trigger. Prefer this\nform for narrow triggers (filter bars) where wrapping chips\nwould balloon the control.\n",
155
+ "type": "string",
156
+ "default": null
157
+ },
153
158
  "value": {
154
159
  "description": "Currently selected option value. With declarative <option> children, a child carrying the native `selected` attribute sets the initial value when this attribute is absent (native <select> parity).",
155
160
  "type": "string",
@@ -60,6 +60,10 @@ export class UISelect extends UIFormElement {
60
60
  // These are no-ops without [multiple] but reflect so [maxChips]
61
61
  // selectors etc. work in CSS even when authored declaratively.
62
62
  maxChips: { type: Number, default: 0, reflect: true, attribute: 'max-chips' },
63
+ // gh#442: summary trigger for [multiple] — when set, the trigger shows
64
+ // one text line (a single selection's own label, or "Label (N)" for
65
+ // N > 1) instead of tag-ui chips. Absent = chip mode, unchanged.
66
+ summaryLabel: { type: String, default: '', reflect: true, attribute: 'summary-label' },
63
67
  min: { type: Number, default: 0, reflect: true },
64
68
  max: { type: Number, default: 0, reflect: true },
65
69
  selectAll: { type: Boolean, default: false, reflect: true, attribute: 'select-all' },
@@ -404,7 +408,9 @@ export class UISelect extends UIFormElement {
404
408
  // optional clear-x + optional "+N more" pill alongside the usual
405
409
  // leading / display / caret. We toggle host attribute [data-multi-chips]
406
410
  // so CSS can switch layout from single-line text to flex-wrap chip row.
407
- if (this.multiple) this.setAttribute('data-multi-chips', '');
411
+ // gh#442 — [summary-label] keeps the single-line text layout: the
412
+ // summary renders through [slot="display"], no chip row.
413
+ if (this.multiple && !this.summaryLabel) this.setAttribute('data-multi-chips', '');
408
414
  else this.removeAttribute('data-multi-chips');
409
415
 
410
416
  // A11y (FB-88): name a placeholder-only select from its placeholder, like
@@ -859,17 +865,18 @@ export class UISelect extends UIFormElement {
859
865
  #stampChips() {
860
866
  const slot = this.querySelector('[slot="trigger"] [data-chips]');
861
867
  if (!slot) return;
868
+ // gh#442 — summary mode renders no chips; clear any stamped ones so a
869
+ // dynamic switch out of chip mode leaves no stale tag-ui behind.
870
+ if (this.summaryLabel) {
871
+ while (slot.firstChild) slot.removeChild(slot.firstChild);
872
+ return;
873
+ }
862
874
  const values = this.#values();
863
875
  const cap = Number(this.maxChips) || 0;
864
876
  const cappedValues = cap > 0 && values.length > cap ? values.slice(0, cap) : values;
865
877
  const overflow = cap > 0 && values.length > cap ? values.length - cap : 0;
866
878
  // Label lookup from internal options model (handles grouped form too).
867
- const flat = this.#options.flatMap((o) => o.options || [o])
868
- .filter((o) => !o.separator && !o.header && o.value != null);
869
- const labelOf = (v) => {
870
- const found = flat.find((o) => String(o.value) === String(v));
871
- return found ? found.label : String(v);
872
- };
879
+ const labelOf = (v) => this.#labelOf(v);
873
880
  // Rebuild — chips are cheap and reconciliation gets us correct order
874
881
  // + correct labels with no per-chip tracking state.
875
882
  while (slot.firstChild) slot.removeChild(slot.firstChild);
@@ -893,8 +900,28 @@ export class UISelect extends UIFormElement {
893
900
  }
894
901
  }
895
902
 
903
+ /**
904
+ * Label for one value from the internal options model (grouped form
905
+ * included); falls back to the raw value string. Shared by the chip
906
+ * stamper and the gh#442 summary trigger.
907
+ */
908
+ #labelOf(v) {
909
+ const flat = this.#options.flatMap((o) => o.options || [o])
910
+ .filter((o) => !o.separator && !o.header && o.value != null);
911
+ const found = flat.find((o) => String(o.value) === String(v));
912
+ return found ? found.label : String(v);
913
+ }
914
+
896
915
  #displayText() {
897
916
  if (this.multiple) {
917
+ // gh#442 — [summary-label]: one selection shows its own label, more
918
+ // show "Label (N)", none shows the placeholder.
919
+ if (this.summaryLabel) {
920
+ const values = this.#values();
921
+ if (values.length === 0) return this.placeholder;
922
+ if (values.length === 1) return this.#labelOf(values[0]);
923
+ return `${this.summaryLabel} (${values.length})`;
924
+ }
898
925
  // SPEC-040 — chips render the selection; display element holds the
899
926
  // placeholder when empty and is otherwise empty (CSS sees [data-multi-chips]).
900
927
  return this.value ? '' : this.placeholder;
@@ -58,6 +58,8 @@ export class UISelect extends UIFormElement {
58
58
  divider: boolean;
59
59
  /** SPEC-040 — cap visible chips in multi-select trigger. 0 = unlimited. */
60
60
  maxChips: number;
61
+ /** gh#442 — multi-select summary trigger: one selection shows its label, N > 1 shows "Label (N)"; empty string = chip mode. */
62
+ summaryLabel: string;
61
63
  /** SPEC-040 — minimum required selections in multi-select mode. */
62
64
  min: number;
63
65
  /** SPEC-040 — maximum allowed selections in multi-select mode. 0 = unlimited. */
@@ -139,6 +139,34 @@ describe('select-ui', () => {
139
139
  expect(chipValues).toContain('c');
140
140
  });
141
141
 
142
+ it('multi-select: [summary-label] renders a text summary instead of chips (gh#442)', async () => {
143
+ const s = mount(`
144
+ <select-ui multiple summary-label="Organizations" value="a,c">
145
+ <option value="a">Riverside Health System</option>
146
+ <option value="b">Bay Area Health Group</option>
147
+ <option value="c">Greenfield Medical Group</option>
148
+ </select-ui>
149
+ `);
150
+ await tick();
151
+ expect(s.hasAttribute('data-multi-chips')).toBe(false);
152
+ expect(s.querySelectorAll('[slot="trigger"] [data-chips] tag-ui').length).toBe(0);
153
+ expect(s.querySelector('[slot="display"]').textContent).toBe('Organizations (2)');
154
+ });
155
+
156
+ it('multi-select summary: one selection shows its own label; none shows the placeholder (gh#442)', async () => {
157
+ const s = mount(`
158
+ <select-ui multiple summary-label="Organizations" placeholder="All organizations" value="a">
159
+ <option value="a">Riverside Health System</option>
160
+ <option value="b">Bay Area Health Group</option>
161
+ </select-ui>
162
+ `);
163
+ await tick();
164
+ expect(s.querySelector('[slot="display"]').textContent).toBe('Riverside Health System');
165
+ s.value = '';
166
+ await tick();
167
+ expect(s.querySelector('[slot="display"]').textContent).toBe('All organizations');
168
+ });
169
+
142
170
  it('multi-select: aria-multiselectable=true is set on the listbox', async () => {
143
171
  const s = mount(`
144
172
  <select-ui multiple open>
@@ -18,7 +18,9 @@ description: |
18
18
  renders <tag-ui> chips per selection, popover shows checkbox-style
19
19
  option list, Backspace removes the last chip, [select-all] adds
20
20
  bulk controls, [max-chips] caps the visible chip count with a
21
- "+N more" pill. Form value stays a comma-separated string under
21
+ "+N more" pill, and [summary-label] swaps the chip trigger for one
22
+ text line — the single selection's label, or "Label (N)" for more.
23
+ Form value stays a comma-separated string under
22
24
  [name] for native <form> compatibility (parse with .split(',')).
23
25
  # Per ADR-0027 — primitives that programmatically create other primitives
24
26
  # do NOT auto-import them. Consumer (or demo shell) must explicitly import.
@@ -120,6 +122,19 @@ props:
120
122
  type: number
121
123
  default: 0
122
124
  reflect: true
125
+ summary-label:
126
+ description: |
127
+ Multi-select only (gh#442). Replaces the chip trigger with one
128
+ text line: a single selection shows its own label; N > 1 shows
129
+ "<summary-label> (N)" — e.g. "Organizations (2)"; none shows the
130
+ placeholder. Popover checkbox rows, Backspace-removes-last, the
131
+ comma-separated form value, [select-all], and [clearable] are
132
+ unchanged. Absent (default) = tag-ui chip trigger. Prefer this
133
+ form for narrow triggers (filter bars) where wrapping chips
134
+ would balloon the control.
135
+ type: string
136
+ default: null
137
+ reflect: true
123
138
  min:
124
139
  description: |
125
140
  Multi-select only. Minimum required selections. Below this floor