@adia-ai/web-components 0.4.2 → 0.4.4
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/README.md +12 -0
- package/components/alert/alert.a2ui.json +17 -2
- package/components/alert/alert.js +100 -9
- package/components/alert/alert.test.js +180 -0
- package/components/alert/alert.yaml +30 -2
- package/components/badge/badge.a2ui.json +4 -0
- package/components/badge/badge.js +1 -0
- package/components/badge/badge.yaml +4 -0
- package/components/button/button.a2ui.json +14 -4
- package/components/button/button.js +1 -0
- package/components/button/button.yaml +18 -3
- package/components/check/check.a2ui.json +8 -1
- package/components/check/check.yaml +11 -2
- package/components/code/code.a2ui.json +4 -0
- package/components/code/code.js +1 -0
- package/components/code/code.yaml +4 -0
- package/components/col/col.a2ui.json +5 -0
- package/components/col/col.js +1 -0
- package/components/col/col.yaml +5 -0
- package/components/field/field.a2ui.json +17 -6
- package/components/field/field.test.js +8 -2
- package/components/field/field.yaml +50 -8
- package/components/index.js +1 -0
- package/components/input/input.a2ui.json +25 -0
- package/components/input/input.js +220 -34
- package/components/input/input.yaml +24 -0
- package/components/link/link.a2ui.json +166 -0
- package/components/link/link.css +102 -0
- package/components/link/link.js +177 -0
- package/components/link/link.test.js +143 -0
- package/components/link/link.yaml +162 -0
- package/components/radio/radio.a2ui.json +8 -1
- package/components/radio/radio.yaml +11 -2
- package/components/row/row.a2ui.json +5 -0
- package/components/row/row.js +1 -0
- package/components/row/row.yaml +5 -0
- package/components/select/select.a2ui.json +15 -0
- package/components/select/select.yaml +14 -0
- package/components/switch/switch.a2ui.json +8 -1
- package/components/switch/switch.yaml +11 -2
- package/components/table/table.a2ui.json +10 -0
- package/components/table/table.yaml +8 -0
- package/components/tag/tag.a2ui.json +4 -0
- package/components/tag/tag.js +1 -0
- package/components/tag/tag.yaml +4 -0
- package/components/text/text.a2ui.json +5 -0
- package/components/text/text.js +1 -0
- package/components/text/text.yaml +5 -0
- package/components/textarea/textarea.a2ui.json +5 -0
- package/components/textarea/textarea.yaml +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,18 @@ A2UI protocol messages into live DOM.
|
|
|
9
9
|
> [`@adia-ai/a2ui-corpus`](../a2ui/corpus); the MCP server in
|
|
10
10
|
> [`@adia-ai/a2ui-mcp`](../a2ui/mcp).
|
|
11
11
|
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @adia-ai/web-components
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
For composite shells (admin / chat / editor / simple / theme clusters), pair with [`@adia-ai/web-modules`](../web-modules):
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @adia-ai/web-components @adia-ai/web-modules
|
|
22
|
+
```
|
|
23
|
+
|
|
12
24
|
## Quick start
|
|
13
25
|
|
|
14
26
|
```html
|
|
@@ -13,21 +13,36 @@
|
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"properties": {
|
|
16
|
+
"title": {
|
|
17
|
+
"description": "Bold headline rendered as the first line of the alert content. Pair with [description] for the canonical \"banner\" pattern (headline + body). When [title] or [description] is set, the [text] prop is ignored.",
|
|
18
|
+
"type": "string",
|
|
19
|
+
"default": ""
|
|
20
|
+
},
|
|
21
|
+
"description": {
|
|
22
|
+
"description": "Body text rendered as the second line of the alert content, below [title]. May be used alone (without [title]) for a single muted-body message.",
|
|
23
|
+
"type": "string",
|
|
24
|
+
"default": ""
|
|
25
|
+
},
|
|
16
26
|
"closable": {
|
|
17
|
-
"description": "Whether a close button is displayed",
|
|
27
|
+
"description": "Whether a close button is displayed. Alias [dismissible] is also accepted (same semantics, different spelling — the corpus and many libraries use both; both map to the same state).",
|
|
18
28
|
"type": "boolean",
|
|
19
29
|
"default": false
|
|
20
30
|
},
|
|
21
31
|
"component": {
|
|
22
32
|
"const": "Alert"
|
|
23
33
|
},
|
|
34
|
+
"dismissible": {
|
|
35
|
+
"description": "Public alias for [closable] — same semantics. Both attributes render the close button. Use whichever spelling matches your authoring style.",
|
|
36
|
+
"type": "boolean",
|
|
37
|
+
"default": false
|
|
38
|
+
},
|
|
24
39
|
"icon": {
|
|
25
40
|
"description": "Icon identifier displayed before the message content",
|
|
26
41
|
"type": "string",
|
|
27
42
|
"default": ""
|
|
28
43
|
},
|
|
29
44
|
"text": {
|
|
30
|
-
"description": "
|
|
45
|
+
"description": "Single-line alert message. For two-line \"headline + body\" alerts, use [title] + [description] instead. For rich content (links, formatting), use the [slot=\"content\"] slot.",
|
|
31
46
|
"type": "string",
|
|
32
47
|
"default": ""
|
|
33
48
|
},
|
|
@@ -13,12 +13,30 @@
|
|
|
13
13
|
|
|
14
14
|
import { UIElement } from '../../core/element.js';
|
|
15
15
|
|
|
16
|
+
// One-time warn cache so the same alias hit doesn't spam the console
|
|
17
|
+
// across hundreds of components per render. We use warnings ONLY for
|
|
18
|
+
// genuine hallucinations the LLM should learn to stop emitting:
|
|
19
|
+
// - variant="error" (canonical: "danger" — explicit in the enum)
|
|
20
|
+
// - [closeable] (canonical: "closable" — established spelling)
|
|
21
|
+
// First-class props ([title], [description], [dismissible]) do NOT warn —
|
|
22
|
+
// they're public, supported, documented in alert.yaml.
|
|
23
|
+
const _aliasWarned = new Set();
|
|
24
|
+
function _warnOnce(key, message) {
|
|
25
|
+
if (_aliasWarned.has(key)) return;
|
|
26
|
+
_aliasWarned.add(key);
|
|
27
|
+
// eslint-disable-next-line no-console
|
|
28
|
+
console.warn(`[alert-ui] ${message}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
16
31
|
class UIAlert extends UIElement {
|
|
17
32
|
static properties = {
|
|
18
|
-
text:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
33
|
+
text: { type: String, default: '', reflect: true },
|
|
34
|
+
title: { type: String, default: '', reflect: true },
|
|
35
|
+
description: { type: String, default: '', reflect: true },
|
|
36
|
+
variant: { type: String, default: 'default', reflect: true },
|
|
37
|
+
closable: { type: Boolean, default: false, reflect: true },
|
|
38
|
+
dismissible: { type: Boolean, default: false, reflect: true },
|
|
39
|
+
icon: { type: String, default: '', reflect: true },
|
|
22
40
|
};
|
|
23
41
|
|
|
24
42
|
static parts = {
|
|
@@ -33,11 +51,47 @@ class UIAlert extends UIElement {
|
|
|
33
51
|
|
|
34
52
|
static template = () => null;
|
|
35
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Normalize alias attrs that the LLM / corpus occasionally emits in
|
|
56
|
+
* non-canonical forms. Runs once at connected() before render(). Two
|
|
57
|
+
* categories:
|
|
58
|
+
*
|
|
59
|
+
* FIRST-CLASS ALIASES (public, supported, no warn):
|
|
60
|
+
* - [dismissible] ↔ [closable] (same semantics; either spelling
|
|
61
|
+
* maps to the same close-button affordance)
|
|
62
|
+
*
|
|
63
|
+
* HALLUCINATION ALIASES (warn-once, encourage canonical form):
|
|
64
|
+
* - variant="error" → variant="danger" (not in the canonical
|
|
65
|
+
* enum [default, info, success, warning, danger, muted, neutral])
|
|
66
|
+
* - [closeable] → [closable] (alternate spelling, less standard
|
|
67
|
+
* than dismissible/closable; warn to discourage)
|
|
68
|
+
*/
|
|
69
|
+
#normalizeAliases() {
|
|
70
|
+
// variant=error → danger (hallucination; warn)
|
|
71
|
+
if (this.getAttribute('variant') === 'error') {
|
|
72
|
+
_warnOnce('variant-error', 'variant="error" is not in the canonical enum [default, info, success, warning, danger, muted, neutral]. Mapping to "danger". Fix the source (LLM prompt / corpus pattern) to emit "danger" directly.');
|
|
73
|
+
this.setAttribute('variant', 'danger');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// closeable → closable (typo-class; warn)
|
|
77
|
+
if (this.hasAttribute('closeable') && !this.hasAttribute('closable') && !this.hasAttribute('dismissible')) {
|
|
78
|
+
_warnOnce('alias-closeable', 'attribute [closeable] is a misspelled alias of canonical [closable]. Mapping. Fix the source to use [closable] or [dismissible].');
|
|
79
|
+
this.setAttribute('closable', '');
|
|
80
|
+
this.removeAttribute('closeable');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// dismissible ↔ closable (first-class alias; no warn)
|
|
84
|
+
if (this.hasAttribute('dismissible') && !this.hasAttribute('closable')) {
|
|
85
|
+
this.setAttribute('closable', '');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
36
89
|
#onPress = (e) => {
|
|
37
90
|
if (e.target.closest('[slot="close"]')) this.#close();
|
|
38
91
|
};
|
|
39
92
|
|
|
40
93
|
connected() {
|
|
94
|
+
this.#normalizeAliases();
|
|
41
95
|
this.#updateRole();
|
|
42
96
|
|
|
43
97
|
// Stamp default DOM if nothing was provided
|
|
@@ -61,14 +115,51 @@ class UIAlert extends UIElement {
|
|
|
61
115
|
this.drop('leading');
|
|
62
116
|
}
|
|
63
117
|
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// (
|
|
118
|
+
// Content rendering — three modes, in precedence order:
|
|
119
|
+
// 1. Author-provided <span slot="content">…</span> with content
|
|
120
|
+
// already inside wins (rich content path)
|
|
121
|
+
// 2. [title] and/or [description] — bolded headline + body paragraph
|
|
122
|
+
// 3. [text] — single-line message
|
|
123
|
+
//
|
|
124
|
+
// Detection of "author content" is by checking whether the slot
|
|
125
|
+
// element has its `data-alert-auto` flag (set by us when we stamp
|
|
126
|
+
// content). If the flag is absent AND the element has any content,
|
|
127
|
+
// the author provided it; leave it alone.
|
|
67
128
|
const content = this.ensure('content');
|
|
68
|
-
if (content
|
|
129
|
+
if (content) {
|
|
130
|
+
const wasAutoStamped = content.hasAttribute('data-alert-auto');
|
|
131
|
+
const hasContent = content.childNodes.length > 0;
|
|
132
|
+
if (!wasAutoStamped && hasContent) {
|
|
133
|
+
// Author-provided rich content. Mirror title/description to
|
|
134
|
+
// aria-label if they were set, but don't touch the markup.
|
|
135
|
+
if (this.title || this.description) {
|
|
136
|
+
const aria = [this.title, this.description].filter(Boolean).join('. ');
|
|
137
|
+
this.setAttribute('aria-label', aria);
|
|
138
|
+
}
|
|
139
|
+
} else if (this.title || this.description) {
|
|
140
|
+
// Mode 2: title + description composed
|
|
141
|
+
content.setAttribute('data-alert-auto', 'title-desc');
|
|
142
|
+
content.replaceChildren();
|
|
143
|
+
if (this.title) {
|
|
144
|
+
const strong = document.createElement('strong');
|
|
145
|
+
strong.textContent = this.title;
|
|
146
|
+
content.appendChild(strong);
|
|
147
|
+
if (this.description) content.appendChild(document.createTextNode(' '));
|
|
148
|
+
}
|
|
149
|
+
if (this.description) {
|
|
150
|
+
content.appendChild(document.createTextNode(this.description));
|
|
151
|
+
}
|
|
152
|
+
const aria = [this.title, this.description].filter(Boolean).join('. ');
|
|
153
|
+
this.setAttribute('aria-label', aria);
|
|
154
|
+
} else if (this.text) {
|
|
155
|
+
// Mode 3: single-line text
|
|
156
|
+
content.setAttribute('data-alert-auto', 'text');
|
|
157
|
+
content.textContent = this.text;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
69
160
|
|
|
70
161
|
// Close button
|
|
71
|
-
if (this.closable) {
|
|
162
|
+
if (this.closable || this.dismissible) {
|
|
72
163
|
this.ensure('close');
|
|
73
164
|
} else {
|
|
74
165
|
this.drop('close');
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* alert-ui tests
|
|
3
|
+
*
|
|
4
|
+
* Three content modes (precedence order):
|
|
5
|
+
* 1. Author-provided <span slot="content">…</span> — wins
|
|
6
|
+
* 2. [title] + [description] — bolded headline + body paragraph
|
|
7
|
+
* 3. [text] — single-line message
|
|
8
|
+
*
|
|
9
|
+
* Two alias categories:
|
|
10
|
+
* - First-class aliases (public, no warn): [dismissible] ↔ [closable]
|
|
11
|
+
* - Hallucination aliases (warn-once): variant="error" → "danger";
|
|
12
|
+
* [closeable] → [closable]
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
16
|
+
import '../../core/element.js';
|
|
17
|
+
import './alert.js';
|
|
18
|
+
|
|
19
|
+
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
20
|
+
|
|
21
|
+
function mount(html) {
|
|
22
|
+
const wrap = document.createElement('div');
|
|
23
|
+
wrap.innerHTML = html;
|
|
24
|
+
document.body.appendChild(wrap);
|
|
25
|
+
return wrap.firstElementChild;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('alert-ui — canonical props', () => {
|
|
29
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
30
|
+
|
|
31
|
+
it('renders text from the canonical `text` prop', async () => {
|
|
32
|
+
const a = mount('<alert-ui text="Hello" variant="danger"></alert-ui>');
|
|
33
|
+
await tick();
|
|
34
|
+
const content = a.querySelector(':scope > [slot="content"]');
|
|
35
|
+
expect(content).not.toBeNull();
|
|
36
|
+
expect(content.textContent).toBe('Hello');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('sets role=alert for danger variant', async () => {
|
|
40
|
+
const a = mount('<alert-ui text="Error" variant="danger"></alert-ui>');
|
|
41
|
+
await tick();
|
|
42
|
+
expect(a.getAttribute('role')).toBe('alert');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('sets role=status for non-danger variants', async () => {
|
|
46
|
+
const a = mount('<alert-ui text="Info" variant="info"></alert-ui>');
|
|
47
|
+
await tick();
|
|
48
|
+
expect(a.getAttribute('role')).toBe('status');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('renders [title] + [description] as bold-headline + body (first-class, no warn)', async () => {
|
|
52
|
+
const a = mount('<alert-ui title="Heads up" description="A warning was raised." variant="warning"></alert-ui>');
|
|
53
|
+
await tick();
|
|
54
|
+
const content = a.querySelector(':scope > [slot="content"]');
|
|
55
|
+
expect(content).not.toBeNull();
|
|
56
|
+
expect(content.textContent).toContain('Heads up');
|
|
57
|
+
expect(content.textContent).toContain('A warning was raised.');
|
|
58
|
+
const strong = content.querySelector('strong');
|
|
59
|
+
expect(strong).not.toBeNull();
|
|
60
|
+
expect(strong.textContent).toBe('Heads up');
|
|
61
|
+
// No warn for canonical title/description (they're first-class)
|
|
62
|
+
expect(a.getAttribute('role')).toBe('alert'); // warning → alert role
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('renders [title] alone (description omitted)', async () => {
|
|
66
|
+
const a = mount('<alert-ui title="Heads up" variant="warning"></alert-ui>');
|
|
67
|
+
await tick();
|
|
68
|
+
const content = a.querySelector(':scope > [slot="content"]');
|
|
69
|
+
expect(content).not.toBeNull();
|
|
70
|
+
expect(content.textContent.trim()).toBe('Heads up');
|
|
71
|
+
expect(content.querySelector('strong').textContent).toBe('Heads up');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('renders [description] alone (title omitted)', async () => {
|
|
75
|
+
const a = mount('<alert-ui description="A quiet notice." variant="info"></alert-ui>');
|
|
76
|
+
await tick();
|
|
77
|
+
const content = a.querySelector(':scope > [slot="content"]');
|
|
78
|
+
expect(content).not.toBeNull();
|
|
79
|
+
expect(content.textContent.trim()).toBe('A quiet notice.');
|
|
80
|
+
expect(content.querySelector('strong')).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('[dismissible] is first-class — same effect as [closable], no warn', async () => {
|
|
84
|
+
const a = mount('<alert-ui text="Banner" dismissible></alert-ui>');
|
|
85
|
+
await tick();
|
|
86
|
+
expect(a.hasAttribute('closable')).toBe(true);
|
|
87
|
+
// dismissible attribute is preserved on the host for forward-compat
|
|
88
|
+
// (consumers can read either)
|
|
89
|
+
expect(a.querySelector('[slot="close"]')).not.toBeNull();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('[closable] works canonically without [dismissible]', async () => {
|
|
93
|
+
const a = mount('<alert-ui text="Banner" closable></alert-ui>');
|
|
94
|
+
await tick();
|
|
95
|
+
expect(a.hasAttribute('closable')).toBe(true);
|
|
96
|
+
expect(a.querySelector('[slot="close"]')).not.toBeNull();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('alert-ui — hallucination aliases (warn-once)', () => {
|
|
101
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
102
|
+
|
|
103
|
+
it('maps variant="error" to variant="danger" (warns)', async () => {
|
|
104
|
+
const a = mount('<alert-ui text="Oops" variant="error"></alert-ui>');
|
|
105
|
+
await tick();
|
|
106
|
+
expect(a.getAttribute('variant')).toBe('danger');
|
|
107
|
+
expect(a.getAttribute('role')).toBe('alert');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('maps [closeable] (misspelling) to [closable] (warns)', async () => {
|
|
111
|
+
const a = mount('<alert-ui text="Banner" closeable></alert-ui>');
|
|
112
|
+
await tick();
|
|
113
|
+
expect(a.hasAttribute('closable')).toBe(true);
|
|
114
|
+
expect(a.hasAttribute('closeable')).toBe(false);
|
|
115
|
+
expect(a.querySelector('[slot="close"]')).not.toBeNull();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('alert-ui — full corpus shape (the §34 scenario)', () => {
|
|
120
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
121
|
+
|
|
122
|
+
it('renders the exact alert-banner.json corpus pattern correctly', async () => {
|
|
123
|
+
// This is `patterns/agent/alert-banner.json` rendered through the
|
|
124
|
+
// component. All four props are now first-class / aliased; the
|
|
125
|
+
// alert renders with full visible content.
|
|
126
|
+
const a = mount(`<alert-ui
|
|
127
|
+
variant="info"
|
|
128
|
+
icon="info-circle"
|
|
129
|
+
title="System Update"
|
|
130
|
+
description="A new version is available. Please refresh to get the latest features."
|
|
131
|
+
dismissible
|
|
132
|
+
></alert-ui>`);
|
|
133
|
+
await tick();
|
|
134
|
+
// Variant stays canonical
|
|
135
|
+
expect(a.getAttribute('variant')).toBe('info');
|
|
136
|
+
// Dismissible maps to closable
|
|
137
|
+
expect(a.hasAttribute('closable')).toBe(true);
|
|
138
|
+
// Role is status (info, not danger/warning)
|
|
139
|
+
expect(a.getAttribute('role')).toBe('status');
|
|
140
|
+
// Content composed correctly
|
|
141
|
+
const content = a.querySelector(':scope > [slot="content"]');
|
|
142
|
+
expect(content.textContent).toContain('System Update');
|
|
143
|
+
expect(content.textContent).toContain('A new version is available.');
|
|
144
|
+
expect(content.querySelector('strong').textContent).toBe('System Update');
|
|
145
|
+
// Close button present (dismissible → closable)
|
|
146
|
+
expect(a.querySelector('[slot="close"]')).not.toBeNull();
|
|
147
|
+
// ARIA composed
|
|
148
|
+
expect(a.getAttribute('aria-label')).toContain('System Update');
|
|
149
|
+
expect(a.getAttribute('aria-label')).toContain('A new version is available');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('renders the exact destructive-confirm.json corpus pattern correctly', async () => {
|
|
153
|
+
// This is the shape used in compositions/forms/destructive-confirm.json:
|
|
154
|
+
// variant=danger + title + description (no close button)
|
|
155
|
+
const a = mount(`<alert-ui
|
|
156
|
+
variant="danger"
|
|
157
|
+
title="This action is permanent"
|
|
158
|
+
description="Deleting your account cannot be undone. All your data will be removed within 30 days."
|
|
159
|
+
></alert-ui>`);
|
|
160
|
+
await tick();
|
|
161
|
+
expect(a.getAttribute('variant')).toBe('danger');
|
|
162
|
+
expect(a.getAttribute('role')).toBe('alert');
|
|
163
|
+
const content = a.querySelector(':scope > [slot="content"]');
|
|
164
|
+
expect(content.querySelector('strong').textContent).toBe('This action is permanent');
|
|
165
|
+
expect(content.textContent).toContain('Deleting your account cannot be undone');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('preserves author-provided [slot="content"] when [title]+[description] are also set', async () => {
|
|
169
|
+
// Authored rich content wins; canonical props become metadata only
|
|
170
|
+
// (still mirrored to aria-label for screen reader semantics).
|
|
171
|
+
const a = mount(`<alert-ui title="hdr" description="body">
|
|
172
|
+
<span slot="content">Custom <a href="/foo">link</a></span>
|
|
173
|
+
</alert-ui>`);
|
|
174
|
+
await tick();
|
|
175
|
+
const content = a.querySelector(':scope > [slot="content"]');
|
|
176
|
+
// Should preserve the author's link
|
|
177
|
+
expect(content.querySelector('a')).not.toBeNull();
|
|
178
|
+
expect(content.querySelector('a').getAttribute('href')).toBe('/foo');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -9,7 +9,17 @@ version: 1
|
|
|
9
9
|
description: Inline alert banner with optional icon and close button.
|
|
10
10
|
props:
|
|
11
11
|
closable:
|
|
12
|
-
description:
|
|
12
|
+
description: >-
|
|
13
|
+
Whether a close button is displayed. Alias [dismissible] is also
|
|
14
|
+
accepted (same semantics, different spelling — the corpus and
|
|
15
|
+
many libraries use both; both map to the same state).
|
|
16
|
+
type: boolean
|
|
17
|
+
default: false
|
|
18
|
+
dismissible:
|
|
19
|
+
description: >-
|
|
20
|
+
Public alias for [closable] — same semantics. Both attributes
|
|
21
|
+
render the close button. Use whichever spelling matches your
|
|
22
|
+
authoring style.
|
|
13
23
|
type: boolean
|
|
14
24
|
default: false
|
|
15
25
|
icon:
|
|
@@ -17,7 +27,25 @@ props:
|
|
|
17
27
|
type: string
|
|
18
28
|
default: ""
|
|
19
29
|
text:
|
|
20
|
-
description:
|
|
30
|
+
description: >-
|
|
31
|
+
Single-line alert message. For two-line "headline + body" alerts,
|
|
32
|
+
use [title] + [description] instead. For rich content (links,
|
|
33
|
+
formatting), use the [slot="content"] slot.
|
|
34
|
+
type: string
|
|
35
|
+
default: ""
|
|
36
|
+
title:
|
|
37
|
+
description: >-
|
|
38
|
+
Bold headline rendered as the first line of the alert content.
|
|
39
|
+
Pair with [description] for the canonical "banner" pattern
|
|
40
|
+
(headline + body). When [title] or [description] is set, the
|
|
41
|
+
[text] prop is ignored.
|
|
42
|
+
type: string
|
|
43
|
+
default: ""
|
|
44
|
+
description:
|
|
45
|
+
description: >-
|
|
46
|
+
Body text rendered as the second line of the alert content,
|
|
47
|
+
below [title]. May be used alone (without [title]) for a single
|
|
48
|
+
muted-body message.
|
|
21
49
|
type: string
|
|
22
50
|
default: ""
|
|
23
51
|
variant:
|
|
@@ -50,6 +50,10 @@
|
|
|
50
50
|
"type": "string",
|
|
51
51
|
"default": ""
|
|
52
52
|
},
|
|
53
|
+
"textContent": {
|
|
54
|
+
"description": "Badge display text. Renderer routes this to the `text` attribute via CSS attr(text) on ::after.",
|
|
55
|
+
"$ref": "common_types.json#/$defs/DynamicString"
|
|
56
|
+
},
|
|
53
57
|
"variant": {
|
|
54
58
|
"description": "Semantic color variant.",
|
|
55
59
|
"type": "string",
|
|
@@ -27,6 +27,7 @@ const STATUS_MAP = {
|
|
|
27
27
|
class UIBadge extends UIElement {
|
|
28
28
|
static properties = {
|
|
29
29
|
text: { type: String, default: '', reflect: true },
|
|
30
|
+
textContent: { type: String, default: '' },
|
|
30
31
|
variant: { type: String, default: 'default', reflect: true },
|
|
31
32
|
size: { type: String, default: 'md', reflect: true },
|
|
32
33
|
icon: { type: String, default: '', reflect: true },
|
|
@@ -40,6 +40,10 @@ props:
|
|
|
40
40
|
description: Badge text content. Falls back to existing textContent.
|
|
41
41
|
type: string
|
|
42
42
|
default: ""
|
|
43
|
+
textContent:
|
|
44
|
+
description: Badge display text. Renderer routes this to the `text` attribute via CSS attr(text) on ::after.
|
|
45
|
+
type: string
|
|
46
|
+
dynamic: true
|
|
43
47
|
variant:
|
|
44
48
|
description: Semantic color variant.
|
|
45
49
|
type: string
|
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
"type": "string",
|
|
19
19
|
"default": "button"
|
|
20
20
|
},
|
|
21
|
+
"aria-label": {
|
|
22
|
+
"description": "Accessible label for screen readers. Auto-set from `text` when text is non-empty; meaningful override for icon-only buttons.",
|
|
23
|
+
"type": "string",
|
|
24
|
+
"default": ""
|
|
25
|
+
},
|
|
21
26
|
"color": {
|
|
22
27
|
"description": "Semantic intent — composes with [variant]. `<button-ui variant=\"solid\" color=\"danger\">` = filled destructive action; `<button-ui variant=\"outline\" color=\"success\">` = outlined success affordance.",
|
|
23
28
|
"type": "string",
|
|
@@ -66,8 +71,12 @@
|
|
|
66
71
|
"type": "string",
|
|
67
72
|
"default": ""
|
|
68
73
|
},
|
|
74
|
+
"textContent": {
|
|
75
|
+
"description": "Button label. Renderer routes this to the `text` attribute, which is rendered via CSS attr(text) on ::after and mirrored to aria-label.",
|
|
76
|
+
"$ref": "common_types.json#/$defs/DynamicString"
|
|
77
|
+
},
|
|
69
78
|
"variant": {
|
|
70
|
-
"description": "Visual style — `solid` (default fill), `outline`, `ghost
|
|
79
|
+
"description": "Visual style — `solid` (default fill), `outline`, `ghost`. `default` / `primary` are aliases of `solid`. Style is independent of semantic intent — to express destructive / success / info / warning intent, set [color=\"…\"] alongside.\nFor **inline navigation** (Terms of Service, Privacy Policy, footer links, \"Sign in\" / \"Sign up\" cross-page affordances) use `<link-ui>` instead — it carries proper `<a href>` semantics, keyboard handling (Enter only, no Space), middle-click open-new-tab, and screen-reader announces \"link\" instead of \"button\". Mixing navigation and action affordances under the same primitive is a category error fixed at this junction.",
|
|
71
80
|
"type": "string",
|
|
72
81
|
"enum": [
|
|
73
82
|
"default",
|
|
@@ -77,8 +86,7 @@
|
|
|
77
86
|
"primary",
|
|
78
87
|
"secondary",
|
|
79
88
|
"soft",
|
|
80
|
-
"current"
|
|
81
|
-
"link"
|
|
89
|
+
"current"
|
|
82
90
|
],
|
|
83
91
|
"default": "solid"
|
|
84
92
|
}
|
|
@@ -98,7 +106,9 @@
|
|
|
98
106
|
"examples": [],
|
|
99
107
|
"keywords": [],
|
|
100
108
|
"name": "UIButton",
|
|
101
|
-
"related": [
|
|
109
|
+
"related": [
|
|
110
|
+
"Link"
|
|
111
|
+
],
|
|
102
112
|
"slots": {
|
|
103
113
|
"leading": {
|
|
104
114
|
"description": "Icon container (start), sized to --content-height"
|
|
@@ -4,6 +4,7 @@ import { getIcon } from '../../core/icons.js';
|
|
|
4
4
|
class UIButton extends UIElement {
|
|
5
5
|
static properties = {
|
|
6
6
|
text: { type: String, default: '', reflect: true },
|
|
7
|
+
textContent: { type: String, default: '' },
|
|
7
8
|
variant: { type: String, default: 'solid', reflect: true },
|
|
8
9
|
color: { type: String, default: '', reflect: true },
|
|
9
10
|
size: { type: String, default: 'md', reflect: true },
|
|
@@ -8,6 +8,10 @@ category: action
|
|
|
8
8
|
version: 1
|
|
9
9
|
description: Clickable button with text, icon, and variant support. Supports submit type for forms.
|
|
10
10
|
props:
|
|
11
|
+
aria-label:
|
|
12
|
+
description: Accessible label for screen readers. Auto-set from `text` when text is non-empty; meaningful override for icon-only buttons.
|
|
13
|
+
type: string
|
|
14
|
+
default: ""
|
|
11
15
|
type:
|
|
12
16
|
description: HTML button type (button, submit, reset)
|
|
13
17
|
type: string
|
|
@@ -40,12 +44,24 @@ props:
|
|
|
40
44
|
description: Button text, rendered via CSS attr(text) on ::after
|
|
41
45
|
type: string
|
|
42
46
|
default: ""
|
|
47
|
+
textContent:
|
|
48
|
+
description: Button label. Renderer routes this to the `text` attribute, which is rendered via CSS attr(text) on ::after and mirrored to aria-label.
|
|
49
|
+
type: string
|
|
50
|
+
dynamic: true
|
|
43
51
|
variant:
|
|
44
52
|
description: >-
|
|
45
|
-
Visual style — `solid` (default fill), `outline`, `ghost
|
|
53
|
+
Visual style — `solid` (default fill), `outline`, `ghost`.
|
|
46
54
|
`default` / `primary` are aliases of `solid`. Style is independent
|
|
47
55
|
of semantic intent — to express destructive / success / info /
|
|
48
56
|
warning intent, set [color="…"] alongside.
|
|
57
|
+
|
|
58
|
+
For **inline navigation** (Terms of Service, Privacy Policy,
|
|
59
|
+
footer links, "Sign in" / "Sign up" cross-page affordances) use
|
|
60
|
+
`<link-ui>` instead — it carries proper `<a href>` semantics,
|
|
61
|
+
keyboard handling (Enter only, no Space), middle-click open-new-tab,
|
|
62
|
+
and screen-reader announces "link" instead of "button". Mixing
|
|
63
|
+
navigation and action affordances under the same primitive is a
|
|
64
|
+
category error fixed at this junction.
|
|
49
65
|
type: string
|
|
50
66
|
default: solid
|
|
51
67
|
enum:
|
|
@@ -57,7 +73,6 @@ props:
|
|
|
57
73
|
- secondary
|
|
58
74
|
- soft
|
|
59
75
|
- current
|
|
60
|
-
- link
|
|
61
76
|
color:
|
|
62
77
|
description: >-
|
|
63
78
|
Semantic intent — composes with [variant]. `<button-ui variant="solid" color="danger">`
|
|
@@ -124,4 +139,4 @@ anti_patterns: []
|
|
|
124
139
|
examples: []
|
|
125
140
|
keywords: []
|
|
126
141
|
synonyms: {}
|
|
127
|
-
related: []
|
|
142
|
+
related: [Link]
|
|
@@ -62,7 +62,14 @@
|
|
|
62
62
|
],
|
|
63
63
|
"unevaluatedProperties": false,
|
|
64
64
|
"x-adiaui": {
|
|
65
|
-
"anti_patterns": [
|
|
65
|
+
"anti_patterns": [
|
|
66
|
+
{
|
|
67
|
+
"description": "Wrapping a check-ui in field-ui. The widget already self-labels.",
|
|
68
|
+
"right": "<check-ui label=\"I agree to the Terms of Service\"></check-ui>\n",
|
|
69
|
+
"rule": "Use [label] on check-ui directly; do not wrap in field-ui.",
|
|
70
|
+
"wrong": "<field-ui inline label=\"Agree\">\n <check-ui></check-ui>\n</field-ui>\n"
|
|
71
|
+
}
|
|
72
|
+
],
|
|
66
73
|
"category": "layout",
|
|
67
74
|
"events": {
|
|
68
75
|
"change": {
|
|
@@ -76,8 +76,17 @@ tokens:
|
|
|
76
76
|
--check-checked-foreground:
|
|
77
77
|
description: Checkmark/dash color
|
|
78
78
|
a2ui:
|
|
79
|
-
rules:
|
|
80
|
-
|
|
79
|
+
rules:
|
|
80
|
+
- "Self-labeling widget — use the [label] attribute directly; do NOT wrap in <field-ui>. The widget renders its own label inline via CSS attr() pattern. Wrapping breaks the consent-row layout (see field-ui anti_patterns)."
|
|
81
|
+
anti_patterns:
|
|
82
|
+
- description: Wrapping a check-ui in field-ui. The widget already self-labels.
|
|
83
|
+
wrong: |
|
|
84
|
+
<field-ui inline label="Agree">
|
|
85
|
+
<check-ui></check-ui>
|
|
86
|
+
</field-ui>
|
|
87
|
+
right: |
|
|
88
|
+
<check-ui label="I agree to the Terms of Service"></check-ui>
|
|
89
|
+
rule: Use [label] on check-ui directly; do not wrap in field-ui.
|
|
81
90
|
examples:
|
|
82
91
|
- name: basic-check
|
|
83
92
|
description: Basic Check usage
|
|
@@ -70,6 +70,10 @@
|
|
|
70
70
|
"description": "Code text content",
|
|
71
71
|
"type": "string",
|
|
72
72
|
"default": ""
|
|
73
|
+
},
|
|
74
|
+
"textContent": {
|
|
75
|
+
"description": "Code body text. Renderer routes this to the `text` attribute, reactively re-rendered into the <pre><code> block.",
|
|
76
|
+
"$ref": "common_types.json#/$defs/DynamicString"
|
|
73
77
|
}
|
|
74
78
|
},
|
|
75
79
|
"required": [
|
package/components/code/code.js
CHANGED
|
@@ -49,6 +49,7 @@ class UICode extends UIElement {
|
|
|
49
49
|
language: { type: String, default: '', reflect: true },
|
|
50
50
|
inline: { type: Boolean, default: false, reflect: true },
|
|
51
51
|
text: { type: String, default: '', reflect: true },
|
|
52
|
+
textContent: { type: String, default: '' },
|
|
52
53
|
lineNumbers: { type: Boolean, default: false, reflect: true, attribute: 'line-numbers' },
|
|
53
54
|
editable: { type: Boolean, default: false, reflect: true },
|
|
54
55
|
bare: { type: Boolean, default: false, reflect: true },
|
|
@@ -25,6 +25,10 @@ props:
|
|
|
25
25
|
description: Code text content
|
|
26
26
|
type: string
|
|
27
27
|
default: ""
|
|
28
|
+
textContent:
|
|
29
|
+
description: Code body text. Renderer routes this to the `text` attribute, reactively re-rendered into the <pre><code> block.
|
|
30
|
+
type: string
|
|
31
|
+
dynamic: true
|
|
28
32
|
editable:
|
|
29
33
|
description: Editable CodeMirror instance (vs read-only display)
|
|
30
34
|
type: boolean
|
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
"type": "string",
|
|
27
27
|
"default": "md"
|
|
28
28
|
},
|
|
29
|
+
"grow": {
|
|
30
|
+
"description": "Fills remaining space in a flex parent (e.g. inside a Row). CSS-only attribute via :scope[grow] in col.css.",
|
|
31
|
+
"type": "boolean",
|
|
32
|
+
"default": false
|
|
33
|
+
},
|
|
29
34
|
"justify": {
|
|
30
35
|
"description": "Justify content",
|
|
31
36
|
"type": "string",
|
package/components/col/col.js
CHANGED
|
@@ -11,6 +11,7 @@ class UICol extends UIElement {
|
|
|
11
11
|
justify: { type: String, default: 'start', reflect: true },
|
|
12
12
|
align: { type: String, default: 'stretch', reflect: true },
|
|
13
13
|
gap: { type: String, default: 'md', reflect: true },
|
|
14
|
+
grow: { type: Boolean, default: false, reflect: true },
|
|
14
15
|
};
|
|
15
16
|
static template = () => null;
|
|
16
17
|
}
|