@adia-ai/web-components 0.2.3 → 0.2.5

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 (118) hide show
  1. package/components/button/button.js +3 -0
  2. package/components/demo-toggle/demo-toggle.a2ui.json +144 -0
  3. package/components/demo-toggle/demo-toggle.css +120 -0
  4. package/components/demo-toggle/demo-toggle.js +144 -0
  5. package/components/demo-toggle/demo-toggle.test.js +102 -0
  6. package/components/demo-toggle/demo-toggle.yaml +144 -0
  7. package/components/fields/fields.a2ui.json +106 -0
  8. package/components/fields/fields.css +60 -0
  9. package/components/fields/fields.js +96 -0
  10. package/components/fields/fields.test.js +88 -0
  11. package/components/fields/fields.yaml +120 -0
  12. package/components/index.js +2 -0
  13. package/components/input/input.js +11 -0
  14. package/components/list/list.css +21 -0
  15. package/components/textarea/textarea.js +10 -0
  16. package/core/icons.js +12 -1
  17. package/package.json +10 -10
  18. package/styles/components.css +2 -0
  19. package/styles/typography.css +1 -1
  20. package/traits/_catalog.json +259 -4
  21. package/traits/active-state.test.js +1 -1
  22. package/traits/anchor-positioning.js +205 -52
  23. package/traits/anchor-positioning.test.js +77 -4
  24. package/traits/announcer-stage.js +157 -0
  25. package/traits/announcer.js +145 -0
  26. package/traits/announcer.test.js +268 -0
  27. package/traits/arrow-grid-nav.js +234 -0
  28. package/traits/arrow-grid-nav.test.js +375 -0
  29. package/traits/attention-pulse.js +1 -1
  30. package/traits/attention-pulse.test.js +1 -1
  31. package/traits/confetti-burst.js +67 -63
  32. package/traits/confetti-burst.test.js +16 -8
  33. package/traits/confetti-stage.js +143 -0
  34. package/traits/confetti.js +44 -47
  35. package/traits/confetti.test.js +24 -5
  36. package/traits/count-up.js +31 -6
  37. package/traits/count-up.test.js +1 -1
  38. package/traits/declarative.test.js +1 -1
  39. package/traits/dirty-state.test.js +1 -1
  40. package/traits/drag-ghost.js +43 -3
  41. package/traits/drag-ghost.test.js +1 -1
  42. package/traits/draggable-list-item.js +458 -0
  43. package/traits/draggable-list-item.test.js +51 -0
  44. package/traits/draggable.js +14 -4
  45. package/traits/draggable.test.js +1 -1
  46. package/traits/drop-target.js +223 -0
  47. package/traits/drop-target.test.js +241 -0
  48. package/traits/droppable-collection.js +89 -0
  49. package/traits/droppable-collection.test.js +99 -0
  50. package/traits/droppable.js +136 -0
  51. package/traits/droppable.test.js +54 -0
  52. package/traits/error-shake.js +157 -0
  53. package/traits/error-shake.test.js +114 -0
  54. package/traits/fade-presence.test.js +1 -1
  55. package/traits/focus-restore.js +135 -0
  56. package/traits/focus-restore.test.js +202 -0
  57. package/traits/focus-trap.test.js +1 -1
  58. package/traits/focusable.test.js +1 -1
  59. package/traits/glow-focus.js +1 -1
  60. package/traits/glow-focus.test.js +1 -1
  61. package/traits/gradient-shift.js +1 -1
  62. package/traits/gradient-shift.test.js +1 -1
  63. package/traits/haptic-feedback.test.js +1 -1
  64. package/traits/hotkey.test.js +1 -1
  65. package/traits/hoverable.test.js +1 -1
  66. package/traits/index.js +15 -0
  67. package/traits/inertia-drag.js +9 -0
  68. package/traits/inertia-drag.test.js +1 -1
  69. package/traits/input-mask.js +328 -0
  70. package/traits/input-mask.test.js +151 -0
  71. package/traits/intersection-observer.test.js +1 -1
  72. package/traits/keyboard-nav.test.js +1 -1
  73. package/traits/keyboard-reorderable.js +254 -0
  74. package/traits/keyboard-reorderable.test.js +45 -0
  75. package/traits/layout-animation.js +229 -0
  76. package/traits/layout-animation.test.js +114 -0
  77. package/traits/long-press.js +212 -0
  78. package/traits/long-press.test.js +244 -0
  79. package/traits/magnetic-hover.js +1 -1
  80. package/traits/magnetic-hover.test.js +1 -1
  81. package/traits/noise-texture.js +7 -3
  82. package/traits/noise-texture.test.js +1 -1
  83. package/traits/parallax.js +1 -1
  84. package/traits/parallax.test.js +1 -1
  85. package/traits/portal.test.js +1 -1
  86. package/traits/pressable.test.js +1 -1
  87. package/traits/resettable.js +29 -3
  88. package/traits/resettable.test.js +34 -1
  89. package/traits/resizable.test.js +1 -1
  90. package/traits/resize-observer.test.js +1 -1
  91. package/traits/ripple.js +1 -1
  92. package/traits/ripple.test.js +1 -1
  93. package/traits/roving-tabindex.test.js +1 -1
  94. package/traits/scale-press.test.js +1 -1
  95. package/traits/scroll-lock.test.js +1 -1
  96. package/traits/scroll-progress.js +201 -0
  97. package/traits/scroll-progress.test.js +182 -0
  98. package/traits/shimmer-loading.js +1 -1
  99. package/traits/shimmer-loading.test.js +1 -1
  100. package/traits/{_smoke.test.js → smoke.test.js} +1 -1
  101. package/traits/snap-to-grid.test.js +1 -1
  102. package/traits/sound-feedback.test.js +1 -1
  103. package/traits/spring-animate.test.js +1 -1
  104. package/traits/success-checkmark.js +222 -0
  105. package/traits/success-checkmark.test.js +120 -0
  106. package/traits/tilt-hover.js +1 -1
  107. package/traits/tilt-hover.test.js +1 -1
  108. package/traits/tossable.js +9 -0
  109. package/traits/tossable.test.js +1 -1
  110. package/traits/traits-host.test.js +1 -1
  111. package/traits/typeahead.test.js +1 -1
  112. package/traits/typewriter.js +1 -1
  113. package/traits/typewriter.test.js +1 -1
  114. package/traits/validation.test.js +1 -1
  115. package/traits/view-transition.js +140 -0
  116. package/traits/view-transition.test.js +268 -0
  117. /package/traits/{_motion.js → motion.js} +0 -0
  118. /package/traits/{_test-helpers.js → test-helpers.js} +0 -0
