@adia-ai/web-components 0.4.3 → 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.
Files changed (49) hide show
  1. package/components/alert/alert.a2ui.json +17 -2
  2. package/components/alert/alert.js +100 -9
  3. package/components/alert/alert.test.js +180 -0
  4. package/components/alert/alert.yaml +30 -2
  5. package/components/badge/badge.a2ui.json +4 -0
  6. package/components/badge/badge.js +1 -0
  7. package/components/badge/badge.yaml +4 -0
  8. package/components/button/button.a2ui.json +14 -4
  9. package/components/button/button.js +1 -0
  10. package/components/button/button.yaml +18 -3
  11. package/components/check/check.a2ui.json +8 -1
  12. package/components/check/check.yaml +11 -2
  13. package/components/code/code.a2ui.json +4 -0
  14. package/components/code/code.js +1 -0
  15. package/components/code/code.yaml +4 -0
  16. package/components/col/col.a2ui.json +5 -0
  17. package/components/col/col.js +1 -0
  18. package/components/col/col.yaml +5 -0
  19. package/components/field/field.a2ui.json +17 -6
  20. package/components/field/field.test.js +8 -2
  21. package/components/field/field.yaml +50 -8
  22. package/components/index.js +1 -0
  23. package/components/input/input.a2ui.json +20 -0
  24. package/components/input/input.yaml +15 -0
  25. package/components/link/link.a2ui.json +166 -0
  26. package/components/link/link.css +102 -0
  27. package/components/link/link.js +177 -0
  28. package/components/link/link.test.js +143 -0
  29. package/components/link/link.yaml +162 -0
  30. package/components/radio/radio.a2ui.json +8 -1
  31. package/components/radio/radio.yaml +11 -2
  32. package/components/row/row.a2ui.json +5 -0
  33. package/components/row/row.js +1 -0
  34. package/components/row/row.yaml +5 -0
  35. package/components/select/select.a2ui.json +15 -0
  36. package/components/select/select.yaml +14 -0
  37. package/components/switch/switch.a2ui.json +8 -1
  38. package/components/switch/switch.yaml +11 -2
  39. package/components/table/table.a2ui.json +10 -0
  40. package/components/table/table.yaml +8 -0
  41. package/components/tag/tag.a2ui.json +4 -0
  42. package/components/tag/tag.js +1 -0
  43. package/components/tag/tag.yaml +4 -0
  44. package/components/text/text.a2ui.json +5 -0
  45. package/components/text/text.js +1 -0
  46. package/components/text/text.yaml +5 -0
  47. package/components/textarea/textarea.a2ui.json +5 -0
  48. package/components/textarea/textarea.yaml +4 -0
  49. package/package.json +1 -1
@@ -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": "Alert message text",
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: { type: String, default: '', reflect: true },
19
- variant: { type: String, default: 'default', reflect: true },
20
- closable: { type: Boolean, default: false, reflect: true },
21
- icon: { type: String, default: '', reflect: true },
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
- // Textonly write from the `text` attribute when it's set, so
65
- // consumers passing rich content via `<span slot="content">…</span>`
66
- // (links, <strong>, etc.) aren't clobbered with empty textContent.
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 && this.text) content.textContent = this.text;
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: Whether a close button is displayed
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: Alert message text
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`, `link`. `default` / `primary` are aliases of `solid`. Style is independent of semantic intent — to express destructive / success / info / warning intent, set [color=\"…\"] alongside.",
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`, `link`.
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
- anti_patterns: []
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": [
@@ -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",
@@ -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
  }
@@ -18,6 +18,11 @@ props:
18
18
  or a numeric rung on the spacing scale ("1"…"16", mapped to --a-space-N).
19
19
  type: string
20
20
  default: md
21
+ grow:
22
+ description: Fills remaining space in a flex parent (e.g. inside a Row). CSS-only attribute via :scope[grow] in col.css.
23
+ type: boolean
24
+ default: false
25
+ reflect: true
21
26
  justify:
22
27
  description: Justify content
23
28
  type: string