@adia-ai/web-components 0.8.26 → 0.8.28

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,19 @@
1
1
  # Changelog — @adia-ai/web-components
2
2
 
3
+ ## [0.8.28] — 2026-08-05
4
+
5
+ ### Maintenance
6
+ - **Lockstep version bump only.** No source changes in this package; bumped to maintain the lockstep version coherence enforced by `scripts/release/check-lockstep.mjs`. Substantive v0.8.28 work shipped in sideEffects tree-shaking fix + CSS barrel regen (gh#625/632). See `0828--2026-08-05` for details.
7
+
8
+ ## [0.8.27] — 2026-08-05
9
+
10
+ ### Added
11
+ - **`menu-item-ui` gains `subtitle` (+ a trailing slot)** — menu rows can carry a secondary line, unblocking composite menu rows without native `<button>` stamps (gh#616, PR #623).
12
+
13
+ ### Maintenance
14
+ - **`components/` touched in this release window** (5 file(s), e.g. `menu/menu-item.a2ui.json`) — carried by the entries above.
15
+ - **`dist/` bundles rebuilt** in this cut's window (4 file(s)) — regenerated from the source changes described above, not independent edits.
16
+
3
17
  ## [0.8.26] — 2026-08-05
4
18
 
5
19
  ### Fixed
@@ -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/MenuItem.json",
4
4
  "title": "MenuItem",
5
- "description": "Child of `<menu-ui>`. One actionable row inside a menu, with optional leading icon + label + value.",
5
+ "description": "Child of `<menu-ui>`. One actionable row inside a menu, with optional leading icon + label + subtitle + value.",
6
6
  "type": "object",
7
7
  "allOf": [
8
8
  {
@@ -26,6 +26,11 @@
26
26
  "type": "string",
27
27
  "default": ""
28
28
  },
29
+ "subtitle": {
30
+ "description": "Optional secondary description line rendered under the label.",
31
+ "type": "string",
32
+ "default": ""
33
+ },
29
34
  "text": {
30
35
  "description": "Item label text.",
31
36
  "type": "string",
@@ -73,8 +78,14 @@
73
78
  "icon": {
74
79
  "description": "Custom leading element override; falls through to `[icon]` prop if not slotted."
75
80
  },
81
+ "subtitle": {
82
+ "description": "Custom secondary-line content override; falls through to `[subtitle]` prop if not slotted."
83
+ },
76
84
  "text": {
77
85
  "description": "Custom label content; falls through to `[text]` prop if not slotted."
86
+ },
87
+ "trailing": {
88
+ "description": "Trailing element pinned to the far edge (shortcut hint, check mark)."
78
89
  }
79
90
  },
80
91
  "states": [],
@@ -13,7 +13,7 @@ component: MenuItem
13
13
  category: navigation
14
14
  version: 1
15
15
  description: |-
16
- Child of `<menu-ui>`. One actionable row inside a menu, with optional leading icon + label + value.
16
+ Child of `<menu-ui>`. One actionable row inside a menu, with optional leading icon + label + subtitle + value.
17
17
 
18
18
  props:
19
19
  icon:
@@ -24,6 +24,10 @@ props:
24
24
  description: Item label text.
25
25
  type: string
26
26
  default: ""
27
+ subtitle:
28
+ description: Optional secondary description line rendered under the label.
29
+ type: string
30
+ default: ""
27
31
  value:
28
32
  description: Stable identifier emitted via the parent menu's `select` event.
29
33
  type: string
@@ -44,6 +48,10 @@ slots:
44
48
  description: Custom leading element override; falls through to `[icon]` prop if not slotted.
45
49
  text:
46
50
  description: Custom label content; falls through to `[text]` prop if not slotted.
51
+ subtitle:
52
+ description: Custom secondary-line content override; falls through to `[subtitle]` prop if not slotted.
53
+ trailing:
54
+ description: Trailing element pinned to the far edge (shortcut hint, check mark).
47
55
 
48
56
  a2ui:
49
57
  rules:
@@ -258,6 +258,7 @@ export class UIMenuItem extends UIElement {
258
258
  static properties = {
259
259
  icon: { type: String, default: '', reflect: true },
260
260
  text: { type: String, default: '', reflect: true },
261
+ subtitle: { type: String, default: '', reflect: true },
261
262
  value: { type: String, default: '', reflect: true },
262
263
  variant: { type: String, default: 'default', reflect: true },
263
264
  disabled: { type: Boolean, default: false, reflect: true },
@@ -265,6 +266,11 @@ export class UIMenuItem extends UIElement {
265
266
 
266
267
  static template = () => null;
267
268
 
269
+ // Set when connected() stamped role="menuitem" itself; a consumer-provided
270
+ // role (e.g. admin-roster's role="menuitemradio", gh#616) is never touched
271
+ // — that consumer also owns tabindex and any checked-state aria.
272
+ #roleStamped = false;
273
+
268
274
  connected() {
269
275
  // role="menuitem" ONLY when actually inside a menu container. A menu-ui
270
276
  // closes by moving its items back into its own light DOM (see #hide()),
@@ -273,12 +279,16 @@ export class UIMenuItem extends UIElement {
273
279
  // open popover (role="menu") adopts items via appendChild, which re-fires
274
280
  // connected(), so the role re-applies when the menu opens and is dropped
275
281
  // again on close.
276
- if (this.closest('[role="menu"], [role="menubar"]')) {
282
+ if (this.hasAttribute('role') && !this.#roleStamped) {
283
+ // Consumer-owned role — leave role/tabindex alone.
284
+ } else if (this.closest('[role="menu"], [role="menubar"]')) {
277
285
  this.setAttribute('role', 'menuitem');
286
+ this.#roleStamped = true;
278
287
  if (!this.hasAttribute('tabindex')) this.setAttribute('tabindex', '-1');
279
288
  } else {
280
289
  this.removeAttribute('role');
281
290
  this.removeAttribute('tabindex');
291
+ this.#roleStamped = false;
282
292
  }
283
293
  this.#stamp();
284
294
  this.#syncAria();
@@ -313,6 +323,13 @@ export class UIMenuItem extends UIElement {
313
323
  span.textContent = this.text;
314
324
  this.appendChild(span);
315
325
  }
326
+
327
+ if (this.subtitle && !this.#ownChild('[slot="subtitle"]')) {
328
+ const sub = this.#stampMark(document.createElement('span'));
329
+ sub.setAttribute('slot', 'subtitle');
330
+ sub.textContent = this.subtitle;
331
+ this.appendChild(sub);
332
+ }
316
333
  }
317
334
 
318
335
  #syncAria() {
@@ -340,6 +357,21 @@ export class UIMenuItem extends UIElement {
340
357
  const textEl = this.#ownChild('[slot="text"]');
341
358
  if (this.#wasStamped(textEl)) textEl.textContent = this.text;
342
359
 
360
+ // Sync subtitle — only touch elements we stamped.
361
+ const subEl = this.#ownChild('[slot="subtitle"]');
362
+ if (this.subtitle) {
363
+ if (subEl) {
364
+ if (this.#wasStamped(subEl)) subEl.textContent = this.subtitle;
365
+ } else {
366
+ const el = this.#stampMark(document.createElement('span'));
367
+ el.setAttribute('slot', 'subtitle');
368
+ el.textContent = this.subtitle;
369
+ this.appendChild(el);
370
+ }
371
+ } else if (this.#wasStamped(subEl)) {
372
+ subEl.remove();
373
+ }
374
+
343
375
  this.#syncAria();
344
376
  }
345
377
  }
@@ -136,6 +136,8 @@ menu-item-ui[variant="danger"]:hover [slot="icon"] {
136
136
 
137
137
  /* ── Typography ── */
138
138
  --menu-item-font-size: var(--a-ui-size);
139
+ --menu-item-subtitle-size: var(--a-ui-sm);
140
+ --menu-item-subtitle-fg: var(--a-fg-muted);
139
141
 
140
142
  /* ── Transition ── */
141
143
  --menu-item-duration: var(--a-duration-fast);
@@ -178,6 +180,47 @@ menu-item-ui[variant="danger"]:hover [slot="icon"] {
178
180
  white-space: nowrap;
179
181
  }
180
182
 
183
+ /* Trailing (shortcut hint, check mark) — pinned to the far edge */
184
+ [slot="trailing"] {
185
+ flex-shrink: 0;
186
+ margin-left: auto;
187
+ }
188
+
189
+ /* ── Subtitle — two-line anatomy ──
190
+ Content-driven: a present [slot="subtitle"] switches the row to a
191
+ grid so the subtitle sits under the label while icon and trailing
192
+ content span both lines. */
193
+ :scope:has(> [slot="subtitle"]) {
194
+ display: grid;
195
+ grid-template-columns: auto 1fr auto;
196
+ align-items: center;
197
+ column-gap: var(--menu-item-gap);
198
+ row-gap: 0;
199
+ }
200
+ :scope:has(> [slot="subtitle"]) > [slot="icon"] {
201
+ grid-column: 1;
202
+ grid-row: 1 / span 2;
203
+ }
204
+ :scope:has(> [slot="subtitle"]) > [slot="text"] {
205
+ grid-column: 2;
206
+ grid-row: 1;
207
+ }
208
+ :scope:has(> [slot="subtitle"]) > [slot="trailing"] {
209
+ grid-column: 3;
210
+ grid-row: 1 / span 2;
211
+ margin-left: 0;
212
+ }
213
+ :scope > [slot="subtitle"] {
214
+ grid-column: 2;
215
+ grid-row: 2;
216
+ min-width: 0;
217
+ font-size: var(--menu-item-subtitle-size);
218
+ color: var(--menu-item-subtitle-fg);
219
+ white-space: nowrap;
220
+ overflow: hidden;
221
+ text-overflow: ellipsis;
222
+ }
223
+
181
224
  /* ── Variant: danger ── */
182
225
  :scope[variant="danger"] {
183
226
  --menu-item-fg: var(--a-danger-bg);
@@ -45,6 +45,8 @@ export class UIMenuItem extends UIElement {
45
45
  disabled: boolean;
46
46
  /** Optional leading Phosphor icon name. */
47
47
  icon: string;
48
+ /** Optional secondary description line rendered under the label. */
49
+ subtitle: string;
48
50
  /** Item label text. */
49
51
  text: string;
50
52
  /** Stable identifier emitted via the parent menu's `select` event. */