@@ -0,0 +1,106 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://adiaui.dev/a2ui/v0_9/components/Fields.json",
4
+ "title": "Fields",
5
+ "description": "Container for a group of <field-ui> children, laid out on a shared 6-column grid. Each <field-ui> spans the full row by default; opt into a narrower span via [rows=\"1..6\"]. Setting [inline] on the host propagates the inline mode to every direct <field-ui> child so a whole sub-form can switch label-position without per-field edits. The grid alignment lets siblings on the same row line up cleanly — consistent label columns + consistent control columns — without the wrap-flex jitter of <row-ui wrap>.",
6
+ "type": "object",
7
+ "allOf": [
8
+ {
9
+ "$ref": "common_types.json#/$defs/ComponentCommon"
10
+ },
11
+ {
12
+ "$ref": "common_types.json#/$defs/CatalogComponentCommon"
13
+ }
14
+ ],
15
+ "properties": {
16
+ "columns": {
17
+ "description": "Number of grid columns the row uses. Defaults to 6 — common multiples (1/2/3/6) divide cleanly. <field-ui rows=\"N\"> spans `N` of these columns. Override per-instance for tighter 4-column or wider 12-column compositions.",
18
+ "type": "number",
19
+ "default": 6
20
+ },
21
+ "component": {
22
+ "const": "Fields"
23
+ },
24
+ "inline": {
25
+ "description": "Propagate the inline layout mode to every direct <field-ui> child. Equivalent to authoring `inline` on each child but drives the whole group from one place. Toggle is reactive — flipping the attribute on the host re-syncs all children.",
26
+ "type": "boolean",
27
+ "default": false
28
+ }
29
+ },
30
+ "required": [
31
+ "component"
32
+ ],
33
+ "unevaluatedProperties": false,
34
+ "x-adiaui": {
35
+ "anti_patterns": [
36
+ {
37
+ "description": "Don't wrap <field-ui> children in <row-ui wrap> when grid alignment is desired. row-ui is flex-wrap, which lets each field size to its content — labels and controls won't share a column with siblings.",
38
+ "severity": "high"
39
+ },
40
+ {
41
+ "description": "Don't set `inline` on individual <field-ui> children when the whole group should switch — set it on the parent <fields-ui> and let the propagation handle every child.",
42
+ "severity": "medium"
43
+ }
44
+ ],
45
+ "category": "form",
46
+ "events": {},
47
+ "examples": [
48
+ {
49
+ "description": "A 3-up stacked field row — Status / Priority / Group — each taking 2 of 6 columns. Equivalent to a Bootstrap \"col-md-4\" pattern (12-col grid, span 4) but with chat-ui's 6-col default.",
50
+ "a2ui": "[\n {\n \"id\": \"root\",\n \"component\": \"Fields\",\n \"children\": [\"status\", \"priority\", \"group\"]\n },\n { \"id\": \"status\", \"component\": \"Field\", \"label\": \"Status\", \"rows\": 2, \"children\": [\"status-sel\"] },\n { \"id\": \"priority\", \"component\": \"Field\", \"label\": \"Priority\", \"rows\": 2, \"children\": [\"priority-sel\"] },\n { \"id\": \"group\", \"component\": \"Field\", \"label\": \"Group\", \"rows\": 2, \"children\": [\"group-sel\"] },\n { \"id\": \"status-sel\", \"component\": \"Select\", \"value\": \"todo\" },\n { \"id\": \"priority-sel\", \"component\": \"Select\", \"value\": \"0\" },\n { \"id\": \"group-sel\", \"component\": \"Select\", \"value\": \"\" }\n]",
51
+ "name": "three-up-stacked"
52
+ },
53
+ {
54
+ "description": "An inline form — every field renders label-beside-control. Single [inline] on the host drives all children.",
55
+ "a2ui": "[\n {\n \"id\": \"root\",\n \"component\": \"Fields\",\n \"inline\": true,\n \"children\": [\"q\", \"kind\"]\n },\n { \"id\": \"q\", \"component\": \"Field\", \"label\": \"Search\", \"children\": [\"q-input\"] },\n { \"id\": \"kind\", \"component\": \"Field\", \"label\": \"Kind\", \"children\": [\"kind-sel\"] },\n { \"id\": \"q-input\", \"component\": \"Input\", \"type\": \"search\" },\n { \"id\": \"kind-sel\", \"component\": \"Select\", \"value\": \"all\" }\n]",
56
+ "name": "inline-search-form"
57
+ }
58
+ ],
59
+ "keywords": [
60
+ "fields",
61
+ "form",
62
+ "grid",
63
+ "layout",
64
+ "group"
65
+ ],
66
+ "name": "UIFields",
67
+ "related": [
68
+ "field",
69
+ "input",
70
+ "select",
71
+ "textarea"
72
+ ],
73
+ "slots": {
74
+ "default": {
75
+ "description": "<field-ui> children. Non-field children (separators, headings) are placed in the grid too — span them by setting `style=\"grid-column: 1 / -1\"` for full-width content. Nested <fields-ui> works (an outer 6-col grid hosting inner 3-col grids inside a single cell)."
76
+ }
77
+ },
78
+ "states": [
79
+ {
80
+ "description": "Default, ready for interaction.",
81
+ "name": "idle"
82
+ }
83
+ ],
84
+ "synonyms": {
85
+ "form": [
86
+ "fields",
87
+ "group",
88
+ "layout"
89
+ ]
90
+ },
91
+ "tag": "fields-ui",
92
+ "tokens": {
93
+ "--fields-column-gap": {
94
+ "description": "Override the column-gap independently of row-gap."
95
+ },
96
+ "--fields-gap": {
97
+ "description": "Gap between adjacent fields (row + column)."
98
+ },
99
+ "--fields-row-gap": {
100
+ "description": "Override the row-gap independently of column-gap."
101
+ }
102
+ },
103
+ "traits": [],
104
+ "version": 1
105
+ }
106
+ }
@@ -0,0 +1,60 @@
1
+ @scope (fields-ui) {
2
+ :where(:scope) {
3
+ /* ── Tokens ── */
4
+ --fields-gap: var(--a-space-3);
5
+ --fields-row-gap: var(--fields-gap);
6
+ --fields-column-gap: var(--fields-gap);
7
+ }
8
+
9
+ /* ── Base — 6-column grid ──
10
+ Children are placed into the grid by their [rows="N"] attr (pure
11
+ CSS, no JS). Default span (no attr) is the full row. The columns
12
+ count is configurable via [columns="N"] on the host — defaults
13
+ handle [columns] absent.
14
+
15
+ `minmax(0, 1fr)` is required so children with overflow content
16
+ (long labels, long select values) don't blow out the grid. */
17
+ :scope {
18
+ box-sizing: border-box;
19
+ display: grid;
20
+ grid-template-columns: repeat(var(--fields-columns, 6), minmax(0, 1fr));
21
+ column-gap: var(--fields-column-gap);
22
+ row-gap: var(--fields-row-gap);
23
+ align-items: start;
24
+ }
25
+
26
+ /* Per-instance column count from [columns="N"] — unrolled for the
27
+ common values 1..12. Beyond 12 the host can set
28
+ `--fields-columns: N` inline. */
29
+ :scope[columns="1"] { --fields-columns: 1; }
30
+ :scope[columns="2"] { --fields-columns: 2; }
31
+ :scope[columns="3"] { --fields-columns: 3; }
32
+ :scope[columns="4"] { --fields-columns: 4; }
33
+ :scope[columns="5"] { --fields-columns: 5; }
34
+ :scope[columns="6"] { --fields-columns: 6; }
35
+ :scope[columns="8"] { --fields-columns: 8; }
36
+ :scope[columns="12"] { --fields-columns: 12; }
37
+
38
+ /* ── field-ui spans ──
39
+ `rows="N"` reads as "this field takes N grid cells". Without the
40
+ attr, the field spans the full row (`grid-column: 1 / -1`) so
41
+ authors can mix narrow + full-width fields without per-row
42
+ wrappers. The attribute is exposed up to 12 to cover larger
43
+ [columns] grids; 13+ also collapses to full-row. */
44
+ :scope > field-ui {
45
+ grid-column: 1 / -1;
46
+ min-width: 0; /* allow children to shrink within the cell */
47
+ }
48
+ :scope > field-ui[rows="1"] { grid-column: span 1; }
49
+ :scope > field-ui[rows="2"] { grid-column: span 2; }
50
+ :scope > field-ui[rows="3"] { grid-column: span 3; }
51
+ :scope > field-ui[rows="4"] { grid-column: span 4; }
52
+ :scope > field-ui[rows="5"] { grid-column: span 5; }
53
+ :scope > field-ui[rows="6"] { grid-column: span 6; }
54
+ :scope > field-ui[rows="7"] { grid-column: span 7; }
55
+ :scope > field-ui[rows="8"] { grid-column: span 8; }
56
+ :scope > field-ui[rows="9"] { grid-column: span 9; }
57
+ :scope > field-ui[rows="10"] { grid-column: span 10; }
58
+ :scope > field-ui[rows="11"] { grid-column: span 11; }
59
+ :scope > field-ui[rows="12"] { grid-column: span 12; }
60
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * fields-ui — Container for a group of <field-ui> children laid out
3
+ * on a shared grid.
4
+ *
5
+ * <fields-ui> <!-- 6-col grid -->
6
+ * <field-ui label="Title">…</field-ui> <!-- rows=6 (full row) -->
7
+ * <field-ui label="Status" rows="2">…</field-ui>
8
+ * <field-ui label="Priority" rows="2">…</field-ui>
9
+ * <field-ui label="Group" rows="2">…</field-ui>
10
+ * </fields-ui>
11
+ *
12
+ * <fields-ui inline> <!-- propagate inline to every child -->
13
+ * <field-ui label="Search">…</field-ui>
14
+ * <field-ui label="Kind">…</field-ui>
15
+ * </fields-ui>
16
+ *
17
+ * The shared grid keeps siblings on the same row aligned. The [inline]
18
+ * attribute is mirrored onto every direct <field-ui> child so a whole
19
+ * sub-form switches layout mode from one host attribute. A small
20
+ * MutationObserver re-syncs when children are added/removed.
21
+ *
22
+ * Per-field column span is driven by the child's [rows="1..6"]
23
+ * attribute — pure CSS via attribute selectors in fields.css. The
24
+ * default span (no attr) is the full row.
25
+ */
26
+ import { UIElement } from '../../core/element.js';
27
+
28
+ class UIFields extends UIElement {
29
+ static properties = {
30
+ inline: { type: Boolean, default: false, reflect: true },
31
+ columns: { type: Number, default: 6, reflect: true },
32
+ };
33
+
34
+ static template = () => null;
35
+
36
+ /** @type {MutationObserver | null} */
37
+ #mo = null;
38
+
39
+ connected() {
40
+ this.#syncInline();
41
+ this.#mo = new MutationObserver((records) => {
42
+ // Only re-sync on childList changes that involve direct <field-ui>
43
+ // children — attribute mutations on grandchildren aren't ours to
44
+ // care about.
45
+ for (const r of records) {
46
+ if (r.type !== 'childList') continue;
47
+ if (this.#hasFieldChild(r.addedNodes) || this.#hasFieldChild(r.removedNodes)) {
48
+ this.#syncInline();
49
+ return;
50
+ }
51
+ }
52
+ });
53
+ this.#mo.observe(this, { childList: true });
54
+ }
55
+
56
+ disconnected() {
57
+ this.#mo?.disconnect();
58
+ this.#mo = null;
59
+ }
60
+
61
+ render() {
62
+ // UIElement calls render() on attribute → property reflection. The
63
+ // inline attr's effect is propagation-to-children, not host markup.
64
+ this.#syncInline();
65
+ }
66
+
67
+ // ── Private ────────────────────────────────────────────────────
68
+
69
+ #syncInline() {
70
+ const inline = this.hasAttribute('inline');
71
+ for (const child of this.children) {
72
+ if (child.localName !== 'field-ui') continue;
73
+ if (inline) {
74
+ if (!child.hasAttribute('inline')) child.setAttribute('inline', '');
75
+ } else {
76
+ // Only remove if WE set it. We can't tell, so we always remove —
77
+ // that's the documented contract: <fields-ui inline> is the
78
+ // single source of truth for child inline-mode within the group.
79
+ if (child.hasAttribute('inline')) child.removeAttribute('inline');
80
+ }
81
+ }
82
+ }
83
+
84
+ /** @param {NodeList} nodes */
85
+ #hasFieldChild(nodes) {
86
+ for (const n of nodes) {
87
+ if (n.nodeType === 1 && /** @type {Element} */ (n).localName === 'field-ui') {
88
+ return true;
89
+ }
90
+ }
91
+ return false;
92
+ }
93
+ }
94
+
95
+ customElements.define('fields-ui', UIFields);
96
+ export { UIFields };
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import '../../core/element.js';
3
+ import './fields.js';
4
+ import '../field/field.js';
5
+
6
+ const tick = () => new Promise((r) => queueMicrotask(r));
7
+
8
+ function mount(html) {
9
+ const wrap = document.createElement('div');
10
+ wrap.innerHTML = html;
11
+ document.body.appendChild(wrap);
12
+ return wrap.firstElementChild;
13
+ }
14
+
15
+ describe('fields-ui', () => {
16
+ beforeEach(() => { document.body.innerHTML = ''; });
17
+
18
+ it('registers the custom element', () => {
19
+ expect(customElements.get('fields-ui')).toBeDefined();
20
+ });
21
+
22
+ it('hosts <field-ui> children without altering them by default', () => {
23
+ const f = mount(`
24
+ <fields-ui>
25
+ <field-ui label="A"><input id="a" /></field-ui>
26
+ <field-ui label="B"><input id="b" /></field-ui>
27
+ </fields-ui>
28
+ `);
29
+ const fields = f.querySelectorAll('field-ui');
30
+ expect(fields).toHaveLength(2);
31
+ // Default — neither child has [inline] propagated.
32
+ expect(fields[0].hasAttribute('inline')).toBe(false);
33
+ expect(fields[1].hasAttribute('inline')).toBe(false);
34
+ });
35
+
36
+ it('propagates [inline] to every direct <field-ui> child on connect', async () => {
37
+ const f = mount(`
38
+ <fields-ui inline>
39
+ <field-ui label="A"><input id="a" /></field-ui>
40
+ <field-ui label="B"><input id="b" /></field-ui>
41
+ </fields-ui>
42
+ `);
43
+ await tick();
44
+ const fields = f.querySelectorAll('field-ui');
45
+ for (const child of fields) {
46
+ expect(child.hasAttribute('inline')).toBe(true);
47
+ }
48
+ });
49
+
50
+ it('removes inline from children when [inline] is removed from host', async () => {
51
+ const f = mount(`
52
+ <fields-ui inline>
53
+ <field-ui label="A"><input id="a" /></field-ui>
54
+ <field-ui label="B"><input id="b" /></field-ui>
55
+ </fields-ui>
56
+ `);
57
+ await tick();
58
+ f.removeAttribute('inline');
59
+ await tick();
60
+ const fields = f.querySelectorAll('field-ui');
61
+ for (const child of fields) {
62
+ expect(child.hasAttribute('inline')).toBe(false);
63
+ }
64
+ });
65
+
66
+ it('re-syncs inline when a new <field-ui> is appended later', async () => {
67
+ const f = mount(`<fields-ui inline></fields-ui>`);
68
+ await tick();
69
+ const child = document.createElement('field-ui');
70
+ child.setAttribute('label', 'Late');
71
+ f.appendChild(child);
72
+ // MutationObserver fires async — give it a microtask.
73
+ await new Promise((r) => setTimeout(r, 0));
74
+ expect(child.hasAttribute('inline')).toBe(true);
75
+ });
76
+
77
+ it('does not propagate [inline] to non-field children', async () => {
78
+ const f = mount(`
79
+ <fields-ui inline>
80
+ <field-ui label="A"><input id="a" /></field-ui>
81
+ <span class="separator">—</span>
82
+ </fields-ui>
83
+ `);
84
+ await tick();
85
+ const span = f.querySelector('.separator');
86
+ expect(span.hasAttribute('inline')).toBe(false);
87
+ });
88
+ });
@@ -0,0 +1,120 @@
1
+ # Edit this file; run `npm run build:components` to regenerate a2ui.json.
2
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
3
+ name: UIFields
4
+ tag: fields-ui
5
+ component: Fields
6
+ category: form
7
+ version: 1
8
+ description: >-
9
+ Container for a group of <field-ui> children, laid out on a shared
10
+ 6-column grid. Each <field-ui> spans the full row by default; opt
11
+ into a narrower span via [rows="1..6"]. Setting [inline] on the host
12
+ propagates the inline mode to every direct <field-ui> child so a
13
+ whole sub-form can switch label-position without per-field edits.
14
+ The grid alignment lets siblings on the same row line up cleanly —
15
+ consistent label columns + consistent control columns — without
16
+ the wrap-flex jitter of <row-ui wrap>.
17
+ props:
18
+ inline:
19
+ description: >-
20
+ Propagate the inline layout mode to every direct <field-ui>
21
+ child. Equivalent to authoring `inline` on each child but
22
+ drives the whole group from one place. Toggle is reactive —
23
+ flipping the attribute on the host re-syncs all children.
24
+ type: boolean
25
+ default: false
26
+ reflect: true
27
+ columns:
28
+ description: >-
29
+ Number of grid columns the row uses. Defaults to 6 — common
30
+ multiples (1/2/3/6) divide cleanly. <field-ui rows="N"> spans
31
+ `N` of these columns. Override per-instance for tighter
32
+ 4-column or wider 12-column compositions.
33
+ type: number
34
+ default: 6
35
+ reflect: true
36
+ slots:
37
+ default:
38
+ description: >-
39
+ <field-ui> children. Non-field children (separators, headings)
40
+ are placed in the grid too — span them by setting `style="grid-column:
41
+ 1 / -1"` for full-width content. Nested <fields-ui> works (an
42
+ outer 6-col grid hosting inner 3-col grids inside a single cell).
43
+ states:
44
+ - name: idle
45
+ description: Default, ready for interaction.
46
+ traits: []
47
+ tokens:
48
+ --fields-gap:
49
+ description: Gap between adjacent fields (row + column).
50
+ --fields-row-gap:
51
+ description: Override the row-gap independently of column-gap.
52
+ --fields-column-gap:
53
+ description: Override the column-gap independently of row-gap.
54
+ a2ui:
55
+ rules: []
56
+ anti_patterns:
57
+ - description: >-
58
+ Don't wrap <field-ui> children in <row-ui wrap> when grid
59
+ alignment is desired. row-ui is flex-wrap, which lets each
60
+ field size to its content — labels and controls won't share
61
+ a column with siblings.
62
+ severity: high
63
+ - description: >-
64
+ Don't set `inline` on individual <field-ui> children when the
65
+ whole group should switch — set it on the parent <fields-ui>
66
+ and let the propagation handle every child.
67
+ severity: medium
68
+ examples:
69
+ - name: three-up-stacked
70
+ description: >-
71
+ A 3-up stacked field row — Status / Priority / Group — each
72
+ taking 2 of 6 columns. Equivalent to a Bootstrap "col-md-4"
73
+ pattern (12-col grid, span 4) but with chat-ui's 6-col default.
74
+ a2ui: >-
75
+ [
76
+ {
77
+ "id": "root",
78
+ "component": "Fields",
79
+ "children": ["status", "priority", "group"]
80
+ },
81
+ { "id": "status", "component": "Field", "label": "Status", "rows": 2, "children": ["status-sel"] },
82
+ { "id": "priority", "component": "Field", "label": "Priority", "rows": 2, "children": ["priority-sel"] },
83
+ { "id": "group", "component": "Field", "label": "Group", "rows": 2, "children": ["group-sel"] },
84
+ { "id": "status-sel", "component": "Select", "value": "todo" },
85
+ { "id": "priority-sel", "component": "Select", "value": "0" },
86
+ { "id": "group-sel", "component": "Select", "value": "" }
87
+ ]
88
+ - name: inline-search-form
89
+ description: >-
90
+ An inline form — every field renders label-beside-control. Single
91
+ [inline] on the host drives all children.
92
+ a2ui: >-
93
+ [
94
+ {
95
+ "id": "root",
96
+ "component": "Fields",
97
+ "inline": true,
98
+ "children": ["q", "kind"]
99
+ },
100
+ { "id": "q", "component": "Field", "label": "Search", "children": ["q-input"] },
101
+ { "id": "kind", "component": "Field", "label": "Kind", "children": ["kind-sel"] },
102
+ { "id": "q-input", "component": "Input", "type": "search" },
103
+ { "id": "kind-sel", "component": "Select", "value": "all" }
104
+ ]
105
+ keywords:
106
+ - fields
107
+ - form
108
+ - grid
109
+ - layout
110
+ - group
111
+ synonyms:
112
+ form:
113
+ - fields
114
+ - group
115
+ - layout
116
+ related:
117
+ - field
118
+ - input
119
+ - select
120
+ - textarea
@@ -56,6 +56,7 @@ export { UITag } from './tag/tag.js';
56
56
  export { UISwatch } from './swatch/swatch.js';
