@adia-ai/web-components 0.6.50 → 0.7.1

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 (106) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/components/action-list/action-list.css +1 -1
  3. package/components/agent-artifact/agent-artifact.class.js +10 -10
  4. package/components/agent-artifact/agent-artifact.css +1 -1
  5. package/components/agent-reasoning/agent-reasoning.class.js +51 -0
  6. package/components/agent-reasoning/agent-reasoning.css +49 -22
  7. package/components/alert/alert.class.js +8 -1
  8. package/components/alert/alert.css +13 -1
  9. package/components/avatar/avatar.a2ui.json +2 -14
  10. package/components/avatar/avatar.class.js +3 -15
  11. package/components/avatar/avatar.d.ts +2 -4
  12. package/components/avatar/avatar.yaml +1 -18
  13. package/components/breadcrumb/breadcrumb.css +4 -1
  14. package/components/button/button.a2ui.json +3 -0
  15. package/components/button/button.css +14 -3
  16. package/components/button/button.yaml +5 -0
  17. package/components/calendar-grid/calendar-grid.css +1 -1
  18. package/components/calendar-picker/calendar-picker.css +5 -2
  19. package/components/chart/chart.a2ui.json +0 -18
  20. package/components/chart/chart.class.js +8 -50
  21. package/components/chart/chart.css +1 -15
  22. package/components/chart/chart.d.ts +0 -4
  23. package/components/chart/chart.yaml +0 -24
  24. package/components/color-input/color-input.css +4 -1
  25. package/components/combobox/combobox.class.js +11 -0
  26. package/components/combobox/combobox.css +8 -0
  27. package/components/date-range-picker/date-range-picker.class.js +5 -1
  28. package/components/date-range-picker/date-range-picker.css +12 -2
  29. package/components/datetime-picker/datetime-picker.class.js +3 -0
  30. package/components/datetime-picker/datetime-picker.css +16 -2
  31. package/components/empty-state/empty-state.css +11 -4
  32. package/components/field/field.css +17 -6
  33. package/components/grid/grid.a2ui.json +5 -0
  34. package/components/grid/grid.class.js +16 -6
  35. package/components/grid/grid.css +17 -3
  36. package/components/grid/grid.d.ts +2 -0
  37. package/components/grid/grid.yaml +9 -0
  38. package/components/heatmap/heatmap.class.js +9 -3
  39. package/components/heatmap/heatmap.css +19 -2
  40. package/components/image/image.css +4 -1
  41. package/components/input/input.class.js +38 -0
  42. package/components/input/input.css +9 -5
  43. package/components/input/input.test.js +57 -0
  44. package/components/integration-card/integration-card.class.js +31 -7
  45. package/components/integration-card/integration-card.test.js +12 -1
  46. package/components/kbd/kbd.a2ui.json +3 -2
  47. package/components/kbd/kbd.css +7 -4
  48. package/components/kbd/kbd.d.ts +2 -2
  49. package/components/kbd/kbd.yaml +2 -1
  50. package/components/list/list.class.js +8 -1
  51. package/components/menu/menu.class.js +12 -3
  52. package/components/menu/menu.css +4 -1
  53. package/components/menu/menu.test.js +130 -0
  54. package/components/modal/modal.class.js +10 -1
  55. package/components/modal/modal.css +9 -0
  56. package/components/option-card/option-card.a2ui.json +3 -0
  57. package/components/option-card/option-card.css +44 -19
  58. package/components/option-card/option-card.yaml +5 -0
  59. package/components/otp-input/otp-input.css +25 -10
  60. package/components/page/page.css +64 -11
  61. package/components/pagination/pagination.class.js +1 -1
  62. package/components/pagination/pagination.css +9 -1
  63. package/components/pipeline-status/pipeline-status.css +6 -0
  64. package/components/popover/popover.css +12 -1
  65. package/components/preview/preview.css +30 -3
  66. package/components/progress-row/progress-row.css +3 -1
  67. package/components/qr-code/qr-code.css +4 -1
  68. package/components/segmented/segmented.css +4 -1
  69. package/components/select/select.a2ui.json +1 -1
  70. package/components/select/select.class.js +63 -7
  71. package/components/select/select.css +18 -0
  72. package/components/select/select.yaml +9 -2
  73. package/components/stack/stack.a2ui.json +12 -1
  74. package/components/stack/stack.d.ts +2 -2
  75. package/components/stack/stack.yaml +13 -1
  76. package/components/stat/stat.a2ui.json +5 -0
  77. package/components/stat/stat.css +55 -0
  78. package/components/stat/stat.d.ts +2 -0
  79. package/components/stat/stat.js +4 -0
  80. package/components/stat/stat.yaml +9 -0
  81. package/components/swiper/swiper.class.js +14 -6
  82. package/components/switch/switch.css +13 -0
  83. package/components/table/table.a2ui.json +2 -2
  84. package/components/table/table.css +13 -1
  85. package/components/table/table.yaml +2 -2
  86. package/components/time-picker/time-picker.css +4 -1
  87. package/components/timeline/timeline.class.js +3 -3
  88. package/components/timeline/timeline.css +23 -5
  89. package/components/toggle-group/toggle-group.css +4 -1
  90. package/components/toggle-scheme/toggle-scheme.css +4 -1
  91. package/components/tree/tree.class.js +24 -4
  92. package/components/tree/tree.test.js +108 -0
  93. package/dist/web-components.min.css +1 -1
  94. package/dist/web-components.min.js +83 -83
  95. package/package.json +3 -3
  96. package/styles/api/layout.css +7 -0
  97. package/styles/api/text.css +9 -5
  98. package/styles/index.css +11 -2
  99. package/styles/prose.css +8 -0
  100. package/styles/resets.css +5 -5
  101. package/styles/themes.css +8 -1
  102. package/styles/tokens.css +3 -3
  103. package/styles/type/elements.css +73 -0
  104. package/styles/type/roles.css +14 -49
  105. package/styles/type/scale.css +0 -5
  106. package/styles/typography.css +3 -3
