@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,84 @@
|
|
|
1
|
+
import type { ReactiveProps } from '../../dom/index.js';
|
|
2
|
+
import { UIContainerElement } from '../../dom/container.js';
|
|
3
|
+
declare const props: {
|
|
4
|
+
align: {
|
|
5
|
+
default: "stretch";
|
|
6
|
+
reflect: true;
|
|
7
|
+
type: import("../../index.js").PropType<"end" | "start" | "center" | "stretch" | "baseline">;
|
|
8
|
+
attribute?: string | false;
|
|
9
|
+
};
|
|
10
|
+
justify: {
|
|
11
|
+
reflect: true;
|
|
12
|
+
type: import("../../index.js").PropType<"end" | "start" | "center" | "between" | "around" | "evenly">;
|
|
13
|
+
default: "end" | "start" | "center" | "between" | "around" | "evenly";
|
|
14
|
+
attribute?: string | false;
|
|
15
|
+
};
|
|
16
|
+
gap: {
|
|
17
|
+
reflect: true;
|
|
18
|
+
type: import("../../index.js").PropType<"none" | "sm" | "md" | "lg" | "xs" | "xl" | "2xl">;
|
|
19
|
+
default: "none" | "sm" | "md" | "lg" | "xs" | "xl" | "2xl";
|
|
20
|
+
attribute?: string | false;
|
|
21
|
+
};
|
|
22
|
+
wrap: {
|
|
23
|
+
reflect: true;
|
|
24
|
+
type: import("../../index.js").PropType<boolean>;
|
|
25
|
+
default: boolean;
|
|
26
|
+
attribute?: string | false;
|
|
27
|
+
};
|
|
28
|
+
elevation: {
|
|
29
|
+
reflect: true;
|
|
30
|
+
type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
|
|
31
|
+
default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
|
|
32
|
+
attribute?: string | false;
|
|
33
|
+
};
|
|
34
|
+
brightness: {
|
|
35
|
+
reflect: true;
|
|
36
|
+
type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
|
|
37
|
+
default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
|
|
38
|
+
attribute?: string | false;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
export interface UIListElement extends ReactiveProps<typeof props> {
|
|
42
|
+
}
|
|
43
|
+
export declare class UIListElement extends UIContainerElement {
|
|
44
|
+
static props: {
|
|
45
|
+
align: {
|
|
46
|
+
default: "stretch";
|
|
47
|
+
reflect: true;
|
|
48
|
+
type: import("../../index.js").PropType<"end" | "start" | "center" | "stretch" | "baseline">;
|
|
49
|
+
attribute?: string | false;
|
|
50
|
+
};
|
|
51
|
+
justify: {
|
|
52
|
+
reflect: true;
|
|
53
|
+
type: import("../../index.js").PropType<"end" | "start" | "center" | "between" | "around" | "evenly">;
|
|
54
|
+
default: "end" | "start" | "center" | "between" | "around" | "evenly";
|
|
55
|
+
attribute?: string | false;
|
|
56
|
+
};
|
|
57
|
+
gap: {
|
|
58
|
+
reflect: true;
|
|
59
|
+
type: import("../../index.js").PropType<"none" | "sm" | "md" | "lg" | "xs" | "xl" | "2xl">;
|
|
60
|
+
default: "none" | "sm" | "md" | "lg" | "xs" | "xl" | "2xl";
|
|
61
|
+
attribute?: string | false;
|
|
62
|
+
};
|
|
63
|
+
wrap: {
|
|
64
|
+
reflect: true;
|
|
65
|
+
type: import("../../index.js").PropType<boolean>;
|
|
66
|
+
default: boolean;
|
|
67
|
+
attribute?: string | false;
|
|
68
|
+
};
|
|
69
|
+
elevation: {
|
|
70
|
+
reflect: true;
|
|
71
|
+
type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
|
|
72
|
+
default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
|
|
73
|
+
attribute?: string | false;
|
|
74
|
+
};
|
|
75
|
+
brightness: {
|
|
76
|
+
reflect: true;
|
|
77
|
+
type: import("../../index.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
|
|
78
|
+
default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
|
|
79
|
+
attribute?: string | false;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
protected connected(): void;
|
|
83
|
+
}
|
|
84
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// list.ts — UIListElement, the semantic vertical-stack layout primitive (goals.md §G9 / ADR-0016 cl.3;
|
|
2
|
+
// decomp g9-containers slice s5). BEHAVIOUR + props + the list role + self-define ONLY; the flex layout +
|
|
3
|
+
// surface seam live in list.css.
|
|
4
|
+
//
|
|
5
|
+
// `ui-list` is a `ui-column` SPECIALIZATION that adds LIST SEMANTICS: it is a `display:flex` vertical stack
|
|
6
|
+
// (the surface base `UIContainerElement`, ADR-0015 — NOT form-associated) whose host carries the ARIA
|
|
7
|
+
// `role="list"`. The role is set THROUGH the protected `internals` (the family discipline, ADR-0010 /
|
|
8
|
+
// element.ts) — NEVER a host `role` attribute, so the host stays free of `role`/`aria-*`. The list imposes
|
|
9
|
+
// NO item element: its children are the list items, composed by the agent as a ChildList (the A2UI child
|
|
10
|
+
// model) — so `render()` stays the inherited VOID (returning nothing → the host render effect commits
|
|
11
|
+
// nothing → the user's light-DOM children are never clobbered).
|
|
12
|
+
//
|
|
13
|
+
// It consumes the two shared spreadable prop sets the container family declares (no static-props prototype
|
|
14
|
+
// merge — the ADR-0013 `formProps` precedent): `surfaceProps` (elevation/brightness, the surface axes) and
|
|
15
|
+
// `flexProps` (align/justify/gap/wrap, the A2UI-faithful layout grammar). The literal-union → CSS-keyword
|
|
16
|
+
// mapping is a role-pure repoint in list.css's `@scope` block; the base owns no flex/surface CSS.
|
|
17
|
+
//
|
|
18
|
+
// IS an A2UI catalog type — `List` (ADR-0087 Fork A, Wave C; supersedes ADR-0016's earlier non-catalog
|
|
19
|
+
// exclusion) renders to `ui-list`. Reach for `List` over `Column` for a homogeneous, itemized collection where
|
|
20
|
+
// list semantics matter to assistive tech; reach for `Row`/`Column` for a heterogeneous arrangement, and
|
|
21
|
+
// `Grid` when the layout should reflow its column count responsively (a2ui-catalog.spec.md §5.2).
|
|
22
|
+
//
|
|
23
|
+
// Imports the surface base from the same-layer dom `../../dom/container.ts` (not re-exported from the dom
|
|
24
|
+
// barrel until s12) + the typed-prop authoring types from the dom barrel. `controls → dom` is the allowed
|
|
25
|
+
// import direction (the layering law: reactive ← dom ← controls).
|
|
26
|
+
import { UIContainerElement } from "../../dom/container.js";
|
|
27
|
+
// The full prop surface = the two shared spreadable sets folded into this control's own `static props` (each
|
|
28
|
+
// already carries `reflect: true` from the base, so the `[elevation]`/`[align]`/etc. attribute selectors in
|
|
29
|
+
// list.css apply to JS-set values too). No re-declaration of the surface/flex grammar — one home, ADR-0016.
|
|
30
|
+
// ADR-0030: `align` default overridden from `start` to `stretch` (parity with ui-column — a ui-list IS a
|
|
31
|
+
// ui-column specialization; its cross axis is inline/width; children should fill the width by default).
|
|
32
|
+
const props = {
|
|
33
|
+
...UIContainerElement.surfaceProps, // elevation / brightness (ADR-0015)
|
|
34
|
+
...UIContainerElement.flexProps, // align / justify / gap / wrap (ADR-0016) — a vertical stack
|
|
35
|
+
align: { ...UIContainerElement.flexProps.align, default: 'stretch' }, // ADR-0030: override default ONLY; `as const` preserves the literal-union type (prevents string widening)
|
|
36
|
+
};
|
|
37
|
+
export class UIListElement extends UIContainerElement {
|
|
38
|
+
static props = props;
|
|
39
|
+
connected() {
|
|
40
|
+
// LIST SEMANTICS — `role=list` rides the host's ElementInternals, never a host `role` attribute (the
|
|
41
|
+
// family discipline). A constant semantic role, so it is set DIRECTLY (not in an effect); re-set on each
|
|
42
|
+
// connect (idempotent). The children are the list items — no item element is imposed, and `render()`
|
|
43
|
+
// stays void so the ChildList children flow through untouched.
|
|
44
|
+
this.internals.role = 'list';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!customElements.get('ui-list'))
|
|
48
|
+
customElements.define('ui-list', UIListElement);
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/* menu.css — ui-menu, the single-file overlay-menu stylesheet (ADR-0003 · overlay-controller.lld.md).
|
|
2
|
+
* Two clearly-SECTIONED blocks, mirroring popover.css / modal.css:
|
|
3
|
+
*
|
|
4
|
+
* [1] TOKEN BLOCK — `:where(ui-menu)` (specificity 0,0,0) sets the menu's OWN default surface
|
|
5
|
+
* tokens and DECLARES the `--ui-menu-*` chain from the colour roles (`--md-sys-color-{family}-{role}`)
|
|
6
|
+
* and the dimensions (`--md-sys-space-*` / `--md-sys-shape-corner-base`). Raw `--md-sys-color-*` roles enter ONLY here;
|
|
7
|
+
* `@scope` never reads them.
|
|
8
|
+
* [2] STYLES BLOCK — `@scope (ui-menu)` CONSUMES only the role-pure `--ui-menu-*` chain.
|
|
9
|
+
* The host is `display: contents` — a logical overlay wrapper that generates no box.
|
|
10
|
+
* The trigger child stays in the document flow (the author provides it; we mark it but do
|
|
11
|
+
* not style it — it is the author's to own). The panel part (`[data-part="panel"]`) is the
|
|
12
|
+
* Container/surface: bg from `--ui-menu-bg`, `--md-sys-shape-corner-base`, `--md-sys-space-xs` padding
|
|
13
|
+
* shell — NOT a control height (`--md-sys-height-*` is never read by the panel itself).
|
|
14
|
+
* Menu items (`[role="menuitem"]`) use the LEGACY ITEM-PAD geometry (§4.6/§5.1 of the
|
|
15
|
+
* listbox-roving LLD): vertical block padding (`--md-sys-space-xs`) + inline padding
|
|
16
|
+
* (`--md-sys-space-md`), giving a comfortable row height without a fixed --md-sys-height-* frame.
|
|
17
|
+
* A `min-inline-size` floor prevents panel collapse (ADR-0021 trap). A
|
|
18
|
+
* `forced-color-adjust` block keeps the surface and highlighted items visible in WHCM.
|
|
19
|
+
*
|
|
20
|
+
* Standalone stylesheet — NOT injected from menu.ts (behaviour-only `.ts`, no runtime injection
|
|
21
|
+
* per plan §2). Linked via the component-styles barrel (s12); the barrel @imports foundation
|
|
22
|
+
* roles FIRST.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/* ════════ [1] TOKEN BLOCK — :where(ui-menu) declares the surface default + --ui-menu-* ════════ */
|
|
26
|
+
|
|
27
|
+
:where(ui-menu) {
|
|
28
|
+
/* Surface plane — the popup panel is an opaque neutral surface (like popover / card). */
|
|
29
|
+
--ui-menu-bg: var(--md-sys-color-neutral-surface);
|
|
30
|
+
--ui-menu-ink: var(--md-sys-color-neutral-on-surface);
|
|
31
|
+
|
|
32
|
+
/* Frame */
|
|
33
|
+
--ui-menu-outline: var(--md-sys-color-neutral-outline-variant);
|
|
34
|
+
--ui-menu-radius: var(--md-sys-shape-corner-base);
|
|
35
|
+
|
|
36
|
+
/* Shell spacing — panel-class geometry (Container/surface; NOT a control height). */
|
|
37
|
+
--ui-menu-padding: var(--md-sys-space-xs);
|
|
38
|
+
|
|
39
|
+
/* Size floor — prevents collapse when the menu has no items or all items are narrow (ADR-0021
|
|
40
|
+
lesson: a surface that can be empty / narrow needs a min-inline-size; the JS positioning
|
|
41
|
+
controller measures bounds on open, so a zero-width panel would mis-position too). */
|
|
42
|
+
--ui-menu-min-inline-size: 10rem;
|
|
43
|
+
|
|
44
|
+
/* Menu-item row geometry — the LEGACY ITEM-PAD (§4.6/§5.1, NOT the widget-box height).
|
|
45
|
+
A row is: vertical block padding + inline padding, giving a comfortable reading row.
|
|
46
|
+
STRUCTURAL DIVERGENCE from ui-select/ui-combo-box (flagged, not forced to fit — 2026-07-06
|
|
47
|
+
size-carrying-model pass): ui-menu has NO `[size]` attribute and renders NO trigger geometry
|
|
48
|
+
at all — the trigger is entirely author-owned content (menu.ts marks it but never sizes it,
|
|
49
|
+
unlike select's own <button data-part=trigger>). There is no `--ui-menu-height` to derive
|
|
50
|
+
against and no h/2 alignment target, so the select "listbox-padding + option-inline == h/2"
|
|
51
|
+
law does not have an analogue here. The item-pad stays its own fixed (`--md-sys-space-*`-scaled)
|
|
52
|
+
register — correct per ADR-0043 cl.5 / geometry.md's Pattern-class row, not a gap to close.
|
|
53
|
+
Adding a `[size]` axis to ui-menu would be a new-attribute contract change (its own decomp/ADR),
|
|
54
|
+
not a CSS-only geometry fix. */
|
|
55
|
+
--ui-menu-item-pad-block: var(--md-sys-space-xs);
|
|
56
|
+
--ui-menu-item-pad-inline: var(--md-sys-space-md);
|
|
57
|
+
--ui-menu-item-font: var(--md-sys-font-md);
|
|
58
|
+
|
|
59
|
+
/* Item row block-size — the REAL rendered row height (font + 2×block-pad; `line-height: 1` on
|
|
60
|
+
the item rule below, the same local derivation choice select/combo-box's option rows already
|
|
61
|
+
make, is what keeps this exact rather than font-metric-dependent). Feeds --ui-menu-max-block-size
|
|
62
|
+
below (TKT-0027) — there is no --ui-menu-height to reuse (the note above), so this is the
|
|
63
|
+
control's OWN public row-height fact, the item-pad siblings' geometry class. */
|
|
64
|
+
--ui-menu-item-block-size: calc(var(--ui-menu-item-font) + 2 * var(--ui-menu-item-pad-block));
|
|
65
|
+
|
|
66
|
+
/* Panel max-block-size (TKT-0027) — public dial, default min(50vh, 12 real item rows): the
|
|
67
|
+
smaller of half the viewport or a scannable dozen rows, so a short list never over-reserves
|
|
68
|
+
and a long list caps at a page instead of the old flat 40vh magic number. 12 rows = 12 item
|
|
69
|
+
boxes + 13 inset margins (leading + 11 collapsed inter-row + trailing) — the panel is
|
|
70
|
+
`[data-box]` (a BFC, container-box.css), which collapses adjacent siblings' block margins to
|
|
71
|
+
ONE inset rather than doubling them. Rides density naturally: --ui-menu-item-block-size reads
|
|
72
|
+
--md-sys-space-xs (rhythm-family, density-linked), so a denser/spacious theme repoints this dial
|
|
73
|
+
for free without a special case here. + 2px — the panel's border-box: max-block-size caps the
|
|
74
|
+
BORDER box (box-sizing: border-box below), but the row math above computed the CONTENT box
|
|
75
|
+
(12 rows + 13 insets, no border term) — without this the 12th row overflowed its own cap by
|
|
76
|
+
exactly the 1px+1px border width (FIXED 2026-07-12, TKT-0027 browser-leg finding). */
|
|
77
|
+
--ui-menu-max-block-size: min(50vh, calc(12 * var(--ui-menu-item-block-size) + 13 * var(--ui-box-inset, 0.375rem) + 2px));
|
|
78
|
+
|
|
79
|
+
/* One-level nested radius: item corners inset from the panel corner (ADR-0018). FIXED
|
|
80
|
+
(2026-07-06): was subtracting --md-sys-space-xs (4px), a DIFFERENT value than the --ui-box-inset
|
|
81
|
+
(6px, container-box.css) actually used for the item's own margin below — the nested-radius
|
|
82
|
+
law (ADR-0018: child radius = parent radius − the REAL inset) was inset-inconsistent. Now
|
|
83
|
+
subtracts the same --ui-box-inset the row margin reads. */
|
|
84
|
+
--ui-menu-item-radius: calc(var(--md-sys-shape-corner-base) - var(--ui-box-inset, 0.375rem));
|
|
85
|
+
|
|
86
|
+
/* Item state colours — hover fill + disabled muted ink.
|
|
87
|
+
FIXED (2026-07-07, overlay-list family-consistency pass): was --md-sys-color-neutral-tint-dimmer
|
|
88
|
+
(a translucent 10% alpha wash) — the ONLY row-hover in the fleet using the tint ladder; every
|
|
89
|
+
other interactive-row/control hover (ui-select's option, ui-button, ui-text-field, ui-checkbox,
|
|
90
|
+
ui-switch, ui-calendar's cell) reads the solid, scheme-inverting --md-sys-color-neutral-surface-high
|
|
91
|
+
role. ui-menu's translucent wash read as a visibly weaker/different hover than ui-select's listbox
|
|
92
|
+
right next to it (the reported inconsistency) — converged onto the existing fleet-wide role,
|
|
93
|
+
no new token minted. */
|
|
94
|
+
--ui-menu-item-bg-hover: var(--md-sys-color-neutral-surface-high);
|
|
95
|
+
--ui-menu-item-ink-disabled: var(--md-sys-color-neutral-outline); /* muted, not invisible */
|
|
96
|
+
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* ════════ [2] STYLES BLOCK — @scope (ui-menu) consumes ONLY --ui-menu-* ════════ */
|
|
100
|
+
|
|
101
|
+
@scope (ui-menu) {
|
|
102
|
+
/* The host is a logical disclosure wrapper — it generates no box; only the trigger (in flow)
|
|
103
|
+
and the panel (in the top layer when open) render as visible surfaces. */
|
|
104
|
+
:scope {
|
|
105
|
+
display: contents;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* The menu panel — Container/surface in the Popover API top layer.
|
|
109
|
+
`position: fixed` and the inset are set by the JS positioning controller (overlay.ts) on open;
|
|
110
|
+
we do NOT fix a position here. `margin: 0` prevents UA-default centering from conflicting with
|
|
111
|
+
the controller's explicit inset. The panel is hidden by the UA `[popover]` rule until
|
|
112
|
+
showPopover() places it in the top layer — our CSS only applies when it is visible. */
|
|
113
|
+
:scope > [data-part='panel'] {
|
|
114
|
+
box-sizing: border-box;
|
|
115
|
+
margin: 0;
|
|
116
|
+
min-inline-size: var(--ui-menu-min-inline-size);
|
|
117
|
+
max-block-size: var(--ui-menu-max-block-size); /* TKT-0027 — public dial, default min(50vh, 12 item rows) */
|
|
118
|
+
overflow-y: auto;
|
|
119
|
+
padding: 0; /* the box-model menuitem margins provide the inset (container-box.css); no shell padding */
|
|
120
|
+
|
|
121
|
+
background-color: var(--ui-menu-bg);
|
|
122
|
+
color: var(--ui-menu-ink);
|
|
123
|
+
border: 1px solid var(--ui-menu-outline);
|
|
124
|
+
border-radius: var(--ui-menu-radius);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Menu-item rows — the LEGACY ITEM-PAD row (§4.6/§5.1). NOT --md-sys-height-* (a menuitem is a
|
|
128
|
+
list row, not a single-line fixed-height control widget). cursor + user-select mirror the
|
|
129
|
+
interactive-row standard (pointer for items, not-allowed for disabled). */
|
|
130
|
+
:scope [role='menuitem'] {
|
|
131
|
+
box-sizing: border-box;
|
|
132
|
+
display: flex;
|
|
133
|
+
align-items: center;
|
|
134
|
+
margin: var(--ui-box-inset, 0.375rem); /* the box-model inset — 6px from the panel edge + between rows */
|
|
135
|
+
padding-block: var(--ui-menu-item-pad-block);
|
|
136
|
+
padding-inline: var(--ui-menu-item-pad-inline);
|
|
137
|
+
/* line-height: 1 makes the --ui-menu-item-block-size calc exact (font + 2×pad == the rendered
|
|
138
|
+
row) — the same local derivation choice select/combo-box's option rows already make for the
|
|
139
|
+
identical reason (TKT-0027: a font-metric-dependent 'normal' line-height would make the
|
|
140
|
+
12-fit/13-scroll cross-engine proof flaky). */
|
|
141
|
+
line-height: 1;
|
|
142
|
+
border-radius: var(--ui-menu-item-radius);
|
|
143
|
+
font-size: var(--ui-menu-item-font);
|
|
144
|
+
color: var(--ui-menu-ink);
|
|
145
|
+
cursor: default;
|
|
146
|
+
user-select: none;
|
|
147
|
+
-webkit-user-select: none; /* WebKit honours selection-none only via the prefix */
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* Hover state — the fleet-wide solid elevated-surface fill (no border — like native menus). */
|
|
151
|
+
:scope [role='menuitem']:not([disabled]):not([aria-disabled='true']):hover {
|
|
152
|
+
background-color: var(--ui-menu-item-bg-hover);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* Keyboard focus ring (ADR-0009) — reads fleet tokens directly (the documented family exception:
|
|
156
|
+
every control draws the identical ring; see button.css for the precedent). Outset (positive
|
|
157
|
+
offset), matching ui-select's option ring and the fleet-wide convention (button/checkbox/
|
|
158
|
+
text-field/select/etc. all use the positive offset; only ui-tabs' inline tab-strip is inset,
|
|
159
|
+
for a different reason — adjacent siblings with no margin). FIXED (2026-07-07,
|
|
160
|
+
family-consistency pass): was the inset (negative-offset) treatment — the item's own
|
|
161
|
+
--ui-box-inset margin (≥6px) comfortably clears an outset ring (width+offset = 4px) without
|
|
162
|
+
touching the panel edge or a neighbouring row. */
|
|
163
|
+
:scope [role='menuitem']:not([disabled]):not([aria-disabled='true']):focus-visible {
|
|
164
|
+
outline: var(--md-sys-state-focus-ring-width) solid var(--md-sys-color-focus-ring);
|
|
165
|
+
outline-offset: var(--md-sys-state-focus-ring-offset);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* Disabled items — muted ink, no pointer, inert to hover/focus. */
|
|
169
|
+
:scope [role='menuitem'][disabled],
|
|
170
|
+
:scope [role='menuitem'][aria-disabled='true'] {
|
|
171
|
+
color: var(--ui-menu-item-ink-disabled);
|
|
172
|
+
cursor: not-allowed;
|
|
173
|
+
pointer-events: none;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* Forced-colors (WHCM) — the surface, frame, and ink survive as system Canvas/CanvasText.
|
|
177
|
+
Hover + focus items use system Highlight/HighlightText so they remain visible.
|
|
178
|
+
`forced-color-adjust: none` on the item hover/focus block keeps the fill from being
|
|
179
|
+
overridden (the platform would discard the solid surface-high fill; we commit to Highlight instead). */
|
|
180
|
+
@media (forced-colors: active) {
|
|
181
|
+
:scope > [data-part='panel'] {
|
|
182
|
+
background-color: Canvas;
|
|
183
|
+
color: CanvasText;
|
|
184
|
+
border-color: CanvasText;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
:scope [role='menuitem']:not([disabled]):not([aria-disabled='true']):hover,
|
|
188
|
+
:scope [role='menuitem']:not([disabled]):not([aria-disabled='true']):focus-visible {
|
|
189
|
+
forced-color-adjust: none;
|
|
190
|
+
background-color: Highlight;
|
|
191
|
+
color: HighlightText;
|
|
192
|
+
outline: none;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -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 UIMenuElement extends ReactiveProps<typeof props> {
|
|
19
|
+
}
|
|
20
|
+
export declare class UIMenuElement 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
|
|
39
|
+
* scope 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,248 @@
|
|
|
1
|
+
// menu.ts — UIMenuElement, the Wave-4 S3 menu overlay control
|
|
2
|
+
// (control-suite-wave4-overlay.decomp.md S3 · overlay-controller.lld.md LLD-C1..C4 · ADR-0043).
|
|
3
|
+
//
|
|
4
|
+
// Composition: a `UIElement` host + `overlay()` (Popover API + JS positioning + two-way `open`) +
|
|
5
|
+
// `rovingFocus()` over [role=menuitem] items (Arrow roves focus; type-ahead; Enter/click commits).
|
|
6
|
+
// NOT form-associated — ui-menu emits an action (`select` with {value,index}), carrying NO form
|
|
7
|
+
// value. Like ui-tabs (non-value commit→select), but in an overlay. A trigger (first element child,
|
|
8
|
+
// marked `data-part="trigger"`) opens the panel; commit closes it.
|
|
9
|
+
//
|
|
10
|
+
// Anatomy (light-DOM, created once — idempotent across reconnect):
|
|
11
|
+
// <ui-menu>
|
|
12
|
+
// <button data-part="trigger" aria-expanded="…" aria-controls="ui-menu-panel-N"
|
|
13
|
+
// aria-haspopup="menu">…</button>
|
|
14
|
+
// <div data-part="panel" role="menu" popover="auto" id="ui-menu-panel-N" tabindex="-1">
|
|
15
|
+
// <!-- author-provided item children moved in at connect; auto-get role=menuitem if absent -->
|
|
16
|
+
// <div role="menuitem" tabindex="-1">Item 1</div>
|
|
17
|
+
// <div role="menuitem" tabindex="-1">Item 2</div>
|
|
18
|
+
// </div>
|
|
19
|
+
// </ui-menu>
|
|
20
|
+
//
|
|
21
|
+
// Two-way `open` (ADR-0019): a scope-owned effect drives model→overlay; the overlay controller's
|
|
22
|
+
// `close` event listener drives overlay→model (light-dismiss syncs the prop back). The overlay
|
|
23
|
+
// trait emits `close` + `toggle` on EVERY real open-state transition — platform dismiss (Escape /
|
|
24
|
+
// outside-click), a commit's programmatic close, or a model-driven `open=false` alike (ADR-0101 —
|
|
25
|
+
// native ToggleEvent timing fidelity; the trait is the sole, uniform announcer for the family).
|
|
26
|
+
//
|
|
27
|
+
// Events: `select` ({value, index}) on commit; `toggle` + `close` on every real close, including a
|
|
28
|
+
// commit's programmatic close (ADR-0101 — see traits/overlay.ts).
|
|
29
|
+
//
|
|
30
|
+
// ARIA: host has no explicit role (logical wrapper); panel has `role=menu` set on the div part
|
|
31
|
+
// directly (a created part, not the host — the FACE rule "internals.role, never host attributes"
|
|
32
|
+
// applies to the HOST element; direct parts use setAttribute). The trigger gets `aria-expanded`,
|
|
33
|
+
// `aria-controls`, `aria-haspopup="menu"`. Item children auto-get `role=menuitem` if absent.
|
|
34
|
+
//
|
|
35
|
+
// `controls → dom → traits` is the one allowed import direction.
|
|
36
|
+
import { prop } from "../../dom/index.js";
|
|
37
|
+
import { UIElement } from "../../dom/index.js";
|
|
38
|
+
import { overlay } from "../../traits/overlay.js";
|
|
39
|
+
import { rovingFocus } from "../../traits/roving-focus.js";
|
|
40
|
+
import { scrollFade } from "../../traits/scroll-fade.js";
|
|
41
|
+
// ── Placement enum values (mirrors the OverlayPlacement union from overlay.ts) ─────────────────
|
|
42
|
+
const PLACEMENTS = [
|
|
43
|
+
'bottom-start',
|
|
44
|
+
'bottom-end',
|
|
45
|
+
'top-start',
|
|
46
|
+
'top-end',
|
|
47
|
+
'left-start',
|
|
48
|
+
'left-end',
|
|
49
|
+
'right-start',
|
|
50
|
+
'right-end',
|
|
51
|
+
];
|
|
52
|
+
// ── Props ────────────────────────────────────────────────────────────────────────────────────────
|
|
53
|
+
const props = {
|
|
54
|
+
// `open` — whether the menu panel is shown. Reflected + BINDABLE: the catalog declares
|
|
55
|
+
// value:{prop:'open',event:'toggle'} so the renderer two-way-binds it (ADR-0019). Drives the
|
|
56
|
+
// overlay handle via a scope-owned effect.
|
|
57
|
+
open: { ...prop.boolean(false), reflect: true },
|
|
58
|
+
// `placement` — the preferred panel placement relative to the trigger (captured once per
|
|
59
|
+
// connection; changing it after connect takes effect on the next reconnect).
|
|
60
|
+
placement: { ...prop.enum(PLACEMENTS, 'bottom-start'), reflect: true },
|
|
61
|
+
};
|
|
62
|
+
// ── Module-level stable-id counter (one per panel, never reused) ─────────────────────────────────
|
|
63
|
+
let _nextPanelId = 0;
|
|
64
|
+
export class UIMenuElement extends UIElement {
|
|
65
|
+
static props = props;
|
|
66
|
+
// The control-created panel PART — created ONCE (idempotent guard in #ensureParts()) and
|
|
67
|
+
// NEVER re-created. Persists through disconnect/reconnect (like the modal/popover panel parts).
|
|
68
|
+
#panel = null;
|
|
69
|
+
/**
|
|
70
|
+
* Protected overlay handle — accessible to test probes (C10 idempotent-cleanup DoD).
|
|
71
|
+
* Replaced on each reconnect (connected() re-runs); the old handle's cleanup fires via the
|
|
72
|
+
* scope effect disposer at disconnect before this is re-assigned.
|
|
73
|
+
*/
|
|
74
|
+
_overlayHandle = null;
|
|
75
|
+
connected() {
|
|
76
|
+
const { panel, trigger, items } = this.#ensureParts();
|
|
77
|
+
// Wire the overlay controller (LLD-C1..C4) — popover=auto (Escape + outside-click
|
|
78
|
+
// light-dismiss), focusOnOpen=true (focus moves INTO the menu on open, restored to trigger
|
|
79
|
+
// on close by the overlay handle).
|
|
80
|
+
const handle = overlay(this, {
|
|
81
|
+
popup: panel,
|
|
82
|
+
anchor: trigger,
|
|
83
|
+
placement: this.placement,
|
|
84
|
+
auto: true,
|
|
85
|
+
focusOnOpen: true,
|
|
86
|
+
});
|
|
87
|
+
this._overlayHandle = handle;
|
|
88
|
+
// Trigger click → toggle the panel.
|
|
89
|
+
//
|
|
90
|
+
// ADR-0101 erratum fix: flip the PROP (`this.open`), not `handle.toggle()` directly — see
|
|
91
|
+
// select.ts's click handler for the full race trace (identical mechanics here: a raw
|
|
92
|
+
// `handle.toggle()` never writes `this.open`, so `#commit`'s later `this.open = false` was a
|
|
93
|
+
// same-value no-op and the panel stuck open after picking a menu item — ticket #28's residual).
|
|
94
|
+
// The prop is the single source of truth; the model→overlay effect below drives
|
|
95
|
+
// `handle.open()`/`handle.close()` and the trait announces (ADR-0101) — the combo-box pattern.
|
|
96
|
+
this.listen(trigger, 'click', () => {
|
|
97
|
+
this.open = !this.open;
|
|
98
|
+
});
|
|
99
|
+
// overlay→model: when the Popover API light-dismisses (Escape / outside-click), the overlay
|
|
100
|
+
// controller emits `close` on the host. Sync the prop back so the two-way bind stays
|
|
101
|
+
// consistent (ADR-0019 — the renderer reads `toggle`, but `open` must be false by then).
|
|
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
|
|
106
|
+
// in sync. Runs immediately on creation (eager first run): open=false → handle.close() (no-op,
|
|
107
|
+
// not 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
|
+
// Delegated click on the panel → commit the clicked menuitem. Handles mouse clicks AND
|
|
117
|
+
// keyboard-activated button items (Enter/Space on a <button> fires a native click event).
|
|
118
|
+
this.listen(panel, 'click', (ev) => {
|
|
119
|
+
const target = ev.target;
|
|
120
|
+
const list = items();
|
|
121
|
+
const index = list.findIndex((item) => item === target || item.contains(target));
|
|
122
|
+
if (index === -1)
|
|
123
|
+
return;
|
|
124
|
+
const item = list[index];
|
|
125
|
+
if (item.hasAttribute('disabled') || item.getAttribute('aria-disabled') === 'true')
|
|
126
|
+
return;
|
|
127
|
+
this.#commit(index, item);
|
|
128
|
+
});
|
|
129
|
+
// Keydown on the panel → Enter/Space on focused menuitems that are NOT native buttons or
|
|
130
|
+
// links (those convert Enter/Space → click natively, which our click listener then commits).
|
|
131
|
+
// For non-interactive elements (`<div role=menuitem>`), we must commit on keydown.
|
|
132
|
+
// DELIBERATE selectionCommit bypass (ratified, TKT-0065 lateral review): that trait reflects
|
|
133
|
+
// `aria-selected` on items — invalid on `role=menuitem` (action semantics, no selected state) —
|
|
134
|
+
// and carries no Space leg; this hand-rolled click+Enter/Space path IS the menu commit contract.
|
|
135
|
+
this.listen(panel, 'keydown', (ev) => {
|
|
136
|
+
const e = ev;
|
|
137
|
+
if (e.key !== 'Enter' && e.key !== ' ')
|
|
138
|
+
return;
|
|
139
|
+
const focused = document.activeElement;
|
|
140
|
+
if (!focused)
|
|
141
|
+
return;
|
|
142
|
+
// Buttons and links fire click on Enter/Space natively — the click listener handles them.
|
|
143
|
+
if (focused instanceof HTMLButtonElement || focused.tagName === 'A')
|
|
144
|
+
return;
|
|
145
|
+
const list = items();
|
|
146
|
+
const index = list.findIndex((item) => item === focused || item.contains(focused));
|
|
147
|
+
if (index === -1)
|
|
148
|
+
return;
|
|
149
|
+
const item = list[index];
|
|
150
|
+
if (item.hasAttribute('disabled') || item.getAttribute('aria-disabled') === 'true')
|
|
151
|
+
return;
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
this.#commit(index, item);
|
|
154
|
+
});
|
|
155
|
+
// Roving keyboard focus — vertical (Arrow Up/Down), loop, type-ahead. Container = panel so
|
|
156
|
+
// only Arrow keys inside the panel are intercepted (not the trigger's own keyboard events).
|
|
157
|
+
rovingFocus(this, {
|
|
158
|
+
container: panel,
|
|
159
|
+
items,
|
|
160
|
+
orientation: 'vertical',
|
|
161
|
+
loop: true,
|
|
162
|
+
typeAhead: true,
|
|
163
|
+
});
|
|
164
|
+
// Edge-aware scroll fade (the gutter-exposure fix, 2026-07-04) — always on, no opt-in prop. The panel
|
|
165
|
+
// is now a bounded scroll viewport (menu.css: max-block-size: 40vh + overflow-y: auto, matching ui-select).
|
|
166
|
+
scrollFade(this, { viewport: panel });
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Create the control's two light-DOM parts ONCE (idempotent across disconnect/reconnect):
|
|
170
|
+
* - trigger: the first element child, marked with `data-part="trigger"` + ARIA affordances.
|
|
171
|
+
* - panel: a `<div data-part="panel" role="menu">` appended to the host; non-trigger children
|
|
172
|
+
* are moved in and auto-assigned `role=menuitem` + `tabindex=-1` if they lack a role.
|
|
173
|
+
*
|
|
174
|
+
* The overlay controller sets `popover="auto"` on the panel. `render()` stays the inherited
|
|
175
|
+
* VOID — the parts are created once and never re-created.
|
|
176
|
+
*/
|
|
177
|
+
#ensureParts() {
|
|
178
|
+
if (this.#panel) {
|
|
179
|
+
// Parts persist through disconnect/reconnect — return the existing ones.
|
|
180
|
+
const trigger = this.querySelector('[data-part="trigger"]');
|
|
181
|
+
if (!trigger)
|
|
182
|
+
throw new Error('ui-menu: trigger part lost on reconnect');
|
|
183
|
+
const panel = this.#panel;
|
|
184
|
+
return { panel, trigger, items: () => this.#itemsIn(panel) };
|
|
185
|
+
}
|
|
186
|
+
// Identify the menu trigger — the first element child provided by the author.
|
|
187
|
+
const trigger = this.firstElementChild;
|
|
188
|
+
if (!trigger) {
|
|
189
|
+
throw new Error('ui-menu: provide a trigger as the first child (e.g. <button>) before the menu items');
|
|
190
|
+
}
|
|
191
|
+
trigger.setAttribute('data-part', 'trigger');
|
|
192
|
+
// Create the menu panel. The overlay controller sets popover="auto" via setAttribute
|
|
193
|
+
// (single-ownership, ADR-0017 pattern — we do NOT set it here).
|
|
194
|
+
// `role=menu` is set on the div part directly (a created element, NOT the host — the FACE
|
|
195
|
+
// internals.role rule applies to the host element only).
|
|
196
|
+
const panel = document.createElement('div');
|
|
197
|
+
panel.setAttribute('data-part', 'panel');
|
|
198
|
+
panel.setAttribute('data-box', ''); // adopt the shared container box-model (inset margins)
|
|
199
|
+
panel.setAttribute('role', 'menu');
|
|
200
|
+
// tabindex="-1" lets moveFocusIn() (overlay LLD-C4) land on the panel itself when there
|
|
201
|
+
// are no enabled menuitems — the fallback panel.focus() requires the element to be focusable.
|
|
202
|
+
panel.setAttribute('tabindex', '-1');
|
|
203
|
+
// Stable id for aria-controls (created once, never reused).
|
|
204
|
+
panel.id = `ui-menu-panel-${++_nextPanelId}`;
|
|
205
|
+
// Move all non-trigger children into the panel (the menu items live in the top-layer surface,
|
|
206
|
+
// not beside the trigger in the document flow — the modal/popover child-move pattern).
|
|
207
|
+
let node = trigger.nextSibling;
|
|
208
|
+
while (node) {
|
|
209
|
+
const next = node.nextSibling;
|
|
210
|
+
panel.appendChild(node);
|
|
211
|
+
node = next;
|
|
212
|
+
}
|
|
213
|
+
// Auto-assign role=menuitem + tabindex=-1 to direct element children that do not already
|
|
214
|
+
// carry a role. The roving-focus trait then manages the tabindexes from this base state.
|
|
215
|
+
for (const child of panel.children) {
|
|
216
|
+
if (child instanceof HTMLElement) {
|
|
217
|
+
if (!child.hasAttribute('role'))
|
|
218
|
+
child.setAttribute('role', 'menuitem');
|
|
219
|
+
if (!child.hasAttribute('tabindex'))
|
|
220
|
+
child.setAttribute('tabindex', '-1');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
this.appendChild(panel);
|
|
224
|
+
this.#panel = panel;
|
|
225
|
+
// Wire the trigger's ARIA affordances.
|
|
226
|
+
trigger.setAttribute('aria-controls', panel.id);
|
|
227
|
+
trigger.setAttribute('aria-haspopup', 'menu');
|
|
228
|
+
return { panel, trigger, items: () => this.#itemsIn(panel) };
|
|
229
|
+
}
|
|
230
|
+
/** Return the live ordered set of [role=menuitem] elements within the panel. */
|
|
231
|
+
#itemsIn(panel) {
|
|
232
|
+
return [...panel.querySelectorAll('[role="menuitem"]')];
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Commit a user-driven item selection: emit `select` with {value, index}, then close the panel.
|
|
236
|
+
* The value is the item's `data-value` attribute, falling back to trimmed text content.
|
|
237
|
+
* Close is driven programmatically (open=false → effect → handle.close()); the overlay trait
|
|
238
|
+
* announces exactly one close+toggle pair for this transition too (ADR-0101), with `el.open`
|
|
239
|
+
* already `false` by the time a listener observes either event (the ordering invariant).
|
|
240
|
+
*/
|
|
241
|
+
#commit(index, item) {
|
|
242
|
+
const value = item.dataset['value'] ?? item.textContent?.trim() ?? String(index);
|
|
243
|
+
this.emit('select', { value, index });
|
|
244
|
+
this.open = false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (!customElements.get('ui-menu'))
|
|
248
|
+
customElements.define('ui-menu', UIMenuElement);
|