57
57
  export { UICol } from './col/col.js';
58
58
  export { UIField } from './field/field.js';
59
+ export { UIFields } from './fields/fields.js';
59
60
  export { UIRow } from './row/row.js';
60
61
  export { UIGrid } from './grid/grid.js';
61
62
  export { UIStack } from './stack/stack.js';
@@ -85,6 +86,7 @@ export { UIEmbed } from './embed/embed.js';
85
86
  export { UIBlock } from './block/block.js';
86
87
  export { UIText } from './text/text.js';
87
88
  export { UIToggleGroup, UIToggleOption } from './toggle-group/toggle-group.js';
89
+ export { UIDemoToggle } from './demo-toggle/demo-toggle.js';
88
90
  export { UIRichText } from './richtext/richtext.js';
89
91
  export { UIStream } from './stream/stream.js';
90
92
  export { UICanvas } from './canvas/canvas.js';
@@ -120,10 +120,14 @@ class UIInput extends UIFormElement {
120
120
  render() {
121
121
  if (!this.#textEl) return;
122
122
 
123
+ const text = this.value || '';
124
+
123
125
  if (this.#isNativeInput) {
124
126
  this.#textEl.placeholder = this.placeholder;
125
127
  this.#textEl.disabled = this.disabled;
126
128
  this.#textEl.readOnly = this.readonly;
129
+ // Sync programmatic value writes (form.reset(), trait assignments).
130
+ if (this.#textEl.value !== text) this.#textEl.value = text;
127
131
  } else {
128
132
  this.#textEl.setAttribute('data-placeholder', this.placeholder);
129
133
  if (this.disabled || this.readonly) {
@@ -131,6 +135,13 @@ class UIInput extends UIFormElement {
131
135
  } else {
132
136
  this.#textEl.contentEditable = 'plaintext-only';
133
137
  }
138
+ // Sync programmatic value writes into the contenteditable surface.
139
+ // Skip when already in sync to avoid clobbering an in-flight edit's
140
+ // caret position.
141
+ if (this.#textEl.textContent !== text) {
142
+ this.#textEl.textContent = text;
143
+ this.#textEl.toggleAttribute('data-empty', !text);
144
+ }
134
145
  }
135
146
 
136
147
  if (this.#labelEl) this.#labelEl.textContent = this.label || '';
@@ -121,6 +121,9 @@
121
121
  font-size: var(--list-item-font-size);
122
122
  color: var(--list-item-text-color);
123
123
  line-height: 1.4;
124
+ /* Defensive floor: empty list-item-ui (no text/icon/children) collapses
125
+ to 0 height and disappears. Keep a visible row instead. */
126
+ min-height: var(--a-space-6);
124
127
  }
125
128
 
126
129
  :scope > [slot="icon"] {
@@ -150,4 +153,22 @@
150
153
  :scope > [slot="content"] {
151
154
  grid-column: 1 / -1;
152
155
  }
156
+
157
+ /* ── Active state ──
158
+ `[data-active]` is the consumer-driven "current item" hook used by
159
+ keyboard-nav-style demos and any host that owns its own active-index
160
+ model (separate from list-ui[selectable] / aria-selected). Reuses
161
+ the accent-muted/accent-strong pair so the two mechanisms paint
162
+ consistently. No border-radius on the row itself — dividers (when
163
+ present) span full-width and rounded corners would clash. */
164
+ :scope[data-active] {
165
+ background: var(--a-accent-muted);
166
+ color: var(--a-accent-strong);
167
+ box-shadow: inset 2px 0 0 var(--a-accent-strong);
168
+ }
169
+
170
+ :scope[data-active] > [slot="icon"],
171
+ :scope[data-active] > [slot="description"] {
172
+ color: var(--a-accent-strong);
173
+ }
153
174
  }
@@ -55,6 +55,16 @@ class UITextarea extends UIFormElement {
55
55
  this.#textEl.contentEditable = 'plaintext-only';
56
56
  }
57
57
 
58
+ // Sync programmatic `value` assignments (form.reset(), trait writes,
59
+ // controlled-input patterns) into the contenteditable surface. Skip
60
+ // when the surface already matches to avoid clobbering an in-flight
61
+ // edit's caret.
62
+ const text = this.value || '';
63
+ if (this.#textEl.textContent !== text) {
64
+ this.#textEl.textContent = text;
65
+ this.#textEl.toggleAttribute('data-empty', !text);
66
+ }
67
+
58
68
  const label = this.querySelector('[slot="label"]');
59
69
  if (label && this.label) label.setAttribute('label', this.label);
60
70
 
package/core/icons.js CHANGED
@@ -212,7 +212,18 @@ const ICON_ALIASES = {
212
212
  'phone': 'phone', 'location': 'map-pin', 'calendar': 'calendar-blank',
213
213
  'time': 'clock', 'user': 'user', 'users': 'users-three',
214
214
  'menu': 'list', 'more': 'dots-three', 'dots-vertical': 'dots-three-vertical', 'filter': 'funnel',
215
- 'inbox': 'tray', 'text-bold': 'text-b',
215
+ 'inbox': 'tray',
216
+ 'text-bold': 'text-b', 'bold': 'text-b', 'italic': 'text-italic', 'underline': 'text-underline',
217
+ 'reset': 'arrow-counter-clockwise', 'undo': 'arrow-counter-clockwise', 'redo': 'arrow-clockwise',
218
+ 'success-checkmark': 'seal-check', 'circuit': 'circuitry',
219
+ 'contrast': 'circle-half', 'theme': 'circle-half', 'dark-mode': 'moon', 'light-mode': 'sun',
220
+ 'alert-triangle': 'warning', 'alert-octagon': 'warning-octagon', 'alert-diamond': 'warning-diamond',
221
+ 'number': 'hash', 'numbered-list': 'list-numbers', 'numeric': 'hash',
222
+ // Phosphor uses suffix-circle for these, but consumers reach for the
223
+ // prefix form ("circle-warning") just as often. Resolve both shapes.
224
+ 'circle-warning': 'warning-circle', 'circle-x': 'x-circle',
225
+ 'circle-check': 'check-circle', 'circle-info': 'info',
226
+ 'circle-question': 'question', 'circle-plus': 'plus-circle', 'circle-minus': 'minus-circle',
216
227
  'sort': 'sort-ascending', 'refresh': 'arrow-clockwise', 'download': 'download-simple',
217
228
  'upload': 'upload-simple', 'share': 'share-network', 'copy': 'copy-simple',
218
229
  'link': 'link-simple', 'external-link': 'arrow-square-out',
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@adia-ai/web-components",
3
- "version": "0.2.3",
4
- "description": "AdiaUI web components vanilla custom elements. A2UI runtime (renderer, registry, streams, wiring) lives in @adia-ai/a2ui-utils.",
3
+ "version": "0.2.5",
4
+ "description": "AdiaUI web components \u2014 vanilla custom elements. A2UI runtime (renderer, registry, streams, wiring) lives in @adia-ai/a2ui-utils.",
5
5
  "type": "module",
6
6
  "exports": {
7
- ".": "./index.js",
8
- "./css": "./index.css",
9
- "./core": "./core/index.js",
10
- "./core/*": "./core/*.js",
11
- "./components": "./components/index.js",
7
+ ".": "./index.js",
8
+ "./css": "./index.css",
9
+ "./core": "./core/index.js",
10
+ "./core/*": "./core/*.js",
11
+ "./components": "./components/index.js",
12
12
  "./components/*": "./components/*/*.js",
13
- "./styles/*": "./styles/*",
14
- "./traits": "./traits/index.js",
15
- "./traits/*": "./traits/*.js",
13
+ "./styles/*": "./styles/*",
14
+ "./traits": "./traits/index.js",
15
+ "./traits/*": "./traits/*.js",
16
16
  "./package.json": "./package.json"
17
17
  },
18
18
  "files": [
@@ -54,6 +54,7 @@
54
54
  @import "../components/swatch/swatch.css";
55
55
  @import "../components/col/col.css";
56
56
  @import "../components/field/field.css";
57
+ @import "../components/fields/fields.css";
57
58
  @import "../components/row/row.css";
58
59
  @import "../components/grid/grid.css";
59
60
  @import "../components/stack/stack.css";
@@ -83,6 +84,7 @@
83
84
  @import "../components/block/block.css";
84
85
  @import "../components/text/text.css";
85
86
  @import "../components/toggle-group/toggle-group.css";
87
+ @import "../components/demo-toggle/demo-toggle.css";
86
88
  @import "../components/richtext/richtext.css";
87
89
  @import "../components/stream/stream.css";
88
90
  @import "../components/canvas/canvas.css";
@@ -256,7 +256,7 @@
256
256
 
257
257
  /* section — h2/h3, major division within content */
258
258
  --a-section-family: var(--a-font-family-heading);
259
- --a-section-weight: var(--a-weight-semibold);
259
+ --a-section-weight: var(--a-weight-normal);
260
260
  --a-section-sm: 16px;
261
261
  --a-section-md: 17px;
262
262
  --a-section-lg: 19px;