@@ -23,10 +23,14 @@ import { parseResponsive, breakpoint } from '../../core/responsive.js';
23
23
 
24
24
  export class UIGrid extends UIElement {
25
25
  static properties = {
26
- columns: { type: String, default: '3', reflect: true },
27
- gap: { type: String, default: 'md', reflect: true },
28
- columnGap: { type: String, default: '', reflect: true, attribute: 'column-gap' },
29
- rowGap: { type: String, default: '', reflect: true, attribute: 'row-gap' },
26
+ columns: { type: String, default: '3', reflect: true },
27
+ gap: { type: String, default: 'md', reflect: true },
28
+ columnGap: { type: String, default: '', reflect: true, attribute: 'column-gap' },
29
+ rowGap: { type: String, default: '', reflect: true, attribute: 'row-gap' },
30
+ // Minimum track width for columns="auto-fit"/"auto-fill" (any CSS length,
31
+ // e.g. "240px", "16rem"). Drives the minmax() floor via --grid-min-col;
32
+ // unset → the 12rem default. No effect on numeric columns.
33
+ minColumnWidth: { type: String, default: '', reflect: true, attribute: 'min-column-width' },
30
34
  };
31
35
  static template = () => null;
32
36
 
@@ -54,6 +58,12 @@ export class UIGrid extends UIElement {
54
58
  this.style.gridAutoColumns = '';
55
59
  }
56
60
 
61
+ // ── min-column-width ───────────────────────────────────────────────────────
62
+ // Feeds the auto-fit/auto-fill minmax() floor (CSS + #colsToTemplate read
63
+ // var(--grid-min-col, 12rem)). Unset → cleared → falls back to 12rem.
64
+ if (this.minColumnWidth) this.style.setProperty('--grid-min-col', this.minColumnWidth);
65
+ else this.style.removeProperty('--grid-min-col');
66
+
57
67
  // ── gap ───────────────────────────────────────────────────────────────────
58
68
  // Same pattern: responsive values set inline column/row-gap directly;
59
69
  // scalar values clear those and let the universal [gap="N"] token rules
@@ -74,8 +84,8 @@ export class UIGrid extends UIElement {
74
84
  #colsToTemplate(v) {
75
85
  if (!v) return '';
76
86
  if (/^\d+$/.test(v)) return `repeat(${v}, 1fr)`;
77
- if (v === 'auto-fill') return 'repeat(auto-fill, minmax(12rem, 1fr))';
78
- if (v === 'auto-fit') return 'repeat(auto-fit, minmax(12rem, 1fr))';
87
+ if (v === 'auto-fill') return 'repeat(auto-fill, minmax(var(--grid-min-col, 12rem), 1fr))';
88
+ if (v === 'auto-fit') return 'repeat(auto-fit, minmax(var(--grid-min-col, 12rem), 1fr))';
79
89
  return v; // passthrough for custom template expressions
80
90
  }
81
91
 
@@ -28,9 +28,23 @@
28
28
  :scope[columns="5"] { grid-auto-flow: row; grid-template-columns: repeat(5, 1fr); grid-auto-columns: auto; }
29
29
  :scope[columns="6"] { grid-auto-flow: row; grid-template-columns: repeat(6, 1fr); grid-auto-columns: auto; }
30
30
 
31
- /* Responsive presets */
32
- :scope[columns="auto-fill"] { grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); }
33
- :scope[columns="auto-fit"] { grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); }
31
+ /* Responsive presets — the minmax() floor reads [min-column-width] via
32
+ --grid-min-col (set inline by grid.class.js), defaulting to 12rem.
33
+ MUST also flip to grid-auto-flow:row + grid-auto-columns:auto (like the
34
+ numeric rules) — the base :scope default is grid-auto-flow:column +
35
+ grid-auto-columns:1fr, which makes items flow into IMPLICIT 1fr columns
36
+ instead of wrapping into the auto-fit row tracks (the cause of the
37
+ "240px 77px 77px" sliver cram). */
38
+ :scope[columns="auto-fill"] {
39
+ grid-auto-flow: row;
40
+ grid-auto-columns: auto;
41
+ grid-template-columns: repeat(auto-fill, minmax(var(--grid-min-col, 12rem), 1fr));
42
+ }
43
+ :scope[columns="auto-fit"] {
44
+ grid-auto-flow: row;
45
+ grid-auto-columns: auto;
46
+ grid-template-columns: repeat(auto-fit, minmax(var(--grid-min-col, 12rem), 1fr));
47
+ }
34
48
 
