@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.
- package/CHANGELOG.md +134 -0
- package/components/action-list/action-list.css +1 -1
- package/components/agent-artifact/agent-artifact.class.js +10 -10
- package/components/agent-artifact/agent-artifact.css +1 -1
- package/components/agent-reasoning/agent-reasoning.class.js +51 -0
- package/components/agent-reasoning/agent-reasoning.css +49 -22
- package/components/alert/alert.class.js +8 -1
- package/components/alert/alert.css +13 -1
- package/components/avatar/avatar.a2ui.json +2 -14
- package/components/avatar/avatar.class.js +3 -15
- package/components/avatar/avatar.d.ts +2 -4
- package/components/avatar/avatar.yaml +1 -18
- package/components/breadcrumb/breadcrumb.css +4 -1
- package/components/button/button.a2ui.json +3 -0
- package/components/button/button.css +14 -3
- package/components/button/button.yaml +5 -0
- package/components/calendar-grid/calendar-grid.css +1 -1
- package/components/calendar-picker/calendar-picker.css +5 -2
- package/components/chart/chart.a2ui.json +0 -18
- package/components/chart/chart.class.js +8 -50
- package/components/chart/chart.css +1 -15
- package/components/chart/chart.d.ts +0 -4
- package/components/chart/chart.yaml +0 -24
- package/components/color-input/color-input.css +4 -1
- package/components/combobox/combobox.class.js +11 -0
- package/components/combobox/combobox.css +8 -0
- package/components/date-range-picker/date-range-picker.class.js +5 -1
- package/components/date-range-picker/date-range-picker.css +12 -2
- package/components/datetime-picker/datetime-picker.class.js +3 -0
- package/components/datetime-picker/datetime-picker.css +16 -2
- package/components/empty-state/empty-state.css +11 -4
- package/components/field/field.css +17 -6
- package/components/grid/grid.a2ui.json +5 -0
- package/components/grid/grid.class.js +16 -6
- package/components/grid/grid.css +17 -3
- package/components/grid/grid.d.ts +2 -0
- package/components/grid/grid.yaml +9 -0
- package/components/heatmap/heatmap.class.js +9 -3
- package/components/heatmap/heatmap.css +19 -2
- package/components/image/image.css +4 -1
- package/components/input/input.class.js +38 -0
- package/components/input/input.css +9 -5
- package/components/input/input.test.js +57 -0
- package/components/integration-card/integration-card.class.js +31 -7
- package/components/integration-card/integration-card.test.js +12 -1
- package/components/kbd/kbd.a2ui.json +3 -2
- package/components/kbd/kbd.css +7 -4
- package/components/kbd/kbd.d.ts +2 -2
- package/components/kbd/kbd.yaml +2 -1
- package/components/list/list.class.js +8 -1
- package/components/menu/menu.class.js +12 -3
- package/components/menu/menu.css +4 -1
- package/components/menu/menu.test.js +130 -0
- package/components/modal/modal.class.js +10 -1
- package/components/modal/modal.css +9 -0
- package/components/option-card/option-card.a2ui.json +3 -0
- package/components/option-card/option-card.css +44 -19
- package/components/option-card/option-card.yaml +5 -0
- package/components/otp-input/otp-input.css +25 -10
- package/components/page/page.css +64 -11
- package/components/pagination/pagination.class.js +1 -1
- package/components/pagination/pagination.css +9 -1
- package/components/pipeline-status/pipeline-status.css +6 -0
- package/components/popover/popover.css +12 -1
- package/components/preview/preview.css +30 -3
- package/components/progress-row/progress-row.css +3 -1
- package/components/qr-code/qr-code.css +4 -1
- package/components/segmented/segmented.css +4 -1
- package/components/select/select.a2ui.json +1 -1
- package/components/select/select.class.js +63 -7
- package/components/select/select.css +18 -0
- package/components/select/select.yaml +9 -2
- package/components/stack/stack.a2ui.json +12 -1
- package/components/stack/stack.d.ts +2 -2
- package/components/stack/stack.yaml +13 -1
- package/components/stat/stat.a2ui.json +5 -0
- package/components/stat/stat.css +55 -0
- package/components/stat/stat.d.ts +2 -0
- package/components/stat/stat.js +4 -0
- package/components/stat/stat.yaml +9 -0
- package/components/swiper/swiper.class.js +14 -6
- package/components/switch/switch.css +13 -0
- package/components/table/table.a2ui.json +2 -2
- package/components/table/table.css +13 -1
- package/components/table/table.yaml +2 -2
- package/components/time-picker/time-picker.css +4 -1
- package/components/timeline/timeline.class.js +3 -3
- package/components/timeline/timeline.css +23 -5
- package/components/toggle-group/toggle-group.css +4 -1
- package/components/toggle-scheme/toggle-scheme.css +4 -1
- package/components/tree/tree.class.js +24 -4
- package/components/tree/tree.test.js +108 -0
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +83 -83
- package/package.json +3 -3
- package/styles/api/layout.css +7 -0
- package/styles/api/text.css +9 -5
- package/styles/index.css +11 -2
- package/styles/prose.css +8 -0
- package/styles/resets.css +5 -5
- package/styles/themes.css +8 -1
- package/styles/tokens.css +3 -3
- package/styles/type/elements.css +73 -0
- package/styles/type/roles.css +14 -49
- package/styles/type/scale.css +0 -5
- 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:
|
|
27
|
-
gap:
|
|
28
|
-
columnGap:
|
|
29
|
-
rowGap:
|
|
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
|
|
package/components/grid/grid.css
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
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
|
|
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},
|
|
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
|
|
77
|
-
|
|
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:
|
|
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
|
|
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
|
-
/*
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
//
|
|
242
|
-
|
|
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 =
|
|
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') ===
|
|
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',
|
|
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') ===
|
|
296
|
+
if (icon && icon.getAttribute('name') === resolved) return;
|
|
273
297
|
this.#logoEl.replaceChildren();
|
|
274
298
|
icon = document.createElement('icon-ui');
|
|
275
|
-
icon.setAttribute('name',
|
|
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('
|
|
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
|
|
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
|
}
|
package/components/kbd/kbd.css
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 */
|
package/components/kbd/kbd.d.ts
CHANGED
package/components/kbd/kbd.yaml
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
90
|
-
|
|
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
|
|
package/components/menu/menu.css
CHANGED
|
@@ -12,10 +12,13 @@
|
|
|
12
12
|
|
|
13
13
|
:scope {
|
|
14
14
|
box-sizing: border-box;
|
|
15
|
-
display:
|
|
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. */
|