@adia-ai/web-components 0.6.35 → 0.6.36
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 +29 -0
- package/components/combobox/combobox.css +12 -0
- package/components/date-range-picker/date-range-picker.class.js +1 -1
- package/components/date-range-picker/date-range-picker.css +4 -1
- package/components/datetime-picker/datetime-picker.css +7 -1
- package/components/input/input.css +15 -1
- package/components/input/input.test.js +40 -0
- package/components/search/search.class.js +2 -0
- package/components/table-toolbar/table-toolbar.class.js +1 -0
- package/components/tag/tag.a2ui.json +9 -0
- package/components/tag/tag.class.js +8 -1
- package/components/tag/tag.css +84 -20
- package/components/tag/tag.test.js +75 -1
- package/components/tag/tag.yaml +14 -0
- package/components/tags-input/tags-input.class.js +10 -3
- package/components/tags-input/tags-input.css +12 -3
- package/components/textarea/textarea.css +10 -1
- package/core/provider.js +19 -2
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +7 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog — @adia-ai/web-components
|
|
2
2
|
|
|
3
|
+
## [0.6.36] — 2026-05-24
|
|
4
|
+
|
|
5
|
+
### Fixed — `<tag-ui>` dismiss X + slotted icons now inherit the variant text color
|
|
6
|
+
|
|
7
|
+
- **Visible after the solid-default flip**: the dismiss X (`[slot="dismiss"]`) was hardcoded to `--a-fg-muted` (a neutral grey) regardless of the host's variant, so the X disappeared against saturated solid pills — a near-invisible grey X on the Info/Success/Warning/Error bgs. Same trap on any slotted leading icon: tag had no `currentColor` inheritance rule, so `<icon-ui>` children rendered in their own foreground color rather than the variant's.
|
|
8
|
+
- **Fix**: dismiss `color` token now defaults to `currentColor` at `opacity: 0.7` (quieter than the label, but tracks every variant — near-white X on saturated bg, dark X on warning amber, fg-muted X on quiet chrome). Hover restores `opacity: 1` and overlays a `color-mix(in oklch, currentColor 15%, transparent)` bg highlight — same color family as the host, not a stark neutral wash. NEW `:scope > icon-ui { color: currentColor; flex-shrink: 0 }` rule mirrors `<badge-ui>`'s convention so leading-icon legends/status chips read as a single color-coded unit.
|
|
9
|
+
- Two new component tokens for the dismiss opacity (`--tag-dismiss-opacity`, `--tag-dismiss-opacity-hover`) replace the prior color-swap tokens (`--tag-dismiss-fg-hover` removed — opacity-driven hover instead). Files: `components/tag/tag.css`.
|
|
10
|
+
|
|
11
|
+
### Fixed — `<tag-ui variant="warning">` solid pill: bright amber bg instead of muddy mid-brown
|
|
12
|
+
|
|
13
|
+
- **Visible after the solid-default flip**: the canonical `--a-warning-bg` + `--a-warning-fg` pair collapses for warning. `--a-warning-fg` resolves to `--a-warning-10-shade` (a dark brown — per `semantics.css:309` "warning is light-colored — text on warning fills should be dark"), but `--a-warning-bg` (= `--a-warning-strong` = `-50`) is a mid-tone amber. Dark text on mid-tone amber gives the muddy brown-on-brown look the user reported. The other family variants (info/success/danger) all have `-text-strong = -05-tint` (near-white) which contrasts cleanly against their `-50` saturated bg, so they don't hit this trap.
|
|
14
|
+
- **Fix at the tag layer**: `:scope[variant="warning"]` now uses the literal `--a-warning-20-tint` step (bright caution-tape amber, scheme-independent) as bg + keeps `--a-warning-fg` as dark text. Restores the bright-amber-with-dark-text shape `semantics.css` intends. Other family variants unchanged. Regression guard added that hard-fails any revert to `--a-warning-bg` for the warning solid bg.
|
|
15
|
+
- **Token-system follow-up flagged (not shipped)**: this is a symptom of `--a-warning-bg` pointing at the wrong step. Same muddy contrast likely surfaces wherever `--a-warning-bg` + `--a-warning-fg` is paired (`button-ui variant="warning"`, `chart-ui` warning bars, `rating-ui` filled fg, `progress-row variant="warning"`). A token-side fix that redirects `--a-warning-bg` to `--a-warning-20-tint` would resolve all consumers in one stroke but needs a visual sweep across those components first. Files: `components/tag/tag.{css,test.js}`.
|
|
16
|
+
|
|
17
|
+
### Changed — `<tag-ui>` family variants default to **solid** fill (BREAKING-visual)
|
|
18
|
+
|
|
19
|
+
- **`<tag-ui variant="info|success|warning|danger">` now renders as a saturated bg + on-strong (near-white) text** — the chip IS the state, not a tinted metadata label. Aligns the chip vocabulary so consumers can read tag-ui as a status pill at a glance. Previously: muted tinted bg with `--a-{family}-bg` (saturated solid) text — an off-canonical pair that read as "passive metadata with bold color" and diverged from `<badge-ui>`'s canonical muted pair.
|
|
20
|
+
- **Opt-out**: NEW `[tone="muted"]` attribute drops a family variant back to the canonical muted pair `--a-{family}-muted` + `--a-{family}-text` (scheme-flipping text — the same pair `<badge-ui>` uses as its default). Use on metadata chips in dense lists where the saturated default would compete for attention.
|
|
21
|
+
- **The `default` variant (no family) is unchanged** — it stays as quiet `--a-bg-muted` + `--a-fg` chrome. Opt in to a high-contrast inverse stamp via `<tag-ui tone="solid">` (no variant); resolves to `--a-fg` bg + `--a-bg` fg.
|
|
22
|
+
- **Author migration**: existing `<tag-ui variant="info|success|warning|danger">` markup auto-flips to the new saturated default — every existing tag in the codebase becomes louder. If any specific surface needs the prior look, append `tone="muted"`. 75 occurrences across 30 substrate/app/playground files identified; review per surface.
|
|
23
|
+
- Source-grep regression guards added: 4 family variants verified to use the solid pair as default; 4 `[tone="muted"][variant=…]` rules verified to use the canonical muted pair; `[variant="default"]` verified unchanged; `[tone="solid"]` neutral inverse verified. Files: `components/tag/tag.{css,yaml,test.js,examples.html,a2ui.json}` + regenerated corpus catalog.
|
|
24
|
+
|
|
25
|
+
### Fixed — placeholder pseudo no longer pushes the caret to its right after type-then-delete
|
|
26
|
+
|
|
27
|
+
- **Bug report**: with `<input-ui placeholder="Select country…">`, typing text then deleting it left the caret visually at the END of the placeholder text instead of at position 0. Same trap on `<textarea-ui>`, `<combobox-ui>`, and `<tags-input-ui>`. Root cause: the empty-state placeholder is implemented via `[data-empty]::before { content: attr(data-placeholder); }`, rendered as an **inline** pseudo box. When the contenteditable is empty, the caret IS at offset 0 of the empty text node — but the in-flow `::before` pseudo precedes the text content, occupying inline space, so the caret renders to the right of the pseudo's box. (Only happens after type-then-delete because that path leaves the contenteditable focused, making the caret visible at its true position-0 location.)
|
|
28
|
+
- **Fix**: pseudo is now `position: absolute; inset: 0; padding: inherit` — fills the host's content-box without occupying inline-flow space, so the caret renders at the actual content-start where it belongs. Host elements gain `position: relative` as the positioning anchor. For `<tags-input-ui>` + `<combobox-ui>` the pseudo also uses `display: flex; align-items: center` so the placeholder text vertically aligns with the host's flex-centered line-box.
|
|
29
|
+
- Regression guards added to `input.test.js` — three source-grep assertions (`:scope` declares `position: relative`, pseudo declares `position: absolute`, pseudo carries `inset: 0` + `padding: inherit`) that hard-fail if the in-flow shape ever re-appears.
|
|
30
|
+
- Files: `components/input/input.{css,test.js}`, `components/textarea/textarea.css`, `components/tags-input/tags-input.css`, `components/combobox/combobox.css`.
|
|
31
|
+
|
|
3
32
|
## [0.6.35] — 2026-05-24
|
|
4
33
|
|
|
5
34
|
### Fixed — `<popover-ui>` + `<menu-ui>` yaml `slots:` blocks declare the canonical `trigger` / `content` vocabulary (FB-62)
|
|
@@ -130,11 +130,23 @@
|
|
|
130
130
|
line-height: 1.4;
|
|
131
131
|
white-space: nowrap;
|
|
132
132
|
overflow: hidden;
|
|
133
|
+
/* Positioning context for the [data-empty]::before placeholder pseudo
|
|
134
|
+
(kept out of inline flow so the caret renders at content-start). */
|
|
135
|
+
position: relative;
|
|
133
136
|
}
|
|
137
|
+
/* Empty-state placeholder. Absolutely positioned (NOT inline) so an
|
|
138
|
+
in-flow pseudo box doesn't push the caret-at-position-0 to the right
|
|
139
|
+
of the placeholder text after type-then-delete. Same pattern
|
|
140
|
+
input-ui + textarea-ui + tags-input-ui use. */
|
|
134
141
|
[data-input][data-empty]::before {
|
|
135
142
|
content: attr(data-placeholder);
|
|
136
143
|
color: var(--combobox-placeholder-fg, var(--combobox-placeholder-fg-default));
|
|
137
144
|
pointer-events: none;
|
|
145
|
+
position: absolute;
|
|
146
|
+
inset: 0;
|
|
147
|
+
padding: inherit;
|
|
148
|
+
display: flex;
|
|
149
|
+
align-items: center;
|
|
138
150
|
}
|
|
139
151
|
|
|
140
152
|
[data-clear] {
|
|
@@ -485,7 +485,7 @@ export class UIDateRangePicker extends UIFormElement {
|
|
|
485
485
|
// Matches select-ui + calendar-picker-ui's anchorPopover pattern.
|
|
486
486
|
this.#anchorCleanup?.();
|
|
487
487
|
this.#anchorCleanup = anchorPopover(this.#triggerRef, this.#popoverRef, {
|
|
488
|
-
placement: this.getAttribute('placement') || 'bottom
|
|
488
|
+
placement: this.getAttribute('placement') || 'bottom',
|
|
489
489
|
gap: 4,
|
|
490
490
|
});
|
|
491
491
|
document.addEventListener('pointerdown', this.#onOutside);
|
|
@@ -62,11 +62,14 @@
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/* ── Block 2 — BASE ── */
|
|
65
|
+
/* Host is a transparent inline wrapper — trigger button-ui paints its
|
|
66
|
+
own surface. Mirrors the datetime-picker decision (2026-05-24): a host
|
|
67
|
+
background paint creates an off-axis rectangle inside field-ui because
|
|
68
|
+
the field-ui's value-cell expands beyond the inline-block host. */
|
|
65
69
|
:scope {
|
|
66
70
|
box-sizing: border-box;
|
|
67
71
|
position: relative;
|
|
68
72
|
display: inline-block;
|
|
69
|
-
background: var(--date-range-picker-bg, var(--date-range-picker-bg-default));
|
|
70
73
|
color: var(--date-range-picker-fg, var(--date-range-picker-fg-default));
|
|
71
74
|
font-size: var(--a-ui-size);
|
|
72
75
|
}
|
|
@@ -38,11 +38,17 @@
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/* ── Block 2 — BASE ── */
|
|
41
|
+
/* Host is a transparent inline wrapper. Painting `background` on the host
|
|
42
|
+
used to create an off-axis rectangle behind the trigger when the host
|
|
43
|
+
sat inside a field-ui (the field-ui's value-cell expanded but the
|
|
44
|
+
inline-block host only sized to its child). Trigger button-ui paints
|
|
45
|
+
its own surface — host stays transparent.
|
|
46
|
+
2026-05-24 QA: "odd background colors and block element container for
|
|
47
|
+
inline trigger" — consumer report on /site/components/datetime-picker. */
|
|
41
48
|
:scope {
|
|
42
49
|
box-sizing: border-box;
|
|
43
50
|
position: relative;
|
|
44
51
|
display: inline-block;
|
|
45
|
-
background: var(--datetime-picker-bg, var(--datetime-picker-bg-default));
|
|
46
52
|
color: var(--datetime-picker-fg, var(--datetime-picker-fg-default));
|
|
47
53
|
font-size: var(--a-ui-size);
|
|
48
54
|
}
|
|
@@ -121,6 +121,10 @@ input-ui:not([disabled]) [slot="field"]:hover [slot="suffix"] {
|
|
|
121
121
|
outline: none;
|
|
122
122
|
white-space: nowrap;
|
|
123
123
|
overflow: hidden;
|
|
124
|
+
/* Positioning context for the [data-empty]::before placeholder
|
|
125
|
+
pseudo, which is taken out of inline flow so the caret renders
|
|
126
|
+
at the actual content-start (not after the pseudo box). */
|
|
127
|
+
position: relative;
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
/* Text (native input — password only) */
|
|
@@ -138,11 +142,21 @@ input-ui:not([disabled]) [slot="field"]:hover [slot="suffix"] {
|
|
|
138
142
|
color: var(--input-placeholder-fg, var(--input-placeholder-fg-default));
|
|
139
143
|
}
|
|
140
144
|
|
|
141
|
-
/* Placeholder (contenteditable only)
|
|
145
|
+
/* Placeholder (contenteditable only).
|
|
146
|
+
Out-of-flow positioning is load-bearing — an in-flow ::before with
|
|
147
|
+
`content: attr(...)` puts the pseudo box INLINE before the empty
|
|
148
|
+
text node, which means the caret-at-position-0 visually renders to
|
|
149
|
+
the RIGHT of the placeholder text (after the user types and deletes).
|
|
150
|
+
`position: absolute; inset: 0; padding: inherit` makes the pseudo
|
|
151
|
+
fill the host's content-box without occupying any inline-flow space,
|
|
152
|
+
so the caret renders at the actual content-start where it belongs. */
|
|
142
153
|
span[slot="text"][data-empty]::before {
|
|
143
154
|
content: attr(data-placeholder);
|
|
144
155
|
color: var(--input-placeholder-fg, var(--input-placeholder-fg-default));
|
|
145
156
|
pointer-events: none;
|
|
157
|
+
position: absolute;
|
|
158
|
+
inset: 0;
|
|
159
|
+
padding: inherit;
|
|
146
160
|
}
|
|
147
161
|
|
|
148
162
|
/* ── Number mode (type="number") ──
|
|
@@ -10,9 +10,15 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { dirname, resolve } from 'node:path';
|
|
13
16
|
import '../../core/element.js';
|
|
14
17
|
import './input.js';
|
|
15
18
|
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const INPUT_CSS = readFileSync(resolve(__dirname, 'input.css'), 'utf8');
|
|
21
|
+
|
|
16
22
|
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
17
23
|
|
|
18
24
|
function mount(html) {
|
|
@@ -220,3 +226,37 @@ describe('input-ui — §220 (v0.5.9) throttle parity', () => {
|
|
|
220
226
|
expect(events).toEqual([]);
|
|
221
227
|
});
|
|
222
228
|
});
|
|
229
|
+
|
|
230
|
+
// ── Placeholder caret-position regression guard ─────────────────────
|
|
231
|
+
//
|
|
232
|
+
// Per the v0.6.35 bug report: with the `::before { content: attr(data-placeholder) }`
|
|
233
|
+
// pseudo rendered in inline flow, typing-then-deleting left the caret
|
|
234
|
+
// visually at the END of the placeholder text (not at position 0) because
|
|
235
|
+
// the in-flow pseudo box pushed the caret rendering position to its right.
|
|
236
|
+
// Fix: pseudo is `position: absolute` so it doesn't occupy inline-flow
|
|
237
|
+
// space; the host carries `position: relative` as its anchor.
|
|
238
|
+
// These tests assert the CSS-source contract that prevents accidental
|
|
239
|
+
// revert to the in-flow pseudo shape.
|
|
240
|
+
|
|
241
|
+
describe('input-ui — CSS source contract: placeholder pseudo is out of flow', () => {
|
|
242
|
+
it('host [slot="text"] declares position: relative (pseudo anchor)', () => {
|
|
243
|
+
expect(INPUT_CSS).toMatch(
|
|
244
|
+
/\[slot="text"\]\s*\{[^}]*position:\s*relative/s
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('[data-empty]::before is position: absolute (not inline-flow)', () => {
|
|
249
|
+
expect(INPUT_CSS).toMatch(
|
|
250
|
+
/span\[slot="text"\]\[data-empty\]::before\s*\{[^}]*position:\s*absolute/s
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('[data-empty]::before fills the host content-box (inset: 0 + padding: inherit)', () => {
|
|
255
|
+
expect(INPUT_CSS).toMatch(
|
|
256
|
+
/span\[slot="text"\]\[data-empty\]::before\s*\{[^}]*inset:\s*0/s
|
|
257
|
+
);
|
|
258
|
+
expect(INPUT_CSS).toMatch(
|
|
259
|
+
/span\[slot="text"\]\[data-empty\]::before\s*\{[^}]*padding:\s*inherit/s
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -40,12 +40,14 @@ export class UISearch extends UIFormElement {
|
|
|
40
40
|
this.setAttribute('role', 'search');
|
|
41
41
|
|
|
42
42
|
if (!this.querySelector('input-ui')) {
|
|
43
|
+
const size = this.getAttribute('size');
|
|
43
44
|
this.innerHTML = `<input-ui
|
|
44
45
|
type="search"
|
|
45
46
|
prefix="magnifying-glass"
|
|
46
47
|
suffix="x-circle"
|
|
47
48
|
placeholder="${this.placeholder}"
|
|
48
49
|
${this.disabled ? 'disabled' : ''}
|
|
50
|
+
${size ? `size="${size}"` : ''}
|
|
49
51
|
></input-ui>`;
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -290,6 +290,7 @@ export class UITableToolbar extends UIElement {
|
|
|
290
290
|
// name renders as literal text before the icon registry resolves.
|
|
291
291
|
const search = document.createElement('search-ui');
|
|
292
292
|
search.setAttribute('data-search', '');
|
|
293
|
+
search.setAttribute('size', 'sm'); // match toolbar buttons + badge + filter chips
|
|
293
294
|
search.setAttribute('placeholder', this.placeholder);
|
|
294
295
|
search.setAttribute('debounce', String(SEARCH_DEBOUNCE));
|
|
295
296
|
search.addEventListener('search', this.#onSearch);
|
|
@@ -44,6 +44,15 @@
|
|
|
44
44
|
"description": "Tag label. Renderer routes this to the `text` attribute, rendered via CSS attr(text) on ::after.",
|
|
45
45
|
"$ref": "common_types.json#/$defs/DynamicString"
|
|
46
46
|
},
|
|
47
|
+
"tone": {
|
|
48
|
+
"description": "Fill style — orthogonal to [variant]. Default (`solid`) for family\nvariants is a saturated bg with on-strong (near-white) text — the\nchip IS the state. Opt-out via `tone=\"muted\"` for a tinted bg with\nscheme-paired text (matches <badge-ui>'s default look) when the\nchip is metadata in a dense list rather than a status stamp. The\n`default` variant stays quiet chrome regardless of tone unless\n`tone=\"solid\"` is set explicitly (high-contrast neutral inverse).\n",
|
|
49
|
+
"type": "string",
|
|
50
|
+
"enum": [
|
|
51
|
+
"solid",
|
|
52
|
+
"muted"
|
|
53
|
+
],
|
|
54
|
+
"default": "solid"
|
|
55
|
+
},
|
|
47
56
|
"variant": {
|
|
48
57
|
"description": "Semantic variant — `default | info | success | warning | danger`.",
|
|
49
58
|
"type": "string",
|
|
@@ -42,13 +42,20 @@ export class UITag extends UIElement {
|
|
|
42
42
|
// `tag.textContent = …` working natively; the prop declaration
|
|
43
43
|
// broke that path. v0.5.x §327.
|
|
44
44
|
variant: { type: String, default: 'default', reflect: true },
|
|
45
|
+
// `tone` is read-only via attribute selectors (CSS), not by the
|
|
46
|
+
// class — declared in yaml for documentation + a2ui hint but NOT
|
|
47
|
+
// in `static properties` so the runtime doesn't reflect an
|
|
48
|
+
// ever-present `tone="solid"` attribute onto every <tag-ui>
|
|
49
|
+
// (which would defeat the variant-default cascade and force CSS
|
|
50
|
+
// selectors to compete with reflected defaults). Author sets
|
|
51
|
+
// `[tone]` directly when opting in.
|
|
45
52
|
size: { type: String, default: 'md', reflect: true },
|
|
46
53
|
removable: { type: Boolean, default: false, reflect: true },
|
|
47
54
|
disabled: { type: Boolean, default: false, reflect: true },
|
|
48
55
|
};
|
|
49
56
|
|
|
50
57
|
static parts = {
|
|
51
|
-
dismiss: '<button slot="dismiss" type="button" aria-label="Remove"><icon-ui name="x"></icon-ui></button>',
|
|
58
|
+
dismiss: '<button slot="dismiss" type="button" aria-label="Remove"><icon-ui name="x" weight="bold"></icon-ui></button>',
|
|
52
59
|
};
|
|
53
60
|
|
|
54
61
|
static template = () => null;
|
package/components/tag/tag.css
CHANGED
|
@@ -27,11 +27,21 @@ tag-ui[removable]:not([disabled]):hover {
|
|
|
27
27
|
--tag-duration-default: var(--a-duration-fast);
|
|
28
28
|
--tag-easing-default: var(--a-easing);
|
|
29
29
|
|
|
30
|
-
/* ── Dismiss ──
|
|
30
|
+
/* ── Dismiss ──
|
|
31
|
+
Rest = inherit the host's text color at reduced opacity (so the X
|
|
32
|
+
reads as quieter chrome than the label, but tracks every variant
|
|
33
|
+
— info/success/danger get a near-white X on saturated bg; warning
|
|
34
|
+
gets a dark-brown X on bright amber; default gets a fg-muted X on
|
|
35
|
+
quiet chrome). Hover restores full opacity + drops a translucent
|
|
36
|
+
overlay in the same color family for the affordance.
|
|
37
|
+
Pre-v0.6.36 used `--a-fg-muted` / `--a-fg` directly, which gave
|
|
38
|
+
a neutral-grey X on saturated solid pills — the X disappeared
|
|
39
|
+
against the variant bg. */
|
|
31
40
|
--tag-dismiss-radius-default: var(--a-radius-full);
|
|
32
|
-
--tag-dismiss-fg-default:
|
|
33
|
-
--tag-dismiss-
|
|
34
|
-
--tag-dismiss-
|
|
41
|
+
--tag-dismiss-fg-default: currentColor;
|
|
42
|
+
--tag-dismiss-opacity-default: 0.85;
|
|
43
|
+
--tag-dismiss-opacity-hover-default: 1;
|
|
44
|
+
--tag-dismiss-bg-hover-default: color-mix(in oklch, currentColor 18%, transparent);
|
|
35
45
|
text-align: start; /* §text-align-reset — blocks inheritance from centered ancestors */
|
|
36
46
|
}
|
|
37
47
|
|
|
@@ -64,37 +74,81 @@ tag-ui[removable]:not([disabled]):hover {
|
|
|
64
74
|
content: attr(text);
|
|
65
75
|
}
|
|
66
76
|
|
|
67
|
-
/* ── Variants ──
|
|
77
|
+
/* ── Variants ──
|
|
78
|
+
Default tone is `solid` for the four family variants:
|
|
79
|
+
`--a-{family}-bg` (saturated surface) + `--a-{family}-fg` (on-strong
|
|
80
|
+
text, near-white in both schemes by design). Reads as a status pill
|
|
81
|
+
where the chip IS the state. Opt out per-tag via [tone="muted"] for
|
|
82
|
+
metadata-chip surfaces in dense lists. */
|
|
68
83
|
:scope[variant="info"] {
|
|
69
|
-
--tag-bg-default: var(--a-info-
|
|
70
|
-
--tag-fg-default: var(--a-info-
|
|
84
|
+
--tag-bg-default: var(--a-info-bg);
|
|
85
|
+
--tag-fg-default: var(--a-info-fg);
|
|
71
86
|
}
|
|
72
87
|
|
|
73
88
|
:scope[variant="success"] {
|
|
74
|
-
--tag-bg-default: var(--a-success-
|
|
75
|
-
--tag-fg-default: var(--a-success-
|
|
89
|
+
--tag-bg-default: var(--a-success-bg);
|
|
90
|
+
--tag-fg-default: var(--a-success-fg);
|
|
76
91
|
}
|
|
77
92
|
|
|
93
|
+
/* `--a-warning-bg` (= --a-warning-strong / -50) is too dark to read
|
|
94
|
+
against `--a-warning-fg` (= -text-strong / -10-shade), giving a
|
|
95
|
+
muddy brown-on-brown chip. Per the semantics.css comment "warning
|
|
96
|
+
is light-colored — text on warning fills should be dark", the
|
|
97
|
+
intent is bright-amber-bg + dark-text. The literal `-20-tint`
|
|
98
|
+
step is bright in BOTH schemes (independent of light-dark swap),
|
|
99
|
+
so warning solid keeps the same caution-tape look across modes.
|
|
100
|
+
The other family variants stay on `-bg` + `-fg` because their
|
|
101
|
+
-text-strong is `-05-tint` (near-white) which contrasts fine
|
|
102
|
+
against their saturated `-strong` bg. */
|
|
78
103
|
:scope[variant="warning"] {
|
|
79
|
-
--tag-bg-default: var(--a-warning-
|
|
80
|
-
--tag-fg-default: var(--a-warning-
|
|
104
|
+
--tag-bg-default: var(--a-warning-20-tint);
|
|
105
|
+
--tag-fg-default: var(--a-warning-fg);
|
|
81
106
|
}
|
|
82
107
|
|
|
83
108
|
:scope[variant="danger"] {
|
|
84
|
-
--tag-bg-default: var(--a-danger-
|
|
85
|
-
--tag-fg-default: var(--a-danger-
|
|
109
|
+
--tag-bg-default: var(--a-danger-bg);
|
|
110
|
+
--tag-fg-default: var(--a-danger-fg);
|
|
86
111
|
}
|
|
87
112
|
|
|
88
|
-
/* `default`
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
base; the explicit selector lets `<tag-ui variant="default">`
|
|
92
|
-
render identically without falling through to base. */
|
|
113
|
+
/* `default` (no family) stays as quiet chrome — a stark fg/bg-inverse
|
|
114
|
+
would be too loud for the no-variant case. Opt in to the inverse
|
|
115
|
+
stamp via `[tone="solid"]` when you want a high-contrast neutral pill. */
|
|
93
116
|
:scope[variant="default"] {
|
|
94
117
|
--tag-bg-default: var(--a-bg-muted);
|
|
95
118
|
--tag-fg-default: var(--a-fg);
|
|
96
119
|
}
|
|
97
120
|
|
|
121
|
+
/* ── Tone modifier — orthogonal to [variant] ──
|
|
122
|
+
`[tone="muted"]` opts each family variant OUT of the solid default
|
|
123
|
+
into the canonical muted pair: `--a-{family}-muted` (tinted surface)
|
|
124
|
+
+ `--a-{family}-text` (scheme-flipping text). Same shape <badge-ui>
|
|
125
|
+
uses as its default. Use on metadata chips in dense lists where the
|
|
126
|
+
saturated default would compete for attention. */
|
|
127
|
+
:scope[tone="muted"][variant="info"] {
|
|
128
|
+
--tag-bg-default: var(--a-info-muted);
|
|
129
|
+
--tag-fg-default: var(--a-info-text);
|
|
130
|
+
}
|
|
131
|
+
:scope[tone="muted"][variant="success"] {
|
|
132
|
+
--tag-bg-default: var(--a-success-muted);
|
|
133
|
+
--tag-fg-default: var(--a-success-text);
|
|
134
|
+
}
|
|
135
|
+
:scope[tone="muted"][variant="warning"] {
|
|
136
|
+
--tag-bg-default: var(--a-warning-muted);
|
|
137
|
+
--tag-fg-default: var(--a-warning-text);
|
|
138
|
+
}
|
|
139
|
+
:scope[tone="muted"][variant="danger"] {
|
|
140
|
+
--tag-bg-default: var(--a-danger-muted);
|
|
141
|
+
--tag-fg-default: var(--a-danger-text);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* `[tone="solid"]` on the neutral default (or no variant) inverts the
|
|
145
|
+
chrome — solid fg-color bg with bg-color text. High-contrast neutral
|
|
146
|
+
stamp. Explicit opt-in; the variant-less default stays quiet chrome. */
|
|
147
|
+
:scope[tone="solid"]:not([variant="info"]):not([variant="success"]):not([variant="warning"]):not([variant="danger"]) {
|
|
148
|
+
--tag-bg-default: var(--a-fg);
|
|
149
|
+
--tag-fg-default: var(--a-bg);
|
|
150
|
+
}
|
|
151
|
+
|
|
98
152
|
/* Size handled by universal [size] attribute system. */
|
|
99
153
|
|
|
100
154
|
/* hover rule moved outside @scope — see Safari 17.x bug note at top. */
|
|
@@ -105,6 +159,15 @@ tag-ui[removable]:not([disabled]):hover {
|
|
|
105
159
|
box-shadow: var(--tag-focus-ring, var(--tag-focus-ring-default));
|
|
106
160
|
}
|
|
107
161
|
|
|
162
|
+
/* ── Slotted icons (leading) ──
|
|
163
|
+
Icons placed inside the tag (e.g. `<icon-ui name="check">`) inherit
|
|
164
|
+
the host's text color so legend / status chips read as a single
|
|
165
|
+
color-coded unit. Mirrors `<badge-ui>`'s convention. */
|
|
166
|
+
:scope > icon-ui {
|
|
167
|
+
color: currentColor;
|
|
168
|
+
flex-shrink: 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
108
171
|
/* ── Dismiss button ── */
|
|
109
172
|
[slot="dismiss"] {
|
|
110
173
|
display: inline-flex;
|
|
@@ -118,13 +181,14 @@ tag-ui[removable]:not([disabled]):hover {
|
|
|
118
181
|
cursor: pointer;
|
|
119
182
|
border-radius: var(--tag-dismiss-radius, var(--tag-dismiss-radius-default));
|
|
120
183
|
color: var(--tag-dismiss-fg, var(--tag-dismiss-fg-default));
|
|
184
|
+
opacity: var(--tag-dismiss-opacity, var(--tag-dismiss-opacity-default));
|
|
121
185
|
--a-icon-size: 0.875rem;
|
|
122
186
|
order: 1; /* push dismiss to end so layout reads [text] [×] */
|
|
123
|
-
transition:
|
|
187
|
+
transition: opacity var(--tag-duration, var(--tag-duration-default)) var(--tag-easing, var(--tag-easing-default)), background var(--tag-duration, var(--tag-duration-default)) var(--tag-easing, var(--tag-easing-default));
|
|
124
188
|
}
|
|
125
189
|
|
|
126
190
|
[slot="dismiss"]:hover {
|
|
127
|
-
|
|
191
|
+
opacity: var(--tag-dismiss-opacity-hover, var(--tag-dismiss-opacity-hover-default));
|
|
128
192
|
background: var(--tag-dismiss-bg-hover, var(--tag-dismiss-bg-hover-default));
|
|
129
193
|
}
|
|
130
194
|
|
|
@@ -41,6 +41,80 @@ describe('tag-ui — content: attr(text) gating', () => {
|
|
|
41
41
|
// [icon] + [dismiss] slot rendering and for proper :scope[text]
|
|
42
42
|
// gap rendering when text IS set.
|
|
43
43
|
expect(TAG_CSS).toMatch(/:scope\s*\{[^}]*display:\s*inline-flex/);
|
|
44
|
-
|
|
44
|
+
// Post OD-5 sweep: tokens read via `var(--prop, var(--prop-default))`
|
|
45
|
+
// chain so consumer-named overrides AND --a-* surface overrides both work.
|
|
46
|
+
expect(TAG_CSS).toMatch(/:scope\s*\{[^}]*gap:\s*var\(--tag-gap,\s*var\(--tag-gap-default\)\)/);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ── Tone defaults (solid by default for families; muted as opt-out) ──
|
|
51
|
+
//
|
|
52
|
+
// Default tone is `solid` for family variants — the base `:scope[variant=…]`
|
|
53
|
+
// rules use the saturated-fill pair `--a-{family}-bg` + `--a-{family}-fg`
|
|
54
|
+
// (the chip IS the state). `[tone="muted"]` opts back into the canonical
|
|
55
|
+
// muted pair `--a-{family}-muted` + `--a-{family}-text` (same look as
|
|
56
|
+
// <badge-ui>'s default). The `default` (no-family) variant stays quiet
|
|
57
|
+
// chrome unless `[tone="solid"]` is set explicitly (then it inverts
|
|
58
|
+
// to fg/bg for a high-contrast neutral stamp).
|
|
59
|
+
|
|
60
|
+
describe('tag-ui — variant defaults to solid fill', () => {
|
|
61
|
+
it.each([
|
|
62
|
+
['info'],
|
|
63
|
+
['success'],
|
|
64
|
+
['danger'],
|
|
65
|
+
])('[variant="%s"] base rule uses --a-{family}-bg + --a-{family}-fg', (family) => {
|
|
66
|
+
const block = new RegExp(
|
|
67
|
+
`:scope\\[variant="${family}"\\]\\s*\\{[^}]*` +
|
|
68
|
+
`--tag-bg-default:\\s*var\\(--a-${family}-bg\\)[^}]*` +
|
|
69
|
+
`--tag-fg-default:\\s*var\\(--a-${family}-fg\\)`,
|
|
70
|
+
's'
|
|
71
|
+
);
|
|
72
|
+
expect(TAG_CSS).toMatch(block);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('[variant="warning"] uses the literal --a-warning-20-tint bg (NOT --a-warning-bg)', () => {
|
|
76
|
+
// The token-system convention `-bg` + `-fg` collapses for warning
|
|
77
|
+
// because `--a-warning-fg` is dark (`-10-shade`) but `--a-warning-bg`
|
|
78
|
+
// (= `-strong` = `-50`) is mid-tone amber — dark-on-mid is muddy.
|
|
79
|
+
// The literal `-20-tint` step is bright in both schemes, restoring
|
|
80
|
+
// the caution-tape look the semantics.css comment intends.
|
|
81
|
+
expect(TAG_CSS).toMatch(
|
|
82
|
+
/:scope\[variant="warning"\]\s*\{[^}]*--tag-bg-default:\s*var\(--a-warning-20-tint\)[^}]*--tag-fg-default:\s*var\(--a-warning-fg\)/s
|
|
83
|
+
);
|
|
84
|
+
// Negative — guard against accidental revert to the muddy pair.
|
|
85
|
+
expect(TAG_CSS).not.toMatch(
|
|
86
|
+
/:scope\[variant="warning"\]\s*\{[^}]*--tag-bg-default:\s*var\(--a-warning-bg\)/s
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('tag-ui — [tone="muted"] opts out to canonical muted pair', () => {
|
|
92
|
+
it.each([
|
|
93
|
+
['info'],
|
|
94
|
+
['success'],
|
|
95
|
+
['warning'],
|
|
96
|
+
['danger'],
|
|
97
|
+
])('[tone="muted"][variant="%s"] uses --a-{family}-muted + --a-{family}-text', (family) => {
|
|
98
|
+
const block = new RegExp(
|
|
99
|
+
`:scope\\[tone="muted"\\]\\[variant="${family}"\\]\\s*\\{[^}]*` +
|
|
100
|
+
`--tag-bg-default:\\s*var\\(--a-${family}-muted\\)[^}]*` +
|
|
101
|
+
`--tag-fg-default:\\s*var\\(--a-${family}-text\\)`,
|
|
102
|
+
's'
|
|
103
|
+
);
|
|
104
|
+
expect(TAG_CSS).toMatch(block);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('tag-ui — neutral default + explicit solid stamp', () => {
|
|
109
|
+
it('[variant="default"] stays quiet chrome (--a-bg-muted + --a-fg)', () => {
|
|
110
|
+
expect(TAG_CSS).toMatch(
|
|
111
|
+
/:scope\[variant="default"\]\s*\{[^}]*--tag-bg-default:\s*var\(--a-bg-muted\)[^}]*--tag-fg-default:\s*var\(--a-fg\)/s
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('[tone="solid"] without a family inverts to fg/bg high-contrast stamp', () => {
|
|
116
|
+
expect(TAG_CSS).toMatch(
|
|
117
|
+
/:scope\[tone="solid"\]:not\(\[variant="info"\]\)[\s\S]*?--tag-bg-default:\s*var\(--a-fg\)[\s\S]*?--tag-fg-default:\s*var\(--a-bg\)/
|
|
118
|
+
);
|
|
45
119
|
});
|
|
46
120
|
});
|
package/components/tag/tag.yaml
CHANGED
|
@@ -51,6 +51,20 @@ props:
|
|
|
51
51
|
- success
|
|
52
52
|
- warning
|
|
53
53
|
- danger
|
|
54
|
+
tone:
|
|
55
|
+
description: |
|
|
56
|
+
Fill style — orthogonal to [variant]. Default (`solid`) for family
|
|
57
|
+
variants is a saturated bg with on-strong (near-white) text — the
|
|
58
|
+
chip IS the state. Opt-out via `tone="muted"` for a tinted bg with
|
|
59
|
+
scheme-paired text (matches <badge-ui>'s default look) when the
|
|
60
|
+
chip is metadata in a dense list rather than a status stamp. The
|
|
61
|
+
`default` variant stays quiet chrome regardless of tone unless
|
|
62
|
+
`tone="solid"` is set explicitly (high-contrast neutral inverse).
|
|
63
|
+
type: string
|
|
64
|
+
default: solid
|
|
65
|
+
enum:
|
|
66
|
+
- solid
|
|
67
|
+
- muted
|
|
54
68
|
events:
|
|
55
69
|
remove:
|
|
56
70
|
description: Fired when the dismiss button is activated.
|
|
@@ -518,12 +518,19 @@ export class UITagsInput extends UIFormElement {
|
|
|
518
518
|
#handleChipRemove(e) {
|
|
519
519
|
const chip = e.target.closest('tag-ui[data-index]');
|
|
520
520
|
if (!chip) return;
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
//
|
|
521
|
+
// tag-ui's `remove` CustomEvent bubbles SYNCHRONOUSLY before tag-ui's
|
|
522
|
+
// own `this.remove()` runs (see tag/tag.class.js:59-63 — dispatch then
|
|
523
|
+
// remove). If we react inside the bubble (call `#renderChips()` now),
|
|
524
|
+
// the DOM still contains the about-to-be-removed chip and the
|
|
525
|
+
// positional reconcile in `#renderChips()` mutates the WRONG chip's
|
|
526
|
+
// text — visually erasing one or more adjacent chips. Pre-emptively
|
|
527
|
+
// detach the source chip ourselves so the DOM matches `#value` before
|
|
528
|
+
// render. tag-ui's later `this.remove()` is a harmless no-op on an
|
|
529
|
+
// already-detached node.
|
|
524
530
|
e.stopPropagation();
|
|
525
531
|
const idx = Number(chip.dataset.index);
|
|
526
532
|
if (!Number.isInteger(idx)) return;
|
|
533
|
+
chip.remove();
|
|
527
534
|
this.#removeByIndex(idx, 'chip-click');
|
|
528
535
|
}
|
|
529
536
|
|
|
@@ -110,14 +110,23 @@
|
|
|
110
110
|
white-space: pre-wrap;
|
|
111
111
|
overflow-wrap: anywhere;
|
|
112
112
|
cursor: text;
|
|
113
|
+
/* Positioning context for the [data-empty]::before placeholder pseudo
|
|
114
|
+
(kept out of inline flow so the caret renders at content-start). */
|
|
115
|
+
position: relative;
|
|
113
116
|
}
|
|
114
|
-
/* Empty-state placeholder
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
/* Empty-state placeholder. The pseudo is absolutely positioned (NOT
|
|
118
|
+
inline) so an in-flow pseudo box doesn't push the caret-at-position-0
|
|
119
|
+
to the right of the placeholder text after type-then-delete. Same
|
|
120
|
+
pattern input-ui + textarea-ui + combobox-ui use. */
|
|
117
121
|
[data-inline-input][data-empty]::before {
|
|
118
122
|
content: attr(data-placeholder);
|
|
119
123
|
color: var(--tags-input-placeholder-fg, var(--tags-input-placeholder-fg-default));
|
|
120
124
|
pointer-events: none;
|
|
125
|
+
position: absolute;
|
|
126
|
+
inset: 0;
|
|
127
|
+
padding: inherit;
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
/* ── Spinner (async validator) ── */
|
|
@@ -60,6 +60,10 @@ textarea-ui:not([disabled]) [slot="text"]:hover {
|
|
|
60
60
|
[slot="label"][label]::after { content: attr(label); }
|
|
61
61
|
|
|
62
62
|
[slot="text"] {
|
|
63
|
+
/* Positioning context for the [data-empty]::before placeholder pseudo,
|
|
64
|
+
which is absolutely positioned (out of inline flow) so the caret
|
|
65
|
+
renders at content-start instead of after the placeholder text. */
|
|
66
|
+
position: relative;
|
|
63
67
|
min-height: var(--textarea-min-height, var(--textarea-min-height-default));
|
|
64
68
|
padding: var(--textarea-py, var(--textarea-py-default)) var(--textarea-px, var(--textarea-px-default));
|
|
65
69
|
border: 1px solid var(--textarea-border, var(--textarea-border-default));
|
|
@@ -99,11 +103,16 @@ textarea-ui:not([disabled]) [slot="text"]:hover {
|
|
|
99
103
|
color: var(--textarea-label-fg-focus, var(--textarea-label-fg-focus-default));
|
|
100
104
|
}
|
|
101
105
|
|
|
102
|
-
/* Placeholder
|
|
106
|
+
/* Placeholder — see input.css companion comment. Out-of-flow positioning
|
|
107
|
+
prevents the inline pseudo box from pushing the caret position to the
|
|
108
|
+
right of the placeholder text after type-then-delete. */
|
|
103
109
|
[slot="text"][data-empty]::before {
|
|
104
110
|
content: attr(data-placeholder);
|
|
105
111
|
color: var(--textarea-placeholder-fg, var(--textarea-placeholder-fg-default));
|
|
106
112
|
pointer-events: none;
|
|
113
|
+
position: absolute;
|
|
114
|
+
inset: 0;
|
|
115
|
+
padding: inherit;
|
|
107
116
|
}
|
|
108
117
|
|
|
109
118
|
/* Disabled */
|
package/core/provider.js
CHANGED
|
@@ -228,14 +228,31 @@ export class UIRouter extends UIProvider {
|
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
html = html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '');
|
|
232
|
-
|
|
233
231
|
// Template resolver hook — lets the site wrap content in a template before injection
|
|
234
232
|
if (this.templateResolver) {
|
|
235
233
|
html = await this.templateResolver(html, route);
|
|
236
234
|
}
|
|
237
235
|
|
|
238
236
|
this.innerHTML = html;
|
|
237
|
+
// Re-stamp executable <script> tags so consumer demos can wire data via
|
|
238
|
+
// imperative JS APIs (the documented `el.items = […]` / `el.invoice = {…}`
|
|
239
|
+
// pattern used by plan-picker, invoice-detail, list-window et al). HTML
|
|
240
|
+
// assigned via innerHTML does NOT execute embedded <script> elements per
|
|
241
|
+
// the HTML spec; we replicate the canonical standalone-wrapper rehydration
|
|
242
|
+
// pattern by cloning each script + swapping the inert original. `<script
|
|
243
|
+
// type="application/json" …>` blocks are left untouched so the receiving
|
|
244
|
+
// custom element's connectedCallback can absorb them (the documented
|
|
245
|
+
// in-band data pattern; see PlanPicker.#absorbInlinePlansScript et al).
|
|
246
|
+
for (const oldScript of this.querySelectorAll('script')) {
|
|
247
|
+
const type = oldScript.getAttribute('type') || '';
|
|
248
|
+
// Skip data blocks — those are consumed by the runtime, not executed.
|
|
249
|
+
if (type === 'application/json' || type === 'application/ld+json') continue;
|
|
250
|
+
const newScript = document.createElement('script');
|
|
251
|
+
if (type) newScript.type = type;
|
|
252
|
+
if (oldScript.src) newScript.src = oldScript.src;
|
|
253
|
+
else newScript.textContent = oldScript.textContent;
|
|
254
|
+
oldScript.replaceWith(newScript);
|
|
255
|
+
}
|
|
239
256
|
this.scrollTo(0, 0);
|
|
240
257
|
(this.closest('section') || this.parentElement)?.scrollTo(0, 0);
|
|
241
258
|
|