@agent-ui-kit/components 0.0.3
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/dist/base-styles.css +8 -0
- package/dist/component-styles.css +147 -0
- package/dist/controls/_base/index.d.ts +3 -0
- package/dist/controls/_base/index.js +9 -0
- package/dist/controls/_base/indicator-element.d.ts +132 -0
- package/dist/controls/_base/indicator-element.js +158 -0
- package/dist/controls/_base/listbox-element.d.ts +70 -0
- package/dist/controls/_base/listbox-element.js +74 -0
- package/dist/controls/_base/range-element.d.ts +120 -0
- package/dist/controls/_base/range-element.js +158 -0
- package/dist/controls/_surface/container-box.css +301 -0
- package/dist/controls/_surface/container.css +156 -0
- package/dist/controls/_token-surface/token-surface.d.ts +35 -0
- package/dist/controls/_token-surface/token-surface.js +97 -0
- package/dist/controls/attachment/attachment-meta.d.ts +8 -0
- package/dist/controls/attachment/attachment-meta.js +84 -0
- package/dist/controls/attachment/attachment.css +91 -0
- package/dist/controls/attachment/attachment.d.ts +40 -0
- package/dist/controls/attachment/attachment.js +70 -0
- package/dist/controls/avatar/avatar-initials.d.ts +4 -0
- package/dist/controls/avatar/avatar-initials.js +30 -0
- package/dist/controls/avatar/avatar.css +87 -0
- package/dist/controls/avatar/avatar.d.ts +40 -0
- package/dist/controls/avatar/avatar.js +77 -0
- package/dist/controls/badge/badge.css +175 -0
- package/dist/controls/badge/badge.d.ts +36 -0
- package/dist/controls/badge/badge.js +71 -0
- package/dist/controls/bar-chart/bar-chart.css +110 -0
- package/dist/controls/bar-chart/bar-chart.d.ts +26 -0
- package/dist/controls/bar-chart/bar-chart.js +80 -0
- package/dist/controls/bar-chart/bar-math.d.ts +31 -0
- package/dist/controls/bar-chart/bar-math.js +106 -0
- package/dist/controls/button/button.css +263 -0
- package/dist/controls/button/button.d.ts +61 -0
- package/dist/controls/button/button.js +115 -0
- package/dist/controls/calendar/calendar.css +495 -0
- package/dist/controls/calendar/calendar.d.ts +161 -0
- package/dist/controls/calendar/calendar.js +1004 -0
- package/dist/controls/card/card-content.d.ts +24 -0
- package/dist/controls/card/card-content.js +133 -0
- package/dist/controls/card/card-footer.d.ts +3 -0
- package/dist/controls/card/card-footer.js +16 -0
- package/dist/controls/card/card-header.d.ts +3 -0
- package/dist/controls/card/card-header.js +18 -0
- package/dist/controls/card/card.css +445 -0
- package/dist/controls/card/card.d.ts +51 -0
- package/dist/controls/card/card.js +61 -0
- package/dist/controls/checkbox/checkbox.css +204 -0
- package/dist/controls/checkbox/checkbox.d.ts +114 -0
- package/dist/controls/checkbox/checkbox.js +130 -0
- package/dist/controls/code/code.css +72 -0
- package/dist/controls/code/code.d.ts +23 -0
- package/dist/controls/code/code.js +40 -0
- package/dist/controls/color-picker/color-picker.css +209 -0
- package/dist/controls/color-picker/color-picker.d.ts +84 -0
- package/dist/controls/color-picker/color-picker.js +545 -0
- package/dist/controls/color-picker/color.d.ts +40 -0
- package/dist/controls/color-picker/color.js +159 -0
- package/dist/controls/column/column.css +151 -0
- package/dist/controls/column/column.d.ts +107 -0
- package/dist/controls/column/column.js +57 -0
- package/dist/controls/combo-box/combo-box.css +397 -0
- package/dist/controls/combo-box/combo-box.d.ts +133 -0
- package/dist/controls/combo-box/combo-box.js +680 -0
- package/dist/controls/command-modal/command-modal.css +226 -0
- package/dist/controls/command-modal/command-modal.d.ts +42 -0
- package/dist/controls/command-modal/command-modal.js +315 -0
- package/dist/controls/disclosure/disclosure.css +162 -0
- package/dist/controls/disclosure/disclosure.d.ts +37 -0
- package/dist/controls/disclosure/disclosure.js +207 -0
- package/dist/controls/field/field.css +140 -0
- package/dist/controls/field/field.d.ts +27 -0
- package/dist/controls/field/field.js +228 -0
- package/dist/controls/form-provider/form-provider.css +40 -0
- package/dist/controls/form-provider/form-provider.d.ts +42 -0
- package/dist/controls/form-provider/form-provider.js +105 -0
- package/dist/controls/grid/grid.css +72 -0
- package/dist/controls/grid/grid.d.ts +60 -0
- package/dist/controls/grid/grid.js +56 -0
- package/dist/controls/icon/icon.css +37 -0
- package/dist/controls/icon/icon.d.ts +25 -0
- package/dist/controls/icon/icon.js +55 -0
- package/dist/controls/index.d.ts +57 -0
- package/dist/controls/index.js +136 -0
- package/dist/controls/ladder/ladder.css +89 -0
- package/dist/controls/ladder/ladder.d.ts +27 -0
- package/dist/controls/ladder/ladder.js +78 -0
- package/dist/controls/list/list.css +118 -0
- package/dist/controls/list/list.d.ts +84 -0
- package/dist/controls/list/list.js +48 -0
- package/dist/controls/menu/menu.css +195 -0
- package/dist/controls/menu/menu.d.ts +44 -0
- package/dist/controls/menu/menu.js +248 -0
- package/dist/controls/modal/modal.css +99 -0
- package/dist/controls/modal/modal.d.ts +61 -0
- package/dist/controls/modal/modal.js +155 -0
- package/dist/controls/popover/popover.css +83 -0
- package/dist/controls/popover/popover.d.ts +44 -0
- package/dist/controls/popover/popover.js +164 -0
- package/dist/controls/progress/progress.css +106 -0
- package/dist/controls/progress/progress.d.ts +28 -0
- package/dist/controls/progress/progress.js +80 -0
- package/dist/controls/radio/radio-group.css +84 -0
- package/dist/controls/radio/radio-group.d.ts +152 -0
- package/dist/controls/radio/radio-group.js +350 -0
- package/dist/controls/radio/radio.css +219 -0
- package/dist/controls/radio/radio.d.ts +19 -0
- package/dist/controls/radio/radio.js +75 -0
- package/dist/controls/ramp/ramp.css +75 -0
- package/dist/controls/ramp/ramp.d.ts +29 -0
- package/dist/controls/ramp/ramp.js +72 -0
- package/dist/controls/row/row.css +152 -0
- package/dist/controls/row/row.d.ts +95 -0
- package/dist/controls/row/row.js +45 -0
- package/dist/controls/segment/segment.css +66 -0
- package/dist/controls/segment/segment.d.ts +3 -0
- package/dist/controls/segment/segment.js +26 -0
- package/dist/controls/segmented-control/segmented-control.css +189 -0
- package/dist/controls/segmented-control/segmented-control.d.ts +20 -0
- package/dist/controls/segmented-control/segmented-control.js +64 -0
- package/dist/controls/select/select.css +467 -0
- package/dist/controls/select/select.d.ts +147 -0
- package/dist/controls/select/select.js +538 -0
- package/dist/controls/slider/slider.css +231 -0
- package/dist/controls/slider/slider.d.ts +111 -0
- package/dist/controls/slider/slider.js +60 -0
- package/dist/controls/slider-multi/slider-multi.css +197 -0
- package/dist/controls/slider-multi/slider-multi.d.ts +149 -0
- package/dist/controls/slider-multi/slider-multi.js +380 -0
- package/dist/controls/sparkline/sparkline-math.d.ts +23 -0
- package/dist/controls/sparkline/sparkline-math.js +79 -0
- package/dist/controls/sparkline/sparkline.css +61 -0
- package/dist/controls/sparkline/sparkline.d.ts +27 -0
- package/dist/controls/sparkline/sparkline.js +74 -0
- package/dist/controls/split/constrain.d.ts +44 -0
- package/dist/controls/split/constrain.js +126 -0
- package/dist/controls/split/split-pane.css +63 -0
- package/dist/controls/split/split-pane.d.ts +49 -0
- package/dist/controls/split/split-pane.js +42 -0
- package/dist/controls/split/split.css +127 -0
- package/dist/controls/split/split.d.ts +42 -0
- package/dist/controls/split/split.js +364 -0
- package/dist/controls/stat/stat-model.d.ts +22 -0
- package/dist/controls/stat/stat-model.js +69 -0
- package/dist/controls/stat/stat.css +101 -0
- package/dist/controls/stat/stat.d.ts +30 -0
- package/dist/controls/stat/stat.js +73 -0
- package/dist/controls/status-stream/status-stream.css +151 -0
- package/dist/controls/status-stream/status-stream.d.ts +108 -0
- package/dist/controls/status-stream/status-stream.js +417 -0
- package/dist/controls/swatch/swatch.css +66 -0
- package/dist/controls/swatch/swatch.d.ts +28 -0
- package/dist/controls/swatch/swatch.js +74 -0
- package/dist/controls/swiper/swiper-item.css +17 -0
- package/dist/controls/swiper/swiper-item.d.ts +31 -0
- package/dist/controls/swiper/swiper-item.js +47 -0
- package/dist/controls/swiper/swiper-label.css +10 -0
- package/dist/controls/swiper/swiper-label.d.ts +4 -0
- package/dist/controls/swiper/swiper-label.js +20 -0
- package/dist/controls/swiper/swiper-paddles.css +26 -0
- package/dist/controls/swiper/swiper-paddles.d.ts +10 -0
- package/dist/controls/swiper/swiper-paddles.js +62 -0
- package/dist/controls/swiper/swiper-pagination.css +60 -0
- package/dist/controls/swiper/swiper-pagination.d.ts +31 -0
- package/dist/controls/swiper/swiper-pagination.js +78 -0
- package/dist/controls/swiper/swiper.css +153 -0
- package/dist/controls/swiper/swiper.d.ts +158 -0
- package/dist/controls/swiper/swiper.js +659 -0
- package/dist/controls/switch/switch.css +194 -0
- package/dist/controls/switch/switch.d.ts +5 -0
- package/dist/controls/switch/switch.js +18 -0
- package/dist/controls/table/table-model.d.ts +43 -0
- package/dist/controls/table/table-model.js +124 -0
- package/dist/controls/table/table.css +90 -0
- package/dist/controls/table/table.d.ts +29 -0
- package/dist/controls/table/table.js +178 -0
- package/dist/controls/tabs/tab-panel.d.ts +10 -0
- package/dist/controls/tabs/tab-panel.js +89 -0
- package/dist/controls/tabs/tab.d.ts +26 -0
- package/dist/controls/tabs/tab.js +67 -0
- package/dist/controls/tabs/tabs.css +181 -0
- package/dist/controls/tabs/tabs.d.ts +61 -0
- package/dist/controls/tabs/tabs.js +176 -0
- package/dist/controls/text/href.d.ts +26 -0
- package/dist/controls/text/href.js +48 -0
- package/dist/controls/text/text.css +328 -0
- package/dist/controls/text/text.d.ts +85 -0
- package/dist/controls/text/text.js +273 -0
- package/dist/controls/text-field/text-field.css +498 -0
- package/dist/controls/text-field/text-field.d.ts +226 -0
- package/dist/controls/text-field/text-field.js +905 -0
- package/dist/controls/textarea/textarea.css +251 -0
- package/dist/controls/textarea/textarea.d.ts +136 -0
- package/dist/controls/textarea/textarea.js +312 -0
- package/dist/controls/theme-provider/theme-provider.css +48 -0
- package/dist/controls/theme-provider/theme-provider.d.ts +59 -0
- package/dist/controls/theme-provider/theme-provider.js +40 -0
- package/dist/controls/timeline/timeline.css +33 -0
- package/dist/controls/timeline/timeline.d.ts +39 -0
- package/dist/controls/timeline/timeline.js +52 -0
- package/dist/controls/timeline-item/timeline-item.css +495 -0
- package/dist/controls/timeline-item/timeline-item.d.ts +79 -0
- package/dist/controls/timeline-item/timeline-item.js +360 -0
- package/dist/controls/toast/toast-region.css +56 -0
- package/dist/controls/toast/toast-region.d.ts +20 -0
- package/dist/controls/toast/toast-region.js +113 -0
- package/dist/controls/toast/toast.css +96 -0
- package/dist/controls/toast/toast.d.ts +28 -0
- package/dist/controls/toast/toast.js +191 -0
- package/dist/controls/toolbar/toolbar.css +125 -0
- package/dist/controls/toolbar/toolbar.d.ts +108 -0
- package/dist/controls/toolbar/toolbar.js +68 -0
- package/dist/controls/tooltip/tooltip.css +80 -0
- package/dist/controls/tooltip/tooltip.d.ts +57 -0
- package/dist/controls/tooltip/tooltip.js +205 -0
- package/dist/descriptor/component-descriptor.d.ts +112 -0
- package/dist/descriptor/component-descriptor.js +492 -0
- package/dist/descriptor/index.d.ts +1 -0
- package/dist/descriptor/index.js +5 -0
- package/dist/dom/container.d.ts +45 -0
- package/dist/dom/container.js +60 -0
- package/dist/dom/element.d.ts +98 -0
- package/dist/dom/element.js +221 -0
- package/dist/dom/form.d.ts +240 -0
- package/dist/dom/form.js +399 -0
- package/dist/dom/index.d.ts +10 -0
- package/dist/dom/index.js +31 -0
- package/dist/dom/props.d.ts +56 -0
- package/dist/dom/props.js +231 -0
- package/dist/dom/repeat.d.ts +10 -0
- package/dist/dom/repeat.js +202 -0
- package/dist/dom/template.d.ts +161 -0
- package/dist/dom/template.js +634 -0
- package/dist/dom/watch.d.ts +11 -0
- package/dist/dom/watch.js +82 -0
- package/dist/foundation-styles.css +9 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/reactive/graph.d.ts +65 -0
- package/dist/reactive/graph.js +310 -0
- package/dist/reactive/index.d.ts +3 -0
- package/dist/reactive/index.js +4 -0
- package/dist/reactive/scheduler.d.ts +13 -0
- package/dist/reactive/scheduler.js +49 -0
- package/dist/traits/area-drag.d.ts +12 -0
- package/dist/traits/area-drag.js +70 -0
- package/dist/traits/form-registry.d.ts +36 -0
- package/dist/traits/form-registry.js +108 -0
- package/dist/traits/index.d.ts +24 -0
- package/dist/traits/index.js +19 -0
- package/dist/traits/overlay.d.ts +50 -0
- package/dist/traits/overlay.js +277 -0
- package/dist/traits/pane-resize.d.ts +26 -0
- package/dist/traits/pane-resize.js +103 -0
- package/dist/traits/press-activation.d.ts +11 -0
- package/dist/traits/press-activation.js +55 -0
- package/dist/traits/roving-focus.d.ts +52 -0
- package/dist/traits/roving-focus.js +214 -0
- package/dist/traits/scroll-fade.d.ts +28 -0
- package/dist/traits/scroll-fade.js +120 -0
- package/dist/traits/selection-commit.d.ts +30 -0
- package/dist/traits/selection-commit.js +146 -0
- package/dist/traits/tabbable.d.ts +12 -0
- package/dist/traits/tabbable.js +59 -0
- package/dist/traits/track-user-invalid.d.ts +36 -0
- package/dist/traits/track-user-invalid.js +52 -0
- package/dist/traits/value-codec.d.ts +120 -0
- package/dist/traits/value-codec.js +497 -0
- package/dist/traits/value-drag.d.ts +18 -0
- package/dist/traits/value-drag.js +84 -0
- package/package.json +263 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// field.ts — UIFieldElement, the visible label/description/error wrapper around ONE slotted form control
|
|
2
|
+
// (goals.md §G7 / decomp g7-field-form-provider slice s2, LLD-C4). BEHAVIOUR + props + the three parts +
|
|
3
|
+
// self-define ONLY; geometry/colour live in field.css (s3), the public contract in field.md (s4).
|
|
4
|
+
//
|
|
5
|
+
// `ui-field` extends `UIElement` (NOT `UIFormElement` — a field carries no form value of its own; wrapping
|
|
6
|
+
// it form-associated would double-submit) and reads the ADR-0050 `ui-form-connect` protocol event the base
|
|
7
|
+
// dispatches on EVERY `UIFormElement` connect. Labelling rides the ADR-0051 seam (`setFieldLabelling`) — the
|
|
8
|
+
// field never reaches into a control's internals or parts; the visible error rides the control's OWN
|
|
9
|
+
// user-invalid timing (`FormConnectDetail.userInvalid`), so there is exactly one timing source, never a
|
|
10
|
+
// second one duplicated here.
|
|
11
|
+
//
|
|
12
|
+
// Error-rendering history (two shipped bugs, same root smell — event-driven OBSERVATION of a timing
|
|
13
|
+
// signal, instead of just reading it reactively): (1) five DOM listeners (input/change/blur/invalid/reset)
|
|
14
|
+
// each calling a render — broke because `blur`/`invalid` are capture-only (they don't bubble): capture runs
|
|
15
|
+
// ancestor→target, so this field's listener (ancestor) could read the control's `userInvalid()` BEFORE the
|
|
16
|
+
// deeper `trackUserInvalid` listener (registered on the control itself) flipped `interacted` on the SAME
|
|
17
|
+
// dispatch — a stale read, first blur showed nothing. (2) a microtask-deferred re-render — passed every
|
|
18
|
+
// scripted/jsdom repro (a synchronous `dispatchEvent` call holds the JS stack through the WHOLE
|
|
19
|
+
// propagation, so a deferred read always lands after), but s11's real cross-engine browser probe falsified
|
|
20
|
+
// it: a genuine user-driven blur inserts a microtask CHECKPOINT between capture listeners (each listener
|
|
21
|
+
// return empties the call stack), so the deferred read still ran before the tracker's, in BOTH engines — no
|
|
22
|
+
// microtask-hop count survives a UA checkpoint. THIS (3rd) design has no event-listener race to have at
|
|
23
|
+
// all: `#associate` installs a scope-owned reactive EFFECT over the connect detail's tracked closures
|
|
24
|
+
// (`userInvalid()` / `validity()`, ADR-0050 §4) — every real trigger (typing, blur/change flipping
|
|
25
|
+
// `interacted`, a live `setCustomValidity` write, a reset writing `interacted` back to false) is a plain
|
|
26
|
+
// signal write the kernel wakes this effect for directly; there is no DOM dispatch order to depend on.
|
|
27
|
+
//
|
|
28
|
+
// Association is event-driven, first-wins, nearest-field-scoped: the field does NOT stop the connect event
|
|
29
|
+
// from bubbling (a provider ancestor still needs to see it), and accepts a control only when
|
|
30
|
+
// `control.closest('ui-field') === this` — the guard that also refuses a control belonging to a nested
|
|
31
|
+
// inner field. The option-A bridge (ADR-0051 cl.3) is the fallback for a control with no seam wire: when
|
|
32
|
+
// the associating control exposes an empty string `label` prop, a scope-owned effect writes this field's
|
|
33
|
+
// `label` text into it, disposed (and the write undone) on dissociation — never touching a consumer-set label.
|
|
34
|
+
//
|
|
35
|
+
// Upgrade order (define order ≠ tree order, ADR-0051 cl.5): a control can upgrade before this field does,
|
|
36
|
+
// so its one connect dispatch lands before any listener is live here. The complement is a one-shot catch-up
|
|
37
|
+
// scan at THIS field's own connect — `querySelectorAll('*')` → `instanceof UIFormElement` →
|
|
38
|
+
// `announceFormConnect()` — re-run again after a dissociation (while the field stays connected) so a second,
|
|
39
|
+
// still-slotted control can associate next (first-wins is per-tenure, not forever).
|
|
40
|
+
//
|
|
41
|
+
// Host carries no role/aria-* attribute (a structural wrapper); the only internals use is the
|
|
42
|
+
// `user-invalid` custom state, mirroring the associated control's own gate. `controls → dom` is the
|
|
43
|
+
// allowed import direction.
|
|
44
|
+
import { UIElement, UIFormElement, FORM_CONNECT_EVENT, prop, } from "../../dom/index.js";
|
|
45
|
+
// A per-instance id seed for the three parts (the text-field `messageSeq` precedent) — one sequence number
|
|
46
|
+
// shared by all three ids on a given instance, so `ui-field-label-3` / `-description-3` / `-error-3` read as
|
|
47
|
+
// a set at a glance.
|
|
48
|
+
let fieldSeq = 0;
|
|
49
|
+
function hasStringLabel(control) {
|
|
50
|
+
return typeof control.label === 'string';
|
|
51
|
+
}
|
|
52
|
+
const props = {
|
|
53
|
+
label: { ...prop.string(), reflect: true }, // the visible label text → [data-part=label] (reflect: false — text-field precedent)
|
|
54
|
+
description: prop.string(), // the visible description text → [data-part=description] (reflect: false)
|
|
55
|
+
};
|
|
56
|
+
export class UIFieldElement extends UIElement {
|
|
57
|
+
static props = props;
|
|
58
|
+
// The three parts, created ONCE (idempotent across reconnect — light-DOM children persist through
|
|
59
|
+
// disconnect, the text-field editor precedent). Nullable until `#ensureParts` runs; every other read
|
|
60
|
+
// happens after `connected()` has run, so a cast is safe there (the `#message as HTMLElement` precedent).
|
|
61
|
+
#label = null;
|
|
62
|
+
#description = null;
|
|
63
|
+
#error = null;
|
|
64
|
+
// The associated control's connect detail — `null` while unassociated. First-wins: set once per tenure,
|
|
65
|
+
// cleared by `#dissociate` (control removal, or the field's own teardown).
|
|
66
|
+
#assoc = null;
|
|
67
|
+
// The option-A bridge (ADR-0051 cl.3). `#bridgeControl` doubles as its own "armed" flag: non-null iff the
|
|
68
|
+
// bridge effect is live for THAT control, so `#dissociate` knows both whether to dispose the effect and
|
|
69
|
+
// whether to clear the control's label (never touching a consumer-set one).
|
|
70
|
+
#bridgeControl = null;
|
|
71
|
+
#bridgeDispose = null;
|
|
72
|
+
// The reactive error-render effect (design #3 — see the header history), installed per association
|
|
73
|
+
// (`#associate`) and disposed at `#dissociate` — the bridge-disposer pattern, applied twice.
|
|
74
|
+
#renderDispose = null;
|
|
75
|
+
connected() {
|
|
76
|
+
const { label, description } = this.#ensureParts();
|
|
77
|
+
// Visible text — reactive; the part ELEMENTS (and ids) stay stable across a prop change, so an
|
|
78
|
+
// associated control's labelling references never need a re-handoff.
|
|
79
|
+
this.effect(() => {
|
|
80
|
+
label.textContent = this.label;
|
|
81
|
+
});
|
|
82
|
+
this.effect(() => {
|
|
83
|
+
description.textContent = this.description;
|
|
84
|
+
});
|
|
85
|
+
// ── association (event-driven, the LLD-C1 event doing double duty) ──
|
|
86
|
+
// Deliberately NO stopPropagation — the event must keep bubbling to a provider ancestor.
|
|
87
|
+
this.listen(this, FORM_CONNECT_EVENT, (event) => {
|
|
88
|
+
const detail = event.detail;
|
|
89
|
+
if (!(detail?.control instanceof UIFormElement))
|
|
90
|
+
return; // defense — the event never fires without one
|
|
91
|
+
if (this.#assoc !== null)
|
|
92
|
+
return; // first-wins — a second control in this field stays un-associated
|
|
93
|
+
if (detail.control.closest('ui-field') !== this)
|
|
94
|
+
return; // nearest-field rule (also rejects a nested field's control)
|
|
95
|
+
this.#associate(detail);
|
|
96
|
+
});
|
|
97
|
+
// Error rendering is no longer a listener here at all — it rides a reactive effect installed per
|
|
98
|
+
// association (`#associate`, design #3 in the header history). No event listener, no dispatch-order
|
|
99
|
+
// to depend on: `assoc.control`'s own `formReset()` (text-field: writes `this.value` + the tracker's
|
|
100
|
+
// `interacted` back to false, both tracked signals) wakes the same effect, so a reset needs no separate
|
|
101
|
+
// observation path either.
|
|
102
|
+
// ── upgrade-path catch-up (LLD-C1 / ADR-0051 cl.5) — AFTER the listeners above install ──
|
|
103
|
+
this.#catchUp();
|
|
104
|
+
}
|
|
105
|
+
disconnected() {
|
|
106
|
+
this.#dissociate(); // resources still live — a field removed from around its control must not leave stale labelling
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Accept a connect detail: store it, hand the parts to the control via the ADR-0051 seam, arm the
|
|
110
|
+
* option-A bridge, install the reactive error-render effect (design #3 — the header history), and watch
|
|
111
|
+
* the control's own connection signal so its removal dissociates this field.
|
|
112
|
+
*/
|
|
113
|
+
#associate(detail) {
|
|
114
|
+
this.#assoc = detail;
|
|
115
|
+
detail.control.setFieldLabelling({ label: this.#label, description: this.#description, error: this.#error });
|
|
116
|
+
this.#armBridge(detail.control);
|
|
117
|
+
this.#renderDispose = this.effect(() => this.#renderValidity(detail));
|
|
118
|
+
this.listen(detail.signal, 'abort', () => this.#dissociate(), { once: true });
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Idempotence insurance (the `#assoc === null` early return) — TWO paths reach this: the associated
|
|
122
|
+
* control's own abort listener (removed from the field) and this field's `disconnected()` (field itself
|
|
123
|
+
* removed). Clears the labelling handoff, the bridge, and the visible error; then, IFF the field itself
|
|
124
|
+
* stays connected, re-runs the catch-up scan so a remaining slotted control can associate next — first-wins
|
|
125
|
+
* is per-tenure, not forever. Skipped on the field-disconnect path: re-associating from a field being torn
|
|
126
|
+
* down would mint labelling refs into a detached subtree.
|
|
127
|
+
*/
|
|
128
|
+
#dissociate() {
|
|
129
|
+
const assoc = this.#assoc;
|
|
130
|
+
if (assoc === null)
|
|
131
|
+
return;
|
|
132
|
+
this.#renderDispose?.(); // stop the reactive render — otherwise it outlives dissociation, reading a removed control
|
|
133
|
+
this.#renderDispose = null;
|
|
134
|
+
this.#bridgeDispose?.(); // stop the scope-owned effect — otherwise it outlives dissociation and keeps writing
|
|
135
|
+
this.#bridgeDispose = null;
|
|
136
|
+
if (this.#bridgeControl)
|
|
137
|
+
this.#bridgeControl.label = ''; // only the bridge's own write; a consumer-set label stands
|
|
138
|
+
this.#bridgeControl = null;
|
|
139
|
+
assoc.control.setFieldLabelling(null); // safe on a detached node — a plain signal write
|
|
140
|
+
this.#assoc = null;
|
|
141
|
+
const error = this.#error;
|
|
142
|
+
error.textContent = '';
|
|
143
|
+
error.hidden = true;
|
|
144
|
+
this.internals.states?.delete('user-invalid');
|
|
145
|
+
if (this.isConnected)
|
|
146
|
+
this.#catchUp(); // per-tenure re-arm — skipped when it is the field itself being removed
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The option-A bridge (ADR-0051 cl.3): the fallback name path for a control with no seam wire. Arms ONLY
|
|
150
|
+
* when the control exposes a string `label` prop currently equal to `''` — a pre-set (consumer-owned)
|
|
151
|
+
* label is never touched, arriving or leaving. The effect tracks this field's `label` prop, so a later
|
|
152
|
+
* edit keeps the bridged control's name current.
|
|
153
|
+
*/
|
|
154
|
+
#armBridge(control) {
|
|
155
|
+
if (!hasStringLabel(control) || control.label !== '')
|
|
156
|
+
return;
|
|
157
|
+
this.#bridgeControl = control;
|
|
158
|
+
this.#bridgeDispose = this.effect(() => {
|
|
159
|
+
control.label = this.label;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* One-shot re-announce of every already-connected `UIFormElement` descendant (LLD-C1 / ADR-0051 cl.5) —
|
|
164
|
+
* covers a control that upgraded BEFORE this field did, whose own connect dispatch landed before any
|
|
165
|
+
* listener here was live. A not-yet-upgraded control is skipped (the `instanceof` filter) and covers
|
|
166
|
+
* itself: its own `connectedCallback` dispatches at upgrade, once this field is already listening.
|
|
167
|
+
*/
|
|
168
|
+
#catchUp() {
|
|
169
|
+
for (const el of this.querySelectorAll('*')) {
|
|
170
|
+
if (el instanceof UIFormElement)
|
|
171
|
+
el.announceFormConnect();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The reactive error render (design #3 — the header history). Reads the associated control's OWN gates
|
|
176
|
+
* through the ADR-0050 connect closures — `assoc.validity()` / `assoc.userInvalid()` are TRACKED reads
|
|
177
|
+
* (an effect subscribes to whatever signals they close over: the control's `value`/`required` props, the
|
|
178
|
+
* `trackUserInvalid` tracker's `interacted` signal, `setCustomValidity`'s signal); `control.validationMessage`
|
|
179
|
+
* (the platform DOM getter) is deliberately NOT read here — an effect cannot subscribe to it, which is
|
|
180
|
+
* exactly the gap the two prior designs papered over with an event listener. Installed once per
|
|
181
|
+
* association (`this.effect(...)` in `#associate`) — its lifetime IS the association's, so there is no
|
|
182
|
+
* `#assoc` null-guard to write: `#dissociate` disposes this effect before anything else. An empty message
|
|
183
|
+
* while user-invalid is treated as not-showing — no empty-but-visible error box.
|
|
184
|
+
*/
|
|
185
|
+
#renderValidity(assoc) {
|
|
186
|
+
const verdict = assoc.validity();
|
|
187
|
+
const message = !verdict.valid ? verdict.message : '';
|
|
188
|
+
const showing = assoc.userInvalid() && message !== '';
|
|
189
|
+
const error = this.#error;
|
|
190
|
+
error.textContent = showing ? message : '';
|
|
191
|
+
error.hidden = !showing;
|
|
192
|
+
if (showing)
|
|
193
|
+
this.internals.states?.add('user-invalid');
|
|
194
|
+
else
|
|
195
|
+
this.internals.states?.delete('user-invalid');
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Create the three parts ONCE (idempotent across reconnect — light-DOM children persist through
|
|
199
|
+
* disconnect, the text-field editor precedent). DOM placement IS the reading order: label PREPENDED,
|
|
200
|
+
* description + error APPENDED (author children — the slotted control — sit between); column flow needs
|
|
201
|
+
* no CSS `order`. A `<div>` label, not a `<label>`: no `for` semantics apply here, and a bare `<label>`
|
|
202
|
+
* invites click-forwarding expectations this seam doesn't need. `error` starts hidden + empty.
|
|
203
|
+
*/
|
|
204
|
+
#ensureParts() {
|
|
205
|
+
if (this.#label && this.#description && this.#error) {
|
|
206
|
+
return { label: this.#label, description: this.#description, error: this.#error };
|
|
207
|
+
}
|
|
208
|
+
const seq = ++fieldSeq;
|
|
209
|
+
const label = document.createElement('div');
|
|
210
|
+
label.setAttribute('data-part', 'label');
|
|
211
|
+
label.id = `ui-field-label-${seq}`;
|
|
212
|
+
this.#label = label;
|
|
213
|
+
this.prepend(label);
|
|
214
|
+
const description = document.createElement('div');
|
|
215
|
+
description.setAttribute('data-part', 'description');
|
|
216
|
+
description.id = `ui-field-description-${seq}`;
|
|
217
|
+
this.#description = description;
|
|
218
|
+
const error = document.createElement('div');
|
|
219
|
+
error.setAttribute('data-part', 'error');
|
|
220
|
+
error.id = `ui-field-error-${seq}`;
|
|
221
|
+
error.hidden = true;
|
|
222
|
+
this.#error = error;
|
|
223
|
+
this.append(description, error);
|
|
224
|
+
return { label, description, error };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (!customElements.get('ui-field'))
|
|
228
|
+
customElements.define('ui-field', UIFieldElement);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/* form-provider.css — ui-form-provider, the single-file component stylesheet (ADR-0003). The degenerate
|
|
2
|
+
* minimal case of the fleet's two-block shape (text-field.css / column.css): a pure coordination element
|
|
3
|
+
* carries NO visual voice, so both sections collapse to their smallest legal form —
|
|
4
|
+
*
|
|
5
|
+
* [1] TOKEN BLOCK — `:where(ui-form-provider)` declares NOTHING. The provider discovers/aggregates
|
|
6
|
+
* `UIFormElement` descendants and composes with an optional native `<form>` (LLD-C7,
|
|
7
|
+
* field-form-provider.lld.md); it has no colour, geometry, or spacing opinion of its own, so there is
|
|
8
|
+
* no `--ui-form-provider-*` chain to declare. An EMPTY token block is deliberately NOT invented here
|
|
9
|
+
* (LLD-C8) — this comment stands in for the section, so the token-hygiene probes need no special
|
|
10
|
+
* case: an absent chain is still zero raw `--md-sys-color-*` leakage, zero non-`--ui-form-provider-*` consumption.
|
|
11
|
+
* [2] STYLES BLOCK — `@scope (ui-form-provider)` sets exactly one declaration: `display: block`. An
|
|
12
|
+
* unstyled custom element defaults to `inline`, which would lay its slotted block-level descendants
|
|
13
|
+
* (fields, buttons) out inline against the host — a first-paint layout defect. No further rule: no
|
|
14
|
+
* colour, no spacing, no geometry: the coordination host paints nothing, the slotted subtree owns
|
|
15
|
+
* every box.
|
|
16
|
+
*
|
|
17
|
+
* Standalone stylesheet — linked via the component-styles barrel (wired in s12); NEVER injected from
|
|
18
|
+
* form-provider.ts (behaviour-only `.ts`, no runtime style injection — plan §2).
|
|
19
|
+
*
|
|
20
|
+
* ADR-0103 re-confirms this contract UNCHANGED: unlike `ui-radio-group` (a value-owning group whose direct-
|
|
21
|
+
* children discovery makes a layout default the only fix, Lane A), the provider's descendants are discovered
|
|
22
|
+
* by event bubbling — arbitrary composition (grids, rows, cards) the provider must not fight — so vertical
|
|
23
|
+
* rhythm stays the page author's taught idiom (a `ui-column[gap]` wrap around the fields, Lane C), not a
|
|
24
|
+
* `display:flex` default here.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/* ════════════════ [1] TOKEN BLOCK — :where(ui-form-provider) declares NOTHING ════════════════ */
|
|
28
|
+
/* Deliberate absence (LLD-C8): a pure coordination element has no visual voice — there is no
|
|
29
|
+
--ui-form-provider-* token chain to declare, and nothing rides [size]/[tone]/state to repoint (LLD-C7:
|
|
30
|
+
NO static props on this element — attributes: []). */
|
|
31
|
+
|
|
32
|
+
/* ════════════════ [2] STYLES BLOCK — @scope (ui-form-provider) consumes NOTHING ════════════════ */
|
|
33
|
+
|
|
34
|
+
@scope (ui-form-provider) {
|
|
35
|
+
:scope {
|
|
36
|
+
display: block; /* the host's only rule: an unstyled custom element defaults to `inline`, which would
|
|
37
|
+
lay its slotted block-level descendants (fields, buttons) out inline against it — `block` is the
|
|
38
|
+
coordination host's structural default, not a styling opinion. */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { UIElement, UIFormElement } from '../../dom/index.js';
|
|
2
|
+
import type { FormValue } from '../../dom/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* The `submit()` aggregate detail (LLD-C7 — pinned event naming). The closed house event vocab has no
|
|
5
|
+
* `submit`; a provider-level submit IS a commit semantically, so it rides `change`. Disambiguation from a
|
|
6
|
+
* bubbled member `change` (which carries `detail: null` through the provider): `event.target === provider`.
|
|
7
|
+
*/
|
|
8
|
+
export interface FormSubmitDetail {
|
|
9
|
+
entries: ReadonlyArray<readonly [string, FormValue]>;
|
|
10
|
+
values: Readonly<Record<string, FormValue>>;
|
|
11
|
+
}
|
|
12
|
+
export declare class UIFormProviderElement extends UIElement {
|
|
13
|
+
#private;
|
|
14
|
+
static props: {};
|
|
15
|
+
protected connected(): void;
|
|
16
|
+
protected disconnected(): void;
|
|
17
|
+
/** Live registered controls, registration order — a reactive read (projects `registry.members`). Empty while disconnected. */
|
|
18
|
+
get controls(): readonly UIFormElement[];
|
|
19
|
+
/** Submission entries — native FormData parity (see form-registry.ts). Empty while disconnected. */
|
|
20
|
+
entries(): ReadonlyArray<readonly [string, FormValue]>;
|
|
21
|
+
/** Keyed convenience view of entries() — last entry wins on a duplicate name. Empty while disconnected. */
|
|
22
|
+
values(): Readonly<Record<string, FormValue>>;
|
|
23
|
+
/** Members whose merged verdict is invalid, registration order. Empty while disconnected. */
|
|
24
|
+
invalid(): readonly UIFormElement[];
|
|
25
|
+
/** invalid().length === 0. Vacuously true while disconnected. */
|
|
26
|
+
valid(): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* If invalid: `reportValidity()` on the FIRST invalid member (registration order — native "focus the
|
|
29
|
+
* first invalid control" parity; the UA anchors/announces it) and return `false`, no event. Else emit
|
|
30
|
+
* the aggregate as a `change` (this module's `FormSubmitDetail` — see the type doc for the
|
|
31
|
+
* disambiguation from a bubbled member `change`) and return `true`.
|
|
32
|
+
*/
|
|
33
|
+
submit(): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Native composition (decomp `provider_and_native_form`): partition members by their public `.form` —
|
|
36
|
+
* each DISTINCT non-null owning `<form>` gets ONE `form.reset()` (the platform itself walks that form's
|
|
37
|
+
* FACE members' `formResetCallback`s — calling reset per-member here would double-reset); form-less
|
|
38
|
+
* members get a direct `formResetCallback()` call (the public platform callback). Both paths end in the
|
|
39
|
+
* base's `ui-form-reset` dispatch (LLD-C1, `dom/form.ts`) — this method never dispatches it itself.
|
|
40
|
+
*/
|
|
41
|
+
reset(): void;
|
|
42
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// form-provider.ts — UIFormProviderElement, the fleet's first context/provider primitive (ADR-0050;
|
|
2
|
+
// LLD-C7, decomp g7-field-form-provider slice s5). BEHAVIOUR + self-define ONLY — geometry lives in
|
|
3
|
+
// form-provider.css (s6), the public contract in form-provider.md (s7).
|
|
4
|
+
//
|
|
5
|
+
// A pure coordination element: NO static props (a coordination element takes no configuration — the
|
|
6
|
+
// descriptor's `attributes: []`), extends `UIElement` directly — NOT `UIFormElement` (the provider carries
|
|
7
|
+
// no value of its own; wrapping it `formAssociated` would double-submit, ADR-0050 alternatives) and NOT
|
|
8
|
+
// `UIContainerElement` (no surface paint). Discovery/aggregation is entirely the ADR-0050 connect-time
|
|
9
|
+
// registration event + `traits/form-registry.ts`'s reactive registry (the listener, the four aggregate
|
|
10
|
+
// computeds, teardown-by-abort all live there); this class is the thin public surface over that registry
|
|
11
|
+
// plus the two coordination methods, submit()/reset().
|
|
12
|
+
//
|
|
13
|
+
// Upgrade-path catch-up (LLD-C1 / ADR-0051 cl.5): "ancestors connect before descendants" holds for
|
|
14
|
+
// INSERTION order but not custom-element UPGRADE order — pre-existing DOM whose control module upgrades
|
|
15
|
+
// before this provider's does dispatches `ui-form-connect` into the void (no listener yet). `connected()`
|
|
16
|
+
// runs a one-shot `querySelectorAll('*')` AFTER the registry's listener installs and re-announces every
|
|
17
|
+
// already-connected `UIFormElement` it finds via `announceFormConnect()` — the registry's dup guard makes
|
|
18
|
+
// the re-announce idempotent, so this scan never double-registers a control that connected in time.
|
|
19
|
+
//
|
|
20
|
+
// `controls → dom + traits` is the allowed import direction.
|
|
21
|
+
import { UIElement, UIFormElement } from "../../dom/index.js";
|
|
22
|
+
import { formRegistry } from "../../traits/form-registry.js";
|
|
23
|
+
export class UIFormProviderElement extends UIElement {
|
|
24
|
+
// EMPTY by design — a coordination element takes no configuration (descriptor `attributes: []`). The
|
|
25
|
+
// field exists (rather than being omitted) for the fleet convention + the s10 descriptor trip-wire
|
|
26
|
+
// (`compareDescriptorToProps` needs a live PropsSchema object to compare against).
|
|
27
|
+
static props = {};
|
|
28
|
+
#registry = null; // created in connected(), nulled in disconnected()
|
|
29
|
+
connected() {
|
|
30
|
+
this.#registry = formRegistry(this);
|
|
31
|
+
// Upgrade-path catch-up (see header) — AFTER the registry's listener is live, so a re-announced
|
|
32
|
+
// control registers immediately rather than racing this scan.
|
|
33
|
+
for (const el of this.querySelectorAll('*')) {
|
|
34
|
+
if (el instanceof UIFormElement)
|
|
35
|
+
el.announceFormConnect();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
disconnected() {
|
|
39
|
+
this.#registry?.release();
|
|
40
|
+
this.#registry = null;
|
|
41
|
+
}
|
|
42
|
+
/** Live registered controls, registration order — a reactive read (projects `registry.members`). Empty while disconnected. */
|
|
43
|
+
get controls() {
|
|
44
|
+
return this.#registry?.members.value.map((member) => member.control) ?? [];
|
|
45
|
+
}
|
|
46
|
+
/** Submission entries — native FormData parity (see form-registry.ts). Empty while disconnected. */
|
|
47
|
+
entries() {
|
|
48
|
+
return this.#registry?.entries.value ?? [];
|
|
49
|
+
}
|
|
50
|
+
/** Keyed convenience view of entries() — last entry wins on a duplicate name. Empty while disconnected. */
|
|
51
|
+
values() {
|
|
52
|
+
return this.#registry?.values.value ?? {};
|
|
53
|
+
}
|
|
54
|
+
/** Members whose merged verdict is invalid, registration order. Empty while disconnected. */
|
|
55
|
+
invalid() {
|
|
56
|
+
return this.#registry?.invalid.value ?? [];
|
|
57
|
+
}
|
|
58
|
+
/** invalid().length === 0. Vacuously true while disconnected. */
|
|
59
|
+
valid() {
|
|
60
|
+
return this.#registry?.valid.value ?? true;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* If invalid: `reportValidity()` on the FIRST invalid member (registration order — native "focus the
|
|
64
|
+
* first invalid control" parity; the UA anchors/announces it) and return `false`, no event. Else emit
|
|
65
|
+
* the aggregate as a `change` (this module's `FormSubmitDetail` — see the type doc for the
|
|
66
|
+
* disambiguation from a bubbled member `change`) and return `true`.
|
|
67
|
+
*/
|
|
68
|
+
submit() {
|
|
69
|
+
// Disconnected FIRST, ahead of the validity check: valid() is a read and degrades gracefully
|
|
70
|
+
// (vacuously true with no registry), but submit() is an ACTION — an empty aggregate from a
|
|
71
|
+
// disconnected provider means "I can't see the form," not "the form is empty," so it must refuse
|
|
72
|
+
// (no reportValidity, no emit), not report a hollow success.
|
|
73
|
+
if (this.#registry === null)
|
|
74
|
+
return false;
|
|
75
|
+
if (!this.valid()) {
|
|
76
|
+
this.invalid()[0]?.reportValidity();
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
this.emit('change', { entries: this.entries(), values: this.values() });
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Native composition (decomp `provider_and_native_form`): partition members by their public `.form` —
|
|
84
|
+
* each DISTINCT non-null owning `<form>` gets ONE `form.reset()` (the platform itself walks that form's
|
|
85
|
+
* FACE members' `formResetCallback`s — calling reset per-member here would double-reset); form-less
|
|
86
|
+
* members get a direct `formResetCallback()` call (the public platform callback). Both paths end in the
|
|
87
|
+
* base's `ui-form-reset` dispatch (LLD-C1, `dom/form.ts`) — this method never dispatches it itself.
|
|
88
|
+
*/
|
|
89
|
+
reset() {
|
|
90
|
+
const resetForms = new Set();
|
|
91
|
+
for (const control of this.controls) {
|
|
92
|
+
const form = control.form;
|
|
93
|
+
if (form === null) {
|
|
94
|
+
control.formResetCallback();
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (resetForms.has(form))
|
|
98
|
+
continue; // already reset via this form
|
|
99
|
+
resetForms.add(form);
|
|
100
|
+
form.reset();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!customElements.get('ui-form-provider'))
|
|
105
|
+
customElements.define('ui-form-provider', UIFormProviderElement);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/* grid.css — ui-grid, the single-file component stylesheet (ADR-0003). Two clearly-SECTIONED blocks, mirroring
|
|
2
|
+
* button.css / text-field.css:
|
|
3
|
+
*
|
|
4
|
+
* [1] TOKEN BLOCK — `:where(ui-grid)` (specificity 0,0,0) DECLARES the `--ui-grid-*` chain: the `--ui-grid-min`
|
|
5
|
+
* track FLOOR (a sensible default; grid.ts repoints it inline for a per-instance `min` override) and the
|
|
6
|
+
* `--ui-grid-gap` off the `--md-sys-space-{step}` density-responsive LAYOUT-spacing ladder (ADR-0015 cl.4 /
|
|
7
|
+
* geometry.md "--space-* is layout spacing, not control geometry"). `[gap=step]` repoints the gap (gap
|
|
8
|
+
* reflects, so the attribute is present). NO `--md-sys-height-*` — a Container/layout primitive has no control
|
|
9
|
+
* height (geometry.md "five size-classes"); spacing rides `--md-sys-space` × `[density]` only.
|
|
10
|
+
* [2] STYLES BLOCK — `@scope (ui-grid)` CONSUMES only `--ui-grid-*` for its layout: `display: grid` with the
|
|
11
|
+
* INTRINSIC auto-fit/`minmax` track model (ADR-0016 cl.3/4) — `repeat(auto-fit, minmax(var(--ui-grid-min),
|
|
12
|
+
* 1fr))` reflows by the grid's OWN rendered width (more tracks when wide, fewer when narrow) with NO
|
|
13
|
+
* explicit column-count prop and NO `@container` rule (auto-fit IS the responsiveness — unlike ui-row/
|
|
14
|
+
* ui-column, which wrap via `@container`). `gap` rides `--ui-grid-gap`.
|
|
15
|
+
*
|
|
16
|
+
* SURFACE + FORCED-COLORS are the shared sheet's job, NOT duplicated here: the `--ui-container-bg`/`-tint`
|
|
17
|
+
* elevation×brightness seam AND the `@media (forced-colors: active)` survival both cover `ui-grid` from
|
|
18
|
+
* controls/_surface/container.css (the `@import`-FIRST sheet in the component-styles barrel, s12). A bare
|
|
19
|
+
* ui-grid is transparent (an unset container is unchanged); `[elevation]`/`[brightness]` repoint the shared
|
|
20
|
+
* seam. `ui-grid` establishes NO `container-type` of its own (ADR-0100 — an intrinsically-sized primitive can
|
|
21
|
+
* never safely be a query container) — moot for its own responsiveness anyway, since the auto-fit/`minmax`
|
|
22
|
+
* track model below is TRACK SIZING, not a container query, so removing the establishment leaves it byte-for-
|
|
23
|
+
* byte unaffected. This sheet owns LAYOUT only.
|
|
24
|
+
*
|
|
25
|
+
* Standalone stylesheet — linked via the component-styles barrel (s12); NEVER injected from grid.ts
|
|
26
|
+
* (behaviour-only `.ts`, no runtime style injection — plan §2; the `min` token thread is the values-API, not a
|
|
27
|
+
* style rule). */
|
|
28
|
+
|
|
29
|
+
/* ════════════════ [1] TOKEN BLOCK — :where(ui-grid) declares --ui-grid-* ════════════════ */
|
|
30
|
+
|
|
31
|
+
:where(ui-grid) {
|
|
32
|
+
/* the auto-fit/minmax track FLOOR (ADR-0016 cl.3) — a sensible default; grid.ts repoints this INLINE for a
|
|
33
|
+
per-instance `min` override (inline custom property outranks this `:where()` declaration). */
|
|
34
|
+
--ui-grid-min: 16rem;
|
|
35
|
+
/* gap off the --md-sys-space density-responsive ladder (ADR-0015 cl.4); default = none (a bare grid has no gap).
|
|
36
|
+
[gap=step] repoints below — gap reflects, so the attribute is present for these selectors. */
|
|
37
|
+
--ui-grid-gap: var(--md-sys-space-none);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* gap → the --md-sys-space ladder (the step vocabulary mirrors flexProps' gap enum 1:1) */
|
|
41
|
+
:where(ui-grid[gap='xs']) {
|
|
42
|
+
--ui-grid-gap: var(--md-sys-space-xs);
|
|
43
|
+
}
|
|
44
|
+
:where(ui-grid[gap='sm']) {
|
|
45
|
+
--ui-grid-gap: var(--md-sys-space-sm);
|
|
46
|
+
}
|
|
47
|
+
:where(ui-grid[gap='md']) {
|
|
48
|
+
--ui-grid-gap: var(--md-sys-space-md);
|
|
49
|
+
}
|
|
50
|
+
:where(ui-grid[gap='lg']) {
|
|
51
|
+
--ui-grid-gap: var(--md-sys-space-lg);
|
|
52
|
+
}
|
|
53
|
+
:where(ui-grid[gap='xl']) {
|
|
54
|
+
--ui-grid-gap: var(--md-sys-space-xl);
|
|
55
|
+
}
|
|
56
|
+
:where(ui-grid[gap='2xl']) {
|
|
57
|
+
--ui-grid-gap: var(--md-sys-space-2xl);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* ════════════════ [2] STYLES BLOCK — @scope (ui-grid) consumes ONLY --ui-grid-* ════════════════ */
|
|
61
|
+
|
|
62
|
+
@scope (ui-grid) {
|
|
63
|
+
:scope {
|
|
64
|
+
display: grid;
|
|
65
|
+
/* the INTRINSIC reflow (ADR-0016 cl.3/4): auto-fit packs as many `--ui-grid-min`-floored tracks as the
|
|
66
|
+
grid's OWN width fits and reflows the column count by width — no explicit column-count prop, no
|
|
67
|
+
`@container` rule. Each track flexes 1fr between the floor and the available share. */
|
|
68
|
+
grid-template-columns: repeat(auto-fit, minmax(var(--ui-grid-min), 1fr));
|
|
69
|
+
gap: var(--ui-grid-gap);
|
|
70
|
+
box-sizing: border-box;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type ReactiveProps } from '../../dom/index.js';
|
|
2
|
+
import { UIContainerElement } from '../../dom/container.js';
|
|
3
|
+
declare const props: {
|
|
4
|
+
gap: {
|
|
5
|
+
reflect: true;
|
|
6
|
+
type: import("../../index.js").PropType<"none" | "sm" | "md" | "lg" | "xs" | "xl" | "2xl">;
|
|
7
|
+
default: "none" | "sm" | "md" | "lg" | "xs" | "xl" | "2xl";
|
|
8
|
+
attribute?: string | false;
|
|
9
|
+
};
|
|
10
|
+
min: {
|
|
11
|
+
reflect: true;
|
|
12
|
+
type: import("../../index.js").PropType<string>;
|
|
13
|
+
default: string;
|
|
14
|
+
attribute?: string | false;
|
|
15
|
+
};
|
|
16
|
+
elevation: {
|
|
17
|
+
reflect: true;
|
|
18
|
+
type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
|
|
19
|
+
default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
|
|
20
|
+
attribute?: string | false;
|
|
21
|
+
};
|
|
22
|
+
brightness: {
|
|
23
|
+
reflect: true;
|
|
24
|
+
type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
|
|
25
|
+
default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
|
|
26
|
+
attribute?: string | false;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export interface UIGridElement extends ReactiveProps<typeof props> {
|
|
30
|
+
}
|
|
31
|
+
export declare class UIGridElement extends UIContainerElement {
|
|
32
|
+
static props: {
|
|
33
|
+
gap: {
|
|
34
|
+
reflect: true;
|
|
35
|
+
type: import("../../index.js").PropType<"none" | "sm" | "md" | "lg" | "xs" | "xl" | "2xl">;
|
|
36
|
+
default: "none" | "sm" | "md" | "lg" | "xs" | "xl" | "2xl";
|
|
37
|
+
attribute?: string | false;
|
|
38
|
+
};
|
|
39
|
+
min: {
|
|
40
|
+
reflect: true;
|
|
41
|
+
type: import("../../index.js").PropType<string>;
|
|
42
|
+
default: string;
|
|
43
|
+
attribute?: string | false;
|
|
44
|
+
};
|
|
45
|
+
elevation: {
|
|
46
|
+
reflect: true;
|
|
47
|
+
type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
|
|
48
|
+
default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
|
|
49
|
+
attribute?: string | false;
|
|
50
|
+
};
|
|
51
|
+
brightness: {
|
|
52
|
+
reflect: true;
|
|
53
|
+
type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
|
|
54
|
+
default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
|
|
55
|
+
attribute?: string | false;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
protected connected(): void;
|
|
59
|
+
}
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// grid.ts — UIGridElement, the auto-fit/minmax track-grid layout primitive (goals.md §G9 / ADR-0016 cl.3;
|
|
2
|
+
// decomp g9-containers slice s6). BEHAVIOUR + props + self-define ONLY; the layout/surface live in grid.css
|
|
3
|
+
// (the @scope block), the public contract in grid.md.
|
|
4
|
+
//
|
|
5
|
+
// `ui-grid` is a STRUCTURAL container — it extends `UIContainerElement` (the shared surface base, ADR-0015),
|
|
6
|
+
// is NOT form-associated (no value/validity), and has NO control height (geometry.md Container/layout class —
|
|
7
|
+
// spacing rides `--md-sys-space` × density, never `--md-sys-height-*`). Its children are arbitrary light-DOM grid
|
|
8
|
+
// items (the default cell); it `render()`s nothing (the inherited void), so the user's children are never
|
|
9
|
+
// clobbered (the host-as-grid pattern — the CSS lays them out).
|
|
10
|
+
//
|
|
11
|
+
// The RESPONSIVENESS is INTRINSIC (ADR-0016 cl.3/4): `grid-template-columns: repeat(auto-fit, minmax(min,
|
|
12
|
+
// 1fr))` reflows by the grid's OWN rendered width — more tracks when wide, fewer when narrow — with NO
|
|
13
|
+
// explicit column-count prop and NO `@container` rule. The one author lever is `min`, the minmax() track
|
|
14
|
+
// floor: an arbitrary CSS `<length>` that cannot ride an attribute selector (and `attr()` typed is not
|
|
15
|
+
// WebKit-safe), so it is threaded into the role-pure `--ui-grid-min` token via the values-API (the effect
|
|
16
|
+
// below) — the same token-repoint the CSS does, but with a runtime value. Unset ⇒ the grid.css default floor.
|
|
17
|
+
//
|
|
18
|
+
// `ui-grid` IS an A2UI catalog type — `Grid` (ADR-0087 Fork A, Wave C; supersedes ADR-0016's earlier
|
|
19
|
+
// non-catalog exclusion) renders to `ui-grid`. Reach for `Grid` when the layout should reflow its column count
|
|
20
|
+
// responsively with available width; prefer `Row`/`Column` (with an explicit `wrap`) for an author-controlled
|
|
21
|
+
// arrangement, and `List` when the children are an itemized collection needing list semantics (a2ui-catalog.
|
|
22
|
+
// spec.md §5.2). Imports the dom layer only (`controls → dom`); `UIContainerElement` is pulled from
|
|
23
|
+
// container.ts directly (the dom barrel re-exports it at s12, the integration slice).
|
|
24
|
+
import { prop } from "../../dom/index.js";
|
|
25
|
+
import { UIContainerElement } from "../../dom/container.js";
|
|
26
|
+
const props = {
|
|
27
|
+
// The two surface axes (elevation/brightness), spread from the shared base — props.ts has no static-props
|
|
28
|
+
// prototype merge, so the base exposes a SPREADABLE bag the subclass folds in (the ADR-0013 formProps pattern).
|
|
29
|
+
...UIContainerElement.surfaceProps,
|
|
30
|
+
// `gap` — the ONE flex-grammar prop a track grid consumes (the `--md-sys-space` density-responsive ladder,
|
|
31
|
+
// ADR-0016 cl.1). Pulled as a single entry from the shared flexProps (a grid has no align/justify/wrap).
|
|
32
|
+
gap: UIContainerElement.flexProps.gap,
|
|
33
|
+
// `min` — the minmax() track FLOOR, an arbitrary CSS `<length>`. Threaded into the role-pure `--ui-grid-min`
|
|
34
|
+
// token (the connected() effect); unset ⇒ the grid.css default floor. Reflects so the attribute is the
|
|
35
|
+
// inspectable public API (the dimension itself rides the inline token, not an attribute selector).
|
|
36
|
+
min: { ...prop.string(), reflect: true },
|
|
37
|
+
};
|
|
38
|
+
export class UIGridElement extends UIContainerElement {
|
|
39
|
+
static props = props;
|
|
40
|
+
connected() {
|
|
41
|
+
// Thread the `min` `<length>` into the role-pure `--ui-grid-min` token seam — the one value the grid.css
|
|
42
|
+
// template's `minmax()` floor reads. An inline custom property outranks the `:where()` default, so a set
|
|
43
|
+
// `min` overrides the floor; unset ⇒ removeProperty ⇒ the grid.css default applies. Scope-owned: re-runs on
|
|
44
|
+
// `min` and re-applies on reconnect; disposed with the connection scope (zero LIVE residue — the inline
|
|
45
|
+
// token persists on the detached host like any light-DOM state, but the effect itself is torn down).
|
|
46
|
+
this.effect(() => {
|
|
47
|
+
const min = this.min;
|
|
48
|
+
if (min)
|
|
49
|
+
this.style.setProperty('--ui-grid-min', min);
|
|
50
|
+
else
|
|
51
|
+
this.style.removeProperty('--ui-grid-min');
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!customElements.get('ui-grid'))
|
|
56
|
+
customElements.define('ui-grid', UIGridElement);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* icon.css — ui-icon, the Display-class icon primitive (LLD-C5 §3, ADR-0065/0066). Two sectioned blocks:
|
|
2
|
+
*
|
|
3
|
+
* [1] TOKEN BLOCK — `:where(ui-icon)` declares --md-sys-icon-size: 1em. ui-icon has NO size/tone lever
|
|
4
|
+
* (no [size] ramp, no [variant] repoint) — it is a 1em×1em cell by design; a consumer sizes it via
|
|
5
|
+
* ambient `font-size` or an explicit `inline-size` override on the host (LLD §3, geometry.md Display
|
|
6
|
+
* class: "intrinsic structural sizing").
|
|
7
|
+
* [2] STYLES BLOCK — `@scope (ui-icon)` consumes ONLY --md-sys-icon-size. `color: inherit` so the injected
|
|
8
|
+
* <svg fill="currentColor"> (resolve.ts) tracks the surrounding ink with no per-icon color plumbing —
|
|
9
|
+
* including in forced-colors mode, where the CONSUMING context's ink already resolves to the platform
|
|
10
|
+
* system color, so ui-icon needs no dedicated `@media (forced-colors: active)` block of its own.
|
|
11
|
+
*
|
|
12
|
+
* The content-into-cell contract (LLD §3) is what makes this file this small: an injected <svg width=100%
|
|
13
|
+
* height=100%> fills whatever cell it lands in — an [data-role='icon'] cell at icon size, a
|
|
14
|
+
* [data-role='caret'] cell inset to font-rhythm by that cell's OWN existing padding — with ZERO change to
|
|
15
|
+
* button.css/select.css. This sheet only styles the STANDALONE <ui-icon> host itself.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/* ════════════════ [1] TOKEN BLOCK — :where(ui-icon) declares --md-sys-icon-* ════════════════ */
|
|
19
|
+
|
|
20
|
+
:where(ui-icon) {
|
|
21
|
+
--md-sys-icon-size: 1em;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* ════════════════ [2] STYLES BLOCK — @scope (ui-icon) consumes ONLY --md-sys-icon-* ════════════════ */
|
|
25
|
+
|
|
26
|
+
@scope (ui-icon) {
|
|
27
|
+
:scope {
|
|
28
|
+
display: inline-flex;
|
|
29
|
+
inline-size: var(--md-sys-icon-size);
|
|
30
|
+
block-size: var(--md-sys-icon-size);
|
|
31
|
+
color: inherit;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
:scope svg {
|
|
35
|
+
display: block;
|
|
36
|
+
}
|
|
37
|
+
}
|