@adia-ai/web-components 0.6.48 → 0.6.50

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,44 @@
1
1
  # Changelog — @adia-ai/web-components
2
2
 
3
+ ## [0.6.50] — 2026-05-30
4
+
5
+ ### Changed — `slot="caret"` disclosure-indicator convention + FEEDBACK-89 (ADR-0036)
6
+
7
+ - **`components/tree/` (`tree-item-ui`) + `components/pane/` (`pane-ui`)** — renamed the expand/collapse indicator slot `slot="chevron"` → **`slot="caret"`** (and tokens `--*-chevron-*` → `--*-caret-*`, e.g. `--tree-caret-size`, `--pane-caret-fg`), standardizing on the slot name already used by `accordion-item-ui` / `nav-group-ui` / `select-ui` and matching the Phosphor `caret-*` icon vocabulary. **No backward-compat alias** (hard rename; the caret is auto-stamped, so blast radius is small). See [ADR-0036](../../.brain/adrs/0036-caret-slot-disclosure-convention.md).
8
+ - **`tree-item-ui` adopt-or-stamp (FEEDBACK-89)** — `#stamp()` now adopts a consumer's declarative `slot="caret"` and `slot="actions"` children into the auto-stamped row instead of leaving them as siblings below it. A `<button-ui slot="actions">` (the natural way to add a per-row action — add / rename / overflow) now lands inline in the styled, hover-revealed actions area. The host `#onClick` already excludes `[slot="actions"] *` from row selection, so adoption is click-safe. +3 unit tests.
9
+ - **`dist/` CDN bundles** (`web-components.min.css` + `web-components.min.js`) regenerated to carry the renamed `--*-caret-*` tokens + caret stamp logic.
10
+
11
+ ## [0.6.49] — 2026-05-30
12
+
13
+ ### Changed — card-ui header slot sweep + `nav-group-ui` fixes (demos)
14
+
15
+ - **`patterns/*` + catalog demo HTML** — card-ui header anatomy swept to the canonical `<header>` slot grammar across admin-dashboard + 5 catalog patterns; `nav-group-ui` `label=` → `text=` + icon fixes. Demo/corpus-facing; absorbed into the chunk corpus regen.
16
+ - **31 module demos** — `<!-- design-plan -->` blocks retrofitted across the composite-demo-protocol campaign (Phase 1+3 artifacts embedded).
17
+
18
+ ### Maintenance
19
+
20
+ - **`dist/web-components.min.js` + `dist/icons-manifest.js`** — bundle rebuild reflecting the calendar-grid `month` prop + date-range-picker class changes.
21
+
22
+ ### Fixed — `<date-range-picker>` showed the same month in both panes
23
+
24
+ - **`components/calendar-grid/calendar-grid.{class.js,yaml}`** — new `month`
25
+ attribute (`YYYY-MM`): sets the displayed month when no `value` is selected, so
26
+ a consumer can show a specific month independent of selection. A selected value
27
+ always takes precedence.
28
+ - **`components/date-range-picker/date-range-picker.class.js`** — the end pane now
29
+ defaults to **one month after** the start pane (via the new `month` hint), so a
30
+ fresh range picker shows two consecutive months (e.g. May + June) instead of the
31
+ current month twice. A selected `to` date still drives its own pane. Propagates
32
+ to `<date-range-selector>` (embeds the picker).
33
+
34
+ ### Fixed — `drop-target` demo card-structure violation
35
+
36
+ - **`traits/drop-target/drop-target.examples.html`** — the 4 `<card-ui>` drop
37
+ surfaces held `<text-ui>` / `<col-ui>` directly (bypassing the card body slot),
38
+ failing `check:card-structure` (a `check`-chain gate). Wrapped each in
39
+ `<section bleed style="display:grid;place-items:center">` — clears the gate while
40
+ keeping the centered drop-zone appearance. `audit:card-structure` now clean.
41
+
3
42
  ## [0.6.48] — 2026-05-29
