@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.
- package/components/button/button.js +3 -0
- package/components/demo-toggle/demo-toggle.a2ui.json +144 -0
- package/components/demo-toggle/demo-toggle.css +120 -0
- package/components/demo-toggle/demo-toggle.js +144 -0
- package/components/demo-toggle/demo-toggle.test.js +102 -0
- package/components/demo-toggle/demo-toggle.yaml +144 -0
- package/components/fields/fields.a2ui.json +106 -0
- package/components/fields/fields.css +60 -0
- package/components/fields/fields.js +96 -0
- package/components/fields/fields.test.js +88 -0
- package/components/fields/fields.yaml +120 -0
- package/components/index.js +2 -0
- package/components/input/input.js +11 -0
- package/components/list/list.css +21 -0
- package/components/textarea/textarea.js +10 -0
- package/core/icons.js +12 -1
- package/package.json +10 -10
- package/styles/components.css +2 -0
- package/styles/typography.css +1 -1
- package/traits/_catalog.json +259 -4
- package/traits/active-state.test.js +1 -1
- package/traits/anchor-positioning.js +205 -52
- package/traits/anchor-positioning.test.js +77 -4
- package/traits/announcer-stage.js +157 -0
- package/traits/announcer.js +145 -0
- package/traits/announcer.test.js +268 -0
- package/traits/arrow-grid-nav.js +234 -0
- package/traits/arrow-grid-nav.test.js +375 -0
- package/traits/attention-pulse.js +1 -1
- package/traits/attention-pulse.test.js +1 -1
- package/traits/confetti-burst.js +67 -63
- package/traits/confetti-burst.test.js +16 -8
- package/traits/confetti-stage.js +143 -0
- package/traits/confetti.js +44 -47
- package/traits/confetti.test.js +24 -5
- package/traits/count-up.js +31 -6
- package/traits/count-up.test.js +1 -1
- package/traits/declarative.test.js +1 -1
- package/traits/dirty-state.test.js +1 -1
- package/traits/drag-ghost.js +43 -3
- package/traits/drag-ghost.test.js +1 -1
- package/traits/draggable-list-item.js +458 -0
- package/traits/draggable-list-item.test.js +51 -0
- package/traits/draggable.js +14 -4
- package/traits/draggable.test.js +1 -1
- package/traits/drop-target.js +223 -0
- package/traits/drop-target.test.js +241 -0
- package/traits/droppable-collection.js +89 -0
- package/traits/droppable-collection.test.js +99 -0
- package/traits/droppable.js +136 -0
- package/traits/droppable.test.js +54 -0
- package/traits/error-shake.js +157 -0
- package/traits/error-shake.test.js +114 -0
- package/traits/fade-presence.test.js +1 -1
- package/traits/focus-restore.js +135 -0
- package/traits/focus-restore.test.js +202 -0
- package/traits/focus-trap.test.js +1 -1
- package/traits/focusable.test.js +1 -1
- package/traits/glow-focus.js +1 -1
- package/traits/glow-focus.test.js +1 -1
- package/traits/gradient-shift.js +1 -1
- package/traits/gradient-shift.test.js +1 -1
- package/traits/haptic-feedback.test.js +1 -1
- package/traits/hotkey.test.js +1 -1
- package/traits/hoverable.test.js +1 -1
- package/traits/index.js +15 -0
- package/traits/inertia-drag.js +9 -0
- package/traits/inertia-drag.test.js +1 -1
- package/traits/input-mask.js +328 -0
- package/traits/input-mask.test.js +151 -0
- package/traits/intersection-observer.test.js +1 -1
- package/traits/keyboard-nav.test.js +1 -1
- package/traits/keyboard-reorderable.js +254 -0
- package/traits/keyboard-reorderable.test.js +45 -0
- package/traits/layout-animation.js +229 -0
- package/traits/layout-animation.test.js +114 -0
- package/traits/long-press.js +212 -0
- package/traits/long-press.test.js +244 -0
- package/traits/magnetic-hover.js +1 -1
- package/traits/magnetic-hover.test.js +1 -1
- package/traits/noise-texture.js +7 -3
- package/traits/noise-texture.test.js +1 -1
- package/traits/parallax.js +1 -1
- package/traits/parallax.test.js +1 -1
- package/traits/portal.test.js +1 -1
- package/traits/pressable.test.js +1 -1
- package/traits/resettable.js +29 -3
- package/traits/resettable.test.js +34 -1
- package/traits/resizable.test.js +1 -1
- package/traits/resize-observer.test.js +1 -1
- package/traits/ripple.js +1 -1
- package/traits/ripple.test.js +1 -1
- package/traits/roving-tabindex.test.js +1 -1
- package/traits/scale-press.test.js +1 -1
- package/traits/scroll-lock.test.js +1 -1
- package/traits/scroll-progress.js +201 -0
- package/traits/scroll-progress.test.js +182 -0
- package/traits/shimmer-loading.js +1 -1
- package/traits/shimmer-loading.test.js +1 -1
- package/traits/{_smoke.test.js → smoke.test.js} +1 -1
- package/traits/snap-to-grid.test.js +1 -1
- package/traits/sound-feedback.test.js +1 -1
- package/traits/spring-animate.test.js +1 -1
- package/traits/success-checkmark.js +222 -0
- package/traits/success-checkmark.test.js +120 -0
- package/traits/tilt-hover.js +1 -1
- package/traits/tilt-hover.test.js +1 -1
- package/traits/tossable.js +9 -0
- package/traits/tossable.test.js +1 -1
- package/traits/traits-host.test.js +1 -1
- package/traits/typeahead.test.js +1 -1
- package/traits/typewriter.js +1 -1
- package/traits/typewriter.test.js +1 -1
- package/traits/validation.test.js +1 -1
- package/traits/view-transition.js +140 -0
- package/traits/view-transition.test.js +268 -0
- /package/traits/{_motion.js → motion.js} +0 -0
- /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
|
package/components/index.js
CHANGED
|
@@ -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 || '';
|
package/components/list/list.css
CHANGED
|
@@ -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',
|
|
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.
|
|
4
|
-
"description": "AdiaUI web components
|
|
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
|
-
".":
|
|
8
|
-
"./css":
|
|
9
|
-
"./core":
|
|
10
|
-
"./core/*":
|
|
11
|
-
"./components":
|
|
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/*":
|
|
14
|
-
"./traits":
|
|
15
|
-
"./traits/*":
|
|
13
|
+
"./styles/*": "./styles/*",
|
|
14
|
+
"./traits": "./traits/index.js",
|
|
15
|
+
"./traits/*": "./traits/*.js",
|
|
16
16
|
"./package.json": "./package.json"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
package/styles/components.css
CHANGED
|
@@ -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";
|
package/styles/typography.css
CHANGED
|
@@ -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-
|
|
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;
|