@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,99 @@
|
|
|
1
|
+
/* modal.css — ui-modal, the single-file component stylesheet (ADR-0003 / ADR-0017). Two clearly-SECTIONED
|
|
2
|
+
* blocks, mirroring button.css / text-field.css:
|
|
3
|
+
*
|
|
4
|
+
* [1] TOKEN BLOCK — `:where(ui-modal)` (specificity 0,0,0) sets the modal's OWN default surface and DECLARES
|
|
5
|
+
* the `--ui-modal-*` chain from the colour roles (`--md-sys-color-{family}-{role}`, family = neutral) and the
|
|
6
|
+
* dimensions (`--md-sys-space-*` / `--md-sys-shape-corner-base`). This is where roles enter, exactly as button.css's
|
|
7
|
+
* `:where()` token block does. The container surface base defaults `--ui-container-bg` to TRANSPARENT (an
|
|
8
|
+
* unset layout primitive is unchanged — ADR-0015 cl.1); a modal is an OPAQUE plane, so it repoints its own
|
|
9
|
+
* default. The signed `elevation`/`brightness` axes still repoint `--ui-container-bg`/`-tint` on the HOST
|
|
10
|
+
* via container.css (higher specificity), and the value INHERITS to the dialog part below.
|
|
11
|
+
* [2] STYLES BLOCK — `@scope (ui-modal)` CONSUMES only the role-pure `--ui-container-*` surface seam + the own
|
|
12
|
+
* `--ui-modal-*` chain (never a raw `--md-sys-color-*`). The host is `display: contents` — a logical wrapper that
|
|
13
|
+
* generates no box; only the control-owned `<dialog data-part="dialog">` PART renders, in the platform TOP
|
|
14
|
+
* LAYER when `showModal()` is called (ADR-0017). The native UA dialog frame (1em padding, solid border,
|
|
15
|
+
* Canvas bg, centring margin) is fully repointed onto the surface seam + the space/radius tokens. The
|
|
16
|
+
* `::backdrop` scrim is the platform-painted blocking layer. Geometry is the Pattern/container size-class
|
|
17
|
+
* (geometry.md): the shell spaces off `--md-sys-space-*` × density — NO control height (`--md-sys-height-*`).
|
|
18
|
+
*
|
|
19
|
+
* Standalone stylesheet — linked via the component-styles barrel (s12); NEVER injected from modal.ts
|
|
20
|
+
* (behaviour-only `.ts`, no runtime style injection — plan §2). component-styles.css @imports container.css
|
|
21
|
+
* FIRST (the shared surface seam), then this sheet.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/* ════════════════ [1] TOKEN BLOCK — :where(ui-modal) declares the surface default + --ui-modal-* ════════════════ */
|
|
25
|
+
|
|
26
|
+
:where(ui-modal) {
|
|
27
|
+
/* The modal's OWN default surface (the container base defaults transparent — ADR-0015 cl.1; a modal is an
|
|
28
|
+
opaque plane). `[elevation]`/`[brightness]` on the host still win (container.css repoints at (0,1,0)), and
|
|
29
|
+
the seam value inherits down to the dialog part. */
|
|
30
|
+
--ui-container-bg: var(--md-sys-color-neutral-surface);
|
|
31
|
+
|
|
32
|
+
/* the own chain — @scope consumes ONLY these (+ the --ui-container-* seam), never a raw --md-sys-color-* role */
|
|
33
|
+
--ui-modal-ink: var(--md-sys-color-neutral-on-surface);
|
|
34
|
+
--ui-modal-outline: var(--md-sys-color-neutral-outline-variant); /* the faint dialog frame */
|
|
35
|
+
--ui-modal-radius: var(--md-sys-shape-corner-base); /* the shared fleet radius (ADR-0015 cl.5) */
|
|
36
|
+
--ui-modal-padding: var(--md-sys-space-lg); /* the shell pad off the density-responsive ladder (NOT a control height) */
|
|
37
|
+
--ui-modal-scrim: var(--md-sys-color-dialog-backdrop); /* the ::backdrop blocking wash — black 80% opacity (TKT-0019; was --md-sys-color-neutral-scrim, too light) */
|
|
38
|
+
/* The FRAME dials — ui-modal's public composition seam (TKT-0017): a composer that nests ui-modal (the
|
|
39
|
+
command-modal precedent) pins these via inline style on the element it owns (the slider --value-pct
|
|
40
|
+
JS-seam exemption, family-coherence B). Defaults reproduce the classic centered shrink-fit exactly. */
|
|
41
|
+
--ui-modal-inline-size: auto; /* set a length for a FIXED-width dialog (shrink-fit by default) */
|
|
42
|
+
--ui-modal-max-inline-size: min(92vw, 32rem);
|
|
43
|
+
--ui-modal-margin-block-start: auto; /* set a length (e.g. 15svh) to TOP-ANCHOR; auto = centered */
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* ════════════════ [2] STYLES BLOCK — @scope (ui-modal) consumes ONLY --ui-modal-* + the --ui-container-* seam ════════════════ */
|
|
47
|
+
|
|
48
|
+
@scope (ui-modal) {
|
|
49
|
+
/* the host is a logical wrapper — it generates no box; only the dialog PART renders (in the top layer when
|
|
50
|
+
shown). The surface custom props set on the host inherit through display:contents to the dialog. */
|
|
51
|
+
:scope {
|
|
52
|
+
display: contents;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* the dialog surface — the modal plane. The base plane (elevation) is the solid --ui-container-bg; the tonal
|
|
56
|
+
wash (brightness composition) is the flat same-colour background-image stacked above it (ADR-0015 cl.3), so
|
|
57
|
+
the two surface axes composite. Everything else is the own --ui-modal-* chain. The native UA frame is fully
|
|
58
|
+
overridden. `margin: auto` keeps the showModal centring; the size caps keep a long modal scrollable. */
|
|
59
|
+
:scope > [data-part='dialog'] {
|
|
60
|
+
box-sizing: border-box;
|
|
61
|
+
/* centre in the top layer by default (the showModal default — restated so a UA reset cannot drop it);
|
|
62
|
+
the block-start dial top-anchors when a composer sets it (TKT-0017). */
|
|
63
|
+
margin-inline: auto;
|
|
64
|
+
margin-block-start: var(--ui-modal-margin-block-start);
|
|
65
|
+
margin-block-end: auto;
|
|
66
|
+
inline-size: var(--ui-modal-inline-size);
|
|
67
|
+
max-inline-size: var(--ui-modal-max-inline-size);
|
|
68
|
+
max-block-size: 85svh;
|
|
69
|
+
overflow: auto; /* a tall modal scrolls within the cap, not off-screen */
|
|
70
|
+
|
|
71
|
+
background-color: var(--ui-container-bg); /* the base plane (elevation) */
|
|
72
|
+
background-image: linear-gradient(var(--ui-container-tint), var(--ui-container-tint)); /* the tonal wash (composition) */
|
|
73
|
+
color: var(--ui-modal-ink);
|
|
74
|
+
border: 1px solid var(--ui-modal-outline);
|
|
75
|
+
border-radius: var(--ui-modal-radius);
|
|
76
|
+
/* Box-model (container-box.css, REVISED 2026-07-04): the dialog is a [data-box] — no shell padding; its
|
|
77
|
+
children carry the inset (author header/content/footer regions get the 12/6 region padding PLUS a 6px
|
|
78
|
+
positional margin — regions are inset, not full-bleed; loose content gets the 6px box inset margin). */
|
|
79
|
+
padding: 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* the ::backdrop scrim — the platform-painted blocking layer behind the modal (top layer). A translucent
|
|
83
|
+
neutral scrim role, so the page dims but is not fully hidden. */
|
|
84
|
+
:scope > [data-part='dialog']::backdrop {
|
|
85
|
+
background-color: var(--ui-modal-scrim);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* forced-colors (WHCM) — keep the dialog surface + frame + ink visible as system colours (the wash drops; a
|
|
89
|
+
translucent overlay would paint over the forced Canvas base and defeat it). The ::backdrop is left to the
|
|
90
|
+
scrim role / UA so the blocking layer still paints (it must not vanish — the modal stays distinguishable). */
|
|
91
|
+
@media (forced-colors: active) {
|
|
92
|
+
:scope > [data-part='dialog'] {
|
|
93
|
+
background-color: Canvas;
|
|
94
|
+
background-image: none;
|
|
95
|
+
border-color: CanvasText;
|
|
96
|
+
color: CanvasText;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type ReactiveProps } from '../../dom/index.js';
|
|
2
|
+
import { UIContainerElement } from '../../dom/container.js';
|
|
3
|
+
declare const props: {
|
|
4
|
+
open: {
|
|
5
|
+
reflect: true;
|
|
6
|
+
type: import("../../index.js").PropType<boolean>;
|
|
7
|
+
default: boolean;
|
|
8
|
+
attribute?: string | false;
|
|
9
|
+
};
|
|
10
|
+
persistent: {
|
|
11
|
+
reflect: true;
|
|
12
|
+
type: import("../../index.js").PropType<boolean>;
|
|
13
|
+
default: boolean;
|
|
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 UIModalElement extends ReactiveProps<typeof props> {
|
|
30
|
+
}
|
|
31
|
+
export declare class UIModalElement extends UIContainerElement {
|
|
32
|
+
#private;
|
|
33
|
+
static props: {
|
|
34
|
+
open: {
|
|
35
|
+
reflect: true;
|
|
36
|
+
type: import("../../index.js").PropType<boolean>;
|
|
37
|
+
default: boolean;
|
|
38
|
+
attribute?: string | false;
|
|
39
|
+
};
|
|
40
|
+
persistent: {
|
|
41
|
+
reflect: true;
|
|
42
|
+
type: import("../../index.js").PropType<boolean>;
|
|
43
|
+
default: boolean;
|
|
44
|
+
attribute?: string | false;
|
|
45
|
+
};
|
|
46
|
+
elevation: {
|
|
47
|
+
reflect: true;
|
|
48
|
+
type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
|
|
49
|
+
default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
|
|
50
|
+
attribute?: string | false;
|
|
51
|
+
};
|
|
52
|
+
brightness: {
|
|
53
|
+
reflect: true;
|
|
54
|
+
type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
|
|
55
|
+
default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
|
|
56
|
+
attribute?: string | false;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
protected connected(): void;
|
|
60
|
+
}
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// modal.ts — UIModalElement, the FACE modal dialog on the native `<dialog>` (goals.md §G9 / ADR-0017;
|
|
2
|
+
// decomp g9-containers slice s9). BEHAVIOUR + props + the control-owned `<dialog>` part + self-define ONLY;
|
|
3
|
+
// surface/backdrop live in modal.css (s9), the public contract in modal.md (s9).
|
|
4
|
+
//
|
|
5
|
+
// Native `<dialog>` showModal() supplies the four hard modal behaviours FREE, in the platform top layer:
|
|
6
|
+
// top-layer stacking (above any z-index/overflow/stacking context), a `::backdrop`, focus CONTAINMENT, and
|
|
7
|
+
// Escape-to-dismiss. ADR-0017 takes the platform element and adds only the gaps it leaves — focus RESTORE on
|
|
8
|
+
// close (cl.4) and the `open`↔platform sync (cl.2/3). A `<dialog>` is NOT a form widget (it submits nothing,
|
|
9
|
+
// carries no value, is not form-associated), so it honours the "no native form elements" rule under the
|
|
10
|
+
// ADR-0014 widgets-not-elements reading — `ui-modal` extends UIContainerElement (surface, NOT form-associated).
|
|
11
|
+
//
|
|
12
|
+
// The dialog is a control-owned light-DOM PART (`<dialog data-part="dialog">`), created ONCE (an idempotent
|
|
13
|
+
// guard) and NEVER re-rendered — `render()` stays the inherited VOID, because re-creating the dialog would drop
|
|
14
|
+
// the top-layer + focus state mid-session (ADR-0017 cl.1). Its three wires are scope-owned:
|
|
15
|
+
// • model→platform — an effect on `open`: true → `dialog.showModal()`, false → `dialog.close()` (idempotent
|
|
16
|
+
// guards; `showModal()` records the opener first so focus can be restored).
|
|
17
|
+
// • platform→model — the dialog's `close` event: a USER/platform close (Escape, backdrop, an external
|
|
18
|
+
// `.close()`) syncs `open=false` and emits the family `close` + `toggle` (the two-way bind, ADR-0019);
|
|
19
|
+
// a close WE drove (the prop already went false) only restores focus — no redundant emit. `this.open` is
|
|
20
|
+
// the discriminator (still true ⇒ the platform closed it), so no async flag is needed.
|
|
21
|
+
// • dismissal gate — `persistent` (default OFF); when set, the dialog's `cancel` event (Escape, the platform
|
|
22
|
+
// light-dismiss request) is preventDefault-ed, and a backdrop click is ignored. Backdrop click is detected
|
|
23
|
+
// rect-wise (a click whose target is the dialog box but lands OUTSIDE its content rect ⇒ the `::backdrop`).
|
|
24
|
+
//
|
|
25
|
+
// The HOST carries no role/aria-* attribute (the family `internals`-only ARIA discipline) — `aria-modal` is set
|
|
26
|
+
// by `showModal()`, and an author accessible name (`aria-label`/`aria-labelledby`) is FORWARDED onto the dialog
|
|
27
|
+
// PART (ADR-0017 cl.5), keeping the host attribute-clean.
|
|
28
|
+
//
|
|
29
|
+
// The dialog also wires `traits/scroll-fade.ts` (default-on, no separate opt-in prop) — the gutter-exposure
|
|
30
|
+
// fix, 2026-07-04: once a [data-box] header/footer became INSET rather than full-bleed, its own background no
|
|
31
|
+
// longer reaches the frame edge, so scrolled content can peek through the thin gutter around it. `controls →
|
|
32
|
+
// dom + traits` is the allowed import direction.
|
|
33
|
+
import { prop } from "../../dom/index.js";
|
|
34
|
+
import { UIContainerElement } from "../../dom/container.js";
|
|
35
|
+
import { scrollFade } from "../../traits/scroll-fade.js";
|
|
36
|
+
const props = {
|
|
37
|
+
// The two surface axes (elevation/brightness) — SPREAD from the container base, not inherited (props.ts has
|
|
38
|
+
// no static-props prototype merge; the ADR-0013 formProps precedent). They style the dialog PART's surface.
|
|
39
|
+
...UIContainerElement.surfaceProps,
|
|
40
|
+
// `open` — whether the modal is shown. Reflected (the [open] attribute drives nothing in CSS — the dialog's
|
|
41
|
+
// own [open] is the platform's — but reflecting keeps the host's declared state inspectable/serializable) and
|
|
42
|
+
// BINDABLE: the catalog declares value:{prop:'open',event:'toggle'} so the renderer two-way-binds it (ADR-0019).
|
|
43
|
+
open: { ...prop.boolean(false), reflect: true },
|
|
44
|
+
// `persistent` (default OFF) — when present, the modal is NON-dismissable: the dialog's `cancel` event (Escape)
|
|
45
|
+
// is preventDefault-ed and a backdrop click is ignored, so the agent owns the close (set open=false). Reflected
|
|
46
|
+
// (presence-boolean: <ui-modal persistent> ⇒ true, absent ⇒ false) — the declarative override the old default-on
|
|
47
|
+
// `dismissable` could not express (attribute presence always coerced true). See ADR-0020.
|
|
48
|
+
persistent: { ...prop.boolean(false), reflect: true },
|
|
49
|
+
};
|
|
50
|
+
export class UIModalElement extends UIContainerElement {
|
|
51
|
+
static props = props;
|
|
52
|
+
// The control-owned dialog PART — a light-DOM child that persists across disconnect/reconnect, so it is
|
|
53
|
+
// created ONCE (the idempotent guard in #ensureDialog) and never re-appended.
|
|
54
|
+
#dialog = null;
|
|
55
|
+
// The element focused when the modal opened — restored on close (the one platform gap, ADR-0017 cl.4).
|
|
56
|
+
#opener = null;
|
|
57
|
+
connected() {
|
|
58
|
+
const dialog = this.#ensureDialog();
|
|
59
|
+
// ── platform→model — a USER/platform close syncs the prop + announces; a close WE drove only restores ──
|
|
60
|
+
this.listen(dialog, 'close', () => {
|
|
61
|
+
if (this.open) {
|
|
62
|
+
// The platform closed it (Escape, backdrop, an external .close()) while the prop still reads open —
|
|
63
|
+
// sync the state down and announce, so the agent learns the user dismissed it (the two-way bind).
|
|
64
|
+
this.open = false; // reflects (drops the attribute) + re-runs the effect → #closeDialog no-ops (already closed)
|
|
65
|
+
this.emit('close');
|
|
66
|
+
this.emit('toggle'); // the value:{event:'toggle'} two-way signal (ADR-0019)
|
|
67
|
+
}
|
|
68
|
+
this.#restoreFocus(); // restore on EVERY close (platform-driven or our own) — the platform omits this
|
|
69
|
+
});
|
|
70
|
+
// ── dismissal gate — Escape / the platform light-dismiss request fires `cancel`; block it when persistent ──
|
|
71
|
+
this.listen(dialog, 'cancel', (event) => {
|
|
72
|
+
if (this.persistent)
|
|
73
|
+
event.preventDefault(); // the dialog stays open; no `close` follows
|
|
74
|
+
});
|
|
75
|
+
// ── backdrop click — a click on the dialog box OUTSIDE its content rect is the `::backdrop`; dismiss if allowed ──
|
|
76
|
+
this.listen(dialog, 'click', (event) => {
|
|
77
|
+
if (this.persistent)
|
|
78
|
+
return;
|
|
79
|
+
const e = event;
|
|
80
|
+
if (e.target !== dialog)
|
|
81
|
+
return; // a click on content targets the child; the backdrop/box targets the dialog
|
|
82
|
+
const r = dialog.getBoundingClientRect();
|
|
83
|
+
const insideContent = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom;
|
|
84
|
+
if (!insideContent)
|
|
85
|
+
dialog.close(); // a true backdrop click → close (routes through the `close` user branch)
|
|
86
|
+
});
|
|
87
|
+
// ── model→platform — `open` drives showModal()/close() (scope-owned; re-arms on reconnect) ──
|
|
88
|
+
this.effect(() => {
|
|
89
|
+
if (this.open)
|
|
90
|
+
this.#openDialog(dialog);
|
|
91
|
+
else
|
|
92
|
+
this.#closeDialog(dialog);
|
|
93
|
+
});
|
|
94
|
+
// ── edge-aware scroll fade (the gutter-exposure fix, 2026-07-04) — always on ──
|
|
95
|
+
// The dialog IS the scroll viewport (modal.css: `overflow: auto` + `max-block-size: 85svh`) and a
|
|
96
|
+
// [data-box] region author may give it a sticky header/footer, whose inset background no longer reaches
|
|
97
|
+
// the frame edge. Unlike ui-card-content's `scroll-fade`, there is no separate opt-in prop here — a
|
|
98
|
+
// modal has no equivalent author-facing switch, so the fade is unconditionally wired (a no-op on a
|
|
99
|
+
// dialog that never overflows — scrollFade's own decision logic gates the flags, not this call site).
|
|
100
|
+
scrollFade(this, { viewport: dialog });
|
|
101
|
+
}
|
|
102
|
+
/** Open the dialog (idempotent) — record the opener FIRST (showModal moves focus in), then enter the top layer. */
|
|
103
|
+
#openDialog(dialog) {
|
|
104
|
+
if (dialog.open)
|
|
105
|
+
return; // already shown — do not re-enter the top layer / re-trap
|
|
106
|
+
this.#opener = document.activeElement; // the element to restore focus to on close
|
|
107
|
+
dialog.showModal(); // platform: top layer + ::backdrop + focus trap + Escape + aria-modal — all free
|
|
108
|
+
}
|
|
109
|
+
/** Close the dialog (idempotent). The `close` event fires with `open` already false ⇒ no redundant emit. */
|
|
110
|
+
#closeDialog(dialog) {
|
|
111
|
+
if (!dialog.open)
|
|
112
|
+
return;
|
|
113
|
+
dialog.close();
|
|
114
|
+
}
|
|
115
|
+
/** Restore focus to the recorded opener (ADR-0017 cl.4 — the platform traps but does not restore). */
|
|
116
|
+
#restoreFocus() {
|
|
117
|
+
const opener = this.#opener;
|
|
118
|
+
this.#opener = null;
|
|
119
|
+
if (opener && opener.isConnected && typeof opener.focus === 'function')
|
|
120
|
+
opener.focus();
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Create the `<dialog data-part="dialog">` PART ONCE (idempotent across reconnect — it is a light-DOM child
|
|
124
|
+
* that persists through disconnect). The modal's children are MOVED into the dialog at creation (they render
|
|
125
|
+
* inside the top-layer surface, not beside it). An author accessible name (`aria-label`/`aria-labelledby`) is
|
|
126
|
+
* FORWARDED off the host onto the dialog part (ADR-0017 cl.5) so the host stays role/aria-clean and the name
|
|
127
|
+
* rides the semantic element. `render()` stays the inherited VOID — the dialog is never re-created.
|
|
128
|
+
*/
|
|
129
|
+
#ensureDialog() {
|
|
130
|
+
if (this.#dialog)
|
|
131
|
+
return this.#dialog;
|
|
132
|
+
const dialog = document.createElement('dialog');
|
|
133
|
+
dialog.setAttribute('data-part', 'dialog');
|
|
134
|
+
dialog.setAttribute('data-box', ''); // adopt the shared container box-model (region padding + inset margins, ADR-0046 revised 2026-07-04)
|
|
135
|
+
// Forward the accessible name onto the dialog part, then strip it from the host (host carries no aria-*).
|
|
136
|
+
const labelledby = this.getAttribute('aria-labelledby');
|
|
137
|
+
if (labelledby !== null) {
|
|
138
|
+
dialog.setAttribute('aria-labelledby', labelledby);
|
|
139
|
+
this.removeAttribute('aria-labelledby');
|
|
140
|
+
}
|
|
141
|
+
const label = this.getAttribute('aria-label');
|
|
142
|
+
if (label !== null) {
|
|
143
|
+
dialog.setAttribute('aria-label', label);
|
|
144
|
+
this.removeAttribute('aria-label');
|
|
145
|
+
}
|
|
146
|
+
// Move the author content into the dialog (the dialog is detached here, so firstChild is real content).
|
|
147
|
+
while (this.firstChild)
|
|
148
|
+
dialog.appendChild(this.firstChild);
|
|
149
|
+
this.appendChild(dialog);
|
|
150
|
+
this.#dialog = dialog;
|
|
151
|
+
return dialog;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!customElements.get('ui-modal'))
|
|
155
|
+
customElements.define('ui-modal', UIModalElement);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/* popover.css — ui-popover, the single-file component stylesheet (ADR-0003 · overlay-controller.lld.md).
|
|
2
|
+
* Two clearly-SECTIONED blocks, mirroring modal.css / button.css:
|
|
3
|
+
*
|
|
4
|
+
* [1] TOKEN BLOCK — `:where(ui-popover)` (specificity 0,0,0) sets the popover's OWN default
|
|
5
|
+
* surface tokens and DECLARES the `--ui-popover-*` chain from the colour roles
|
|
6
|
+
* (`--md-sys-color-{family}-{role}`, family = neutral) and the dimensions (`--md-sys-space-*` /
|
|
7
|
+
* `--md-sys-shape-corner-base`). Raw `--md-sys-color-*` roles enter ONLY here; `@scope` never reads them.
|
|
8
|
+
* [2] STYLES BLOCK — `@scope (ui-popover)` CONSUMES only the role-pure `--ui-popover-*` chain.
|
|
9
|
+
* The host is `display: contents` — a logical disclosure wrapper that generates no box.
|
|
10
|
+
* The trigger child stays in the document flow (the author provides it; we mark it with
|
|
11
|
+
* `data-part="trigger"` but do not style it — it is the author's to own). The panel part
|
|
12
|
+
* (`[data-part="panel"]`) is the Container/surface: `--ui-container-bg` equivalent sourced
|
|
13
|
+
* from `--ui-popover-bg`, `--md-sys-shape-corner-base`, `--md-sys-space-md` padding — NOT a control height
|
|
14
|
+
* (`--md-sys-height-*` is never read). The panel needs a `min-inline-size` floor to prevent
|
|
15
|
+
* collapse when content is absent (the ui-text-field #74 / ui-slider trap, ADR-0021).
|
|
16
|
+
* A `forced-color-adjust: auto` block keeps the surface and frame in system colours.
|
|
17
|
+
*
|
|
18
|
+
* Standalone stylesheet — NOT injected from popover.ts (behaviour-only `.ts`, no runtime injection
|
|
19
|
+
* per plan §2). Linked via the component-styles barrel (s12); the barrel @imports foundation roles
|
|
20
|
+
* FIRST. The panel's `popover` attribute (set by the overlay controller) gives it `display: none`
|
|
21
|
+
* in the UA stylesheet until `showPopover()` places it in the top layer — our CSS only applies to
|
|
22
|
+
* the visible (open) panel, which is correct since `[popover]` elements are normally hidden.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/* ════════════════ [1] TOKEN BLOCK — :where(ui-popover) declares the surface default + --ui-popover-* ════════════════ */
|
|
26
|
+
|
|
27
|
+
:where(ui-popover) {
|
|
28
|
+
/* Surface plane — the popup panel is an opaque neutral surface (like a modal / card).
|
|
29
|
+
@scope consumes --ui-popover-bg, never the raw --md-sys-color-* role. */
|
|
30
|
+
--ui-popover-bg: var(--md-sys-color-neutral-surface);
|
|
31
|
+
--ui-popover-ink: var(--md-sys-color-neutral-on-surface);
|
|
32
|
+
|
|
33
|
+
/* Frame */
|
|
34
|
+
--ui-popover-outline: var(--md-sys-color-neutral-outline-variant);
|
|
35
|
+
--ui-popover-radius: var(--md-sys-shape-corner-base); /* the shared fleet radius (ADR-0015 cl.5) */
|
|
36
|
+
|
|
37
|
+
/* Shell spacing — panel-class geometry (Container/surface, NOT a control height) */
|
|
38
|
+
--ui-popover-padding: var(--md-sys-space-md);
|
|
39
|
+
|
|
40
|
+
/* Size floor — prevents collapse when content is absent or narrow (the ADR-0021 lesson:
|
|
41
|
+
a surface that can be empty needs a min-inline-size; the overlay JS positions against
|
|
42
|
+
the measured bounds so a zero-width panel would place at the wrong coords too). */
|
|
43
|
+
--ui-popover-min-inline-size: 8rem;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* ════════════════ [2] STYLES BLOCK — @scope (ui-popover) consumes ONLY --ui-popover-* ════════════════ */
|
|
47
|
+
|
|
48
|
+
@scope (ui-popover) {
|
|
49
|
+
/* The host is a logical disclosure wrapper — it generates no box; only the trigger (in flow)
|
|
50
|
+
and the panel (in the top layer when open) render as visible surfaces. */
|
|
51
|
+
:scope {
|
|
52
|
+
display: contents;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* The surface panel — a Container/surface in the top layer (the Popover API places it there).
|
|
56
|
+
The UA `[popover]` rule hides this with display:none until showPopover(); our padding / bg
|
|
57
|
+
only paint when the panel is in the top layer (already visible). `position: fixed` and the
|
|
58
|
+
inset are set by the JS positioning controller (overlay.ts) on open — we do NOT fix a position
|
|
59
|
+
here; the controller owns the placement completely. `margin: 0` prevents UA-default centering
|
|
60
|
+
from conflicting with the controller's explicit inset. */
|
|
61
|
+
:scope > [data-part='panel'] {
|
|
62
|
+
box-sizing: border-box;
|
|
63
|
+
margin: 0; /* the controller sets position:fixed + top/left — UA margin must not override */
|
|
64
|
+
min-inline-size: var(--ui-popover-min-inline-size);
|
|
65
|
+
|
|
66
|
+
background-color: var(--ui-popover-bg);
|
|
67
|
+
color: var(--ui-popover-ink);
|
|
68
|
+
border: 1px solid var(--ui-popover-outline);
|
|
69
|
+
border-radius: var(--ui-popover-radius);
|
|
70
|
+
padding: var(--ui-popover-padding);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* forced-colors (WHCM) — the surface, frame, and ink stay visible as system colours.
|
|
74
|
+
The opacity-based overlay wash (if any) is not used here — the surface is a solid
|
|
75
|
+
Canvas plane so the paint cannot defeat the forced-color Canvas base. */
|
|
76
|
+
@media (forced-colors: active) {
|
|
77
|
+
:scope > [data-part='panel'] {
|
|
78
|
+
background-color: Canvas;
|
|
79
|
+
color: CanvasText;
|
|
80
|
+
border-color: CanvasText;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type ReactiveProps } from '../../dom/index.js';
|
|
2
|
+
import { UIElement } from '../../dom/index.js';
|
|
3
|
+
import { type OverlayHandle } from '../../traits/overlay.js';
|
|
4
|
+
declare const props: {
|
|
5
|
+
open: {
|
|
6
|
+
reflect: true;
|
|
7
|
+
type: import("../../index.js").PropType<boolean>;
|
|
8
|
+
default: boolean;
|
|
9
|
+
attribute?: string | false;
|
|
10
|
+
};
|
|
11
|
+
placement: {
|
|
12
|
+
reflect: true;
|
|
13
|
+
type: import("../../index.js").PropType<"bottom-start" | "bottom-end" | "top-start" | "top-end" | "left-start" | "left-end" | "right-start" | "right-end">;
|
|
14
|
+
default: "bottom-start" | "bottom-end" | "top-start" | "top-end" | "left-start" | "left-end" | "right-start" | "right-end";
|
|
15
|
+
attribute?: string | false;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export interface UIPopoverElement extends ReactiveProps<typeof props> {
|
|
19
|
+
}
|
|
20
|
+
export declare class UIPopoverElement extends UIElement {
|
|
21
|
+
#private;
|
|
22
|
+
static props: {
|
|
23
|
+
open: {
|
|
24
|
+
reflect: true;
|
|
25
|
+
type: import("../../index.js").PropType<boolean>;
|
|
26
|
+
default: boolean;
|
|
27
|
+
attribute?: string | false;
|
|
28
|
+
};
|
|
29
|
+
placement: {
|
|
30
|
+
reflect: true;
|
|
31
|
+
type: import("../../index.js").PropType<"bottom-start" | "bottom-end" | "top-start" | "top-end" | "left-start" | "left-end" | "right-start" | "right-end">;
|
|
32
|
+
default: "bottom-start" | "bottom-end" | "top-start" | "top-end" | "left-start" | "left-end" | "right-start" | "right-end";
|
|
33
|
+
attribute?: string | false;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Protected overlay handle — accessible to test probes (C10 idempotent-cleanup DoD).
|
|
38
|
+
* Replaced on each reconnect (connected() re-runs); the old handle's cleanup fires via the scope
|
|
39
|
+
* effect disposer at disconnect before this is re-assigned.
|
|
40
|
+
*/
|
|
41
|
+
protected _overlayHandle: OverlayHandle | null;
|
|
42
|
+
protected connected(): void;
|
|
43
|
+
}
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// popover.ts — UIPopoverElement, the Wave-4 S1 disclosure-popover overlay control
|
|
2
|
+
// (control-suite-wave4-overlay.decomp.md S1 · overlay-controller.lld.md LLD-C1..C4 · ADR-0043).
|
|
3
|
+
//
|
|
4
|
+
// Composition: a `UIElement` host + `overlay(this, { popup, anchor, placement })` — the direct trait-call
|
|
5
|
+
// pattern (there is no `host.use()`; traits are invoked directly, like `trackUserInvalid`/`rovingFocus`) — the BARE
|
|
6
|
+
// overlay (Popover API surface + JS positioning + light-dismiss + two-way `open`, ADR-0019). NOT
|
|
7
|
+
// form-associated — no value/validity. A disclosure trigger (first element child, default slot)
|
|
8
|
+
// toggles a control-created surface panel part (`[popover]`).
|
|
9
|
+
//
|
|
10
|
+
// Two-way `open` (ADR-0019): a scope-owned effect drives model→overlay (open/close the handle);
|
|
11
|
+
// a `close` event listener on the host drives overlay→model (light-dismiss syncs the prop back).
|
|
12
|
+
// The overlay trait emits `close` + `toggle` on the host for EVERY real transition — platform
|
|
13
|
+
// dismiss, a trigger-click close, or a model-driven `open=false` alike (ADR-0101); this control
|
|
14
|
+
// listens for `close` and sets `this.open = false` so the prop stays consistent on the light-dismiss
|
|
15
|
+
// path (the commit/model paths already set the prop first).
|
|
16
|
+
//
|
|
17
|
+
// Anatomy (light-DOM, created once — idempotent across reconnect):
|
|
18
|
+
// <ui-popover>
|
|
19
|
+
// <button data-part="trigger" aria-expanded="…" aria-controls="ui-popover-panel-N">…</button>
|
|
20
|
+
// <div data-part="panel" popover="auto" id="ui-popover-panel-N" tabindex="-1">…</div>
|
|
21
|
+
// </ui-popover>
|
|
22
|
+
//
|
|
23
|
+
// The trigger is the first element child provided by the author. Remaining children are moved
|
|
24
|
+
// into the panel at connect time (like the modal's child-move pattern, ADR-0017). The overlay
|
|
25
|
+
// controller sets `popover="auto"` on the panel. `render()` stays the inherited VOID — the parts
|
|
26
|
+
// are created once and never re-created (re-creating would drop top-layer / focus state).
|
|
27
|
+
//
|
|
28
|
+
// Placement is fixed per connection: the overlay controller captures `prefPlacement` at call time.
|
|
29
|
+
// Changing `placement` after connect takes effect on the NEXT reconnect (documented limitation).
|
|
30
|
+
//
|
|
31
|
+
// ARIA: minimal via internals — the host has no explicit role (a logical disclosure wrapper); the
|
|
32
|
+
// trigger gets `aria-expanded` + `aria-controls` set directly (child attributes, not host internals).
|
|
33
|
+
// The panel gets `tabindex="-1"` so `moveFocusIn()` can land on it when there are no interactive
|
|
34
|
+
// descendants. `controls → dom → traits` is the one allowed import direction.
|
|
35
|
+
import { prop } from "../../dom/index.js";
|
|
36
|
+
import { UIElement } from "../../dom/index.js";
|
|
37
|
+
import { overlay } from "../../traits/overlay.js";
|
|
38
|
+
// ── Placement enum values (mirrors the OverlayPlacement union from overlay.ts) ─────────────────
|
|
39
|
+
const PLACEMENTS = [
|
|
40
|
+
'bottom-start',
|
|
41
|
+
'bottom-end',
|
|
42
|
+
'top-start',
|
|
43
|
+
'top-end',
|
|
44
|
+
'left-start',
|
|
45
|
+
'left-end',
|
|
46
|
+
'right-start',
|
|
47
|
+
'right-end',
|
|
48
|
+
];
|
|
49
|
+
// ── Props ────────────────────────────────────────────────────────────────────────────────────────
|
|
50
|
+
const props = {
|
|
51
|
+
// `open` — whether the popover panel is currently shown. Reflected (the [open] attribute keeps the
|
|
52
|
+
// host's declared state inspectable) and BINDABLE: the catalog declares value:{prop:'open',
|
|
53
|
+
// event:'toggle'} so the renderer two-way-binds it (ADR-0019). Drives the overlay handle.
|
|
54
|
+
open: { ...prop.boolean(false), reflect: true },
|
|
55
|
+
// `placement` — the preferred popup placement (flip + shift at viewport edges — LLD-C3). Captured
|
|
56
|
+
// once per connection; changing it after connect takes effect on the next reconnect. Reflected so
|
|
57
|
+
// `<ui-popover placement="top-end">` works declaratively.
|
|
58
|
+
placement: { ...prop.enum(PLACEMENTS, 'bottom-start'), reflect: true },
|
|
59
|
+
};
|
|
60
|
+
// ── Module-level stable-id counter (one per panel, never reused across instances) ──────────────
|
|
61
|
+
let _nextPanelId = 0;
|
|
62
|
+
export class UIPopoverElement extends UIElement {
|
|
63
|
+
static props = props;
|
|
64
|
+
// The control-created popover panel PART — created ONCE (idempotent guard in #ensureParts()) and
|
|
65
|
+
// NEVER re-created. Persists through disconnect/reconnect just like the modal dialog part.
|
|
66
|
+
#panel = null;
|
|
67
|
+
/**
|
|
68
|
+
* Protected overlay handle — accessible to test probes (C10 idempotent-cleanup DoD).
|
|
69
|
+
* Replaced on each reconnect (connected() re-runs); the old handle's cleanup fires via the scope
|
|
70
|
+
* effect disposer at disconnect before this is re-assigned.
|
|
71
|
+
*/
|
|
72
|
+
_overlayHandle = null;
|
|
73
|
+
connected() {
|
|
74
|
+
const { panel, trigger } = this.#ensureParts();
|
|
75
|
+
// Wire the overlay controller (LLD-C1..C4) — this is the PROOF of the overlay controller.
|
|
76
|
+
// placement is read ONCE per connection (the controller captures prefPlacement at call time).
|
|
77
|
+
const handle = overlay(this, {
|
|
78
|
+
popup: panel,
|
|
79
|
+
anchor: trigger,
|
|
80
|
+
placement: this.placement,
|
|
81
|
+
auto: true, // popover=auto → Escape + outside-click light-dismiss (LLD-C2)
|
|
82
|
+
focusOnOpen: true, // disclosure popover: focus moves into the panel on open (LLD-C4)
|
|
83
|
+
});
|
|
84
|
+
this._overlayHandle = handle;
|
|
85
|
+
// Trigger click → toggle the panel (the disclosure interaction).
|
|
86
|
+
//
|
|
87
|
+
// ADR-0101 erratum fix: flip the PROP (`this.open`), not `handle.toggle()` directly — see
|
|
88
|
+
// select.ts's click handler for the full race trace (identical mechanics here: a raw
|
|
89
|
+
// `handle.toggle()` never writes `this.open`, so any later programmatic close via the prop was
|
|
90
|
+
// a same-value no-op, the model→overlay effect never re-ran, and the panel could stick open).
|
|
91
|
+
// The prop is the single source of truth; the model→overlay effect below drives
|
|
92
|
+
// `handle.open()`/`handle.close()` and the trait announces (ADR-0101) — the combo-box pattern.
|
|
93
|
+
this.listen(trigger, 'click', () => {
|
|
94
|
+
this.open = !this.open;
|
|
95
|
+
});
|
|
96
|
+
// overlay→model: when the Popover API light-dismisses (Escape / outside-click), the overlay
|
|
97
|
+
// controller emits `close` on the host. Sync the prop back so the two-way bind stays consistent
|
|
98
|
+
// (ADR-0019 — the renderer reads `toggle`, but we need `open` to already be false by then).
|
|
99
|
+
// At the time this listener fires, the overlay's internal `isOpen` is already false (the toggle
|
|
100
|
+
// listener in overlay.ts ran first), so the subsequent effect re-run calls `handle.close()` as
|
|
101
|
+
// a no-op (the idempotent guard in overlay.ts: `if (cleaned || !isOpen) return`).
|
|
102
|
+
this.listen(this, 'close', () => {
|
|
103
|
+
this.open = false;
|
|
104
|
+
});
|
|
105
|
+
// model→overlay: a scope-owned effect drives open/close from the prop and keeps aria-expanded in
|
|
106
|
+
// sync. Runs immediately on creation (eager first run): open=false → handle.close() (no-op, not
|
|
107
|
+
// yet open) + aria-expanded='false'. Re-runs whenever this.open changes.
|
|
108
|
+
this.effect(() => {
|
|
109
|
+
const isOpen = this.open;
|
|
110
|
+
if (isOpen)
|
|
111
|
+
handle.open();
|
|
112
|
+
else
|
|
113
|
+
handle.close();
|
|
114
|
+
trigger.setAttribute('aria-expanded', String(isOpen));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Create the control's two light-DOM parts ONCE (idempotent across disconnect/reconnect):
|
|
119
|
+
* - trigger: the first element child, marked with `data-part="trigger"` + ARIA affordances.
|
|
120
|
+
* - panel: a `<div data-part="panel">` appended to the host; non-trigger children are moved in.
|
|
121
|
+
* The overlay controller sets `popover="auto"` on the panel. `render()` stays the inherited VOID.
|
|
122
|
+
*/
|
|
123
|
+
#ensureParts() {
|
|
124
|
+
if (this.#panel) {
|
|
125
|
+
// Parts persist through disconnect/reconnect — return the existing ones.
|
|
126
|
+
const trigger = this.querySelector('[data-part="trigger"]');
|
|
127
|
+
// The trigger must still be present (it's a light-DOM child, so it travels with the host).
|
|
128
|
+
if (!trigger)
|
|
129
|
+
throw new Error('ui-popover: trigger part lost on reconnect');
|
|
130
|
+
return { panel: this.#panel, trigger };
|
|
131
|
+
}
|
|
132
|
+
// Identify the disclosure trigger — the first element child provided by the author.
|
|
133
|
+
const trigger = this.firstElementChild;
|
|
134
|
+
if (!trigger) {
|
|
135
|
+
throw new Error('ui-popover: provide a trigger as the first child (e.g. <button>) before the panel content');
|
|
136
|
+
}
|
|
137
|
+
trigger.setAttribute('data-part', 'trigger');
|
|
138
|
+
// Create the surface panel. The overlay controller sets popover="auto" via
|
|
139
|
+
// `popup.setAttribute('popover', 'auto')` — we do NOT set it here (single-ownership, ADR-0017).
|
|
140
|
+
const panel = document.createElement('div');
|
|
141
|
+
panel.setAttribute('data-part', 'panel');
|
|
142
|
+
// tabindex="-1" lets `moveFocusIn()` (overlay LLD-C4) land on the panel itself when there are
|
|
143
|
+
// no interactive descendants — the fallback `popup.focus()` requires the element to be focusable.
|
|
144
|
+
panel.setAttribute('tabindex', '-1');
|
|
145
|
+
// Stable id for the trigger's `aria-controls` (created once, never reused).
|
|
146
|
+
panel.id = `ui-popover-panel-${++_nextPanelId}`;
|
|
147
|
+
// Move all non-trigger children into the panel (the disclosure content lives in the top-layer
|
|
148
|
+
// surface, not beside the trigger in the document flow — mirrors the modal child-move pattern).
|
|
149
|
+
let node = trigger.nextSibling;
|
|
150
|
+
while (node) {
|
|
151
|
+
const next = node.nextSibling;
|
|
152
|
+
panel.appendChild(node);
|
|
153
|
+
node = next;
|
|
154
|
+
}
|
|
155
|
+
this.appendChild(panel);
|
|
156
|
+
this.#panel = panel;
|
|
157
|
+
// Wire the trigger's ARIA affordances (present from the first render — the effect wires
|
|
158
|
+
// aria-expanded after connect, but aria-controls is stable and set here once).
|
|
159
|
+
trigger.setAttribute('aria-controls', panel.id);
|
|
160
|
+
return { panel, trigger };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (!customElements.get('ui-popover'))
|
|
164
|
+
customElements.define('ui-popover', UIPopoverElement);
|