4
43
 
5
44
  ### Demos — library-wide consolidation sweep (HTML-first)
@@ -379,7 +379,7 @@ export class UIAgentReasoning extends UIElement {
379
379
  <span data-reasoning-meta>
380
380
  ${total ? `<span data-reasoning-counter>${done}/${total}</span>` : ''}
381
381
  <span data-reasoning-time>${elapsed}s</span>
382
- <icon-ui name="${this.collapsed ? 'caret-right' : 'caret-up'}" color="muted" data-reasoning-chevron></icon-ui>
382
+ <icon-ui name="${this.collapsed ? 'caret-right' : 'caret-up'}" color="muted" data-reasoning-caret></icon-ui>
383
383
  </span>
384
384
  `;
385
385
 
@@ -27,7 +27,7 @@
27
27
  --agent-trace-detail-label-col-default: 9rem;
28
28
 
29
29
  /* ── Motion ── */
30
- --agent-trace-chevron-dur-default: var(--a-duration-fast);
30
+ --agent-trace-caret-dur-default: var(--a-duration-fast);
31
31
  }
32
32
 
33
33
  :scope {
@@ -50,7 +50,7 @@
50
50
  border-radius: var(--a-radius-sm);
51
51
  padding: var(--a-space-0-5) var(--a-space-1);
52
52
  margin: calc(var(--a-space-0-5) * -1) calc(var(--a-space-1) * -1);
53
- transition: background var(--agent-trace-chevron-dur, var(--agent-trace-chevron-dur-default));
53
+ transition: background var(--agent-trace-caret-dur, var(--agent-trace-caret-dur-default));
54
54
  }
55
55
 
56
56
  [data-trace-root] > summary:hover {
@@ -94,12 +94,12 @@
94
94
  margin-inline: 2px;
95
95
  }
96
96
 
97
- [data-trace-chevron] {
97
+ [data-trace-caret] {
98
98
  flex-shrink: 0;
99
- transition: transform var(--agent-trace-chevron-dur, var(--agent-trace-chevron-dur-default));
99
+ transition: transform var(--agent-trace-caret-dur, var(--agent-trace-caret-dur-default));
100
100
  }
101
101
 
102
- [data-trace-root][open] [data-trace-chevron] {
102
+ [data-trace-root][open] [data-trace-caret] {
103
103
  transform: rotate(90deg);
104
104
  }
105
105
 
@@ -225,20 +225,20 @@
225
225
  text-align: end;
226
226
  }
227
227
 
228
- /* Chevron in the trailing column */
229
- [data-trace-row-chevron] {
228
+ /* Caret in the trailing column */
229
+ [data-trace-row-caret] {
230
230
  --a-icon-size: 0.875em;
231
231
  justify-self: end;
232
232
  align-self: center;
233
- transition: transform var(--a-duration-fast) var(--agent-trace-chevron-dur, var(--a-duration-fast));
233
+ transition: transform var(--a-duration-fast) var(--agent-trace-caret-dur, var(--a-duration-fast));
234
234
  color: var(--agent-trace-fg-muted, var(--agent-trace-fg-muted-default));
235
235
  }
236
236
 
237
- details[data-trace-row][open] > summary > [data-trace-row-chevron] {
237
+ details[data-trace-row][open] > summary > [data-trace-row-caret] {
238
238
  transform: rotate(90deg);
239
239
  }
240
240
 
241
- [data-trace-row-chevron-spacer] {
241
+ [data-trace-row-caret-spacer] {
242
242
  width: 0.875em;
243
243
  justify-self: end;
244
244
  }
@@ -128,7 +128,7 @@ class UIAgentTrace extends UIElement {
128
128
  this.#rootEl.innerHTML = `
129
129
  <summary>
130
130
  <span data-trace-status>${pillsHTML}</span>
131
- <icon-ui name="caret-right" color="muted" size="sm" data-trace-chevron></icon-ui>
131
+ <icon-ui name="caret-right" color="muted" size="sm" data-trace-caret></icon-ui>
132
132
  </summary>
133
133
  <div data-trace-body>
134
134
  ${this.#metrics.length ? `<div data-trace-rows${hasAnyDetails ? ' data-has-details' : ''}>${headerRow}${rowsHTML}</div>` : ''}
@@ -156,8 +156,8 @@ class UIAgentTrace extends UIElement {
156
156
  <span data-trace-aux>${aux}</span>
157
157
  ${hasAnyDetails
158
158
  ? (hasDetails
159
- ? '<icon-ui name="caret-right" color="muted" data-trace-row-chevron></icon-ui>'
160
- : '<span data-trace-row-chevron-spacer aria-hidden="true"></span>')
159
+ ? '<icon-ui name="caret-right" color="muted" data-trace-row-caret></icon-ui>'
160
+ : '<span data-trace-row-caret-spacer aria-hidden="true"></span>')
161
161
  : ''}
162
162
  `;
163
163
 
@@ -31,6 +31,11 @@
31
31
  "type": "string",
32
32
  "default": ""
33
33
  },
34
+ "month": {
35
+ "description": "Displayed month (YYYY-MM) used when no value is selected — shows a specific month independent of selection. A selected value always takes precedence. Used by date-range-picker to show its end pane one month ahead of the start.",
36
+ "type": "string",
37
+ "default": ""
38
+ },
34
39
  "rangeEnd": {
35
40
  "description": "End of a date range (ISO YYYY-MM-DD). See `rangeStart` for the full contract.",
36
41
  "type": "string",
@@ -61,6 +61,10 @@ export class UICalendarGrid extends UIElement {
61
61
 
62
62
  static properties = {
63
63
  value: { type: String, default: '', reflect: true },
64
+ // Displayed month (`YYYY-MM`) when no `value` is selected — lets a consumer
65
+ // show a specific month independent of selection (e.g. a date-range picker
66
+ // showing its end pane one month ahead of the start pane). Selection wins.
67
+ month: { type: String, default: '', reflect: true },
64
68
  min: { type: String, default: '', reflect: true },
65
69
  max: { type: String, default: '', reflect: true },
66
70
  disabled: { type: Boolean, default: false, reflect: true },
@@ -90,11 +94,14 @@ export class UICalendarGrid extends UIElement {
90
94
  this.setAttribute('role', 'group');
91
95
  if (!this.hasAttribute('aria-label')) this.setAttribute('aria-label', 'Calendar');
92
96
 
93
- // Initialize view to selected date or today.
97
+ // Initialize view to selected date, else the [month] hint, else today.
94
98
  const sel = parseISO(this.value);
95
99
  if (sel) {
96
100
  this.#viewYear = sel.getFullYear();
97
101
  this.#viewMonth = sel.getMonth();
102
+ } else {
103
+ const vm = /^(\d{4})-(\d{2})$/.exec(String(this.month || '').trim());
104
+ if (vm) { this.#viewYear = +vm[1]; this.#viewMonth = +vm[2] - 1; }
98
105
  }
99
106
 
100
107
  if (!this.#bound) {
@@ -22,6 +22,8 @@ export class UICalendarGrid extends UIElement {
22
22
  max: string;
23
23
  /** Minimum selectable date in ISO format (YYYY-MM-DD). */
24
24
  min: string;
25
+ /** Displayed month (YYYY-MM) used when no value is selected — shows a specific month independent of selection. A selected value always takes precedence. Used by date-range-picker to show its end pane one month ahead of the start. */
26
+ month: string;
25
27
  /** End of a date range (ISO YYYY-MM-DD). See `rangeStart` for the full contract. */
26
28
  rangeEnd: string;
27
29
  /** Start of a date range (ISO YYYY-MM-DD). When both rangeStart and rangeEnd are set + ordered, day cells strictly between the endpoints get `[data-in-range]` stamped for visual continuity. Used by `<date-range-picker-ui>` which pushes the same from/to onto both calendar panes. Endpoints themselves render via the `value` prop's `[data-selected]` state. */
@@ -22,6 +22,14 @@ props:
22
22
  type: string
23
23
  default: ''
24
24
  reflect: true
25
+ month:
26
+ description: >-
27
+ Displayed month (YYYY-MM) used when no value is selected — shows a specific
28
+ month independent of selection. A selected value always takes precedence.
29
+ Used by date-range-picker to show its end pane one month ahead of the start.
30
+ type: string
31
+ default: ''
32
+ reflect: true
25
33
  min:
26
34
  description: Minimum selectable date in ISO format (YYYY-MM-DD).
27
35
  type: string
@@ -411,6 +411,15 @@ export class UIDateRangePicker extends UIFormElement {
411
411
  }
412
412
  if (!calArea.querySelector(':scope > [data-cal-to]')) {
413
413
  const calTo = this.constructor._pp.calTo.cloneNode(true);
414
+ // Default the end pane to one month after the start pane so a fresh range
415
+ // picker shows two consecutive months (not the same month twice). A `to`
416
+ // value, once selected, overrides this hint inside the grid.
417
+ const fromISO = parseRange(this.value)?.from;
418
+ let y, mo; // mo: 0-based month
419
+ if (fromISO) { const [yy, mm] = fromISO.split('-').map(Number); y = yy; mo = mm - 1; }
420
+ else { const now = new Date(); y = now.getFullYear(); mo = now.getMonth(); }
421
+ mo += 1; if (mo > 11) { mo = 0; y += 1; }
422
+ calTo.setAttribute('month', `${y}-${pad(mo + 1)}`);
414
423
  calArea.appendChild(calTo);
415
424
  }
416
425
  this.#calFromRef = calArea.querySelector(':scope > [data-cal-from]');
@@ -227,9 +227,14 @@ describe('field-ui', () => {
227
227
  expect(scopeBlock[0]).not.toMatch(/row-gap\s*:/);
228
228
  });
229
229
 
230
- it('CSS source contract: `:scope:has(> [slot="hint"], > [slot="error"])` declares row-gap', () => {
230
+ it('CSS source contract: `:scope:has([slot="hint"], [slot="error"])` declares row-gap', () => {
231
+ // The `>` direct-child combinator is OPTIONAL — field.css uses the
232
+ // descendant form `:has([slot="hint"], [slot="error"])` (v0.6.x); either
233
+ // satisfies the FB-54 row-gap-presence-guard contract.
231
234
  expect(FIELD_CSS).toMatch(
232
- /:scope:has\(>\s*\[slot="hint"\]\s*,\s*>\s*\[slot="error"\]\)\s*\{[^}]*row-gap:\s*var\(--field-gap\)/
235
+ // `var(--field-gap[,)]` accepts both the bare token and the override-aware
236
+ // `var(--field-gap, var(--field-gap-default))` chain (OD-5 token-shadowing).
237
+ /:scope:has\((?:>\s*)?\[slot="hint"\]\s*,\s*(?:>\s*)?\[slot="error"\]\)\s*\{[^}]*row-gap:\s*var\(--field-gap[,)]/
233
238
  );
234
239
  });
235
240
  });
@@ -82,6 +82,9 @@
82
82
  "text-area"
83
83
  ],
84
84
  "slots": {
85
+ "caret": {
86
+ "description": "Collapse caret inside the header. Auto-stamped as `<icon-ui slot=\"caret\" name=\"caret-right\">` (rotates on `[collapsed]`). Supply your own `slot=\"caret\"` child in the header to customize — adopt-or-stamp honors a declarative caret instead of stamping."
87
+ },
85
88
  "header": {
86
89
  "description": "Auto-created header element with label text and toggle arrow"
87
90
  }
@@ -100,12 +100,13 @@ export class UIPane extends UIElement {
100
100
  header.setAttribute('tabindex', '0');
101
101
  header.setAttribute('aria-expanded', String(!this.collapsed));
102
102
 
103
- // Stamp chevron icon if not present
104
- if (!header.querySelector('[slot="chevron"]')) {
105
- const chevron = document.createElement('icon-ui');
106
- chevron.setAttribute('slot', 'chevron');
107
- chevron.setAttribute('name', 'caret-right');
108
- header.append(chevron);
103
+ // Stamp the caret icon if not present (adopt-or-stamp: a declarative
104
+ // [slot="caret"] child is honored, else we stamp the default).
105
+ if (!header.querySelector('[slot="caret"]')) {
106
+ const caret = document.createElement('icon-ui');
107
+ caret.setAttribute('slot', 'caret');
108
+ caret.setAttribute('name', 'caret-right');
109
+ header.append(caret);
109
110
  }
110
111
  }
111
112
  }
@@ -29,7 +29,7 @@
29
29
 
30
30
  /* ── Header interaction ── */
31
31
  --pane-header-bg-hover-default: var(--a-bg-subtle);
32
- --pane-chevron-fg-default: var(--a-fg-muted);
32
+ --pane-caret-fg-default: var(--a-fg-muted);
33
33
 
34
34
  /* ── Section header ── */
35
35
  --pane-section-header-weight-default: var(--a-weight-medium);
@@ -107,17 +107,17 @@
107
107
  box-shadow: var(--a-focus-ring) inset;
108
108
  }