35
49
  /* Column span — children can span multiple columns */
36
50
  & > [span="2"] { grid-column: span 2; }
@@ -19,6 +19,8 @@ export class UIGrid extends UIElement {
19
19
  columns: string;
20
20
  /** Grid gap. Accepts numeric space-scale values (0–12) or named sizes (xs/sm/md/lg/xl). Responsive notation supported: "2 4@md" = 2 below md, 4 from md upward. */
21
21
  gap: string;
22
+ /** Minimum track width for columns="auto-fit"/"auto-fill" (any CSS length, e.g. "240px", "16rem"). Sets the minmax() floor so cards don't shrink below it before wrapping; unset uses the 12rem default. No effect on numeric columns. */
23
+ minColumnWidth: string;
22
24
  /** Row gap override */
23
25
  rowGap: string;
24
26
  }
@@ -35,6 +35,15 @@ props:
35
35
  md, 4 from md upward.
36
36
  type: string
37
37
  default: md
38
+ minColumnWidth:
39
+ description: >-
40
+ Minimum track width for columns="auto-fit"/"auto-fill" (any CSS length,
41
+ e.g. "240px", "16rem"). Sets the minmax() floor so cards don't shrink
42
+ below it before wrapping; unset uses the 12rem default. No effect on
43
+ numeric columns.
44
+ type: string
45
+ default: ""
46
+ attribute: min-column-width
38
47
  rowGap:
39
48
  description: Row gap override
40
49
  type: string
@@ -181,15 +181,21 @@ export class UIHeatmap extends UIElement {
181
181
  labels.appendChild(span);
182
182
  }
183
183
  }
184
- labels.style.gridTemplateColumns = `repeat(${cols}, minmax(0, 1fr))`;
184
+ // day-grid month labels align to the fixed-size cell columns.
185
+ labels.style.gridTemplateColumns = `repeat(${cols}, var(--heatmap-cell-min-size, 0.75rem))`;
185
186
  this.appendChild(labels);
186
187
  }
187
188
 
188
189
  // ── Grid of cells ──
190
+ // day-grid keeps FIXED cell tracks (host scrolls horizontally when the
191
+ // 52-week grid exceeds the container); matrix/density use minmax(0,1fr)
192
+ // so they fit-and-shrink to the container.
193
+ const isDayGrid = this.type === 'day-grid';
194
+ const cellTrack = isDayGrid ? 'var(--heatmap-cell-min-size, 0.75rem)' : 'minmax(0, 1fr)';
189
195
  const grid = document.createElement('div');
190
196
  grid.setAttribute('data-grid', '');
