@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,226 @@
|
|
|
1
|
+
/* command-modal.css — ui-command-modal, the single-file component stylesheet (ADR-0003 / ADR-0125 / LLD-C11).
|
|
2
|
+
* Two clearly-SECTIONED blocks, mirroring combo-box.css / modal.css:
|
|
3
|
+
*
|
|
4
|
+
* [1] TOKEN BLOCK — `:where(ui-command-modal)` (specificity 0,0,0) declares the `--ui-command-modal-*`
|
|
5
|
+
* chain: the search field borrows the entry-control height register (--md-sys-height-md), item/group/active
|
|
6
|
+
* geometry rides the `--md-sys-space` ladder (ambient under [scale]/[density]). Raw `--md-sys-color-*` roles
|
|
7
|
+
* enter ONLY here.
|
|
8
|
+
* [2] STYLES BLOCK — `@scope (ui-command-modal)` CONSUMES only `--ui-command-modal-*`. The host itself is
|
|
9
|
+
* `display: contents` — it paints NOTHING; the nested `ui-modal` owns the whole surface (the modal's own
|
|
10
|
+
* sheet supplies the dialog/backdrop/frame). This sheet styles only the parts THIS control creates:
|
|
11
|
+
* search / list / status / empty / option / group, all rendered INSIDE the nested modal's <dialog>.
|
|
12
|
+
*
|
|
13
|
+
* Standalone stylesheet — NOT injected from command-modal.ts (behaviour-only `.ts`, plan §2). Linked via the
|
|
14
|
+
* component-styles barrel, appended after every other control (ADR-0003 append-only import order).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/* ════════════════ [1] TOKEN BLOCK — :where(ui-command-modal) ════════════════ */
|
|
18
|
+
|
|
19
|
+
:where(ui-command-modal) {
|
|
20
|
+
/* The search field borrows the entry-control height register (the combo-box/select trigger precedent) — no
|
|
21
|
+
new geometry row (Pattern class, geometry.md; no §1 control-height row of its own). */
|
|
22
|
+
--ui-command-modal-search-block-size: var(--md-sys-height-md);
|
|
23
|
+
--ui-command-modal-search-font: var(--md-sys-font-md);
|
|
24
|
+
--ui-command-modal-search-padding-inline: calc(var(--ui-command-modal-search-block-size) / 2); /* the fleet h/2 value-edge standard */
|
|
25
|
+
|
|
26
|
+
/* Frame — the §1b entry-control law applied DEGENERATELY (TKT-0068 item 4 ruling): the palette search
|
|
27
|
+
is always focused while the modal is open, so it wears §1b's FOCUS row permanently — container-low
|
|
28
|
+
bg · transparent border · on-surface ink (the field/panel bg contrast now carries the search/list
|
|
29
|
+
separation the old solid divider drew; the 1px border-block-end stays for layout, transparent).
|
|
30
|
+
The placeholder aliases the ink per §1b's alias shape (no sixth row). */
|
|
31
|
+
--ui-command-modal-search-border: transparent;
|
|
32
|
+
--ui-command-modal-search-bg: var(--md-sys-color-neutral-container-low);
|
|
33
|
+
--ui-command-modal-search-ink: var(--md-sys-color-neutral-on-surface);
|
|
34
|
+
--ui-command-modal-search-placeholder: var(--ui-command-modal-search-ink);
|
|
35
|
+
|
|
36
|
+
/* The bounded scroll viewport (the list is the dialog's body; scroll-fade wired in .ts). */
|
|
37
|
+
--ui-command-modal-list-max-block: 50vh;
|
|
38
|
+
/* Hidden-but-scrollable, the fleet's --ui-surface-host-scrollbar-width "hidden scroller, live scroll"
|
|
39
|
+
seam (TKT-0084 shape): a CMD-K overlay is app chrome, and a raw platform scrollbar reads as broken
|
|
40
|
+
chrome there — the scroll-fade edge treatment (wired in .ts, above) stays the scroll affordance.
|
|
41
|
+
Consumer-overridable (the same repoint pattern), never forced. */
|
|
42
|
+
--ui-command-modal-scrollbar-width: none;
|
|
43
|
+
/* TKT-0017 — the FIXED FRAME (the CMD-K convention): a set width and a top anchor so the search field
|
|
44
|
+
never moves as filtering changes the result count; only the list below grows/shrinks (downward, the
|
|
45
|
+
VS Code palette convention). command-modal.ts forwards these onto the nested ui-modal's frame dials. */
|
|
46
|
+
--ui-command-modal-inline-size: min(92vw, 36rem);
|
|
47
|
+
--ui-command-modal-block-anchor: 15svh;
|
|
48
|
+
|
|
49
|
+
/* Item/group geometry off the --md-sys-space ladder (ambient under [scale]/[density] — no new geometry row). */
|
|
50
|
+
--ui-command-modal-item-pad-inline: var(--md-sys-space-sm);
|
|
51
|
+
--ui-command-modal-item-pad-block: var(--md-sys-space-xs);
|
|
52
|
+
--ui-command-modal-item-gap: var(--md-sys-space-xs);
|
|
53
|
+
--ui-command-modal-group-gap: var(--md-sys-space-sm);
|
|
54
|
+
--ui-command-modal-caption-font: var(--md-sys-font-sm); /* group-label + shortcut display — a caption-weight read, off the live ramp (--ui-type-* is retired, ADR-0078 cl.2) */
|
|
55
|
+
|
|
56
|
+
--ui-command-modal-ink: var(--md-sys-color-neutral-on-surface);
|
|
57
|
+
--ui-command-modal-muted-ink: var(--md-sys-color-neutral-on-surface-variant);
|
|
58
|
+
--ui-command-modal-group-label-ink: var(--md-sys-color-neutral-on-surface-variant);
|
|
59
|
+
--ui-command-modal-radius: var(--md-sys-shape-corner-base);
|
|
60
|
+
|
|
61
|
+
/* The [data-active] active-descendant highlight — a background treatment paired with the ARIA/attribute
|
|
62
|
+
signifier itself (never color-only intent, ADR-0057). */
|
|
63
|
+
--ui-command-modal-active-bg: var(--md-sys-color-neutral-tint-dim);
|
|
64
|
+
--ui-command-modal-active-ink: var(--md-sys-color-neutral-on-surface);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* ════════════════ [2] STYLES BLOCK — @scope (ui-command-modal) ════════════════ */
|
|
68
|
+
|
|
69
|
+
@scope (ui-command-modal) {
|
|
70
|
+
/* The palette host paints nothing of its own — the nested ui-modal owns the entire surface. */
|
|
71
|
+
:scope {
|
|
72
|
+
display: contents;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* ── Search field (role=combobox, Control-class height) ─────────────────────────────────────── */
|
|
76
|
+
|
|
77
|
+
:scope [data-part='search'] {
|
|
78
|
+
box-sizing: border-box;
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
min-block-size: var(--ui-command-modal-search-block-size);
|
|
82
|
+
line-height: 1;
|
|
83
|
+
padding-block: 0;
|
|
84
|
+
padding-inline: var(--ui-command-modal-search-padding-inline);
|
|
85
|
+
font-size: var(--ui-command-modal-search-font);
|
|
86
|
+
background-color: var(--ui-command-modal-search-bg);
|
|
87
|
+
color: var(--ui-command-modal-search-ink);
|
|
88
|
+
border: none;
|
|
89
|
+
border-block-end: 1px solid var(--ui-command-modal-search-border);
|
|
90
|
+
cursor: text;
|
|
91
|
+
outline: none; /* the shared focus-ring convention — a palette search field is always focused while open */
|
|
92
|
+
/* single-line — the contenteditable UA default is pre-wrap; every fleet entry field overrides it */
|
|
93
|
+
white-space: nowrap;
|
|
94
|
+
overflow: hidden;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
:scope [data-part='search'][data-empty]::before {
|
|
98
|
+
content: attr(data-placeholder);
|
|
99
|
+
color: var(--ui-command-modal-search-placeholder);
|
|
100
|
+
pointer-events: none;
|
|
101
|
+
user-select: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* ── List (role=listbox) — the bounded scroll viewport, [data-box] region rhythm ─────────────── */
|
|
105
|
+
|
|
106
|
+
:scope [data-part='list'] {
|
|
107
|
+
box-sizing: border-box;
|
|
108
|
+
max-block-size: var(--ui-command-modal-list-max-block);
|
|
109
|
+
overflow-y: auto;
|
|
110
|
+
/* Hidden scroller, live scroll (token block, above) — both shipped engines support scrollbar-width
|
|
111
|
+
(Chromium 121+/Safari 18.2+), no ::-webkit-scrollbar leg (the split-pane.css note). */
|
|
112
|
+
scrollbar-width: var(--ui-command-modal-scrollbar-width);
|
|
113
|
+
padding: 0;
|
|
114
|
+
margin: 0;
|
|
115
|
+
list-style: none;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* ── Groups + their heading ───────────────────────────────────────────────────────────────────── */
|
|
119
|
+
|
|
120
|
+
:scope [role='group'] {
|
|
121
|
+
display: block;
|
|
122
|
+
margin-block-end: var(--ui-command-modal-group-gap);
|
|
123
|
+
}
|
|
124
|
+
:scope [role='group'][hidden] {
|
|
125
|
+
display: none;
|
|
126
|
+
}
|
|
127
|
+
:scope [data-role='group-label'] {
|
|
128
|
+
padding-inline: var(--ui-command-modal-item-pad-inline);
|
|
129
|
+
padding-block: var(--ui-command-modal-item-pad-block);
|
|
130
|
+
font-size: var(--ui-command-modal-caption-font);
|
|
131
|
+
color: var(--ui-command-modal-group-label-ink);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* ── Option rows ───────────────────────────────────────────────────────────────────────────────── */
|
|
135
|
+
|
|
136
|
+
:scope [role='option'] {
|
|
137
|
+
display: flex;
|
|
138
|
+
flex-wrap: wrap; /* TKT-0019 — lets [data-role=description] (flex-basis:100%, below) push onto its own line;
|
|
139
|
+
the title row (icon/label/shortcut) is unaffected while it still fits one line */
|
|
140
|
+
align-items: center;
|
|
141
|
+
gap: var(--ui-command-modal-item-gap);
|
|
142
|
+
padding-inline: var(--ui-command-modal-item-pad-inline);
|
|
143
|
+
padding-block: var(--ui-command-modal-item-pad-block);
|
|
144
|
+
border-radius: var(--ui-command-modal-radius);
|
|
145
|
+
color: var(--ui-command-modal-ink);
|
|
146
|
+
cursor: pointer;
|
|
147
|
+
}
|
|
148
|
+
:scope [role='option'][hidden] {
|
|
149
|
+
display: none;
|
|
150
|
+
}
|
|
151
|
+
/* TKT-0047 — converges on the fleet's dominant disabled mechanism (token-repoint, not opacity): reuses
|
|
152
|
+
the EXISTING `--ui-command-modal-muted-ink` role (already used for the group-label ink, above) rather
|
|
153
|
+
than dimming via opacity, matching select.css's fully-repointed `[disabled]` precedent. */
|
|
154
|
+
:scope [role='option'][aria-disabled='true'] {
|
|
155
|
+
color: var(--ui-command-modal-muted-ink);
|
|
156
|
+
pointer-events: none;
|
|
157
|
+
}
|
|
158
|
+
/* The [data-active] active-descendant highlight — CSS face of aria-activedescendant (focus stays on search). */
|
|
159
|
+
:scope [role='option'][data-active] {
|
|
160
|
+
background-color: var(--ui-command-modal-active-bg);
|
|
161
|
+
color: var(--ui-command-modal-active-ink);
|
|
162
|
+
}
|
|
163
|
+
:scope [data-role='shortcut'] {
|
|
164
|
+
margin-inline-start: auto;
|
|
165
|
+
color: var(--ui-command-modal-muted-ink);
|
|
166
|
+
font-size: var(--ui-command-modal-caption-font);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* TKT-0019 — the option's second line: a clamped, muted, single-line description below the title row.
|
|
170
|
+
flex-basis:100% is the wrap-forcing idiom (the sibling title-row content stays on line 1 as long as it
|
|
171
|
+
fits; this item never does, so it always starts a fresh flex line). min-inline-size:0 overrides the flex
|
|
172
|
+
default (min-width:auto, i.e. "never shrink below intrinsic content size") — without it, overflow/
|
|
173
|
+
text-overflow below would never actually engage and the line would just push the option wider instead of
|
|
174
|
+
clamping. Ink stays the muted role UNCONDITIONALLY (the [data-role=shortcut] precedent, two rules up) —
|
|
175
|
+
it does not re-color under [data-active]; the muted role is AA-proven against the active-bg wash by the
|
|
176
|
+
same composed-surface math tokens.test.ts already runs for --md-sys-color-neutral-on-surface-variant. */
|
|
177
|
+
:scope [data-role='description'] {
|
|
178
|
+
flex-basis: 100%;
|
|
179
|
+
min-inline-size: 0;
|
|
180
|
+
color: var(--ui-command-modal-muted-ink);
|
|
181
|
+
font-size: var(--ui-command-modal-caption-font);
|
|
182
|
+
overflow: hidden;
|
|
183
|
+
text-overflow: ellipsis;
|
|
184
|
+
white-space: nowrap;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* ── Result-count live region — visually hidden, present to AT ───────────────────────────────────── */
|
|
188
|
+
|
|
189
|
+
:scope [data-part='status'] {
|
|
190
|
+
position: absolute;
|
|
191
|
+
inline-size: 1px;
|
|
192
|
+
block-size: 1px;
|
|
193
|
+
overflow: hidden;
|
|
194
|
+
clip-path: inset(50%);
|
|
195
|
+
white-space: nowrap;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* ── Empty state (default row; an author [slot=empty]/[data-role=empty] overrides it) ────────────── */
|
|
199
|
+
|
|
200
|
+
:scope [data-part='empty'] {
|
|
201
|
+
padding-inline: var(--ui-command-modal-item-pad-inline);
|
|
202
|
+
padding-block: var(--ui-command-modal-item-pad-block);
|
|
203
|
+
color: var(--ui-command-modal-muted-ink);
|
|
204
|
+
cursor: default;
|
|
205
|
+
pointer-events: none;
|
|
206
|
+
}
|
|
207
|
+
:scope [data-part='empty'][hidden] {
|
|
208
|
+
display: none;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* forced-colors (WHCM) — the active-option highlight becomes Highlight/HighlightText (not a faint bg that
|
|
212
|
+
vanishes under WHCM); the aria-activedescendant + [data-active] pairing is the non-color signifier, so no
|
|
213
|
+
color-only intent exists (the combo-box [data-active] forced-colors precedent). */
|
|
214
|
+
@media (forced-colors: active) {
|
|
215
|
+
:scope [data-part='search'] {
|
|
216
|
+
background-color: Field;
|
|
217
|
+
color: FieldText;
|
|
218
|
+
border-block-end-color: FieldText;
|
|
219
|
+
}
|
|
220
|
+
:scope [role='option'][data-active] {
|
|
221
|
+
background-color: Highlight;
|
|
222
|
+
color: HighlightText;
|
|
223
|
+
forced-color-adjust: none;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { UIElement, type ReactiveProps } from '../../dom/index.js';
|
|
2
|
+
declare const props: {
|
|
3
|
+
open: {
|
|
4
|
+
reflect: true;
|
|
5
|
+
type: import("../../index.js").PropType<boolean>;
|
|
6
|
+
default: boolean;
|
|
7
|
+
attribute?: string | false;
|
|
8
|
+
};
|
|
9
|
+
label: {
|
|
10
|
+
reflect: true;
|
|
11
|
+
type: import("../../index.js").PropType<string>;
|
|
12
|
+
default: string;
|
|
13
|
+
attribute?: string | false;
|
|
14
|
+
};
|
|
15
|
+
placeholder: import("../../index.js").PropConfig<string>;
|
|
16
|
+
hotkey: import("../../index.js").PropConfig<string>;
|
|
17
|
+
filter: import("../../index.js").PropConfig<"substring" | "regex">;
|
|
18
|
+
};
|
|
19
|
+
export interface UICommandModalElement extends ReactiveProps<typeof props> {
|
|
20
|
+
}
|
|
21
|
+
export declare class UICommandModalElement extends UIElement {
|
|
22
|
+
#private;
|
|
23
|
+
static props: {
|
|
24
|
+
open: {
|
|
25
|
+
reflect: true;
|
|
26
|
+
type: import("../../index.js").PropType<boolean>;
|
|
27
|
+
default: boolean;
|
|
28
|
+
attribute?: string | false;
|
|
29
|
+
};
|
|
30
|
+
label: {
|
|
31
|
+
reflect: true;
|
|
32
|
+
type: import("../../index.js").PropType<string>;
|
|
33
|
+
default: string;
|
|
34
|
+
attribute?: string | false;
|
|
35
|
+
};
|
|
36
|
+
placeholder: import("../../index.js").PropConfig<string>;
|
|
37
|
+
hotkey: import("../../index.js").PropConfig<string>;
|
|
38
|
+
filter: import("../../index.js").PropConfig<"substring" | "regex">;
|
|
39
|
+
};
|
|
40
|
+
protected connected(): void;
|
|
41
|
+
}
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// command-modal.ts — UICommandModalElement, the CMD-K command palette (ADR-0125; command-modal.lld.md LLD-C1..C10;
|
|
2
|
+
// command-modal.spec.md SPEC-R1..R10). A Pattern-class UIElement COORDINATOR — it nests a `ui-modal` for the whole
|
|
3
|
+
// modal surface + dismissal contract (ADR-0017/0019/0020), and inside that modal renders a control-created
|
|
4
|
+
// role=combobox search field over a control-created role=listbox list built from the consumer's author-declared
|
|
5
|
+
// [role=option]/[role=group] children (ADR-0017 child-move), re-deriving `ui-combo-box`'s active-descendant
|
|
6
|
+
// filter pattern (ADR-0085; the ~60 lines are re-implemented — combo-box's own methods are private). Choosing a
|
|
7
|
+
// command emits `select` + closes. It owns no form value, no command registry, no router, and no recents state.
|
|
8
|
+
//
|
|
9
|
+
// Composes on: UIElement (the coordinator base — the ui-theme-provider/ui-form-provider precedent, no surface of
|
|
10
|
+
// its own) + a nested ui-modal (a sanctioned sibling-control import, avatar.ts:25 precedent) + the scroll-fade
|
|
11
|
+
// trait over the list viewport. No new package, no new trait, no new base class, no new event, no new geometry row.
|
|
12
|
+
//
|
|
13
|
+
// Layer: controls → dom + traits (inward-only ✓).
|
|
14
|
+
import { UIElement, prop } from "../../dom/index.js";
|
|
15
|
+
import { UIModalElement } from "../modal/modal.js"; // sanctioned sibling-control import — self-defines ui-modal (avatar.ts:25 precedent)
|
|
16
|
+
import { scrollFade } from "../../traits/scroll-fade.js";
|
|
17
|
+
let _nextId = 0;
|
|
18
|
+
const props = {
|
|
19
|
+
// `open` — whether the palette is shown. Reflected + bindable two-way (drives the nested modal's `open`; synced
|
|
20
|
+
// back on the modal's platform dismissal). The ui-modal `open` shape.
|
|
21
|
+
open: { ...prop.boolean(false), reflect: true },
|
|
22
|
+
// `label` — the accessible name for BOTH the search field (reactive → aria-label) and the dialog (connect-time
|
|
23
|
+
// → forwarded onto the nested ui-modal). NOT reflected (an a11y hint, not a styling hook).
|
|
24
|
+
label: { ...prop.string(''), reflect: true },
|
|
25
|
+
// `placeholder` — the search field placeholder (shown via [data-empty]::before, the combo-box CSS read-back).
|
|
26
|
+
placeholder: prop.string(''),
|
|
27
|
+
// `hotkey` — the F2 opt-in convenience. '' = NO document listener. A non-empty chord (e.g. 'mod+k') binds ONE
|
|
28
|
+
// per-instance document keydown that toggles THIS instance's open. NOT a global singleton (ADR-0082).
|
|
29
|
+
hotkey: prop.string(''),
|
|
30
|
+
// `filter` — ADR-0127: the match-test mode. 'substring' (default) is the original, byte-identical hay.includes(q)
|
|
31
|
+
// test; 'regex' runs a case-insensitive RegExp test over the SAME haystack, falling back to the substring test
|
|
32
|
+
// for that keystroke only when the pattern is invalid (never throws — SPEC-R5 AC5). NOT reflected (a behavior
|
|
33
|
+
// switch, not a styling hook).
|
|
34
|
+
filter: prop.enum(['substring', 'regex'], 'substring'),
|
|
35
|
+
};
|
|
36
|
+
export class UICommandModalElement extends UIElement {
|
|
37
|
+
static props = props;
|
|
38
|
+
#modal = null;
|
|
39
|
+
#search = null;
|
|
40
|
+
#list = null;
|
|
41
|
+
#status = null;
|
|
42
|
+
#emptyRow = null;
|
|
43
|
+
#activeIdx = -1;
|
|
44
|
+
connected() {
|
|
45
|
+
const { modal, search, list } = this.#ensureParts();
|
|
46
|
+
// LLD-C3 — the nested-modal surface seam. open → modal.open + aria-expanded; the modal owns showModal/backdrop/
|
|
47
|
+
// Escape/focus-restore (NO own dialog machinery here — SPEC-R3 AC1).
|
|
48
|
+
this.effect(() => {
|
|
49
|
+
const isOpen = this.open;
|
|
50
|
+
modal.open = isOpen;
|
|
51
|
+
search.setAttribute('aria-expanded', String(isOpen));
|
|
52
|
+
if (isOpen)
|
|
53
|
+
requestAnimationFrame(() => search.focus()); // focus the search field on open (SPEC-R4 AC2)
|
|
54
|
+
});
|
|
55
|
+
// modal → model: a platform dismissal (Escape/backdrop) — the modal emits `close`+`toggle`; sync open back and
|
|
56
|
+
// re-emit at the palette level (the two-way `open` pattern one level up, ADR-0019). `open` is the discriminator:
|
|
57
|
+
// if it is still true, the platform closed it; guard the re-emit so a palette-driven close does not double-fire.
|
|
58
|
+
// stopPropagation is load-bearing here: `emit()` (element.ts:172) dispatches a BUBBLING CustomEvent, and the
|
|
59
|
+
// nested modal is a light-DOM DESCENDANT of the palette host — its bubbled `close` would otherwise ALSO reach
|
|
60
|
+
// any `close` listener on the palette itself, on top of this handler's own re-emit (a genuine double-fire; the
|
|
61
|
+
// fleet's first stateful nested-control relay, so no earlier precedent needed this guard).
|
|
62
|
+
this.listen(modal, 'close', (event) => {
|
|
63
|
+
if (this.open) {
|
|
64
|
+
event.stopPropagation();
|
|
65
|
+
this.open = false;
|
|
66
|
+
this.emit('close');
|
|
67
|
+
this.emit('toggle');
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
// The modal emits `close` THEN `toggle` as a pair (ADR-0019) — the `toggle` half needs the SAME containment
|
|
71
|
+
// (it always rides alongside `close`, never alone), so it is stopped unconditionally rather than re-guarded.
|
|
72
|
+
this.listen(modal, 'toggle', (event) => { event.stopPropagation(); });
|
|
73
|
+
// LLD-C4 — search field: reactive aria-label (falls back to a default name when label=''), placeholder.
|
|
74
|
+
this.effect(() => {
|
|
75
|
+
search.setAttribute('aria-label', this.label || 'Search commands');
|
|
76
|
+
search.setAttribute('data-placeholder', this.placeholder);
|
|
77
|
+
});
|
|
78
|
+
this.listen(search, 'input', () => { this.#filter(search.textContent ?? ''); });
|
|
79
|
+
this.listen(search, 'keydown', (event) => this.#onKeydown(event, search));
|
|
80
|
+
// LLD-C7 — option click commits (listen on the list; disabled/hidden guarded).
|
|
81
|
+
this.listen(list, 'click', (event) => {
|
|
82
|
+
const opt = event.target.closest('[role=option]');
|
|
83
|
+
if (!opt || opt.hidden || opt.getAttribute('aria-disabled') === 'true' || opt.hasAttribute('disabled'))
|
|
84
|
+
return;
|
|
85
|
+
this.#commit(opt);
|
|
86
|
+
});
|
|
87
|
+
// LLD-C8/C11 — the list is a bounded scroll viewport; wire the edge fade (combo-box/modal precedent).
|
|
88
|
+
scrollFade(this, { viewport: list });
|
|
89
|
+
// LLD-C10 — the opt-in hotkey: a TEARDOWN-RETURNING effect installs ONE document keydown ONLY while hotkey is
|
|
90
|
+
// non-empty; the returned cleanup aborts it on hotkey change AND on disconnect (scope-owned). '' ⇒ no listener
|
|
91
|
+
// is ever added (SPEC-R10 AC1) — NOT an always-on gated listener.
|
|
92
|
+
this.effect(() => {
|
|
93
|
+
const chord = this.hotkey;
|
|
94
|
+
if (chord === '')
|
|
95
|
+
return;
|
|
96
|
+
const controller = new AbortController();
|
|
97
|
+
document.addEventListener('keydown', (e) => {
|
|
98
|
+
if (this.#matchesChord(e, chord)) {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
this.open = !this.open;
|
|
101
|
+
}
|
|
102
|
+
}, { signal: controller.signal });
|
|
103
|
+
return () => controller.abort();
|
|
104
|
+
});
|
|
105
|
+
this.#filter(''); // initial: all options visible, status seeded, empty-state resolved
|
|
106
|
+
}
|
|
107
|
+
// LLD-C2 — create the nested modal + parts ONCE, child-move author items in. ORDER IS LOAD-BEARING (see the LLD
|
|
108
|
+
// §3 note: `ui-modal`'s #ensureDialog() moves its OWN children into its <dialog> at ITS connect, so the parts
|
|
109
|
+
// must go INTO the ui-modal element while it is still detached, and only THEN get appended to the host).
|
|
110
|
+
#ensureParts() {
|
|
111
|
+
if (this.#modal && this.#search && this.#list)
|
|
112
|
+
return { modal: this.#modal, search: this.#search, list: this.#list };
|
|
113
|
+
const seq = ++_nextId;
|
|
114
|
+
const search = document.createElement('div');
|
|
115
|
+
search.setAttribute('data-part', 'search');
|
|
116
|
+
search.setAttribute('contenteditable', 'plaintext-only');
|
|
117
|
+
search.setAttribute('role', 'combobox');
|
|
118
|
+
search.setAttribute('aria-autocomplete', 'list');
|
|
119
|
+
search.setAttribute('aria-expanded', 'false');
|
|
120
|
+
for (const a of ['autocorrect', 'autocapitalize', 'autocomplete'])
|
|
121
|
+
search.setAttribute(a, 'off');
|
|
122
|
+
search.setAttribute('spellcheck', 'false');
|
|
123
|
+
search.toggleAttribute('data-empty', true);
|
|
124
|
+
this.#search = search;
|
|
125
|
+
const list = document.createElement('div');
|
|
126
|
+
list.setAttribute('data-part', 'list');
|
|
127
|
+
list.setAttribute('data-box', ''); // adopt the shared container box-model (ADR-0046)
|
|
128
|
+
list.setAttribute('role', 'listbox');
|
|
129
|
+
list.setAttribute('tabindex', '-1');
|
|
130
|
+
list.id = `ui-cmd-list-${seq}`;
|
|
131
|
+
search.setAttribute('aria-controls', list.id);
|
|
132
|
+
this.#list = list;
|
|
133
|
+
// Child-move author [role=option]/[role=group] (and the [slot=empty]) into the list (ADR-0017). Stable ids
|
|
134
|
+
// for aria-activedescendant, assigned once.
|
|
135
|
+
let node = this.firstChild;
|
|
136
|
+
while (node) {
|
|
137
|
+
const next = node.nextSibling;
|
|
138
|
+
list.appendChild(node);
|
|
139
|
+
node = next;
|
|
140
|
+
}
|
|
141
|
+
let optSeq = 0;
|
|
142
|
+
for (const opt of list.querySelectorAll('[role=option]'))
|
|
143
|
+
if (!opt.id)
|
|
144
|
+
opt.id = `ui-cmd${seq}-opt-${++optSeq}`;
|
|
145
|
+
const status = document.createElement('div');
|
|
146
|
+
status.setAttribute('data-part', 'status');
|
|
147
|
+
status.setAttribute('aria-live', 'polite');
|
|
148
|
+
status.setAttribute('role', 'status'); // visually hidden in CSS
|
|
149
|
+
this.#status = status;
|
|
150
|
+
const emptyRow = document.createElement('div'); // default empty-state (LLD-C9) — an authored [slot=empty] wins
|
|
151
|
+
emptyRow.setAttribute('data-part', 'empty');
|
|
152
|
+
emptyRow.setAttribute('role', 'presentation');
|
|
153
|
+
emptyRow.textContent = 'No results';
|
|
154
|
+
emptyRow.hidden = true;
|
|
155
|
+
list.append(emptyRow);
|
|
156
|
+
this.#emptyRow = emptyRow;
|
|
157
|
+
// The nested ui-modal — accessible name forwarded from the INITIAL label (the modal forwards aria-label→dialog
|
|
158
|
+
// ONCE at its ensureDialog; a reactive label updates the SEARCH field name, not the dialog's — LLD §3 note).
|
|
159
|
+
const modal = document.createElement('ui-modal');
|
|
160
|
+
if (this.label)
|
|
161
|
+
modal.setAttribute('aria-label', this.label);
|
|
162
|
+
// TKT-0017 — the FIXED FRAME: pin the nested modal's frame dials to this control's own tokens so the
|
|
163
|
+
// palette holds a set width and a top anchor (the search field never moves as filtering resizes the
|
|
164
|
+
// list). Inline custom properties on the element we OWN — the slider --value-pct JS-seam exemption
|
|
165
|
+
// (family-coherence B); the var() references resolve through inheritance from :where(ui-command-modal).
|
|
166
|
+
modal.style.setProperty('--ui-modal-inline-size', 'var(--ui-command-modal-inline-size)');
|
|
167
|
+
modal.style.setProperty('--ui-modal-margin-block-start', 'var(--ui-command-modal-block-anchor)');
|
|
168
|
+
// ORDER: put the parts inside the modal BEFORE appending the modal to the host, so when ui-modal connects it
|
|
169
|
+
// moves search/status/list into its <dialog> together (identity preserved; our refs stay valid). LLD §3 note.
|
|
170
|
+
modal.append(search, status, list);
|
|
171
|
+
this.#modal = modal;
|
|
172
|
+
this.append(modal);
|
|
173
|
+
return { modal, search, list };
|
|
174
|
+
}
|
|
175
|
+
// LLD-C6 — the visible+enabled option set; active-descendant move (focus NEVER leaves the search field).
|
|
176
|
+
#options() { return this.#list ? [...this.#list.querySelectorAll('[role=option]')] : []; }
|
|
177
|
+
#getVisibleOptions() {
|
|
178
|
+
return this.#options().filter((o) => !o.hidden && o.getAttribute('aria-disabled') !== 'true' && !o.hasAttribute('disabled'));
|
|
179
|
+
}
|
|
180
|
+
#setActive(idx) {
|
|
181
|
+
const opts = this.#getVisibleOptions();
|
|
182
|
+
for (const o of opts)
|
|
183
|
+
o.removeAttribute('data-active');
|
|
184
|
+
if (idx < 0 || idx >= opts.length) {
|
|
185
|
+
this.#activeIdx = -1;
|
|
186
|
+
this.#search?.removeAttribute('aria-activedescendant');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
this.#activeIdx = idx;
|
|
190
|
+
const opt = opts[idx];
|
|
191
|
+
opt.setAttribute('data-active', '');
|
|
192
|
+
if (!opt.id)
|
|
193
|
+
opt.id = `ui-cmd-opt-${++_nextId}`; // lazy stable id for options added after connect (combo-box.ts:436 guard)
|
|
194
|
+
// scrollIntoView is absent in jsdom (the status-stream.ts precedent) — a real browser always has it.
|
|
195
|
+
if (typeof opt.scrollIntoView === 'function')
|
|
196
|
+
opt.scrollIntoView({ block: 'nearest' });
|
|
197
|
+
this.#search?.setAttribute('aria-activedescendant', opt.id);
|
|
198
|
+
}
|
|
199
|
+
#moveActive(delta) {
|
|
200
|
+
const opts = this.#getVisibleOptions();
|
|
201
|
+
if (opts.length === 0)
|
|
202
|
+
return;
|
|
203
|
+
const next = this.#activeIdx < 0 ? (delta === 1 ? 0 : opts.length - 1) : (this.#activeIdx + delta + opts.length) % opts.length;
|
|
204
|
+
this.#setActive(next);
|
|
205
|
+
}
|
|
206
|
+
// The option's PRIMARY label text — excludes decorative descendants ([data-role=shortcut]/[data-role=icon]/
|
|
207
|
+
// aria-hidden and <ui-icon>) so a `⌘H` shortcut is neither in select.detail.label nor matchable by the filter
|
|
208
|
+
// (SPEC §2 Item label). TKT-0019 — [data-role=description] joins the exclusion for the SAME reason (`select`
|
|
209
|
+
// detail.label and the `^ui-` anchored labelText must stay the title, never the second line); unlike the
|
|
210
|
+
// shortcut this is a labelText-ONLY exclusion — the description carries no aria-hidden and stays fully in
|
|
211
|
+
// the accessibility tree (a visible line SHOULD be announced; only its ROLE in this control's own label/
|
|
212
|
+
// filter text is excluded). Filterability is unaffected — the site's own data-keywords already folds the
|
|
213
|
+
// description text into the (separate) filter haystack. Iterates childNodes: skips decorative element
|
|
214
|
+
// children, keeps text + real label spans.
|
|
215
|
+
#labelText(opt) {
|
|
216
|
+
let s = '';
|
|
217
|
+
for (const node of opt.childNodes) {
|
|
218
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
219
|
+
const el = node;
|
|
220
|
+
if (el.tagName === 'UI-ICON' || el.matches('[data-role=shortcut],[data-role=icon],[data-role=description],[aria-hidden=true]'))
|
|
221
|
+
continue;
|
|
222
|
+
s += el.textContent ?? '';
|
|
223
|
+
}
|
|
224
|
+
else if (node.nodeType === Node.TEXT_NODE) {
|
|
225
|
+
s += node.textContent ?? '';
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return s.trim();
|
|
229
|
+
}
|
|
230
|
+
// LLD-C5 — substring/keyword filter (or ADR-0127 regex, mode-gated); hide empty group headings; reset active;
|
|
231
|
+
// update status + empty-state.
|
|
232
|
+
#filter(text) {
|
|
233
|
+
const raw = text.trim();
|
|
234
|
+
const q = raw.toLowerCase();
|
|
235
|
+
// ADR-0127 — build the regex ONCE per keystroke, not per option, from the RAW (un-lowercased) text: the 'i'
|
|
236
|
+
// flag alone delivers case-insensitivity (SPEC-R5 AC4) — lowercasing the PATTERN text first would corrupt
|
|
237
|
+
// case-sensitive regex metacharacters (\D/\S/\W/\B → \d/\s/\w/\b), a materially different match, not merely
|
|
238
|
+
// a case change. An invalid pattern throws a SyntaxError at `new RegExp`, caught here and never retried
|
|
239
|
+
// per-option, then EVERY option falls back to the substring test for this keystroke (SPEC-R5 AC5 — degrade
|
|
240
|
+
// gracefully, never throw, never surface an error to the user).
|
|
241
|
+
let re = null;
|
|
242
|
+
if (this.filter === 'regex' && raw !== '') {
|
|
243
|
+
try {
|
|
244
|
+
re = new RegExp(raw, 'i');
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
re = null;
|
|
248
|
+
} // invalid pattern ⇒ fall back to substring below
|
|
249
|
+
}
|
|
250
|
+
for (const opt of this.#options()) {
|
|
251
|
+
const hay = (this.#labelText(opt) + ' ' + (opt.getAttribute('data-keywords') ?? '')).toLowerCase();
|
|
252
|
+
const matches = q === '' || (re ? re.test(hay) : hay.includes(q)); // item label + data-keywords ONLY (shortcut excluded — SPEC-R5 AC1)
|
|
253
|
+
opt.hidden = !matches;
|
|
254
|
+
}
|
|
255
|
+
for (const group of this.#list?.querySelectorAll('[role=group]') ?? []) {
|
|
256
|
+
group.hidden = ![...group.querySelectorAll('[role=option]')].some((o) => !o.hidden);
|
|
257
|
+
}
|
|
258
|
+
this.#setActive(-1);
|
|
259
|
+
const n = this.#getVisibleOptions().length;
|
|
260
|
+
if (this.#status)
|
|
261
|
+
this.#status.textContent = n === 0 ? 'No results' : `${n} result${n === 1 ? '' : 's'}`;
|
|
262
|
+
// Empty-state (LLD-C9): an author [slot=empty]/[data-role=empty] child was child-moved into the list at
|
|
263
|
+
// connect, so the CONTROL must toggle its visibility — it is a bare attribute in light DOM, nothing hides it
|
|
264
|
+
// implicitly. The author element wins over the default row; both gate on the visible count (SPEC-R8).
|
|
265
|
+
const authorEmpty = this.#list?.querySelector('[slot=empty],[data-role=empty]') ?? null;
|
|
266
|
+
if (authorEmpty)
|
|
267
|
+
authorEmpty.hidden = n > 0;
|
|
268
|
+
if (this.#emptyRow)
|
|
269
|
+
this.#emptyRow.hidden = n > 0 || authorEmpty !== null;
|
|
270
|
+
this.#search?.toggleAttribute('data-empty', text === '');
|
|
271
|
+
}
|
|
272
|
+
#onKeydown(e, _search) {
|
|
273
|
+
// NO Escape branch — the nested modal owns dismissal (SPEC-R9). Only Arrow/Home/End/Enter here.
|
|
274
|
+
if (e.key === 'ArrowDown') {
|
|
275
|
+
e.preventDefault();
|
|
276
|
+
this.#moveActive(1);
|
|
277
|
+
}
|
|
278
|
+
else if (e.key === 'ArrowUp') {
|
|
279
|
+
e.preventDefault();
|
|
280
|
+
this.#moveActive(-1);
|
|
281
|
+
}
|
|
282
|
+
else if (e.key === 'Home') {
|
|
283
|
+
e.preventDefault();
|
|
284
|
+
this.#setActive(0);
|
|
285
|
+
}
|
|
286
|
+
else if (e.key === 'End') {
|
|
287
|
+
e.preventDefault();
|
|
288
|
+
this.#setActive(this.#getVisibleOptions().length - 1);
|
|
289
|
+
}
|
|
290
|
+
else if (e.key === 'Enter') {
|
|
291
|
+
e.preventDefault();
|
|
292
|
+
const opt = this.#getVisibleOptions()[this.#activeIdx];
|
|
293
|
+
if (opt)
|
|
294
|
+
this.#commit(opt);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// LLD-C7 — emit select {value,label,group} then close. NO router import, NO command bus.
|
|
298
|
+
#commit(opt) {
|
|
299
|
+
const label = this.#labelText(opt); // primary label (shortcut/icon excluded — SPEC-R6)
|
|
300
|
+
const value = opt.getAttribute('value') ?? label;
|
|
301
|
+
const group = opt.closest('[role=group]')?.querySelector('[data-role=group-label]')?.textContent?.trim() ?? '';
|
|
302
|
+
this.emit('select', { value, label, group });
|
|
303
|
+
this.open = false; // closes the nested modal; the modal's `close` fires with open already false ⇒ no re-emit
|
|
304
|
+
}
|
|
305
|
+
#matchesChord(e, chord) {
|
|
306
|
+
// Minimal chord parse: 'mod+k' → (metaKey||ctrlKey) && key==='k'. Extend per the builder's needs; the CONTRACT
|
|
307
|
+
// is per-instance + preventDefault + toggle (SPEC-R10), not an exhaustive chord grammar.
|
|
308
|
+
const parts = chord.toLowerCase().split('+');
|
|
309
|
+
const key = parts.at(-1);
|
|
310
|
+
const needMod = parts.includes('mod');
|
|
311
|
+
return (!needMod || e.metaKey || e.ctrlKey) && e.key.toLowerCase() === key;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (!customElements.get('ui-command-modal'))
|
|
315
|
+
customElements.define('ui-command-modal', UICommandModalElement);
|