@adia-ai/web-modules 0.8.20 → 0.8.22
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.
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://adiaui.dev/a2ui/v0_9/components/FormPopover.json",
|
|
4
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
|
|
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 (gh#474 — identical to select-ui[summary-label], so the\ntwo multi-select triggers speak one language): none checked reads just\n`{label}` (which doubles as the placeholder, since [label] is required\nhere), exactly one reads THAT check-ui's own label, and more than one\nreads `{label} (N)`. Radio groups, inputs, and other controls\ndeliberately do not count — checkboxes are the only selection semantic\nthat reads 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
6
|
"type": "object",
|
|
7
7
|
"allOf": [
|
|
8
8
|
{
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"default": ""
|
|
23
23
|
},
|
|
24
24
|
"label": {
|
|
25
|
-
"description": "Summary-trigger
|
|
25
|
+
"description": "Summary-trigger text (gh#474 — same contract as\nselect-ui[summary-label]). With nothing checked it renders alone,\ndoubling as the placeholder; with exactly one check-ui checked the\ntrigger shows that control's own label instead; beyond one it\nrenders as \"{label} (N)\". Required — with no label AND no check-ui\nchildren the trigger would otherwise be an empty, nameless button;\nthe runtime falls back to \"Options\" in that state as a defensive\naccessible name, but authors must set a real label.\n",
|
|
26
26
|
"type": "string",
|
|
27
27
|
"default": ""
|
|
28
28
|
},
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* <form-popover-ui …>
|
|
10
10
|
* <popover-ui placement="…">
|
|
11
11
|
* <button-ui slot="trigger" data-form-popover-trigger
|
|
12
|
-
* icon-trailing="caret-down" text="Option items
|
|
12
|
+
* icon-trailing="caret-down" text="Option items (3)">
|
|
13
13
|
* <div slot="content" data-form-popover-body>
|
|
14
14
|
* [data-form-popover-heading]? ← from [heading]
|
|
15
15
|
* …the author's fragment, moved, order preserved…
|
|
@@ -43,6 +43,14 @@ class FormPopover extends UIElement {
|
|
|
43
43
|
// Stable handler ref so disconnected() can remove what connected()
|
|
44
44
|
// added (lifecycle symmetry — inline arrows can't be removed).
|
|
45
45
|
#onFragmentChange = () => this.#updateSummary();
|
|
46
|
+
// Programmatic mutations fire no change/input event — `el.checked = true`
|
|
47
|
+
// reflects to the [checked] attribute (check-ui) but bubbles nothing, so
|
|
48
|
+
// an event-only summary goes stale, contradicting the module's own
|
|
49
|
+
// "DOM is the source of truth" rule (reviewer finding, 2026-07-29). The
|
|
50
|
+
// observer covers the non-event paths: reflected [checked] flips and
|
|
51
|
+
// check-ui population changes. Scoped to the BODY element so trigger
|
|
52
|
+
// attribute writes (our own summary text) can't feed back.
|
|
53
|
+
#summaryObserver = null;
|
|
46
54
|
|
|
47
55
|
connected() {
|
|
48
56
|
if (!this.#stamped) {
|
|
@@ -50,11 +58,22 @@ class FormPopover extends UIElement {
|
|
|
50
58
|
this.#stamped = true;
|
|
51
59
|
}
|
|
52
60
|
for (const type of SUMMARY_EVENTS) this.addEventListener(type, this.#onFragmentChange);
|
|
61
|
+
if (this.#bodyEl && !this.#summaryObserver) {
|
|
62
|
+
this.#summaryObserver = new MutationObserver(this.#onFragmentChange);
|
|
63
|
+
this.#summaryObserver.observe(this.#bodyEl, {
|
|
64
|
+
subtree: true,
|
|
65
|
+
childList: true,
|
|
66
|
+
attributes: true,
|
|
67
|
+
attributeFilter: ['checked'],
|
|
68
|
+
});
|
|
69
|
+
}
|
|
53
70
|
this.#updateSummary();
|
|
54
71
|
}
|
|
55
72
|
|
|
56
73
|
disconnected() {
|
|
57
74
|
for (const type of SUMMARY_EVENTS) this.removeEventListener(type, this.#onFragmentChange);
|
|
75
|
+
this.#summaryObserver?.disconnect();
|
|
76
|
+
this.#summaryObserver = null;
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
updated(changed) {
|
|
@@ -66,12 +85,16 @@ class FormPopover extends UIElement {
|
|
|
66
85
|
}
|
|
67
86
|
}
|
|
68
87
|
|
|
69
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* { selected, total, checked } over the slotted check-ui population.
|
|
90
|
+
* `checked` is the checked elements themselves — the summary needs the
|
|
91
|
+
* single-selection element to read its own label (gh#474).
|
|
92
|
+
*/
|
|
70
93
|
get summary() {
|
|
71
94
|
const boxes = this.#bodyEl ? this.#bodyEl.querySelectorAll('check-ui') : [];
|
|
72
|
-
|
|
73
|
-
for (const box of boxes) if (box.hasAttribute('checked') || box.checked)
|
|
74
|
-
return { selected, total: boxes.length };
|
|
95
|
+
const checked = [];
|
|
96
|
+
for (const box of boxes) if (box.hasAttribute('checked') || box.checked) checked.push(box);
|
|
97
|
+
return { selected: checked.length, total: boxes.length, checked };
|
|
75
98
|
}
|
|
76
99
|
|
|
77
100
|
#stamp() {
|
|
@@ -113,14 +136,23 @@ class FormPopover extends UIElement {
|
|
|
113
136
|
el.textContent = this.heading;
|
|
114
137
|
}
|
|
115
138
|
|
|
139
|
+
/** The own label of a slotted check-ui — [label] prop, else its text. */
|
|
140
|
+
#labelOf(box) {
|
|
141
|
+
return (box.getAttribute('label') || box.label || box.textContent || '').trim();
|
|
142
|
+
}
|
|
143
|
+
|
|
116
144
|
#updateSummary() {
|
|
117
145
|
if (!this.#triggerEl) return;
|
|
118
|
-
const { selected, total } = this.summary;
|
|
146
|
+
const { selected, total, checked } = this.summary;
|
|
147
|
+
// gh#474 — ONE summary contract across the two multi-select triggers:
|
|
148
|
+
// none → the label alone (it doubles as select-ui's placeholder, since
|
|
149
|
+
// [label] is required here), one → that control's own label, more →
|
|
150
|
+
// "Label (N)". Was "Label · N selected", which contradicted
|
|
151
|
+
// select-ui[summary-label]'s ruled format (gh#442) and blocked
|
|
152
|
+
// consumers from swapping a hand-rolled composition for this module.
|
|
119
153
|
let text = this.label;
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
text = text ? `${text} · ${count}` : count;
|
|
123
|
-
}
|
|
154
|
+
if (selected === 1 && checked[0]) text = this.#labelOf(checked[0]) || text;
|
|
155
|
+
else if (selected > 1) text = text ? `${text} (${selected})` : `${selected}`;
|
|
124
156
|
// [label] is required by contract; this fallback only guards the
|
|
125
157
|
// no-label + no-checkbox misuse so the trigger never renders as an
|
|
126
158
|
// empty, nameless button (a11y — CodeRabbit finding on #441).
|
|
@@ -15,12 +15,13 @@ description: |
|
|
|
15
15
|
summary; the slotted controls keep their own name/value/event
|
|
16
16
|
contracts untouched.
|
|
17
17
|
|
|
18
|
-
Summary contract
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
check-ui
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
Summary contract (gh#474 — identical to select-ui[summary-label], so the
|
|
19
|
+
two multi-select triggers speak one language): none checked reads just
|
|
20
|
+
`{label}` (which doubles as the placeholder, since [label] is required
|
|
21
|
+
here), exactly one reads THAT check-ui's own label, and more than one
|
|
22
|
+
reads `{label} (N)`. Radio groups, inputs, and other controls
|
|
23
|
+
deliberately do not count — checkboxes are the only selection semantic
|
|
24
|
+
that reads unambiguously in a summary.
|
|
24
25
|
|
|
25
26
|
Decision rule vs adjacent surfaces (inherits popover-ui's): a pure
|
|
26
27
|
action list is menu-ui; a single-value choice is select-ui (which owns
|
|
@@ -37,12 +38,14 @@ composes:
|
|
|
37
38
|
props:
|
|
38
39
|
label:
|
|
39
40
|
description: |
|
|
40
|
-
Summary-trigger
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
trigger
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
Summary-trigger text (gh#474 — same contract as
|
|
42
|
+
select-ui[summary-label]). With nothing checked it renders alone,
|
|
43
|
+
doubling as the placeholder; with exactly one check-ui checked the
|
|
44
|
+
trigger shows that control's own label instead; beyond one it
|
|
45
|
+
renders as "{label} (N)". Required — with no label AND no check-ui
|
|
46
|
+
children the trigger would otherwise be an empty, nameless button;
|
|
47
|
+
the runtime falls back to "Options" in that state as a defensive
|
|
48
|
+
accessible name, but authors must set a real label.
|
|
46
49
|
type: string
|
|
47
50
|
required: true
|
|
48
51
|
default: ""
|
|
@@ -97,12 +100,13 @@ a2ui:
|
|
|
97
100
|
small MIXED fragment (checks + radios + an input) behind a summary
|
|
98
101
|
trigger.
|
|
99
102
|
- >-
|
|
100
|
-
The trigger summary counts CheckBox children only
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
The trigger summary counts CheckBox children only — one checked
|
|
104
|
+
shows its own label, more show "Label (N)", none shows the bare
|
|
105
|
+
label (same contract as Select[summary-label]); Radio and Input
|
|
106
|
+
children never count. Give every Radio child one shared name per
|
|
107
|
+
group or the browser treats them as independent.
|
|
104
108
|
props:
|
|
105
|
-
label: { type: string, description: "Summary-trigger prefix
|
|
109
|
+
label: { type: string, description: "Summary-trigger text — shown alone when nothing is checked (its placeholder role) and as the \"Label (N)\" prefix when more than one is" }
|
|
106
110
|
heading: { type: string, description: "Optional panel heading" }
|
|
107
111
|
placement: { type: string, description: "Popover placement (default bottom-start)" }
|
|
108
112
|
children: form primitives (CheckBox, Radio, Input, Divider, Field)
|
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.22",
|
|
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": {
|