109
109
 
110
- /* Collapse indicator — stamped by JS as icon-ui */
111
- & > header > [slot="chevron"] {
110
+ /* Collapse indicator (caret) — stamped by JS as icon-ui */
111
+ & > header > [slot="caret"] {
112
112
  --a-icon-size: var(--a-caret-size);
113
113
  flex-shrink: 0;
114
114
  margin-inline-start: auto;
115
- color: var(--pane-chevron-fg, var(--pane-chevron-fg-default));
115
+ color: var(--pane-caret-fg, var(--pane-caret-fg-default));
116
116
  transition: transform var(--pane-duration, var(--pane-duration-default)) var(--pane-easing, var(--pane-easing-default));
117
117
  transform: rotate(90deg);
118
118
  }
119
119
 
120
- :scope[collapsed] > header > [slot="chevron"] {
120
+ :scope[collapsed] > header > [slot="caret"] {
121
121
  transform: rotate(0deg);
122
122
  }
123
123
 
@@ -62,6 +62,12 @@ events:
62
62
  slots:
63
63
  header:
64
64
  description: Auto-created header element with label text and toggle arrow
65
+ caret:
66
+ description: >-
67
+ Collapse caret inside the header. Auto-stamped as
68
+ `<icon-ui slot="caret" name="caret-right">` (rotates on `[collapsed]`).
69
+ Supply your own `slot="caret"` child in the header to customize —
70
+ adopt-or-stamp honors a declarative caret instead of stamping.
65
71
  states:
66
72
  - name: idle
67
73
  description: Default, ready for interaction.
@@ -65,6 +65,12 @@
65
65
  "Accordion"
66
66
  ],
67
67
  "slots": {
68
+ "actions": {
69
+ "description": "Per-row action buttons (rename / add / overflow menu), right-aligned and hover-revealed. A declarative `slot=\"actions\"` child is adopted into the auto-stamped row (FEEDBACK-89), so it sits inline in the row rather than wrapping below it."
70
+ },
71
+ "caret": {
72
+ "description": "Override slot for the expand/collapse caret. Auto-stamped as `<icon-ui slot=\"caret\" name=\"caret-right\">` (rotates on `[open]`, hidden for leaf nodes). Supply your own `slot=\"caret\"` child to customize — adopt-or-stamp moves a declarative caret into the row."
73
+ },
68
74
  "icon": {
69
75
  "description": "Override the leading [icon] glyph with a custom slotted element (custom icon-ui, folder open/closed glyph, file-type marker). Mutually exclusive with the [icon] attribute — slot child wins."
70
76
  }
@@ -47,6 +47,18 @@ slots:
47
47
  Override the leading [icon] glyph with a custom slotted element (custom
48
48
  icon-ui, folder open/closed glyph, file-type marker). Mutually exclusive
49
49
  with the [icon] attribute — slot child wins.
50
+ caret:
51
+ description: >-
52
+ Override slot for the expand/collapse caret. Auto-stamped as
53
+ `<icon-ui slot="caret" name="caret-right">` (rotates on `[open]`,
54
+ hidden for leaf nodes). Supply your own `slot="caret"` child to
55
+ customize — adopt-or-stamp moves a declarative caret into the row.
56
+ actions:
57
+ description: >-
58
+ Per-row action buttons (rename / add / overflow menu), right-aligned
59
+ and hover-revealed. A declarative `slot="actions"` child is adopted
60
+ into the auto-stamped row (FEEDBACK-89), so it sits inline in the row
61
+ rather than wrapping below it.
50
62
 
51
63
  a2ui:
52
64
  rules:
@@ -72,7 +84,7 @@ a2ui:
72
84
  - >-
73
85
  Nest further <tree-item-ui> in the default slot only — no
74
86
  <list-item-ui>, <nav-item-ui>, or arbitrary content inside a
75
- tree row. The chevron is auto-stamped when the row has nested
87
+ tree row. The caret is auto-stamped when the row has nested
76
88
  tree-item-ui children.
77
89
 
78
90
  keywords:
@@ -126,8 +126,8 @@
126
126
  "--tree-bg-selected": {
127
127
  "description": "Background color when selected"
128
128
  },
129
- "--tree-chevron-size": {
130
- "description": "Size of the collapse chevron icon"
129
+ "--tree-caret-size": {
130
+ "description": "Size of the collapse caret icon"
131
131
  },
132
132
  "--tree-duration": {
133
133
  "description": "Transition duration"
@@ -139,7 +139,7 @@
139
139
  "description": "Primary text color"
140
140
  },
141
141
  "--tree-fg-muted": {
142
- "description": "Muted text color (icons, chevrons)"
142
+ "description": "Muted text color (icons, carets)"
143
143
  },
144
144
  "--tree-focus-ring": {
145
145
  "description": "Focus ring box-shadow"
@@ -293,11 +293,17 @@ export class UITreeItem extends UIElement {
293
293
  row.setAttribute('slot', 'row');
294
294
  row.setAttribute('tabindex', '0');
295
295
 
296
- // Chevron
297
- const chevron = document.createElement('icon-ui');
298
- chevron.setAttribute('slot', 'chevron');
299
- chevron.setAttribute('name', 'caret-right');
300
- row.appendChild(chevron);
296
+ // Caret — adopt a declarative [slot="caret"] child if the consumer supplied
297
+ // one, else stamp the default. (Adopt-or-stamp; same pattern as actions.)
298
+ const declaredCaret = this.querySelector(':scope > [slot="caret"]');
299
+ if (declaredCaret) {
300
+ row.appendChild(declaredCaret);
301
+ } else {
302
+ const caret = document.createElement('icon-ui');
303
+ caret.setAttribute('slot', 'caret');
304
+ caret.setAttribute('name', 'caret-right');
305
+ row.appendChild(caret);
306
+ }
301
307
 
302
308
  // Icon
303
309
  if (this.icon) {
@@ -320,10 +326,19 @@ export class UITreeItem extends UIElement {
320
326
  if (this.badge) badgeEl.textContent = this.badge;
321
327
  row.appendChild(badgeEl);
322
328
 
323
- // Actions slot placeholder
324
- const actions = document.createElement('span');
325
- actions.setAttribute('slot', 'actions');
326
- row.appendChild(actions);
329
+ // Actions — adopt pre-existing declarative [slot="actions"] children into
330
+ // the row (FEEDBACK-89) so per-row action buttons land in the styled,
331
+ // hover-revealed actions area; else stamp an empty placeholder. The host
332
+ // #onClick already excludes [slot="actions"] * from row selection, so
333
+ // adoption is click-safe.
334
+ const declaredActions = this.querySelectorAll(':scope > [slot="actions"]');
335
+ if (declaredActions.length) {
336
+ for (const a of declaredActions) row.appendChild(a);
337
+ } else {
338
+ const actions = document.createElement('span');
339
+ actions.setAttribute('slot', 'actions');
340
+ row.appendChild(actions);
341
+ }
327
342
 
328
343
  this.prepend(row);
329
344
  }
@@ -7,7 +7,7 @@
7
7
  --tree-row-gap-default: var(--a-space-1);
8
8
  --tree-actions-gap-default: var(--a-space-0-5);
9
9
  --tree-indent-default: var(--a-space-4);
10
- --tree-chevron-size-default: var(--a-space-2);
10
+ --tree-caret-size-default: var(--a-space-2);
11
11
  --tree-icon-size-default: var(--a-space-3);
12
12
 
13
13
  /* ── Typography ── */
@@ -47,7 +47,7 @@
47
47
  --tree-item-row-gap: var(--tree-row-gap, var(--a-space-1));
48
48
  --tree-item-actions-gap: var(--tree-actions-gap, var(--a-space-0-5));
49
49
  --tree-item-indent: var(--tree-indent, var(--a-space-4));
50
- --tree-item-chevron-size: var(--tree-chevron-size, var(--a-space-2));
50
+ --tree-item-caret-size: var(--tree-caret-size, var(--a-space-2));
51
51
  --tree-item-icon-size: var(--tree-icon-size, var(--a-space-3));
52
52
 
53
53
  --tree-item-fg: var(--tree-fg, var(--a-fg));
@@ -92,21 +92,21 @@
92
92
 
93
93
  /* :scope[selected] rules moved outside @scope — see tree-ui Safari note at end of file. */
94
94
 
95
- /* ── Chevron ── */
96
- [slot="chevron"] {
97
- --a-icon-size: var(--tree-chevron-size, var(--tree-chevron-size-default));
95
+ /* ── Caret ── */
96
+ [slot="caret"] {
97
+ --a-icon-size: var(--tree-caret-size, var(--tree-caret-size-default));
98
98
  flex-shrink: 0;
99
99
  color: var(--tree-fg-muted, var(--tree-fg-muted-default));
100
100
  transition: transform var(--tree-duration, var(--tree-duration-default)) var(--tree-easing, var(--tree-easing-default));
101
101
  transform: rotate(90deg);
102
102
  }
103
103
 
104
- :scope:not([open]) > [slot="row"] > [slot="chevron"] {
104
+ :scope:not([open]) > [slot="row"] > [slot="caret"] {
105
105
  transform: rotate(0deg);
106
106
  }
107
107
 
108
- /* Hide chevron for leaf nodes */
109
- :scope:not(:has(> tree-item-ui)) > [slot="row"] > [slot="chevron"] {
108
+ /* Hide caret for leaf nodes */
109
+ :scope:not(:has(> tree-item-ui)) > [slot="row"] > [slot="caret"] {
110
110
  visibility: hidden;
111
111
  }
112
112
 
@@ -101,3 +101,55 @@ describe('<tree-ui> tree-select forwards modifier keys (FB-46)', () => {
101
101
  expect(received.shiftKey).toBe(false);
102
102
  });
103
103
  });
104
+
105
+ describe('<tree-item-ui> caret slot + actions adoption (caret convention + FB-89)', () => {
106
+ let host;
107
+ const settle = () => new Promise((r) => setTimeout(r, 30));
108
+
109
+ beforeEach(() => {
110
+ host = document.createElement('div');
111
+ document.body.appendChild(host);
112
+ });
113
+ afterEach(() => host.remove());
114
+
115
+ it('auto-stamps a slot="caret" icon (renamed from slot="chevron")', async () => {
116
+ const item = document.createElement('tree-item-ui');
117
+ item.setAttribute('text', 'Colors');
118
+ host.appendChild(item);
119
+ await settle();
120
+ const row = item.querySelector(':scope > [slot="row"]');
121
+ expect(row.querySelector('[slot="caret"]')).toBeTruthy();
122
+ expect(row.querySelector('[slot="chevron"]')).toBeNull();
123
+ });
124
+
125
+ it('adopts a declarative slot="actions" child into the stamped row (FB-89)', async () => {
126
+ const item = document.createElement('tree-item-ui');
127
+ item.setAttribute('text', 'Colors');
128
+ const btn = document.createElement('button-ui');
129
+ btn.setAttribute('slot', 'actions');
130
+ btn.setAttribute('icon', 'plus');
131
+ item.appendChild(btn);
132
+ host.appendChild(item);
133
+ await settle();
134
+ const row = item.querySelector(':scope > [slot="row"]');
135
+ // The action button is now INSIDE the row, not a sibling left below it.
136
+ expect(btn.parentElement).toBe(row);
137
+ expect(item.querySelector(':scope > [slot="actions"]')).toBeNull();
138
+ });
139
+
140
+ it('adopts a declarative slot="caret" child instead of stamping a default', async () => {
141
+ const item = document.createElement('tree-item-ui');
142
+ item.setAttribute('text', 'Colors');
143
+ const customCaret = document.createElement('icon-ui');
144
+ customCaret.setAttribute('slot', 'caret');
145
+ customCaret.setAttribute('name', 'folder');
146
+ item.appendChild(customCaret);
147
+ host.appendChild(item);
148
+ await settle();
149
+ const row = item.querySelector(':scope > [slot="row"]');
150
+ const carets = row.querySelectorAll('[slot="caret"]');
151
+ expect(carets.length).toBe(1); // no double-stamp
152
+ expect(carets[0]).toBe(customCaret); // consumer's caret used
153
+ expect(carets[0].getAttribute('name')).toBe('folder');
154
+ });
155
+ });
@@ -59,8 +59,8 @@ tokens:
59
59
  description: Background color on hover
60
60
  --tree-bg-selected:
61
61
  description: Background color when selected
62
- --tree-chevron-size:
63
- description: Size of the collapse chevron icon
62
+ --tree-caret-size:
63
+ description: Size of the collapse caret icon
64
64
  --tree-duration:
65
65
  description: Transition duration
66
66
  --tree-easing:
@@ -68,7 +68,7 @@ tokens:
68
68
  --tree-fg:
69
69
  description: Primary text color
70
70
  --tree-fg-muted:
71
- description: Muted text color (icons, chevrons)
71
+ description: Muted text color (icons, carets)
72
72
  --tree-focus-ring:
73
73
  description: Focus ring box-shadow
74
74
  --tree-font-size:
@@ -121,7 +121,7 @@ a2ui:
121
121
  rows — selection is managed by the parent and bubbles once.
122
122
  Detail = {item, text, value, ctrlKey, metaKey, shiftKey}.
123
123
  - >-
124
- Per ADR-0027, <tree-ui> composes <icon-ui> (for chevrons) but
124
+ Per ADR-0027, <tree-ui> composes <icon-ui> (for carets) but
125
125
  does NOT auto-import its children. Consumer pages must
126
126
  explicitly import both <tree-ui> and <tree-item-ui>.
127
127
  anti_patterns: []