@adia-ai/web-modules 0.8.18 → 0.8.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/everything.min.js +21 -21
- package/form/form-popover/form-popover.a2ui.json +75 -0
- package/form/form-popover/form-popover.css +48 -0
- package/form/form-popover/form-popover.js +140 -0
- package/form/form-popover/form-popover.yaml +123 -0
- package/form/index.js +1 -0
- package/package.json +20 -1
- package/chat/chat-composer/chat-composer.test.js +0 -112
- package/chat/chat-sidebar/chat-sidebar.test.js +0 -110
- package/chat/chat-thread/chat-thread.test.js +0 -82
- package/editor/editor-canvas/editor-canvas.test.js +0 -100
- package/editor/editor-sidebar/editor-sidebar.test.js +0 -383
- package/editor/editor-toolbar/editor-toolbar.test.js +0 -99
- package/shell/admin-command/admin-command.test.js +0 -115
- package/shell/admin-shell/admin-shell.test.js +0 -185
- package/shell/admin-sidebar/admin-sidebar.test.js +0 -173
- package/shell/embed-shell/embed-shell.test.js +0 -127
- package/simple/simple-shell/simple-shell.test.js +0 -83
- package/theme/theme-panel/theme-panel.test.js +0 -431
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://adiaui.dev/a2ui/v0_9/components/FormPopover.json",
|
|
4
|
+
"title": "FormPopover",
|
|
5
|
+
"description": "Module-tier \"form fragment in a popover\" (operator mock, 2026-07-27): a\nselect-style summary trigger that opens an anchored panel holding ANY\nform primitives — check-ui groups, radio-ui groups, input-ui,\ndivider-ui, field-ui — slotted as ordinary light-DOM children. The\nmodule owns the trigger + popover packaging and a live selection\nsummary; the slotted controls keep their own name/value/event\ncontracts untouched.\n\nSummary contract: the trigger reads `{label} · N selected` while any\nslotted check-ui is checked, `{label} · N total` when none are (N =\ncheck-ui count), and just `{label}` when the fragment contains no\ncheck-ui at all. Radio groups, inputs, and other controls deliberately\ndo not count — checkboxes are the only \"N selected\" semantic that\nreads unambiguously in a summary.\n\nDecision rule vs adjacent surfaces (inherits popover-ui's): a pure\naction list is menu-ui; a single-value choice is select-ui (which owns\nits option list — do NOT rebuild select inside this module); an\nedge-anchored multi-field form is drawer-ui. form-popover-ui is for the\nin-between: a small anchored fragment mixing selection controls and\ninputs, e.g. a filter panel or a quick-save form.\n",
|
|
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
|
+
"component": {
|
|
17
|
+
"const": "FormPopover"
|
|
18
|
+
},
|
|
19
|
+
"heading": {
|
|
20
|
+
"description": "Optional panel heading rendered above the slotted fragment inside\nthe popover (\"Select options\"). Empty = no heading row.\n",
|
|
21
|
+
"type": "string",
|
|
22
|
+
"default": ""
|
|
23
|
+
},
|
|
24
|
+
"label": {
|
|
25
|
+
"description": "Summary-trigger prefix (\"Option items\"). The live count is\nappended after a middle dot; with no count the label renders\nalone. Required — with no label AND no check-ui children the\ntrigger would otherwise be an empty, nameless button; the runtime\nfalls back to \"Options\" in that state as a defensive accessible\nname, but authors must set a real label.\n",
|
|
26
|
+
"type": "string",
|
|
27
|
+
"default": ""
|
|
28
|
+
},
|
|
29
|
+
"placement": {
|
|
30
|
+
"description": "Passed through to the internal popover-ui. `bottom-start` default\nper ADR-0034 — the panel is trigger-anchored and roughly\ntrigger-width, like a select listbox.\n",
|
|
31
|
+
"type": "string",
|
|
32
|
+
"default": "bottom-start"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"required": [
|
|
36
|
+
"component",
|
|
37
|
+
"label"
|
|
38
|
+
],
|
|
39
|
+
"unevaluatedProperties": false,
|
|
40
|
+
"x-adiaui": {
|
|
41
|
+
"anti_patterns": [],
|
|
42
|
+
"category": "input",
|
|
43
|
+
"composes": [
|
|
44
|
+
"popover-ui",
|
|
45
|
+
"button-ui"
|
|
46
|
+
],
|
|
47
|
+
"events": {
|
|
48
|
+
"summary-change": {
|
|
49
|
+
"description": "Fired (bubbling) whenever the selection summary recomputes —\ndetail: { selected, total }. The slotted controls' own change /\ninput events bubble through untouched; listen to those for\nvalues.\n"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"examples": [
|
|
53
|
+
{
|
|
54
|
+
"description": "Filter fragment — checkbox group + divider + radio pair + name input behind a summary trigger.",
|
|
55
|
+
"a2ui": "[\n {\"id\": \"root\", \"component\": \"FormPopover\", \"label\": \"Option items\", \"heading\": \"Select options\", \"children\": [\"c1\", \"c2\", \"c3\", \"d1\", \"r1\", \"r2\", \"d2\", \"name\"]},\n {\"id\": \"c1\", \"component\": \"CheckBox\", \"label\": \"Option item\", \"checked\": true},\n {\"id\": \"c2\", \"component\": \"CheckBox\", \"label\": \"Option item\"},\n {\"id\": \"c3\", \"component\": \"CheckBox\", \"label\": \"Option item\", \"checked\": true},\n {\"id\": \"d1\", \"component\": \"Divider\"},\n {\"id\": \"r1\", \"component\": \"Radio\", \"name\": \"mode\", \"label\": \"Save\", \"checked\": true},\n {\"id\": \"r2\", \"component\": \"Radio\", \"name\": \"mode\", \"label\": \"Delete\"},\n {\"id\": \"d2\", \"component\": \"Divider\"},\n {\"id\": \"name\", \"component\": \"Input\", \"placeholder\": \"Option name\"}\n]",
|
|
56
|
+
"name": "filter-popover"
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"keywords": [],
|
|
60
|
+
"name": "FormPopover",
|
|
61
|
+
"related": [],
|
|
62
|
+
"slots": {
|
|
63
|
+
"default": {
|
|
64
|
+
"description": "The form fragment. Any AdiaUI form primitives, in author order —\ncheck-ui, radio-ui, input-ui, divider-ui, field-ui, textarea-ui.\nAdopted into the popover panel on connect; DOM order is layout\norder (light-DOM contract).\n"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"states": [],
|
|
68
|
+
"status": "experimental",
|
|
69
|
+
"synonyms": {},
|
|
70
|
+
"tag": "form-popover-ui",
|
|
71
|
+
"tokens": {},
|
|
72
|
+
"traits": [],
|
|
73
|
+
"version": 1
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
2
|
+
<form-popover-ui> — summary trigger + form-fragment panel.
|
|
3
|
+
Two-block @scope pattern per component-token-contract.md.
|
|
4
|
+
The panel surface itself (bg/border/shadow) belongs to popover-ui —
|
|
5
|
+
this module styles only its own trigger chrome and body layout.
|
|
6
|
+
═══════════════════════════════════════════════════════════════ */
|
|
7
|
+
|
|
8
|
+
/* ── 1. Token declarations ── */
|
|
9
|
+
|
|
10
|
+
form-popover-ui {
|
|
11
|
+
--form-popover-min-width: 16rem;
|
|
12
|
+
--form-popover-gap: var(--a-space-2);
|
|
13
|
+
--form-popover-trigger-bg: var(--a-ui-bg);
|
|
14
|
+
--form-popover-trigger-fg: var(--a-ui-text);
|
|
15
|
+
--form-popover-trigger-border: var(--a-ui-border);
|
|
16
|
+
--form-popover-heading-fg: var(--a-fg-strong);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* ── 2. Scoped layout ── */
|
|
20
|
+
|
|
21
|
+
@scope (form-popover-ui) {
|
|
22
|
+
:scope {
|
|
23
|
+
display: block;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* Field-look trigger: bordered, full-width, summary left / chevron
|
|
27
|
+
right — reads as a select trigger, not a button. Re-points
|
|
28
|
+
button-ui's own tokens (the sanctioned consumer override path). */
|
|
29
|
+
button-ui[data-form-popover-trigger] {
|
|
30
|
+
--button-bg: var(--form-popover-trigger-bg);
|
|
31
|
+
--button-fg: var(--form-popover-trigger-fg);
|
|
32
|
+
--button-border: var(--form-popover-trigger-border);
|
|
33
|
+
width: 100%;
|
|
34
|
+
justify-content: space-between;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
[data-form-popover-body] {
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
gap: var(--form-popover-gap);
|
|
41
|
+
min-width: var(--form-popover-min-width);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
[data-form-popover-heading] {
|
|
45
|
+
font-weight: var(--a-weight-medium);
|
|
46
|
+
color: var(--form-popover-heading-fg);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <form-popover-ui> — a form fragment behind a select-style summary trigger.
|
|
3
|
+
*
|
|
4
|
+
* Anatomy (all light-DOM, stamped once in connected()):
|
|
5
|
+
* <form-popover-ui label="Option items" heading="Select options">
|
|
6
|
+
* <check-ui label="…"></check-ui> … ← author's fragment
|
|
7
|
+
* </form-popover>
|
|
8
|
+
* becomes
|
|
9
|
+
* <form-popover-ui …>
|
|
10
|
+
* <popover-ui placement="…">
|
|
11
|
+
* <button-ui slot="trigger" data-form-popover-trigger
|
|
12
|
+
* icon-trailing="caret-down" text="Option items · 3 selected">
|
|
13
|
+
* <div slot="content" data-form-popover-body>
|
|
14
|
+
* [data-form-popover-heading]? ← from [heading]
|
|
15
|
+
* …the author's fragment, moved, order preserved…
|
|
16
|
+
* </div>
|
|
17
|
+
* </popover-ui>
|
|
18
|
+
* </form-popover>
|
|
19
|
+
*
|
|
20
|
+
* The module owns packaging + summary only. Slotted controls keep their
|
|
21
|
+
* own name/value/event contracts; their change/input events bubble
|
|
22
|
+
* through untouched (we listen to the same bubbling events to recompute
|
|
23
|
+
* the summary — no state is duplicated here, the DOM is the source of
|
|
24
|
+
* truth, same rule as chart-legend-ui's peer contract).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { UIElement } from '../../../web-components/core/element.js';
|
|
28
|
+
|
|
29
|
+
const SUMMARY_EVENTS = ['change', 'input'];
|
|
30
|
+
|
|
31
|
+
class FormPopover extends UIElement {
|
|
32
|
+
static properties = {
|
|
33
|
+
label: { type: String, default: '', reflect: true },
|
|
34
|
+
heading: { type: String, default: '', reflect: true },
|
|
35
|
+
placement: { type: String, default: 'bottom-start', reflect: true },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
static template = () => null;
|
|
39
|
+
|
|
40
|
+
#stamped = false;
|
|
41
|
+
#triggerEl = null;
|
|
42
|
+
#bodyEl = null;
|
|
43
|
+
// Stable handler ref so disconnected() can remove what connected()
|
|
44
|
+
// added (lifecycle symmetry — inline arrows can't be removed).
|
|
45
|
+
#onFragmentChange = () => this.#updateSummary();
|
|
46
|
+
|
|
47
|
+
connected() {
|
|
48
|
+
if (!this.#stamped) {
|
|
49
|
+
this.#stamp();
|
|
50
|
+
this.#stamped = true;
|
|
51
|
+
}
|
|
52
|
+
for (const type of SUMMARY_EVENTS) this.addEventListener(type, this.#onFragmentChange);
|
|
53
|
+
this.#updateSummary();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
disconnected() {
|
|
57
|
+
for (const type of SUMMARY_EVENTS) this.removeEventListener(type, this.#onFragmentChange);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
updated(changed) {
|
|
61
|
+
if (!this.#stamped) return;
|
|
62
|
+
if (changed.has('label')) this.#updateSummary();
|
|
63
|
+
if (changed.has('heading')) this.#renderHeading();
|
|
64
|
+
if (changed.has('placement')) {
|
|
65
|
+
this.querySelector(':scope > popover-ui')?.setAttribute('placement', this.placement);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** { selected, total } over the slotted check-ui population. */
|
|
70
|
+
get summary() {
|
|
71
|
+
const boxes = this.#bodyEl ? this.#bodyEl.querySelectorAll('check-ui') : [];
|
|
72
|
+
let selected = 0;
|
|
73
|
+
for (const box of boxes) if (box.hasAttribute('checked') || box.checked) selected += 1;
|
|
74
|
+
return { selected, total: boxes.length };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#stamp() {
|
|
78
|
+
// Adopt the author's fragment BEFORE stamping our own skeleton so the
|
|
79
|
+
// move loop never eats the popover we create (gh#284 class of bug).
|
|
80
|
+
const fragment = document.createDocumentFragment();
|
|
81
|
+
while (this.firstChild) fragment.appendChild(this.firstChild);
|
|
82
|
+
|
|
83
|
+
const popover = document.createElement('popover-ui');
|
|
84
|
+
popover.setAttribute('placement', this.placement);
|
|
85
|
+
|
|
86
|
+
const trigger = document.createElement('button-ui');
|
|
87
|
+
trigger.setAttribute('slot', 'trigger');
|
|
88
|
+
trigger.setAttribute('data-form-popover-trigger', '');
|
|
89
|
+
trigger.setAttribute('icon-trailing', 'caret-down');
|
|
90
|
+
this.#triggerEl = trigger;
|
|
91
|
+
popover.appendChild(trigger);
|
|
92
|
+
|
|
93
|
+
const body = document.createElement('div');
|
|
94
|
+
body.setAttribute('slot', 'content');
|
|
95
|
+
body.setAttribute('data-form-popover-body', '');
|
|
96
|
+
body.appendChild(fragment);
|
|
97
|
+
this.#bodyEl = body;
|
|
98
|
+
popover.appendChild(body);
|
|
99
|
+
|
|
100
|
+
this.appendChild(popover);
|
|
101
|
+
this.#renderHeading();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#renderHeading() {
|
|
105
|
+
if (!this.#bodyEl) return;
|
|
106
|
+
let el = this.#bodyEl.querySelector(':scope > [data-form-popover-heading]');
|
|
107
|
+
if (!this.heading) { el?.remove(); return; }
|
|
108
|
+
if (!el) {
|
|
109
|
+
el = document.createElement('div');
|
|
110
|
+
el.setAttribute('data-form-popover-heading', '');
|
|
111
|
+
this.#bodyEl.prepend(el);
|
|
112
|
+
}
|
|
113
|
+
el.textContent = this.heading;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#updateSummary() {
|
|
117
|
+
if (!this.#triggerEl) return;
|
|
118
|
+
const { selected, total } = this.summary;
|
|
119
|
+
let text = this.label;
|
|
120
|
+
if (total > 0) {
|
|
121
|
+
const count = selected > 0 ? `${selected} selected` : `${total} total`;
|
|
122
|
+
text = text ? `${text} · ${count}` : count;
|
|
123
|
+
}
|
|
124
|
+
// [label] is required by contract; this fallback only guards the
|
|
125
|
+
// no-label + no-checkbox misuse so the trigger never renders as an
|
|
126
|
+
// empty, nameless button (a11y — CodeRabbit finding on #441).
|
|
127
|
+
if (!text) text = 'Options';
|
|
128
|
+
this.#triggerEl.setAttribute('text', text);
|
|
129
|
+
this.dispatchEvent(new CustomEvent('summary-change', {
|
|
130
|
+
bubbles: true,
|
|
131
|
+
detail: { selected, total },
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!customElements.get('form-popover-ui')) {
|
|
137
|
+
customElements.define('form-popover-ui', FormPopover);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export { FormPopover };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Edit this file; run `npm run build:components` to regenerate a2ui.json.
|
|
2
|
+
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
3
|
+
name: FormPopover
|
|
4
|
+
tag: form-popover-ui
|
|
5
|
+
status: experimental
|
|
6
|
+
component: FormPopover
|
|
7
|
+
category: input
|
|
8
|
+
version: 1
|
|
9
|
+
description: |
|
|
10
|
+
Module-tier "form fragment in a popover" (operator mock, 2026-07-27): a
|
|
11
|
+
select-style summary trigger that opens an anchored panel holding ANY
|
|
12
|
+
form primitives — check-ui groups, radio-ui groups, input-ui,
|
|
13
|
+
divider-ui, field-ui — slotted as ordinary light-DOM children. The
|
|
14
|
+
module owns the trigger + popover packaging and a live selection
|
|
15
|
+
summary; the slotted controls keep their own name/value/event
|
|
16
|
+
contracts untouched.
|
|
17
|
+
|
|
18
|
+
Summary contract: the trigger reads `{label} · N selected` while any
|
|
19
|
+
slotted check-ui is checked, `{label} · N total` when none are (N =
|
|
20
|
+
check-ui count), and just `{label}` when the fragment contains no
|
|
21
|
+
check-ui at all. Radio groups, inputs, and other controls deliberately
|
|
22
|
+
do not count — checkboxes are the only "N selected" semantic that
|
|
23
|
+
reads unambiguously in a summary.
|
|
24
|
+
|
|
25
|
+
Decision rule vs adjacent surfaces (inherits popover-ui's): a pure
|
|
26
|
+
action list is menu-ui; a single-value choice is select-ui (which owns
|
|
27
|
+
its option list — do NOT rebuild select inside this module); an
|
|
28
|
+
edge-anchored multi-field form is drawer-ui. form-popover-ui is for the
|
|
29
|
+
in-between: a small anchored fragment mixing selection controls and
|
|
30
|
+
inputs, e.g. a filter panel or a quick-save form.
|
|
31
|
+
|
|
32
|
+
# Per ADR-0027 — primitives that programmatically create other primitives
|
|
33
|
+
# do NOT auto-import them. Consumer (or demo shell) must explicitly import.
|
|
34
|
+
composes:
|
|
35
|
+
- popover-ui
|
|
36
|
+
- button-ui
|
|
37
|
+
props:
|
|
38
|
+
label:
|
|
39
|
+
description: |
|
|
40
|
+
Summary-trigger prefix ("Option items"). The live count is
|
|
41
|
+
appended after a middle dot; with no count the label renders
|
|
42
|
+
alone. Required — with no label AND no check-ui children the
|
|
43
|
+
trigger would otherwise be an empty, nameless button; the runtime
|
|
44
|
+
falls back to "Options" in that state as a defensive accessible
|
|
45
|
+
name, but authors must set a real label.
|
|
46
|
+
type: string
|
|
47
|
+
required: true
|
|
48
|
+
default: ""
|
|
49
|
+
reflect: true
|
|
50
|
+
|
|
51
|
+
heading:
|
|
52
|
+
description: |
|
|
53
|
+
Optional panel heading rendered above the slotted fragment inside
|
|
54
|
+
the popover ("Select options"). Empty = no heading row.
|
|
55
|
+
type: string
|
|
56
|
+
default: ""
|
|
57
|
+
reflect: true
|
|
58
|
+
|
|
59
|
+
placement:
|
|
60
|
+
description: |
|
|
61
|
+
Passed through to the internal popover-ui. `bottom-start` default
|
|
62
|
+
per ADR-0034 — the panel is trigger-anchored and roughly
|
|
63
|
+
trigger-width, like a select listbox.
|
|
64
|
+
type: string
|
|
65
|
+
default: bottom-start
|
|
66
|
+
reflect: true
|
|
67
|
+
|
|
68
|
+
events:
|
|
69
|
+
summary-change:
|
|
70
|
+
description: |
|
|
71
|
+
Fired (bubbling) whenever the selection summary recomputes —
|
|
72
|
+
detail: { selected, total }. The slotted controls' own change /
|
|
73
|
+
input events bubble through untouched; listen to those for
|
|
74
|
+
values.
|
|
75
|
+
slots:
|
|
76
|
+
default:
|
|
77
|
+
description: |
|
|
78
|
+
The form fragment. Any AdiaUI form primitives, in author order —
|
|
79
|
+
check-ui, radio-ui, input-ui, divider-ui, field-ui, textarea-ui.
|
|
80
|
+
Adopted into the popover panel on connect; DOM order is layout
|
|
81
|
+
order (light-DOM contract).
|
|
82
|
+
icons:
|
|
83
|
+
- caret-down # summary-trigger chevron (button-ui[icon-trailing])
|
|
84
|
+
a2ui:
|
|
85
|
+
category: input
|
|
86
|
+
description: Summary-trigger popover holding an arbitrary form fragment (checkbox groups, radio groups, inputs).
|
|
87
|
+
rules:
|
|
88
|
+
- >-
|
|
89
|
+
Children are the form fragment, in author order: CheckBox, Radio,
|
|
90
|
+
Input, Textarea, Divider, Field. Do NOT put Option children here —
|
|
91
|
+
a single-value option list is Select's contract; FormPopover never
|
|
92
|
+
rebuilds it.
|
|
93
|
+
- >-
|
|
94
|
+
Decision rule vs siblings: single-value choice → Select (with
|
|
95
|
+
multiple:true for multi-select chips); pure action list → Menu;
|
|
96
|
+
edge-anchored multi-field form → Drawer. FormPopover is only for a
|
|
97
|
+
small MIXED fragment (checks + radios + an input) behind a summary
|
|
98
|
+
trigger.
|
|
99
|
+
- >-
|
|
100
|
+
The trigger summary counts CheckBox children only ("N selected" /
|
|
101
|
+
"N total"); Radio and Input children never count. Give every Radio
|
|
102
|
+
child one shared name per group or the browser treats them as
|
|
103
|
+
independent.
|
|
104
|
+
props:
|
|
105
|
+
label: { type: string, description: "Summary-trigger prefix text" }
|
|
106
|
+
heading: { type: string, description: "Optional panel heading" }
|
|
107
|
+
placement: { type: string, description: "Popover placement (default bottom-start)" }
|
|
108
|
+
children: form primitives (CheckBox, Radio, Input, Divider, Field)
|
|
109
|
+
examples:
|
|
110
|
+
- name: filter-popover
|
|
111
|
+
description: Filter fragment — checkbox group + divider + radio pair + name input behind a summary trigger.
|
|
112
|
+
a2ui: >-
|
|
113
|
+
[
|
|
114
|
+
{"id": "root", "component": "FormPopover", "label": "Option items", "heading": "Select options", "children": ["c1", "c2", "c3", "d1", "r1", "r2", "d2", "name"]},
|
|
115
|
+
{"id": "c1", "component": "CheckBox", "label": "Option item", "checked": true},
|
|
116
|
+
{"id": "c2", "component": "CheckBox", "label": "Option item"},
|
|
117
|
+
{"id": "c3", "component": "CheckBox", "label": "Option item", "checked": true},
|
|
118
|
+
{"id": "d1", "component": "Divider"},
|
|
119
|
+
{"id": "r1", "component": "Radio", "name": "mode", "label": "Save", "checked": true},
|
|
120
|
+
{"id": "r2", "component": "Radio", "name": "mode", "label": "Delete"},
|
|
121
|
+
{"id": "d2", "component": "Divider"},
|
|
122
|
+
{"id": "name", "component": "Input", "placeholder": "Option name"}
|
|
123
|
+
]
|
package/form/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FormPopover } from './form-popover/form-popover.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adia-ai/web-modules",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.20",
|
|
4
4
|
"description": "AdiaUI composite custom elements \u2014 shell, chat, editor, runtime clusters built from @adia-ai/web-components primitives. Subpath exports per cluster.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -203,6 +203,20 @@
|
|
|
203
203
|
"import": "./generative/index.js",
|
|
204
204
|
"default": "./generative/index.js"
|
|
205
205
|
},
|
|
206
|
+
"./form": {
|
|
207
|
+
"types": "./form/index.d.ts",
|
|
208
|
+
"import": "./form/index.js",
|
|
209
|
+
"default": "./form/index.js"
|
|
210
|
+
},
|
|
211
|
+
"./form/*": {
|
|
212
|
+
"types": "./form/*/*.d.ts",
|
|
213
|
+
"import": "./form/*/*.js",
|
|
214
|
+
"default": "./form/*/*.js"
|
|
215
|
+
},
|
|
216
|
+
"./form/*.css": {
|
|
217
|
+
"types": "./css-module.d.ts",
|
|
218
|
+
"default": "./form/*/*.css"
|
|
219
|
+
},
|
|
206
220
|
"./package.json": "./package.json"
|
|
207
221
|
},
|
|
208
222
|
"files": [
|
|
@@ -235,6 +249,11 @@
|
|
|
235
249
|
"!generative/**/*.examples.html",
|
|
236
250
|
"!generative/**/*.examples.js",
|
|
237
251
|
"!generative/**/*.html",
|
|
252
|
+
"form/",
|
|
253
|
+
"!form/**/*.examples.html",
|
|
254
|
+
"!form/**/*.examples.js",
|
|
255
|
+
"!form/**/*.html",
|
|
256
|
+
"!**/*.test.js",
|
|
238
257
|
"index.js",
|
|
239
258
|
"css-module.d.ts",
|
|
240
259
|
"web-modules.sheet.d.ts",
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
-
import '../../../web-components/core/element.js';
|
|
3
|
-
import './chat-composer.js';
|
|
4
|
-
|
|
5
|
-
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
6
|
-
|
|
7
|
-
function mount(html) {
|
|
8
|
-
const wrap = document.createElement('div');
|
|
9
|
-
wrap.innerHTML = html;
|
|
10
|
-
document.body.appendChild(wrap);
|
|
11
|
-
return wrap.firstElementChild;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
document.body.innerHTML = '';
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe('chat-composer', () => {
|
|
19
|
-
it('registers chat-composer as a custom element', () => {
|
|
20
|
-
expect(customElements.get('chat-composer')).toBeDefined();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('defaults to disabled=false', () => {
|
|
24
|
-
const c = mount('<chat-composer></chat-composer>');
|
|
25
|
-
expect(c.disabled).toBe(false);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('reflects [disabled] via property assignment', async () => {
|
|
29
|
-
const c = mount('<chat-composer></chat-composer>');
|
|
30
|
-
c.disabled = true;
|
|
31
|
-
await tick();
|
|
32
|
-
expect(c.hasAttribute('disabled')).toBe(true);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('exposes .focus() / .clear() / .input getter', () => {
|
|
36
|
-
const c = mount('<chat-composer></chat-composer>');
|
|
37
|
-
expect(typeof c.focus).toBe('function');
|
|
38
|
-
expect(typeof c.clear).toBe('function');
|
|
39
|
-
expect('input' in c).toBe(true);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('returns null .input when no inner input element', () => {
|
|
43
|
-
const c = mount('<chat-composer></chat-composer>');
|
|
44
|
-
expect(c.input).toBeNull();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('forwards inner submit as composer-submit event', async () => {
|
|
48
|
-
// Plain input-like element with submit dispatch
|
|
49
|
-
const wrap = document.createElement('div');
|
|
50
|
-
wrap.innerHTML = `<chat-composer>
|
|
51
|
-
<chat-input-ui></chat-input-ui>
|
|
52
|
-
</chat-composer>`;
|
|
53
|
-
document.body.appendChild(wrap);
|
|
54
|
-
const composer = wrap.firstElementChild;
|
|
55
|
-
const innerInput = composer.querySelector('chat-input-ui');
|
|
56
|
-
|
|
57
|
-
const onComposerSubmit = vi.fn();
|
|
58
|
-
composer.addEventListener('composer-submit', onComposerSubmit);
|
|
59
|
-
|
|
60
|
-
innerInput.dispatchEvent(new CustomEvent('submit', { detail: { text: 'hello' } }));
|
|
61
|
-
await tick();
|
|
62
|
-
|
|
63
|
-
expect(onComposerSubmit).toHaveBeenCalledTimes(1);
|
|
64
|
-
expect(onComposerSubmit.mock.calls[0][0].detail).toEqual({ text: 'hello' });
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('propagates [disabled] to inner input via attribute', async () => {
|
|
68
|
-
const wrap = document.createElement('div');
|
|
69
|
-
wrap.innerHTML = `<chat-composer>
|
|
70
|
-
<chat-input-ui></chat-input-ui>
|
|
71
|
-
</chat-composer>`;
|
|
72
|
-
document.body.appendChild(wrap);
|
|
73
|
-
const composer = wrap.firstElementChild;
|
|
74
|
-
const innerInput = composer.querySelector('chat-input-ui');
|
|
75
|
-
|
|
76
|
-
composer.disabled = true;
|
|
77
|
-
await tick();
|
|
78
|
-
|
|
79
|
-
expect(innerInput.hasAttribute('disabled')).toBe(true);
|
|
80
|
-
|
|
81
|
-
composer.disabled = false;
|
|
82
|
-
await tick();
|
|
83
|
-
|
|
84
|
-
expect(innerInput.hasAttribute('disabled')).toBe(false);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('cleanup on disconnect — removes inner submit listener', () => {
|
|
88
|
-
const wrap = document.createElement('div');
|
|
89
|
-
wrap.innerHTML = `<chat-composer>
|
|
90
|
-
<chat-input-ui></chat-input-ui>
|
|
91
|
-
</chat-composer>`;
|
|
92
|
-
document.body.appendChild(wrap);
|
|
93
|
-
const composer = wrap.firstElementChild;
|
|
94
|
-
const innerInput = composer.querySelector('chat-input-ui');
|
|
95
|
-
const removeSpy = vi.spyOn(innerInput, 'removeEventListener');
|
|
96
|
-
|
|
97
|
-
composer.remove();
|
|
98
|
-
|
|
99
|
-
const removedTypes = removeSpy.mock.calls.map((args) => args[0]);
|
|
100
|
-
expect(removedTypes).toContain('submit');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('handles input-ui as inner input fallback', () => {
|
|
104
|
-
const wrap = document.createElement('div');
|
|
105
|
-
wrap.innerHTML = `<chat-composer>
|
|
106
|
-
<input-ui></input-ui>
|
|
107
|
-
</chat-composer>`;
|
|
108
|
-
document.body.appendChild(wrap);
|
|
109
|
-
const composer = wrap.firstElementChild;
|
|
110
|
-
expect(composer.input?.tagName?.toLowerCase()).toBe('input-ui');
|
|
111
|
-
});
|
|
112
|
-
});
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import '../../../web-components/core/element.js';
|
|
3
|
-
import './chat-sidebar.js';
|
|
4
|
-
|
|
5
|
-
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
6
|
-
|
|
7
|
-
function mount(html) {
|
|
8
|
-
const wrap = document.createElement('div');
|
|
9
|
-
wrap.innerHTML = html;
|
|
10
|
-
document.body.appendChild(wrap);
|
|
11
|
-
return wrap.firstElementChild;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
let originalRect;
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
document.body.innerHTML = '';
|
|
17
|
-
try { localStorage.clear(); } catch {}
|
|
18
|
-
globalThis.ResizeObserver = class {
|
|
19
|
-
observe() {} unobserve() {} disconnect() {}
|
|
20
|
-
};
|
|
21
|
-
originalRect = HTMLElement.prototype.getBoundingClientRect;
|
|
22
|
-
HTMLElement.prototype.getBoundingClientRect = function () {
|
|
23
|
-
const inline = this.style?.width || '';
|
|
24
|
-
const w = parseFloat(inline) || 240;
|
|
25
|
-
return { width: w, height: 600, top: 0, left: 0, right: w, bottom: 600, x: 0, y: 0 };
|
|
26
|
-
};
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
afterEach(() => {
|
|
30
|
-
if (originalRect) HTMLElement.prototype.getBoundingClientRect = originalRect;
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
describe('chat-sidebar', () => {
|
|
34
|
-
it('registers chat-sidebar as a custom element', () => {
|
|
35
|
-
expect(customElements.get('chat-sidebar')).toBeDefined();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('defaults to collapsed=false on connect', () => {
|
|
39
|
-
const sb = mount('<chat-sidebar slot="leading"></chat-sidebar>');
|
|
40
|
-
expect(sb.collapsed).toBe(false);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('reflects [collapsed] via property assignment', async () => {
|
|
44
|
-
const sb = mount('<chat-sidebar slot="leading"></chat-sidebar>');
|
|
45
|
-
sb.collapsed = true;
|
|
46
|
-
await tick();
|
|
47
|
-
expect(sb.hasAttribute('collapsed')).toBe(true);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('reflects [resizing] via property assignment', async () => {
|
|
51
|
-
const sb = mount('<chat-sidebar slot="leading"></chat-sidebar>');
|
|
52
|
-
sb.resizing = true;
|
|
53
|
-
await tick();
|
|
54
|
-
expect(sb.hasAttribute('resizing')).toBe(true);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('exposes .toggle() / .collapse() / .expand() public methods', () => {
|
|
58
|
-
const sb = mount('<chat-sidebar slot="leading" collapsible></chat-sidebar>');
|
|
59
|
-
expect(typeof sb.toggle).toBe('function');
|
|
60
|
-
expect(typeof sb.collapse).toBe('function');
|
|
61
|
-
expect(typeof sb.expand).toBe('function');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('persists width to chat-namespaced localStorage key (not collide with admin)', async () => {
|
|
65
|
-
const sb = mount('<chat-sidebar slot="leading" collapsible></chat-sidebar>');
|
|
66
|
-
sb.style.width = '240px';
|
|
67
|
-
sb.collapse();
|
|
68
|
-
expect(sb.collapsed).toBe(true);
|
|
69
|
-
// localStorage key should be chat-namespaced
|
|
70
|
-
expect(localStorage.getItem('adia-chat-sidebar-leading')).not.toBeNull();
|
|
71
|
-
// Should NOT have written to the admin namespace
|
|
72
|
-
expect(localStorage.getItem('adia-sidebar-leading')).toBeNull();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('uses [name] override for the localStorage key', () => {
|
|
76
|
-
const sb = mount('<chat-sidebar slot="leading" name="conversations" collapsible></chat-sidebar>');
|
|
77
|
-
sb.style.width = '200px';
|
|
78
|
-
sb.collapse();
|
|
79
|
-
expect(localStorage.getItem('adia-chat-sidebar-conversations')).not.toBeNull();
|
|
80
|
-
expect(localStorage.getItem('adia-chat-sidebar-leading')).toBeNull();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('toggle() returns the new collapsed value', () => {
|
|
84
|
-
const sb = mount('<chat-sidebar slot="leading" collapsible></chat-sidebar>');
|
|
85
|
-
sb.style.width = '200px';
|
|
86
|
-
const result = sb.toggle();
|
|
87
|
-
expect(result).toBe(sb.collapsed);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('dispatches sidebar-toggle event on toggle()', () => {
|
|
91
|
-
const sb = mount('<chat-sidebar slot="leading" collapsible></chat-sidebar>');
|
|
92
|
-
sb.style.width = '200px';
|
|
93
|
-
const onToggle = vi.fn();
|
|
94
|
-
sb.addEventListener('sidebar-toggle', onToggle);
|
|
95
|
-
sb.toggle();
|
|
96
|
-
expect(onToggle).toHaveBeenCalledTimes(1);
|
|
97
|
-
expect(onToggle.mock.calls[0][0].detail).toEqual(
|
|
98
|
-
expect.objectContaining({ name: expect.any(String), expanded: expect.any(Boolean) })
|
|
99
|
-
);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('cleans up resize handlers on disconnect', () => {
|
|
103
|
-
const sb = mount('<chat-sidebar slot="leading" resizable><div data-resize></div></chat-sidebar>');
|
|
104
|
-
const handle = sb.querySelector('[data-resize]');
|
|
105
|
-
const removeSpy = vi.spyOn(handle, 'removeEventListener');
|
|
106
|
-
sb.remove();
|
|
107
|
-
const removedTypes = removeSpy.mock.calls.map((args) => args[0]);
|
|
108
|
-
expect(removedTypes).toContain('pointerdown');
|
|
109
|
-
});
|
|
110
|
-
});
|