@ds-mo/ui 8.3.1 → 9.1.0
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/README.md +28 -3
- package/dist/.build-stamp +1 -1
- package/dist/agent-patterns.json +233 -0
- package/dist/agent.json +155 -174
- package/dist/angular/ds-menu.d.ts +6 -1
- package/dist/angular/ds-menu.js +7 -4
- package/dist/angular/ds-swatch-picker.d.ts +15 -0
- package/dist/angular/{ds-pagination.js → ds-swatch-picker.js} +15 -15
- package/dist/angular/index.d.ts +1 -1
- package/dist/angular/index.js +1 -2
- package/dist/angular/proxies.d.ts +1 -2
- package/dist/angular/proxies.js +1 -2
- package/dist/components/ds-app-shell.js +1 -1
- package/dist/components/ds-badge.js +1 -1
- package/dist/components/ds-banner.js +1 -1
- package/dist/components/ds-bar-nav.js +1 -1
- package/dist/components/ds-bar-nav.js.map +1 -1
- package/dist/components/ds-button-filled.js +1 -1
- package/dist/components/ds-button-unfilled.js +1 -1
- package/dist/components/ds-card-data-viz-donut.js +1 -1
- package/dist/components/ds-card-setting.js +1 -1
- package/dist/components/ds-card-shell-data-viz.js +1 -1
- package/dist/components/ds-card.js +1 -1
- package/dist/components/ds-chart-bar.js +1 -1
- package/dist/components/ds-chart-bar.js.map +1 -1
- package/dist/components/ds-chart-donut.js +1 -1
- package/dist/components/ds-chart-legend.js +1 -1
- package/dist/components/ds-chart-line.js +1 -1
- package/dist/components/ds-checkbox.js +1 -1
- package/dist/components/ds-chip.js +1 -1
- package/dist/components/ds-empty-state.js +1 -1
- package/dist/components/ds-field.js +1 -1
- package/dist/components/ds-input.js +1 -1
- package/dist/components/ds-menu.js +1 -1
- package/dist/components/ds-modal.js +1 -1
- package/dist/components/ds-panel-nav.js +1 -1
- package/dist/components/ds-panel-nav.js.map +1 -1
- package/dist/components/ds-panel-sub-nav.js +1 -1
- package/dist/components/ds-panel-tools.js +1 -1
- package/dist/components/ds-radio.js +1 -1
- package/dist/components/ds-select-multi.js +1 -1
- package/dist/components/ds-select-multi.js.map +1 -1
- package/dist/components/ds-select.js +1 -1
- package/dist/components/ds-select.js.map +1 -1
- package/dist/components/ds-shell-gradient-picker.js +1 -1
- package/dist/components/ds-shell-gradient-picker.js.map +1 -1
- package/dist/components/ds-shell-gradient-swatch.js +1 -1
- package/dist/components/ds-shell-gradient-swatch.js.map +1 -1
- package/dist/components/ds-skeleton.js +1 -1
- package/dist/components/ds-skeleton.js.map +1 -1
- package/dist/components/ds-slider.js +1 -1
- package/dist/components/ds-slider.js.map +1 -1
- package/dist/components/{ds-pagination.d.ts → ds-swatch-picker.d.ts} +4 -4
- package/dist/components/ds-swatch-picker.js +2 -0
- package/dist/components/ds-swatch-picker.js.map +1 -0
- package/dist/components/ds-tab-group.js +1 -1
- package/dist/components/ds-tag.js +1 -1
- package/dist/components/ds-text.js +1 -1
- package/dist/components/ds-tooltip-data-viz.js +1 -1
- package/dist/components/ds-tooltip.js +1 -1
- package/dist/components/{p-CysJosvS.js → p-B-zhgAvL.js} +2 -2
- package/dist/components/{p-C21sIElQ.js → p-BLzivrE3.js} +2 -2
- package/dist/components/{p-DAuadHYu.js → p-BbV68F23.js} +2 -2
- package/dist/components/{p-_y99wWNU.js → p-C7RObioO.js} +2 -2
- package/dist/components/{p-DxQ4gzbA.js → p-CZ9wemOA.js} +2 -2
- package/dist/components/{p-BKA6houZ.js → p-CbS_WWs2.js} +2 -2
- package/dist/components/p-D1HFMTdC.js +2 -0
- package/dist/components/p-D1HFMTdC.js.map +1 -0
- package/dist/components/{p-BGcSrHKl.js → p-DT2MK8Kt.js} +2 -2
- package/dist/components/{p-DV23KMU-.js → p-DyZj3k9S.js} +2 -2
- package/dist/components/p-HBIxvS5C.js +2 -0
- package/dist/components/p-HBIxvS5C.js.map +1 -0
- package/dist/components/{p-D6hvtWDk.js → p-KRGBGTu5.js} +2 -2
- package/dist/components/p-l6PmnW_P.js +2 -0
- package/dist/components/p-l6PmnW_P.js.map +1 -0
- package/dist/components/p-tQ1QHeOE.js +2 -0
- package/dist/components/p-tQ1QHeOE.js.map +1 -0
- package/dist/docs/components.json +546 -1024
- package/dist/lib/components/Menu/menu-placement.d.ts +6 -3
- package/dist/lib/shell/index.d.ts +2 -2
- package/dist/lib/shell/index.js +40 -39
- package/dist/lib/shell/index.js.map +3 -3
- package/dist/lib/shell/shell-gradient-presets.d.ts +15 -0
- package/dist/lib/utils/choice-popup-alignment.d.ts +20 -0
- package/dist/lib/utils/index.d.ts +2 -0
- package/dist/lib/utils/index.js +16 -0
- package/dist/lib/utils/index.js.map +3 -3
- package/dist/mcp/cli.js +31409 -0
- package/dist/mcp-data/registry/app-shell.json +210 -0
- package/dist/mcp-data/registry/badge.json +289 -0
- package/dist/mcp-data/registry/banner.json +206 -0
- package/dist/mcp-data/registry/bar-nav.json +341 -0
- package/dist/mcp-data/registry/button-filled.json +386 -0
- package/dist/mcp-data/registry/button-unfilled.json +537 -0
- package/dist/mcp-data/registry/card-data-viz-donut.json +143 -0
- package/dist/mcp-data/registry/card-setting.json +194 -0
- package/dist/mcp-data/registry/card-shell-data-viz.json +173 -0
- package/dist/mcp-data/registry/card.json +126 -0
- package/dist/mcp-data/registry/chart-bar.json +121 -0
- package/dist/mcp-data/registry/chart-donut.json +257 -0
- package/dist/mcp-data/registry/chart-legend.json +209 -0
- package/dist/mcp-data/registry/chart-line.json +153 -0
- package/dist/mcp-data/registry/checkbox.json +361 -0
- package/dist/mcp-data/registry/chip.json +276 -0
- package/dist/mcp-data/registry/divider.json +225 -0
- package/dist/mcp-data/registry/empty-state.json +197 -0
- package/dist/mcp-data/registry/field.json +222 -0
- package/dist/mcp-data/registry/icon.json +242 -0
- package/dist/mcp-data/registry/input.json +597 -0
- package/dist/mcp-data/registry/loader.json +192 -0
- package/dist/mcp-data/registry/menu.json +459 -0
- package/dist/mcp-data/registry/modal.json +150 -0
- package/dist/mcp-data/registry/panel-nav.json +622 -0
- package/dist/mcp-data/registry/panel-sub-nav.json +249 -0
- package/dist/mcp-data/registry/panel-tools.json +374 -0
- package/dist/mcp-data/registry/radio.json +374 -0
- package/dist/mcp-data/registry/registry.json +5389 -0
- package/dist/mcp-data/registry/select-multi.json +755 -0
- package/dist/mcp-data/registry/select.json +740 -0
- package/dist/mcp-data/registry/shell-gradient-picker.json +195 -0
- package/dist/mcp-data/registry/shell-gradient-swatch.json +215 -0
- package/dist/mcp-data/registry/skeleton.json +282 -0
- package/dist/mcp-data/registry/slider.json +657 -0
- package/dist/mcp-data/registry/swatch-picker.json +231 -0
- package/dist/mcp-data/registry/switch.json +361 -0
- package/dist/mcp-data/registry/tab-group.json +255 -0
- package/dist/mcp-data/registry/tag.json +352 -0
- package/dist/mcp-data/registry/text.json +371 -0
- package/dist/mcp-data/registry/tooltip-data-viz.json +158 -0
- package/dist/mcp-data/registry/tooltip.json +311 -0
- package/dist/react/components.d.ts +1 -2
- package/dist/react/components.js +1 -2
- package/dist/react/ds-menu.d.ts +1 -0
- package/dist/react/ds-menu.js +2 -1
- package/dist/react/ds-swatch-picker.d.ts +12 -0
- package/dist/react/ds-swatch-picker.js +12 -0
- package/dist/types/components/Menu/Menu.d.ts +7 -1
- package/dist/types/components/Menu/menu-placement.d.ts +6 -3
- package/dist/types/components/Menu/menu-types.d.ts +14 -1
- package/dist/types/components/ShellGradientPicker/ShellGradientPicker.d.ts +2 -5
- package/dist/types/components/ShellGradientSwatch/ShellGradientSwatch.d.ts +1 -0
- package/dist/types/components/SwatchPicker/SwatchPicker.d.ts +21 -0
- package/dist/types/components/SwatchPicker/swatch-picker-types.d.ts +26 -0
- package/dist/types/components.d.ts +117 -237
- package/dist/types/shell/index.d.ts +2 -2
- package/dist/types/shell/shell-gradient-presets.d.ts +15 -0
- package/dist/types/utils/choice-popup-alignment.d.ts +21 -0
- package/dist/types/utils/index.d.ts +2 -0
- package/package.json +6 -2
- package/dist/angular/ds-pagination.d.ts +0 -15
- package/dist/angular/ds-table.d.ts +0 -33
- package/dist/angular/ds-table.js +0 -48
- package/dist/components/ds-pagination.js +0 -2
- package/dist/components/ds-pagination.js.map +0 -1
- package/dist/components/ds-table.d.ts +0 -11
- package/dist/components/ds-table.js +0 -2
- package/dist/components/ds-table.js.map +0 -1
- package/dist/components/p-CKV6rOGa.js +0 -2
- package/dist/components/p-CKV6rOGa.js.map +0 -1
- package/dist/components/p-CSTr2132.js +0 -2
- package/dist/components/p-CSTr2132.js.map +0 -1
- package/dist/components/p-C_cu-HIT.js +0 -2
- package/dist/components/p-C_cu-HIT.js.map +0 -1
- package/dist/components/p-D0B9uDTP.js +0 -2
- package/dist/components/p-D0B9uDTP.js.map +0 -1
- package/dist/components/p-DhxWfdX0.js +0 -2
- package/dist/components/p-DhxWfdX0.js.map +0 -1
- package/dist/components/p-Pip-LNhl.js +0 -2
- package/dist/components/p-Pip-LNhl.js.map +0 -1
- package/dist/react/ds-pagination.d.ts +0 -12
- package/dist/react/ds-pagination.js +0 -12
- package/dist/react/ds-table.d.ts +0 -21
- package/dist/react/ds-table.js +0 -16
- package/dist/types/components/Pagination/Pagination.d.ts +0 -14
- package/dist/types/components/Table/Table.d.ts +0 -56
- /package/dist/components/{p-CysJosvS.js.map → p-B-zhgAvL.js.map} +0 -0
- /package/dist/components/{p-C21sIElQ.js.map → p-BLzivrE3.js.map} +0 -0
- /package/dist/components/{p-DAuadHYu.js.map → p-BbV68F23.js.map} +0 -0
- /package/dist/components/{p-_y99wWNU.js.map → p-C7RObioO.js.map} +0 -0
- /package/dist/components/{p-DxQ4gzbA.js.map → p-CZ9wemOA.js.map} +0 -0
- /package/dist/components/{p-BKA6houZ.js.map → p-CbS_WWs2.js.map} +0 -0
- /package/dist/components/{p-BGcSrHKl.js.map → p-DT2MK8Kt.js.map} +0 -0
- /package/dist/components/{p-DV23KMU-.js.map → p-DyZj3k9S.js.map} +0 -0
- /package/dist/components/{p-D6hvtWDk.js.map → p-KRGBGTu5.js.map} +0 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "menu",
|
|
4
|
+
"title": "Menu",
|
|
5
|
+
"description": "Anchored action menu with flat commands, grouped sections, selected command state, supplemental notification dots, switches, and destructive actions.",
|
|
6
|
+
"type": "registry:ui",
|
|
7
|
+
"meta": {
|
|
8
|
+
"library": "@ds-mo/ui",
|
|
9
|
+
"distribution": "npm",
|
|
10
|
+
"storybook": "https://zainadeel.github.io/compomo/?path=/story/menu",
|
|
11
|
+
"source": "src/wc/components/Menu/Menu.tsx",
|
|
12
|
+
"intentStatus": "complete",
|
|
13
|
+
"intent": {
|
|
14
|
+
"audience": "general",
|
|
15
|
+
"status": "stable",
|
|
16
|
+
"summary": "Anchored action menu with flat commands, grouped sections, selected command state, supplemental notification dots, switches, and destructive actions.",
|
|
17
|
+
"useWhen": [
|
|
18
|
+
"A trigger reveals a short contextual list of commands or state-changing actions.",
|
|
19
|
+
"Actions need grouping, selected state, supplemental notification dots, switch rows, or destructive treatment."
|
|
20
|
+
],
|
|
21
|
+
"avoidWhen": [
|
|
22
|
+
"The user is selecting a value for a form field.",
|
|
23
|
+
"The content requires rich dialog interaction or a multi-step workflow.",
|
|
24
|
+
"The actions should remain persistently visible."
|
|
25
|
+
],
|
|
26
|
+
"alternatives": [
|
|
27
|
+
{
|
|
28
|
+
"when": "The choice is a form value with a label, validation, and submitted value.",
|
|
29
|
+
"component": "component:ds-select",
|
|
30
|
+
"reason": "Select provides form-control semantics and value ownership."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"when": "Several form values may be selected.",
|
|
34
|
+
"component": "component:ds-select-multi",
|
|
35
|
+
"reason": "SelectMulti provides multi-select listbox semantics, array value ownership, and count presentation."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"when": "The interaction needs substantial content, confirmation, or a form.",
|
|
39
|
+
"component": "component:ds-modal",
|
|
40
|
+
"reason": "Modal owns focus containment and dialog-level interaction."
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"commonlyComposedWith": [
|
|
44
|
+
"component:ds-button-unfilled",
|
|
45
|
+
"component:ds-panel-nav",
|
|
46
|
+
"component:ds-switch",
|
|
47
|
+
"component:ds-swatch-picker"
|
|
48
|
+
],
|
|
49
|
+
"patterns": [
|
|
50
|
+
"pattern:menu-trigger"
|
|
51
|
+
],
|
|
52
|
+
"accessibility": [
|
|
53
|
+
"Anchor the menu to the control that opens it and return focus to that control when the menu closes.",
|
|
54
|
+
"Open with visible initial focus only when the trigger was activated from the keyboard.",
|
|
55
|
+
"Keep disabled items discoverable only when their unavailable state is meaningful.",
|
|
56
|
+
"Switch rows use menuitemcheckbox semantics on the row; the nested ds-switch is an aria-hidden md presentation indicator.",
|
|
57
|
+
"A swatch-picker section changes the popup to a non-modal dialog so its radio group and ordinary preference buttons keep valid nested semantics; Tab moves between its controls, then leaves and closes at the popup boundary.",
|
|
58
|
+
"Use notification dots only as supplemental status and not as the sole indication of important information."
|
|
59
|
+
],
|
|
60
|
+
"states": [
|
|
61
|
+
"The host owns open state and responds to selection and close events.",
|
|
62
|
+
"Use either flat items or sections, not both.",
|
|
63
|
+
"After dsSelect from a switch row, update switchValue in the items property so the controlled indicator reflects the new state.",
|
|
64
|
+
"Arrays, objects, and element anchors must be assigned as JavaScript properties.",
|
|
65
|
+
"Use a swatch-picker section for a small curated visual preference and apply dsSwatchSelect immediately without closing the popup; the legacy gradient-picker section and dsGradientSelect remain compatibility-only."
|
|
66
|
+
],
|
|
67
|
+
"responsiveBehavior": [
|
|
68
|
+
"Keep the default choice-cell anchor alignment so the first or last interactive row edge aligns with its trigger, matching Select; use popup-frame only for deliberate custom frame geometry.",
|
|
69
|
+
"Choose placement relative to the trigger and allow the component to clamp the popup to the viewport.",
|
|
70
|
+
"Avoid fixed widths that exceed narrow viewports."
|
|
71
|
+
],
|
|
72
|
+
"frameworkCaveats": {
|
|
73
|
+
"customElements": [
|
|
74
|
+
"Assign items, sections, and HTMLElement anchors as properties after the element is defined."
|
|
75
|
+
],
|
|
76
|
+
"react": [
|
|
77
|
+
"Pass arrays and objects as wrapper props and handle generated onDsSelect and onDsClose event props."
|
|
78
|
+
],
|
|
79
|
+
"angular": [
|
|
80
|
+
"Use property bindings for arrays, objects, and anchors and event bindings for dsSelect and dsClose."
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
"references": [
|
|
84
|
+
{
|
|
85
|
+
"label": "Storybook examples",
|
|
86
|
+
"path": "src/wc/components/Menu/Menu.stories.ts"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"label": "Framework integration",
|
|
90
|
+
"path": "docs/framework-integration.md"
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
"api": {
|
|
95
|
+
"props": {
|
|
96
|
+
"align": {
|
|
97
|
+
"type": "MenuAlign",
|
|
98
|
+
"resolvedType": "\"center\" | \"end\" | \"start\"",
|
|
99
|
+
"attribute": "align",
|
|
100
|
+
"default": "'start'",
|
|
101
|
+
"required": false,
|
|
102
|
+
"mutable": false
|
|
103
|
+
},
|
|
104
|
+
"alignOffset": {
|
|
105
|
+
"type": "number | string",
|
|
106
|
+
"resolvedType": "number | string",
|
|
107
|
+
"attribute": "align-offset",
|
|
108
|
+
"default": "0",
|
|
109
|
+
"required": false,
|
|
110
|
+
"mutable": false,
|
|
111
|
+
"description": "Cross-axis offset — number (px) or TokoMo length."
|
|
112
|
+
},
|
|
113
|
+
"anchor": {
|
|
114
|
+
"type": "HTMLElement | undefined",
|
|
115
|
+
"resolvedType": "HTMLElement | undefined",
|
|
116
|
+
"required": false,
|
|
117
|
+
"mutable": false,
|
|
118
|
+
"description": "External trigger element to position against. Set via JS: menuEl.anchor = buttonEl"
|
|
119
|
+
},
|
|
120
|
+
"anchorAlignment": {
|
|
121
|
+
"type": "ChoicePopupAnchorAlignment",
|
|
122
|
+
"resolvedType": "\"choice-cell\" | \"popup-frame\"",
|
|
123
|
+
"attribute": "anchor-alignment",
|
|
124
|
+
"default": "'choice-cell'",
|
|
125
|
+
"required": false,
|
|
126
|
+
"mutable": false,
|
|
127
|
+
"description": "Align choice-row edges to the anchor by default; use `popup-frame` only for custom frame geometry."
|
|
128
|
+
},
|
|
129
|
+
"anchorId": {
|
|
130
|
+
"type": "string | undefined",
|
|
131
|
+
"resolvedType": "string | undefined",
|
|
132
|
+
"attribute": "anchor-id",
|
|
133
|
+
"required": false,
|
|
134
|
+
"mutable": false,
|
|
135
|
+
"description": "ID of the external trigger element for positioning"
|
|
136
|
+
},
|
|
137
|
+
"initialFocusVisible": {
|
|
138
|
+
"type": "boolean",
|
|
139
|
+
"resolvedType": "boolean",
|
|
140
|
+
"attribute": "initial-focus-visible",
|
|
141
|
+
"default": "false",
|
|
142
|
+
"required": false,
|
|
143
|
+
"mutable": false,
|
|
144
|
+
"description": "Show a visible ring on the initially focused menu item. Use only when the opener was keyboard-driven."
|
|
145
|
+
},
|
|
146
|
+
"items": {
|
|
147
|
+
"type": "MenuItemData[]",
|
|
148
|
+
"resolvedType": "MenuItemData[]",
|
|
149
|
+
"default": "[]",
|
|
150
|
+
"required": false,
|
|
151
|
+
"mutable": false
|
|
152
|
+
},
|
|
153
|
+
"menuLabel": {
|
|
154
|
+
"type": "string",
|
|
155
|
+
"resolvedType": "string",
|
|
156
|
+
"attribute": "menu-label",
|
|
157
|
+
"default": "'Menu'",
|
|
158
|
+
"required": false,
|
|
159
|
+
"mutable": false,
|
|
160
|
+
"description": "Accessible name for the popup menu."
|
|
161
|
+
},
|
|
162
|
+
"menuWidth": {
|
|
163
|
+
"type": "string | undefined",
|
|
164
|
+
"resolvedType": "string | undefined",
|
|
165
|
+
"attribute": "menu-width",
|
|
166
|
+
"required": false,
|
|
167
|
+
"mutable": false
|
|
168
|
+
},
|
|
169
|
+
"minWidth": {
|
|
170
|
+
"type": "string | undefined",
|
|
171
|
+
"resolvedType": "string | undefined",
|
|
172
|
+
"attribute": "min-width",
|
|
173
|
+
"required": false,
|
|
174
|
+
"mutable": false
|
|
175
|
+
},
|
|
176
|
+
"open": {
|
|
177
|
+
"type": "boolean",
|
|
178
|
+
"resolvedType": "boolean",
|
|
179
|
+
"attribute": "open",
|
|
180
|
+
"default": "false",
|
|
181
|
+
"required": false,
|
|
182
|
+
"mutable": true
|
|
183
|
+
},
|
|
184
|
+
"sections": {
|
|
185
|
+
"type": "MenuSection[]",
|
|
186
|
+
"resolvedType": "MenuSection[]",
|
|
187
|
+
"default": "[]",
|
|
188
|
+
"required": false,
|
|
189
|
+
"mutable": false
|
|
190
|
+
},
|
|
191
|
+
"side": {
|
|
192
|
+
"type": "MenuSide",
|
|
193
|
+
"resolvedType": "\"bottom\" | \"left\" | \"right\" | \"top\"",
|
|
194
|
+
"attribute": "side",
|
|
195
|
+
"default": "'bottom'",
|
|
196
|
+
"required": false,
|
|
197
|
+
"mutable": false
|
|
198
|
+
},
|
|
199
|
+
"sideOffset": {
|
|
200
|
+
"type": "number | string",
|
|
201
|
+
"resolvedType": "number | string",
|
|
202
|
+
"attribute": "side-offset",
|
|
203
|
+
"default": "TOKEN_CSS_LENGTHS.space050",
|
|
204
|
+
"required": false,
|
|
205
|
+
"mutable": false,
|
|
206
|
+
"description": "Gap between anchor and menu — number (px) or TokoMo length (`var(--dimension-space-050)`, etc.)."
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
"events": [
|
|
210
|
+
{
|
|
211
|
+
"name": "dsClose",
|
|
212
|
+
"detail": "void",
|
|
213
|
+
"bubbles": true,
|
|
214
|
+
"cancelable": true,
|
|
215
|
+
"composed": true
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"name": "dsGradientSelect",
|
|
219
|
+
"detail": "ShellGradientPreset",
|
|
220
|
+
"bubbles": true,
|
|
221
|
+
"cancelable": true,
|
|
222
|
+
"composed": true,
|
|
223
|
+
"description": "Emitted when a `gradient-picker` section swatch is chosen."
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"name": "dsSelect",
|
|
227
|
+
"detail": "MenuItemData",
|
|
228
|
+
"bubbles": true,
|
|
229
|
+
"cancelable": true,
|
|
230
|
+
"composed": true
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"name": "dsSwatchSelect",
|
|
234
|
+
"detail": "string",
|
|
235
|
+
"bubbles": true,
|
|
236
|
+
"cancelable": true,
|
|
237
|
+
"composed": true,
|
|
238
|
+
"description": "Emitted when a generic `swatch-picker` section option is chosen."
|
|
239
|
+
}
|
|
240
|
+
],
|
|
241
|
+
"methods": [],
|
|
242
|
+
"slots": []
|
|
243
|
+
},
|
|
244
|
+
"props": {
|
|
245
|
+
"align": {
|
|
246
|
+
"type": "MenuAlign",
|
|
247
|
+
"resolvedType": "\"center\" | \"end\" | \"start\"",
|
|
248
|
+
"attribute": "align",
|
|
249
|
+
"default": "'start'",
|
|
250
|
+
"required": false,
|
|
251
|
+
"mutable": false
|
|
252
|
+
},
|
|
253
|
+
"alignOffset": {
|
|
254
|
+
"type": "number | string",
|
|
255
|
+
"resolvedType": "number | string",
|
|
256
|
+
"attribute": "align-offset",
|
|
257
|
+
"default": "0",
|
|
258
|
+
"required": false,
|
|
259
|
+
"mutable": false,
|
|
260
|
+
"description": "Cross-axis offset — number (px) or TokoMo length."
|
|
261
|
+
},
|
|
262
|
+
"anchor": {
|
|
263
|
+
"type": "HTMLElement | undefined",
|
|
264
|
+
"resolvedType": "HTMLElement | undefined",
|
|
265
|
+
"required": false,
|
|
266
|
+
"mutable": false,
|
|
267
|
+
"description": "External trigger element to position against. Set via JS: menuEl.anchor = buttonEl"
|
|
268
|
+
},
|
|
269
|
+
"anchorAlignment": {
|
|
270
|
+
"type": "ChoicePopupAnchorAlignment",
|
|
271
|
+
"resolvedType": "\"choice-cell\" | \"popup-frame\"",
|
|
272
|
+
"attribute": "anchor-alignment",
|
|
273
|
+
"default": "'choice-cell'",
|
|
274
|
+
"required": false,
|
|
275
|
+
"mutable": false,
|
|
276
|
+
"description": "Align choice-row edges to the anchor by default; use `popup-frame` only for custom frame geometry."
|
|
277
|
+
},
|
|
278
|
+
"anchorId": {
|
|
279
|
+
"type": "string | undefined",
|
|
280
|
+
"resolvedType": "string | undefined",
|
|
281
|
+
"attribute": "anchor-id",
|
|
282
|
+
"required": false,
|
|
283
|
+
"mutable": false,
|
|
284
|
+
"description": "ID of the external trigger element for positioning"
|
|
285
|
+
},
|
|
286
|
+
"initialFocusVisible": {
|
|
287
|
+
"type": "boolean",
|
|
288
|
+
"resolvedType": "boolean",
|
|
289
|
+
"attribute": "initial-focus-visible",
|
|
290
|
+
"default": "false",
|
|
291
|
+
"required": false,
|
|
292
|
+
"mutable": false,
|
|
293
|
+
"description": "Show a visible ring on the initially focused menu item. Use only when the opener was keyboard-driven."
|
|
294
|
+
},
|
|
295
|
+
"items": {
|
|
296
|
+
"type": "MenuItemData[]",
|
|
297
|
+
"resolvedType": "MenuItemData[]",
|
|
298
|
+
"default": "[]",
|
|
299
|
+
"required": false,
|
|
300
|
+
"mutable": false
|
|
301
|
+
},
|
|
302
|
+
"menuLabel": {
|
|
303
|
+
"type": "string",
|
|
304
|
+
"resolvedType": "string",
|
|
305
|
+
"attribute": "menu-label",
|
|
306
|
+
"default": "'Menu'",
|
|
307
|
+
"required": false,
|
|
308
|
+
"mutable": false,
|
|
309
|
+
"description": "Accessible name for the popup menu."
|
|
310
|
+
},
|
|
311
|
+
"menuWidth": {
|
|
312
|
+
"type": "string | undefined",
|
|
313
|
+
"resolvedType": "string | undefined",
|
|
314
|
+
"attribute": "menu-width",
|
|
315
|
+
"required": false,
|
|
316
|
+
"mutable": false
|
|
317
|
+
},
|
|
318
|
+
"minWidth": {
|
|
319
|
+
"type": "string | undefined",
|
|
320
|
+
"resolvedType": "string | undefined",
|
|
321
|
+
"attribute": "min-width",
|
|
322
|
+
"required": false,
|
|
323
|
+
"mutable": false
|
|
324
|
+
},
|
|
325
|
+
"open": {
|
|
326
|
+
"type": "boolean",
|
|
327
|
+
"resolvedType": "boolean",
|
|
328
|
+
"attribute": "open",
|
|
329
|
+
"default": "false",
|
|
330
|
+
"required": false,
|
|
331
|
+
"mutable": true
|
|
332
|
+
},
|
|
333
|
+
"sections": {
|
|
334
|
+
"type": "MenuSection[]",
|
|
335
|
+
"resolvedType": "MenuSection[]",
|
|
336
|
+
"default": "[]",
|
|
337
|
+
"required": false,
|
|
338
|
+
"mutable": false
|
|
339
|
+
},
|
|
340
|
+
"side": {
|
|
341
|
+
"type": "MenuSide",
|
|
342
|
+
"resolvedType": "\"bottom\" | \"left\" | \"right\" | \"top\"",
|
|
343
|
+
"attribute": "side",
|
|
344
|
+
"default": "'bottom'",
|
|
345
|
+
"required": false,
|
|
346
|
+
"mutable": false
|
|
347
|
+
},
|
|
348
|
+
"sideOffset": {
|
|
349
|
+
"type": "number | string",
|
|
350
|
+
"resolvedType": "number | string",
|
|
351
|
+
"attribute": "side-offset",
|
|
352
|
+
"default": "TOKEN_CSS_LENGTHS.space050",
|
|
353
|
+
"required": false,
|
|
354
|
+
"mutable": false,
|
|
355
|
+
"description": "Gap between anchor and menu — number (px) or TokoMo length (`var(--dimension-space-050)`, etc.)."
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
"events": [
|
|
359
|
+
{
|
|
360
|
+
"name": "dsClose",
|
|
361
|
+
"detail": "void",
|
|
362
|
+
"bubbles": true,
|
|
363
|
+
"cancelable": true,
|
|
364
|
+
"composed": true
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
"name": "dsGradientSelect",
|
|
368
|
+
"detail": "ShellGradientPreset",
|
|
369
|
+
"bubbles": true,
|
|
370
|
+
"cancelable": true,
|
|
371
|
+
"composed": true,
|
|
372
|
+
"description": "Emitted when a `gradient-picker` section swatch is chosen."
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
"name": "dsSelect",
|
|
376
|
+
"detail": "MenuItemData",
|
|
377
|
+
"bubbles": true,
|
|
378
|
+
"cancelable": true,
|
|
379
|
+
"composed": true
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
"name": "dsSwatchSelect",
|
|
383
|
+
"detail": "string",
|
|
384
|
+
"bubbles": true,
|
|
385
|
+
"cancelable": true,
|
|
386
|
+
"composed": true,
|
|
387
|
+
"description": "Emitted when a generic `swatch-picker` section option is chosen."
|
|
388
|
+
}
|
|
389
|
+
],
|
|
390
|
+
"methods": [],
|
|
391
|
+
"slots": [],
|
|
392
|
+
"exports": {
|
|
393
|
+
"customElement": "ds-menu",
|
|
394
|
+
"react": "DsMenu",
|
|
395
|
+
"angular": "DsMenu"
|
|
396
|
+
},
|
|
397
|
+
"consumption": {
|
|
398
|
+
"install": "npm install @ds-mo/ui @ds-mo/tokens @ds-mo/icons",
|
|
399
|
+
"cssSetup": "import '@ds-mo/tokens';\nimport '@ds-mo/tokens/reset';\nimport '@ds-mo/tokens/globals';",
|
|
400
|
+
"customElements": {
|
|
401
|
+
"import": "import '@ds-mo/ui/dist/components/ds-menu.js';",
|
|
402
|
+
"example": "<ds-menu></ds-menu>"
|
|
403
|
+
},
|
|
404
|
+
"react": {
|
|
405
|
+
"import": "import { DsMenu } from '@ds-mo/ui/react';",
|
|
406
|
+
"example": "<DsMenu />"
|
|
407
|
+
},
|
|
408
|
+
"angular": {
|
|
409
|
+
"import": "import { DsMenu } from '@ds-mo/ui/angular/ds-menu';",
|
|
410
|
+
"example": "<ds-menu></ds-menu>"
|
|
411
|
+
},
|
|
412
|
+
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: anchor, items, sections.",
|
|
413
|
+
"peerDependencies": {
|
|
414
|
+
"required": [
|
|
415
|
+
"@ds-mo/tokens ^3.1.1",
|
|
416
|
+
"@ds-mo/icons ^6.2.0"
|
|
417
|
+
],
|
|
418
|
+
"frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
"dependencies": [
|
|
423
|
+
"@ds-mo/ui",
|
|
424
|
+
"@ds-mo/tokens"
|
|
425
|
+
],
|
|
426
|
+
"registryDependencies": [
|
|
427
|
+
"badge",
|
|
428
|
+
"swatch-picker",
|
|
429
|
+
"switch",
|
|
430
|
+
"text"
|
|
431
|
+
],
|
|
432
|
+
"files": [
|
|
433
|
+
{
|
|
434
|
+
"path": "src/wc/components/Menu/Menu.css",
|
|
435
|
+
"content": "@import '../../utils/choice-list.css';\n\n/* Inactive: `.ds-control-inactive` (25% opacity). */\n\n.menu-item--destructive .menu-item__label {\n /* Beat ds-text color-* host classes for destructive rows. */\n color: var(--color-foreground-bold-negative) !important;\n}\n\n/* Standard md icon zone keeps the dot aligned with other control suffixes. */\n.menu-item__dot-box {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--ds-control-icon, var(--dimension-iconography-md));\n height: var(--ds-control-icon, var(--dimension-iconography-md));\n pointer-events: none;\n}\n\n/* ── Switch suffix ───────────────────────────────────────────────────────── */\n\n.menu-item__switch {\n flex-shrink: 0;\n}\n",
|
|
436
|
+
"type": "registry:ui"
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
"path": "src/wc/components/Menu/Menu.tsx",
|
|
440
|
+
"content": "import { Component, Prop, State, Event, EventEmitter, Element, Watch, Listen, h, Host } from '@stencil/core';\nimport {\n choicePopupMinWidth,\n resolveChoicePopupAlignOffset,\n resolveCssLengthPx,\n resolveMotionTimeMs,\n TOKEN_CSS_LENGTHS,\n TOKEN_DEFAULTS,\n type ChoicePopupAnchorAlignment,\n} from '../../utils';\nimport { computeMenuPosition, type MenuAlign, type MenuSide } from './menu-position';\nimport type { MenuItemData, MenuSection } from './menu-types';\nimport {\n isMenuGradientPickerSection,\n isMenuPickerSection,\n isMenuSwatchPickerSection,\n} from './menu-types';\nimport {\n shellGradientPickerSections,\n type ShellGradientPreset,\n} from '../../shell/shell-gradient-presets';\n\n/** rAF retries while the popup mounts or the anchor resolves. */\nconst POSITION_RETRY_BUDGET = 8;\nconst MENU_FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled]):not([type=\"hidden\"])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(',');\n\n@Component({\n tag: 'ds-menu',\n styleUrl: 'Menu.css',\n scoped: true,\n})\nexport class Menu {\n @Element() el!: HTMLElement;\n\n @Prop({ mutable: true }) open: boolean = false;\n @Prop() items: MenuItemData[] = [];\n @Prop() sections: MenuSection[] = [];\n @Prop() side: MenuSide = 'bottom';\n @Prop() align: MenuAlign = 'start';\n /** Align choice-row edges to the anchor by default; use `popup-frame` only for custom frame geometry. */\n @Prop() anchorAlignment: ChoicePopupAnchorAlignment = 'choice-cell';\n /** Gap between anchor and menu — number (px) or TokoMo length (`var(--dimension-space-050)`, etc.). */\n @Prop() sideOffset: number | string = TOKEN_CSS_LENGTHS.space050;\n /** Cross-axis offset — number (px) or TokoMo length. */\n @Prop() alignOffset: number | string = 0;\n @Prop() menuWidth: string | undefined;\n @Prop() minWidth: string | undefined;\n /** External trigger element to position against. Set via JS: menuEl.anchor = buttonEl */\n @Prop() anchor: HTMLElement | undefined;\n /** ID of the external trigger element for positioning */\n @Prop() anchorId: string | undefined;\n /** Show a visible ring on the initially focused menu item. Use only when the opener was keyboard-driven. */\n @Prop() initialFocusVisible: boolean = false;\n /** Accessible name for the popup menu. */\n @Prop() menuLabel: string = 'Menu';\n\n @State() private shouldRender: boolean = false;\n @State() private closing: boolean = false;\n @State() private pos: { x: number; y: number } = { x: 0, y: 0 };\n @State() private focusedIndex: number = 0;\n @State() private positionReady: boolean = false;\n @State() private focusRingVisible: boolean = false;\n\n @Event() dsClose!: EventEmitter<void>;\n @Event() dsSelect!: EventEmitter<MenuItemData>;\n /** Emitted when a `gradient-picker` section swatch is chosen. */\n @Event() dsGradientSelect!: EventEmitter<ShellGradientPreset>;\n /** Emitted when a generic `swatch-picker` section option is chosen. */\n @Event() dsSwatchSelect!: EventEmitter<string>;\n\n private clickOutsideHandler: ((e: MouseEvent) => void) | null = null;\n private scrollResizeHandler: (() => void) | null = null;\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n private itemEls: HTMLElement[] = [];\n private positionRetryRaf: number | null = null;\n private listenersReady = false;\n\n componentDidLoad() {\n if (this.open) this.onOpenChange(true);\n }\n\n disconnectedCallback() {\n this.cancelPositionRetry();\n this.teardownListeners();\n }\n\n @Watch('open')\n onOpenChange(isOpen: boolean) {\n if (isOpen) {\n this.teardownListeners();\n this.shouldRender = true;\n this.closing = false;\n this.positionReady = false;\n this.listenersReady = false;\n this.focusRingVisible = this.initialFocusVisible;\n this.listenersReady = true;\n this.setupListeners();\n this.schedulePositionUpdate(() => {\n this.focusInitialItem();\n });\n } else if (this.shouldRender) {\n this.cancelPositionRetry();\n this.closing = true;\n this.teardownListeners();\n this.listenersReady = false;\n const closeAnimationMs = this.closeAnimationMs;\n if (closeAnimationMs <= 0) {\n this.finishClose();\n return;\n }\n this.closeTimer = setTimeout(() => {\n this.closeTimer = null;\n this.finishClose();\n }, closeAnimationMs);\n }\n }\n\n @Watch('anchor')\n @Watch('anchorId')\n onAnchorChange() {\n if (this.open) this.schedulePositionUpdate();\n }\n\n @Watch('side')\n @Watch('align')\n @Watch('anchorAlignment')\n @Watch('sideOffset')\n @Watch('alignOffset')\n onPositionPropsChange() {\n if (this.open) this.schedulePositionUpdate();\n }\n\n private get viewportPadPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n }\n\n private get sideOffsetPx(): number {\n return resolveCssLengthPx(this.sideOffset, TOKEN_DEFAULTS.space050);\n }\n\n private get alignOffsetPx(): number {\n return resolveCssLengthPx(this.alignOffset, 0);\n }\n\n private get popupFallbackWidthPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuWidthXs, TOKEN_DEFAULTS.menuWidthXs);\n }\n\n private get popupFallbackHeightPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuFallbackHeight, TOKEN_DEFAULTS.menuFallbackHeight);\n }\n\n private get closeAnimationMs(): number {\n return resolveMotionTimeMs(TOKEN_DEFAULTS.motionShort2, TOKEN_DEFAULTS.animationDurationShort3);\n }\n\n private finishClose() {\n this.shouldRender = false;\n this.closing = false;\n }\n\n private get resolvedAnchor(): HTMLElement | null {\n if (this.anchor) return this.anchor;\n if (this.anchorId) return document.getElementById(this.anchorId);\n return null;\n }\n\n private get anchorFocusTarget(): HTMLElement | null {\n const anchor = this.resolvedAnchor;\n if (!anchor) return null;\n if (anchor.matches(MENU_FOCUSABLE_SELECTOR)) return anchor;\n return anchor.querySelector<HTMLElement>(MENU_FOCUSABLE_SELECTOR) ?? anchor;\n }\n\n private focusAnchor() {\n const anchor = this.resolvedAnchor as (HTMLElement & { setFocus?: () => Promise<void> | void }) | null;\n if (anchor?.setFocus) {\n anchor.setFocus();\n return;\n }\n this.anchorFocusTarget?.focus();\n }\n\n private moveFocusAfterTab(backwards: boolean) {\n const anchor = this.anchorFocusTarget;\n if (!anchor) return;\n if (backwards) {\n anchor.focus();\n return;\n }\n\n const candidates = Array.from(document.querySelectorAll<HTMLElement>(MENU_FOCUSABLE_SELECTOR))\n .filter(element => (\n !element.closest('.menu-popup') &&\n !element.closest('[inert]') &&\n element.getClientRects().length > 0\n ));\n const index = candidates.indexOf(anchor);\n (candidates[index + 1] ?? anchor).focus();\n }\n\n private compositeTabLeavesPopup(event: KeyboardEvent): boolean {\n const popup = this.el.querySelector<HTMLElement>('.menu-popup');\n if (!popup) return true;\n const focusables = Array.from(popup.querySelectorAll<HTMLElement>(MENU_FOCUSABLE_SELECTOR))\n .filter(element => element.getClientRects().length > 0);\n const currentIndex = focusables.indexOf(event.target as HTMLElement);\n if (currentIndex < 0) return true;\n return event.shiftKey ? currentIndex === 0 : currentIndex === focusables.length - 1;\n }\n\n private get activeSections(): MenuSection[] {\n if (this.sections.length > 0) return this.sections;\n if (this.items.length > 0) return [{ items: this.items }];\n return [];\n }\n\n private get flatItems(): MenuItemData[] {\n return this.activeSections.flatMap(section =>\n isMenuPickerSection(section) ? [] : section.items,\n );\n }\n\n /** Rich preference content is a non-modal dialog, not an ARIA action menu. */\n private get hasCompositeSections(): boolean {\n return this.activeSections.some(isMenuPickerSection);\n }\n\n private cancelPositionRetry() {\n if (this.positionRetryRaf !== null) {\n cancelAnimationFrame(this.positionRetryRaf);\n this.positionRetryRaf = null;\n }\n }\n\n /** Retry until anchor + popup exist — do not reveal at 0,0 on a failed first pass. */\n private schedulePositionUpdate(onReady?: () => void) {\n if (!this.open) return;\n\n this.cancelPositionRetry();\n this.positionReady = false;\n\n let remaining = POSITION_RETRY_BUDGET;\n\n const attempt = () => {\n this.positionRetryRaf = null;\n if (!this.open) return;\n\n if (this.calculatePosition()) {\n this.positionReady = true;\n onReady?.();\n return;\n }\n\n if (remaining > 0) {\n remaining -= 1;\n this.positionRetryRaf = requestAnimationFrame(attempt);\n }\n };\n\n this.positionRetryRaf = requestAnimationFrame(attempt);\n }\n\n /** @returns `true` when anchor and popup were found and `pos` was updated. */\n private calculatePosition(): boolean {\n const anchorEl = this.resolvedAnchor;\n if (!anchorEl) return false;\n const popup = this.el.querySelector('.menu-popup') as HTMLElement | null;\n if (!popup) return false;\n\n const anchorRect = anchorEl.getBoundingClientRect();\n const sectionInsetPx = this.viewportPadPx;\n if (!this.minWidth) {\n if (this.anchorAlignment === 'choice-cell' && (this.side === 'top' || this.side === 'bottom')) {\n popup.style.minWidth = `max(var(--dimension-menu-width-xs), ${choicePopupMinWidth(anchorRect.width, sectionInsetPx)}px)`;\n } else {\n popup.style.removeProperty('min-width');\n }\n }\n\n this.pos = computeMenuPosition({\n anchorRect,\n popupWidth: popup.offsetWidth || this.popupFallbackWidthPx,\n popupHeight: popup.offsetHeight || this.popupFallbackHeightPx,\n side: this.side,\n align: this.align,\n sideOffsetPx: this.sideOffsetPx,\n alignOffsetPx: resolveChoicePopupAlignOffset({\n align: this.align,\n alignOffsetPx: this.alignOffsetPx,\n sectionInsetPx,\n anchorAlignment: this.anchorAlignment,\n }),\n viewportPadPx: this.viewportPadPx,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n });\n return true;\n }\n\n /** Focus the selected item when present, otherwise the first enabled item. */\n private focusInitialItem() {\n if (this.hasCompositeSections) {\n requestAnimationFrame(() => {\n this.el.querySelector<HTMLElement>('.menu-popup [tabindex=\"0\"]')?.focus();\n });\n return;\n }\n\n const flat = this.flatItems;\n const selectedIdx = flat.findIndex(it => it.isSelected && !it.isInactive);\n const firstEnabledIdx = flat.findIndex(it => !it.isInactive);\n this.focusedIndex = selectedIdx >= 0\n ? selectedIdx\n : (firstEnabledIdx >= 0 ? firstEnabledIdx : 0);\n\n requestAnimationFrame(() => {\n const btns = this.el.querySelectorAll<HTMLElement>('.menu-item');\n btns[this.focusedIndex]?.focus();\n });\n }\n\n private setupListeners() {\n this.clickOutsideHandler = (e: MouseEvent) => {\n const t = e.target as Node;\n const popup = this.el.querySelector('.menu-popup');\n const anchorEl = this.resolvedAnchor;\n if (popup?.contains(t) || anchorEl?.contains(t)) return;\n this.close();\n };\n\n this.scrollResizeHandler = () => {\n if (this.shouldRender && !this.closing) this.calculatePosition();\n };\n\n document.addEventListener('mousedown', this.clickOutsideHandler, true);\n window.addEventListener('scroll', this.scrollResizeHandler, true);\n window.addEventListener('resize', this.scrollResizeHandler);\n }\n\n private teardownListeners() {\n if (this.clickOutsideHandler) {\n document.removeEventListener('mousedown', this.clickOutsideHandler, true);\n this.clickOutsideHandler = null;\n }\n if (this.scrollResizeHandler) {\n window.removeEventListener('scroll', this.scrollResizeHandler, true);\n window.removeEventListener('resize', this.scrollResizeHandler);\n this.scrollResizeHandler = null;\n }\n if (this.closeTimer) {\n clearTimeout(this.closeTimer);\n this.closeTimer = null;\n }\n }\n\n private close(restoreFocus = true) {\n if (restoreFocus) this.focusAnchor();\n this.dsClose.emit();\n this.open = false;\n this.onOpenChange(false);\n }\n\n @Listen('keydown')\n handleKeyDown(e: KeyboardEvent) {\n if (!this.shouldRender || this.closing) return;\n\n if (e.key === 'Escape') {\n e.preventDefault();\n this.close();\n return;\n }\n\n if (e.key === 'Tab') {\n if (this.hasCompositeSections && !this.compositeTabLeavesPopup(e)) return;\n e.preventDefault();\n this.close(false);\n this.moveFocusAfterTab(e.shiftKey);\n return;\n }\n\n if (this.hasCompositeSections) return;\n\n const flat = this.flatItems;\n const enabled = flat.map((it, i) => ({ it, i })).filter(({ it }) => !it.isInactive).map(({ i }) => i);\n if (!enabled.length) return;\n\n const cur = enabled.indexOf(this.focusedIndex);\n const safe = cur < 0 ? 0 : cur;\n\n switch (e.key) {\n case 'ArrowDown':\n e.preventDefault(); e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[(safe + 1) % enabled.length];\n this.focusItem(this.focusedIndex);\n break;\n case 'ArrowUp':\n e.preventDefault(); e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[(safe - 1 + enabled.length) % enabled.length];\n this.focusItem(this.focusedIndex);\n break;\n case 'Home':\n e.preventDefault(); e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[0];\n this.focusItem(this.focusedIndex);\n break;\n case 'End':\n e.preventDefault(); e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[enabled.length - 1];\n this.focusItem(this.focusedIndex);\n break;\n }\n }\n\n private focusItem(idx: number) {\n const btns = this.el.querySelectorAll<HTMLElement>('.menu-item');\n btns[idx]?.focus();\n }\n\n private handleItemClick(item: MenuItemData) {\n if (item.isInactive) return;\n this.dsSelect.emit(item);\n }\n\n private handleGradientSelect(preset: ShellGradientPreset) {\n this.dsGradientSelect.emit(preset);\n }\n\n private handleSwatchSelect(value: string) {\n this.dsSwatchSelect.emit(value);\n }\n\n render() {\n if (!this.shouldRender) return <Host style={{ display: 'contents' }} />;\n\n const sections = this.activeSections;\n let flatIdx = 0;\n\n const popupStyle: Record<string, string> = {\n position: 'fixed',\n left: '0',\n top: '0',\n transform: `translate(${Math.round(this.pos.x)}px, ${Math.round(this.pos.y)}px)`,\n zIndex: '9998',\n visibility: this.positionReady ? 'visible' : 'hidden',\n };\n\n if (this.menuWidth) popupStyle['width'] = this.menuWidth;\n if (this.minWidth) popupStyle['min-width'] = this.minWidth;\n\n return (\n <Host style={{ display: 'contents' }}>\n <div\n class={{\n 'menu-popup': true,\n 'menu-popup--closing': this.closing,\n 'ds-choice-popup': true,\n 'ds-choice-popup--closing': this.closing,\n }}\n style={popupStyle}\n role={this.hasCompositeSections ? 'dialog' : 'menu'}\n aria-label={this.menuLabel}\n aria-orientation={this.hasCompositeSections ? undefined : 'vertical'}\n >\n <div class=\"ds-choice-list\">\n {sections.map((section, si) => (\n <div\n key={si}\n class={{\n 'menu-section': true,\n 'menu-section--divided': si < sections.length - 1,\n 'menu-section--gradient-picker': isMenuPickerSection(section),\n 'ds-choice-section': true,\n 'ds-choice-section--divided': si < sections.length - 1,\n }}\n role={section.header ? 'group' : undefined}\n aria-label={section.header}\n >\n {section.header && (\n <ds-text\n class=\"section-header ds-choice-section__header ds-control--md\"\n as=\"span\"\n variant=\"text-body-small\"\n emphasis\n color=\"primary\"\n aria-hidden=\"true\"\n >\n {section.header}\n </ds-text>\n )}\n {isMenuGradientPickerSection(section) ? (\n <ds-swatch-picker\n value={section.value}\n groupLabel={section.header ?? 'Shell gradient theme'}\n sections={shellGradientPickerSections()}\n onDsChange={(e: CustomEvent<string>) => {\n e.stopPropagation();\n this.handleGradientSelect(e.detail as ShellGradientPreset);\n }}\n />\n ) : isMenuSwatchPickerSection(section) ? (\n <ds-swatch-picker\n value={section.value}\n groupLabel={section.groupLabel ?? section.header ?? 'Swatch options'}\n options={section.options ?? []}\n sections={section.sections ?? []}\n onDsChange={(e: CustomEvent<string>) => {\n e.stopPropagation();\n this.handleSwatchSelect(e.detail);\n }}\n />\n ) : (\n section.items.map(item => {\n const idx = flatIdx++;\n const isFocused = this.focusedIndex === idx;\n return (\n <button\n key={idx}\n type=\"button\"\n class={{\n 'menu-item': true,\n 'ds-choice-item': true,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-focus-ring--visible': isFocused && this.focusRingVisible,\n 'ds-interaction-fill': !item.isInactive,\n 'ds-interaction-fill--selected': !!item.isSelected && !item.isInactive,\n 'menu-item--selected': !!item.isSelected,\n 'ds-control-inactive': !!item.isInactive,\n 'menu-item--destructive': !!item.isDestructive,\n 'menu-item--focused': isFocused,\n }}\n role={this.hasCompositeSections ? undefined : item.showSwitch ? 'menuitemcheckbox' : 'menuitem'}\n aria-checked={!this.hasCompositeSections && item.showSwitch ? String(!!item.switchValue) : undefined}\n aria-pressed={this.hasCompositeSections\n ? String(item.showSwitch ? !!item.switchValue : !!item.isSelected)\n : undefined}\n aria-current={!this.hasCompositeSections && !item.showSwitch && item.isSelected ? 'true' : undefined}\n disabled={item.isInactive}\n tabIndex={this.hasCompositeSections ? 0 : isFocused ? 0 : -1}\n onMouseDown={() => { this.focusRingVisible = false; }}\n onClick={() => this.handleItemClick(item)}\n onFocus={() => { this.focusedIndex = idx; }}\n >\n <div class=\"menu-item__content ds-choice-item__content ds-interaction-fill__content\">\n <ds-text\n class=\"menu-item__label ds-choice-item__label\"\n as=\"span\"\n variant=\"text-body-medium\"\n color={item.isSelected ? 'primary' : 'secondary'}\n >\n {item.label}\n </ds-text>\n {item.subtext && (\n <ds-text class=\"menu-item__subtext ds-choice-item__subtext\" as=\"span\" variant=\"text-body-small\" color=\"secondary\">\n {item.subtext}\n </ds-text>\n )}\n </div>\n {item.dot && (\n <span class=\"menu-item__dot-box ds-interaction-fill__content\" aria-hidden=\"true\">\n <ds-badge\n class=\"menu-item__dot\"\n variant=\"dot\"\n hasRing={false}\n label=\"\"\n />\n </span>\n )}\n {item.showSwitch && (\n <ds-switch\n class=\"menu-item__switch ds-interaction-fill__content\"\n size=\"md\"\n checked={!!item.switchValue}\n presentation\n />\n )}\n </button>\n );\n }))}\n </div>\n ))}\n </div>\n </div>\n </Host>\n );\n }\n}\n",
|
|
441
|
+
"type": "registry:ui"
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
"path": "src/wc/components/Menu/menu-placement.ts",
|
|
445
|
+
"content": "import type { ChoicePopupAnchorAlignment } from '../../utils/choice-popup-alignment';\nimport type { MenuAlign, MenuSide } from './menu-position';\n\n/** Placement props bindable on `ds-menu` (side, align, offsets). */\nexport interface MenuPlacement {\n side: MenuSide;\n align: MenuAlign;\n anchorAlignment: ChoicePopupAnchorAlignment;\n sideOffset: number | string;\n alignOffset: number | string;\n}\n\n/**\n * Canonical `ds-menu` placement for the **panel-nav footer user menu**.\n * Anchor from `dsNavUserAction` detail `{ anchor }` (id `ds-panel-nav-user-menu-anchor`).\n *\n * Not for BarNav overflow menus — those use `side=\"bottom\"` / `align=\"end\"` internally.\n * `align: 'end'` bottom-aligns the last choice cell on the anchor; the side offset nudges into the content area:\n * - `sideOffset` space-100 + space-050 (12px): 4px past the panel-nav outer edge (anchor is ~8px inset).\n * - `anchorAlignment: 'choice-cell'`: the shared section inset puts the last row edge on the footer user row.\n */\nexport const PANEL_NAV_USER_MENU_PLACEMENT = {\n side: 'right',\n align: 'end',\n anchorAlignment: 'choice-cell',\n sideOffset: 'calc(var(--dimension-space-100) + var(--dimension-space-050))',\n alignOffset: 0,\n} as const satisfies MenuPlacement;\n",
|
|
446
|
+
"type": "registry:ui"
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
"path": "src/wc/components/Menu/menu-position.ts",
|
|
450
|
+
"content": "import {\n computeAnchoredPopupPosition,\n type AnchoredPopupAlign,\n type AnchoredPopupPositionInput,\n type AnchoredPopupSide,\n} from '../../utils/anchored-popup';\n\nexport type MenuSide = AnchoredPopupSide;\nexport type MenuAlign = AnchoredPopupAlign;\nexport type MenuPositionInput = AnchoredPopupPositionInput;\n\n/** Backward-compatible menu alias for the shared anchored-popup layout math. */\nexport const computeMenuPosition = computeAnchoredPopupPosition;\n",
|
|
451
|
+
"type": "registry:ui"
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
"path": "src/wc/components/Menu/menu-types.ts",
|
|
455
|
+
"content": "import type { ShellGradientPreset } from '../../shell/shell-gradient-presets';\nimport type { SwatchPickerOption, SwatchPickerSection } from '../SwatchPicker/swatch-picker-types';\n\nexport type { MenuAlign, MenuSide } from './menu-position';\nexport {\n PANEL_NAV_USER_MENU_PLACEMENT,\n type MenuPlacement,\n} from './menu-placement';\n\nexport interface MenuItemData {\n label: string;\n value?: string;\n subtext?: string;\n /** Show a supplemental brand notification dot. */\n dot?: boolean;\n isSelected?: boolean;\n isInactive?: boolean;\n isDestructive?: boolean;\n /** Render a compact switch indicator; the menu row owns interaction. */\n showSwitch?: boolean;\n switchValue?: boolean;\n}\n\nexport interface MenuItemsSection {\n header?: string;\n items: MenuItemData[];\n}\n\nexport interface MenuGradientPickerSection {\n header?: string;\n /** @deprecated Use the generic `swatch-picker` section. */\n variant: 'gradient-picker';\n value: ShellGradientPreset;\n}\n\nexport interface MenuSwatchPickerSection {\n header?: string;\n variant: 'swatch-picker';\n value: string;\n options?: SwatchPickerOption[];\n sections?: SwatchPickerSection[];\n groupLabel?: string;\n}\n\nexport type MenuSection =\n | MenuItemsSection\n | MenuGradientPickerSection\n | MenuSwatchPickerSection;\n\nexport function isMenuGradientPickerSection(\n section: MenuSection,\n): section is MenuGradientPickerSection {\n return 'variant' in section && section.variant === 'gradient-picker';\n}\n\nexport function isMenuSwatchPickerSection(\n section: MenuSection,\n): section is MenuSwatchPickerSection {\n return 'variant' in section && section.variant === 'swatch-picker';\n}\n\nexport function isMenuPickerSection(\n section: MenuSection,\n): section is MenuGradientPickerSection | MenuSwatchPickerSection {\n return isMenuGradientPickerSection(section) || isMenuSwatchPickerSection(section);\n}\n\nexport type { ShellGradientPreset };\nexport type { SwatchPickerOption, SwatchPickerSection };\n",
|
|
456
|
+
"type": "registry:ui"
|
|
457
|
+
}
|
|
458
|
+
]
|
|
459
|
+
}
|