191
- grid.style.gridTemplateColumns = `repeat(${cols}, minmax(0, 1fr))`;
192
- grid.style.gridTemplateRows = `repeat(${rows}, 1fr)`;
197
+ grid.style.gridTemplateColumns = `repeat(${cols}, ${cellTrack})`;
198
+ grid.style.gridTemplateRows = `repeat(${rows}, ${isDayGrid ? 'var(--heatmap-cell-min-size, 0.75rem)' : '1fr'})`;
193
199
  for (let r = 0; r < rows; r++) {
194
200
  for (let c = 0; c < cols; c++) {
195
201
  const d = map.get(`${r},${c}`);
@@ -36,6 +36,19 @@
36
36
  gap: var(--heatmap-gap, var(--heatmap-gap-default));
37
37
  color: var(--heatmap-text, var(--heatmap-text-default));
38
38
  font-size: var(--a-body-size);
39
+ /* A wide day-grid (52+ week columns) keeps a FIXED cell size and scrolls
40
+ horizontally (the GitHub-contributions model) rather than crushing
41
+ fixed-size cells into overlap. matrix/density fit-and-shrink instead. */
42
+ overflow-x: auto;
43
+ scrollbar-width: thin;
44
+ }
45
+
46
+ /* day-grid: fixed-size cell tracks + intrinsic width so the host scrolls
47
+ instead of squishing. (class.js writes the fixed gridTemplateColumns
48
+ inline for day-grid; matrix/density keep minmax(0,1fr) to fit.) */
49
+ :scope[type="day-grid"] > [data-grid],
50
+ :scope[type="day-grid"] > [data-months] {
51
+ width: max-content;
39
52
  }
40
53
 
41
54
  /* Title slot */
@@ -73,8 +86,12 @@
73
86
  :scope [data-cell] {
74
87
  border-radius: var(--heatmap-cell-radius, var(--heatmap-cell-radius-default));
75
88
  background: var(--heatmap-empty-bg, var(--heatmap-empty-bg-default));
76
- min-width: var(--heatmap-cell-min-size, var(--heatmap-cell-min-size-default));
77
- min-height: var(--heatmap-cell-min-size, var(--heatmap-cell-min-size-default));
89
+ /* No hard min on the cell box — it conflicted with matrix/density's
90
+ minmax(0,1fr) tracks (the floor refused to shrink, so cells overlapped
91
+ into a smear). day-grid keeps fixed size via its fixed tracks; the
92
+ fit-to-container modes shrink with the track. */
93
+ min-width: 0;
94
+ min-height: 0;
78
95
  aspect-ratio: 1 / 1;
79
96
  cursor: default;
80
97
  transition: transform var(--a-duration-fast) var(--a-easing-out);
@@ -14,13 +14,16 @@
14
14
  :scope {
15
15
  /* ── Base ── */
16
16
  box-sizing: border-box;
17
- display: inline-block;
17
+ display: block;
18
18
  position: relative;
19
19
  overflow: hidden;
20
20
  background: var(--image-bg, var(--image-bg-default));
21
21
  border-radius: var(--image-radius, var(--image-radius-default));
22
22
  }
23
23
 
24
+ /* Display convention (ADR-0037): block-level by default; [inline] opts back to inline-level. */
25
+ :scope[inline] { display: inline-block; }
26
+
24
27
  /* ── Image ── */
25
28
  [slot="image"] {
26
29
  display: block;
@@ -327,6 +327,41 @@ export class UIInput extends UIFormElement {
327
327
  }
328
328
  }
329
329
 
330
+ /**
331
+ * FEEDBACK-93 — re-resolve the prefix/suffix slots from the live property.
332
+ *
333
+ * `connected()` builds the shell ONCE (`this.innerHTML = #shellHTML()`),
334
+ * reading `this.prefix` / `this.suffix` at that moment. For a reactive
335
+ * attribute binding (`suffix="${expr}"`) the framework writes the literal
336
+ * placeholder marker (`{{p:N}}`) into the attribute first and resolves it on
337
+ * a later reactive tick — so the marker gets baked into the slot as text and,
338
+ * since `render()` never rebuilt the affixes, it stuck. This mirrors the
339
+ * slider-ui FEEDBACK-45 fix, but input affixes can be icon names
340
+ * (`renderAffix` → `<icon-ui>`), so we handle text↔icon transitions here.
341
+ * The static-deploy icon-registry race is still owned by `#promoteAffixes()`.
342
+ */
343
+ #syncAffixSlots() {
344
+ for (const which of ['prefix', 'suffix']) {
345
+ const el = this.querySelector(`:scope > [slot="field"] > [slot="${which}"]`);
346
+ if (!el) continue;
347
+ const value = this[which] || '';
348
+ const icon = el.querySelector(':scope > icon-ui');
349
+ if (value && isIconName(value)) {
350
+ if (icon) {
351
+ if (icon.getAttribute('name') !== value) icon.setAttribute('name', value);
352
+ } else {
353
+ el.replaceChildren();
354
+ const created = document.createElement('icon-ui');
355
+ created.setAttribute('name', value);
356
+ el.appendChild(created);
357
+ }
358
+ } else {
359
+ if (icon) el.replaceChildren();
360
+ if (el.textContent !== value) el.textContent = value;
361
+ }
362
+ }
363
+ }
364
+
330
365
  render() {
331
366
  if (!this.#textEl) return;
332
367
 
@@ -360,6 +395,9 @@ export class UIInput extends UIFormElement {
360
395
 
361
396
  if (this.#labelEl) this.#labelEl.textContent = this.label || '';
362
397
 
398
+ // Re-resolve prefix/suffix from the live property each render (FEEDBACK-93).
399
+ this.#syncAffixSlots();
400
+
363
401
  if (this.label) {
364
402
  this.removeAttribute('aria-label');
365
403
  } else if (this.placeholder) {
@@ -229,7 +229,7 @@ input-ui:not([disabled]) [slot="field"]:hover [slot="suffix"] {
229
229
  past the half-column cell). Targeting icon-ui directly is required
230
230
  because its own `:where(:scope)` declaration of --icon-size wins over
231
231
  any value inherited from its parent button-ui. Tying it to
232
- --input-height keeps the chevron proportional across sm/md/lg. */
232
+ --input-height keeps the caret proportional across sm/md/lg. */
233
233
  [data-number] [slot="controls"] icon-ui {
234
234
  --icon-size: calc(var(--input-height, var(--input-height-default)) * 0.4);
235
235
  }
@@ -304,10 +304,14 @@ input-ui:not([disabled]) [slot="field"]:hover [slot="suffix"] {
304
304
  [slot="field"] > [slot="leading"],
305
305
  [slot="field"] > [slot="trailing"] {
306
306
  flex-shrink: 0;
307
- /* Sized to chrome height per yaml contract. The button-ui or icon-ui
308
- child receives the sizing tokens; we just constrain the slot box
309
- and align it to the field's baseline. */
310
- align-self: stretch;
307
+ /* Vertically CENTER the affordance in the chrome do not `stretch`.
308
+ Stretch forces the slot box to the full chrome height; for a button-ui
309
+ child that overrode its own `--button-height` (chrome − 4px), and for a
310
+ fixed-height affordance like <kbd-ui> (definite `height`) stretch can't
311
+ grow the box so it pins to flex-start (top) instead — the ⌘K hint sat
312
+ ~4px high (bug-60). `center` lets each child keep its own token height
313
+ and sit on the field's vertical center. */
314
+ align-self: center;
311
315
  display: inline-flex;
312
316
  align-items: center;
313
317
  /* Inline padding moves from [slot="text"] (handled by the field's px)
@@ -15,6 +15,7 @@ import { fileURLToPath } from 'node:url';
15
15
  import { dirname, resolve } from 'node:path';
16
16
  import '../../core/element.js';
17
17
  import './input.js';
18
+ import { html, stamp } from '../../core/template.js';
18
19
 
19
20
  const __dirname = dirname(fileURLToPath(import.meta.url));
20
21
  const INPUT_CSS = readFileSync(resolve(__dirname, 'input.css'), 'utf8');
@@ -260,3 +261,59 @@ describe('input-ui — CSS source contract: placeholder pseudo is out of flow',
260
261
  );
261
262
  });
262
263
  });
264
+
265
+ describe('input-ui — FEEDBACK-93 reactive prefix/suffix re-sync', () => {
266
+ const settle = () => new Promise((r) => setTimeout(r, 30));
267
+
268
+ beforeEach(() => { document.body.innerHTML = ''; });
269
+
270
+ function suffixOf(el) { return el.querySelector(':scope > [slot="field"] > [slot="suffix"]'); }
271
+ function prefixOf(el) { return el.querySelector(':scope > [slot="field"] > [slot="prefix"]'); }
272
+
273
+ it('resolves a reactive suffix binding instead of leaking the {{p:N}} marker (the bug)', async () => {
274
+ const container = document.createElement('div');
275
+ document.body.appendChild(container);
276
+ // Interpolated attribute → the engine writes {{p:0}} into `suffix` before
277
+ // connected() reads it, then resolves on the update() tick. Pre-fix the
278
+ // baked marker stuck; post-fix render() re-resolves it.
279
+ stamp(html`<input-ui type="number" suffix="${'px'}" value="2"></input-ui>`, container);
280
+ await settle();
281
+
282
+ const el = container.querySelector('input-ui');
283
+ const suffix = suffixOf(el);
284
+ expect(suffix).not.toBeNull();
285
+ expect(suffix.textContent).toBe('px');
286
+ expect(suffix.textContent).not.toMatch(/\{\{p:/);
287
+ });
288
+
289
+ it('resolves a reactive prefix binding', async () => {
290
+ const container = document.createElement('div');
291
+ document.body.appendChild(container);
292
+ stamp(html`<input-ui prefix="${'$'}" value="9.99"></input-ui>`, container);
293
+ await settle();
294
+
295
+ const el = container.querySelector('input-ui');
296
+ expect(prefixOf(el).textContent).toBe('$');
297
+ expect(prefixOf(el).textContent).not.toMatch(/\{\{p:/);
298
+ });
299
+
300
+ it('re-resolves when the bound value later changes', async () => {
301
+ const container = document.createElement('div');
302
+ document.body.appendChild(container);
303
+ stamp(html`<input-ui type="number" suffix="${'px'}" value="2"></input-ui>`, container);
304
+ await settle();
305
+ const el = container.querySelector('input-ui');
306
+ expect(suffixOf(el).textContent).toBe('px');
307
+
308
+ // Simulate the binding resolving to a new value on a later reactive tick.
309
+ el.setAttribute('suffix', 'rem');
310
+ await settle();
311
+ expect(suffixOf(el).textContent).toBe('rem');
312
+ });
313
+
314
+ it('literal suffix still renders (no regression)', async () => {
315
+ const el = mount('<input-ui suffix="%"></input-ui>');
316
+ await settle();
317
+ expect(suffixOf(el).textContent).toBe('%');
318
+ });
319
+ });
@@ -55,6 +55,25 @@ const BADGE_FOR_STATUS = Object.freeze({
55
55
  'coming-soon':{ variant: 'muted', text: 'Coming soon', icon: '' },
56
56
  });
57
57
 
58
+ // Fallback glyph per known provider when no explicit [logo] is supplied, so
59
+ // a card set up with just [provider] never renders a blank logo disc. All
60
+ // names are verified Phosphor icons. Unknown providers fall back to a generic
61
+ // plug glyph.
62
+ const PROVIDER_LOGO = Object.freeze({
63
+ slack: 'chat-circle',
64
+ github: 'git-branch',
65
+ gitlab: 'git-branch',
66
+ linear: 'kanban',
67
+ stripe: 'credit-card',
68
+ zapier: 'lightning',
69
+ figma: 'figma-logo',
70
+ notion: 'notion-logo',
71
+ webhook: 'lightning',
72
+ discord: 'discord-logo',
73
+ google: 'google-logo',
74
+ });
75
+ const DEFAULT_LOGO = 'plug';
76
+
58
77
  export class UIIntegrationCard extends UIElement {
59
78
  // Phosphor icons this primitive auto-stamps (without consumer markup).
60
79
  // Aggregated by installIconLoadersForRegistered() across all defined
@@ -238,8 +257,13 @@ export class UIIntegrationCard extends UIElement {
238
257
  // a "not found" warn for every {{p:4}} it receives on first connect.
239
258
  if (logo.startsWith('{{p:')) return;
240
259
 
241
- // No logo strip any prior content and hide.
242
- if (!logo) {
260
+ // Resolve the visual: an explicit [logo] wins; otherwise fall back to a
261
+ // per-provider glyph (or a generic plug) so a card with only [provider]
262
+ // never renders a blank disc. Hide only when there's neither.
263
+ const provider = (this.provider || '').trim().toLowerCase();
264
+ const resolved = logo || PROVIDER_LOGO[provider] || (provider ? DEFAULT_LOGO : '');
265
+
266
+ if (!resolved) {
243
267
  this.#logoEl.replaceChildren();
244
268
  this.#logoEl.hidden = true;
245
269
  return;
@@ -247,18 +271,18 @@ export class UIIntegrationCard extends UIElement {
247
271
  this.#logoEl.hidden = false;
248
272
 
249
273
  // URL vs icon-name sniff: presence of '/' → URL.
250
- const isUrl = logo.includes('/');
274
+ const isUrl = resolved.includes('/');
251
275
 
252
276
  if (isUrl) {
253
277
  // Reuse existing <img> if same src; otherwise re-stamp.
254
278
  let img = this.#logoEl.querySelector(':scope > img');
255
- if (img && img.getAttribute('src') === logo) {
279
+ if (img && img.getAttribute('src') === resolved) {
256
280
  img.setAttribute('alt', `${this.name || this.provider || 'Integration'} logo`);
257
281
  return;
258
282
  }
259
283
  this.#logoEl.replaceChildren();
260
284
  img = document.createElement('img');
261
- img.setAttribute('src', logo);
285
+ img.setAttribute('src', resolved);
262
286
  img.setAttribute('alt', `${this.name || this.provider || 'Integration'} logo`);
263
287
  img.setAttribute('data-integration-logo', '');
264
288
  img.setAttribute('loading', 'lazy');
@@ -269,10 +293,10 @@ export class UIIntegrationCard extends UIElement {
269
293
 
270
294
  // Icon name → <icon-ui>.
271
295
  let icon = this.#logoEl.querySelector(':scope > icon-ui');
272
- if (icon && icon.getAttribute('name') === logo) return;
296
+ if (icon && icon.getAttribute('name') === resolved) return;
273
297
  this.#logoEl.replaceChildren();
274
298
  icon = document.createElement('icon-ui');
275
- icon.setAttribute('name', logo);
299
+ icon.setAttribute('name', resolved);
276
300
  icon.setAttribute('aria-hidden', 'true');
277
301
  this.#logoEl.appendChild(icon);
278
302
  }
@@ -256,11 +256,22 @@ describe('integration-card-ui — logo rendering', () => {
256
256
  expect(icon.getAttribute('name')).toBe('lightning');
257
257
  });
258
258
 
259
- it('hides logo wrapper when logo prop is empty', async () => {
259
+ it('renders a provider fallback glyph when logo prop is empty', async () => {
260
+ // Empty [logo] + a [provider] → the per-provider Phosphor glyph fallback
261
+ // (instead of a blank logo disc): wrapper stays visible with an <icon-ui>.
260
262
  const c = mount('<integration-card-ui provider="slack" name="Slack"></integration-card-ui>');
261
263
  await tick2();
262
264
  const wrap = c.querySelector('[data-integration-card-logo]');
263
265
  expect(wrap).not.toBeNull();
266
+ expect(wrap.hidden).toBe(false);
267
+ expect(wrap.querySelector('icon-ui')).not.toBeNull();
268
+ });
269
+
270
+ it('hides logo wrapper when both logo and provider are empty', async () => {
271
+ const c = mount('<integration-card-ui name="Custom"></integration-card-ui>');
272
+ await tick2();
273
+ const wrap = c.querySelector('[data-integration-card-logo]');
274
+ expect(wrap).not.toBeNull();
264
275
  expect(wrap.hidden).toBe(true);
265
276
  });
266
277
  });
@@ -17,11 +17,12 @@
17
17
  "const": "Kbd"
18
18
  },
19
19
  "size": {
20
- "description": "Sizing scale (compact tier — sm / md only).",
20
+ "description": "Sizing scale: sm, md (default), lg.",
21
21
  "type": "string",
22
22
  "enum": [
23
23
  "sm",
24
- "md"
24
+ "md",
25
+ "lg"
25
26
  ],
26
27
  "default": ""
27
28
  }
@@ -7,8 +7,10 @@
7
7
  --kbd-radius-default: var(--a-radius-sm);
8
8
  --kbd-font-default: var(--a-font-family-code);
9
9
 
10
- /* Size — defaults to md */
11
- --kbd-font-size-default: var(--a-ui-tiny);
10
+ /* Size — defaults to md. Glyph is --a-ui-xs (12px) so md reads distinctly
11
+ larger than sm (--a-ui-tiny 10px) inside its 20px cap — previously both
12
+ md and sm resolved to --a-ui-tiny, so the glyphs were identical. */
13
+ --kbd-font-size-default: var(--a-ui-xs);
12
14
  --kbd-height-default: 1.25rem;
13
15
  --kbd-min-width-default: 1.25rem;
14
16
  --kbd-px-default: var(--a-space-1);
@@ -19,8 +21,9 @@
19
21
  --kbd-height-sm-default: 1rem;
20
22
  --kbd-min-width-sm-default: 1rem;
21
23
 
22
- /* Size: lg */
23
- --kbd-font-size-lg-default: var(--a-ui-sm);
24
+ /* Size: lg — glyph --a-ui-md (14px), the top of the three-tier ramp
25
+ (sm 10 / md 12 / lg 14). */
26
+ --kbd-font-size-lg-default: var(--a-ui-md);
24
27
  --kbd-height-lg-default: 1.5rem;
25
28
  --kbd-min-width-lg-default: 1.5rem;
26
29
  text-align: start; /* §text-align-reset — blocks inheritance from centered ancestors */
@@ -13,6 +13,6 @@
13
13
  import { UIElement } from '../../core/element.js';
14
14
 
15
15
  export class UIKbd extends UIElement {
16
- /** Sizing scale (compact tier — sm / md only). */
17
- size: 'sm' | 'md';
16
+ /** Sizing scale: sm, md (default), lg. */
17
+ size: 'sm' | 'md' | 'lg';
18
18
  }
@@ -11,12 +11,13 @@ description: >-
11
11
  menu items, tooltips, command hints, and shortcut documentation.
12
12
  props:
13
13
  size:
14
- description: Sizing scale (compact tier — sm / md only).
14
+ description: 'Sizing scale: sm, md (default), lg.'
15
15
  type: string
16
16
  default: ''
17
17
  enum:
18
18
  - sm
19
19
  - md
20
+ - lg
20
21
  reflect: true
21
22
  events: {}
22
23
  slots:
@@ -87,8 +87,15 @@ export class UIList extends UIElement {
87
87
  }
88
88
 
89
89
  #items() {
90
+ // Match by TAG as well as role: a child <list-item-ui> sets role="listitem"
91
+ // in its OWN connected(), which (light-DOM upgrade order) has NOT run yet
92
+ // when the parent list first render()s. A role-only filter therefore found
93
+ // zero items on the initial selectable render → aria-selected was never
94
+ // stamped → the [aria-selected="true"] selection CSS never matched. The tag
95
+ // name is present pre-upgrade, so this finds the rows regardless of timing;
96
+ // the attribute (aria-selected) we set persists through their later upgrade.
90
97
  return [...this.children].filter(
91
- (el) => el.getAttribute && el.getAttribute('role') === 'listitem',
98
+ (el) => el.tagName === 'LIST-ITEM-UI' || el.getAttribute?.('role') === 'listitem',
92
99
  );
93
100
  }
94
101
 
@@ -85,9 +85,18 @@ export class UIMenu extends UIElement {
85
85
  if (!trigger) return;
86
86
  const pop = this.#ensurePopover();
87
87
 
88
- // Move menu items into popover (so they render in the top layer).
89
- const items = this.querySelectorAll(':scope > menu-item-ui, :scope > menu-divider-ui');
90
- for (const item of items) pop.appendChild(item);
88
+ // Move menu items into the popover (so they render in the top layer).
89
+ // Use a DESCENDANT query not `:scope >` — so items rendered via the
90
+ // template engine's `.map()` / `repeat()` are collected too: those wrap
91
+ // every interpolated child in a `display:contents` <span> (core/template.js
92
+ // wrap()), making the items grandchildren that a direct-child query skips
93
+ // → an empty popover (FEEDBACK-92). Skip anything already relocated into
94
+ // the popover so a re-entrant #show() (reactive re-render while open)
95
+ // doesn't reorder items. Mirrors the descendant query #hide() already uses.
96
+ const items = this.querySelectorAll('menu-item-ui, menu-divider-ui');
97
+ for (const item of items) {
98
+ if (!pop.contains(item)) pop.appendChild(item);
99
+ }
91
100
 
92
101
  if (!pop.matches(':popover-open')) pop.showPopover?.();
93
102
 
@@ -12,10 +12,13 @@
12
12
 
13
13
  :scope {
14
14
  box-sizing: border-box;
15
- display: inline-flex;
15
+ display: flex;
16
16
  position: relative;
17
17
  }
18
18
 
19
+ /* Display convention (ADR-0037): block-level by default; [inline] opts back to inline-level. */
20
+ :scope[inline] { display: inline-flex; }
21
+
19
22
  /* Items/dividers in Light DOM are hidden unless they've been adopted
20
23
  into the popover on open. Popover API also hides the popover itself
21
24
  when closed. */