@ds-mo/ui 9.0.0 → 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 -0
- package/dist/.build-stamp +1 -1
- package/dist/agent-patterns.json +233 -0
- package/dist/agent.json +155 -19
- 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-swatch-picker.js +42 -0
- package/dist/angular/index.d.ts +1 -1
- package/dist/angular/index.js +1 -0
- package/dist/angular/proxies.d.ts +1 -0
- package/dist/angular/proxies.js +1 -0
- package/dist/components/ds-app-shell.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-menu.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-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-slider.js +1 -1
- package/dist/components/ds-slider.js.map +1 -1
- package/dist/components/ds-swatch-picker.d.ts +11 -0
- package/dist/components/ds-swatch-picker.js +2 -0
- package/dist/components/ds-swatch-picker.js.map +1 -0
- package/dist/components/ds-tooltip-data-viz.js +1 -1
- package/dist/components/p-D1HFMTdC.js +2 -0
- package/dist/components/p-D1HFMTdC.js.map +1 -0
- package/dist/components/p-HBIxvS5C.js +2 -0
- package/dist/components/p-HBIxvS5C.js.map +1 -0
- 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 +268 -30
- 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 -0
- package/dist/react/components.js +1 -0
- 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 -0
- 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/components/p-Bafp65bw.js +0 -2
- package/dist/components/p-Bafp65bw.js.map +0 -1
- package/dist/components/p-BdnR3jxi.js +0 -2
- package/dist/components/p-BdnR3jxi.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-DF0yLZG_.js +0 -2
- package/dist/components/p-DF0yLZG_.js.map +0 -1
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "panel-nav",
|
|
4
|
+
"title": "PanelNav",
|
|
5
|
+
"description": "The single persistent primary navigation for an application, providing grouped top-level destinations, active-route state, standard dashboard/settings switching, user access, and collapsible desktop and tablet presentation.",
|
|
6
|
+
"type": "registry:ui",
|
|
7
|
+
"meta": {
|
|
8
|
+
"library": "@ds-mo/ui",
|
|
9
|
+
"distribution": "npm",
|
|
10
|
+
"storybook": "https://zainadeel.github.io/compomo/?path=/story/panel-nav",
|
|
11
|
+
"source": "src/wc/components/PanelNav/PanelNav.tsx",
|
|
12
|
+
"intentStatus": "complete",
|
|
13
|
+
"intent": {
|
|
14
|
+
"audience": "specialized",
|
|
15
|
+
"status": "stable",
|
|
16
|
+
"summary": "The single persistent primary navigation for an application, providing grouped top-level destinations, active-route state, standard dashboard/settings switching, user access, and collapsible desktop and tablet presentation.",
|
|
17
|
+
"useWhen": [
|
|
18
|
+
"Building the one primary navigation surface for an application that uses the CompoMo system.",
|
|
19
|
+
"Top-level destinations must remain consistently available while the application's page content changes.",
|
|
20
|
+
"Primary destinations need visual grouping, icons, notification indicators, active-route state, and expanded or collapsed presentation."
|
|
21
|
+
],
|
|
22
|
+
"avoidWhen": [
|
|
23
|
+
"A second navigation surface is needed within the same application; an application must have only one PanelNav.",
|
|
24
|
+
"The choices represent sections within the active primary destination; use BarNav instead.",
|
|
25
|
+
"The choices switch local content without changing the application's primary destination; use TabGroup instead.",
|
|
26
|
+
"The interaction performs a command or presents temporary contextual choices; use a button or Menu instead.",
|
|
27
|
+
"Building navigation below the mobile breakpoint; use the application's dedicated mobile navigation component instead."
|
|
28
|
+
],
|
|
29
|
+
"alternatives": [
|
|
30
|
+
{
|
|
31
|
+
"when": "The choices represent route sections within the active primary destination.",
|
|
32
|
+
"component": "component:ds-bar-nav",
|
|
33
|
+
"reason": "BarNav owns secondary section navigation within the destination selected by PanelNav."
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"when": "The choices switch local content without changing the application's primary destination.",
|
|
37
|
+
"component": "component:ds-tab-group",
|
|
38
|
+
"reason": "TabGroup owns local view selection and tab semantics."
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"when": "The choices are commands or temporary contextual actions.",
|
|
42
|
+
"component": "component:ds-menu",
|
|
43
|
+
"reason": "Menu owns transient action and option collections rather than persistent destinations."
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"commonlyComposedWith": [
|
|
47
|
+
"component:ds-app-shell",
|
|
48
|
+
"component:ds-bar-nav",
|
|
49
|
+
"component:ds-panel-tools",
|
|
50
|
+
"component:ds-menu"
|
|
51
|
+
],
|
|
52
|
+
"patterns": [
|
|
53
|
+
"pattern:application-shell"
|
|
54
|
+
],
|
|
55
|
+
"accessibility": [
|
|
56
|
+
"Give every primary destination a concise visible label and an icon that supports rather than replaces that label.",
|
|
57
|
+
"Provide localized dashboard, settings, account, navigation, collapse, and expand labels.",
|
|
58
|
+
"Preserve accessible destination names when the panel is collapsed to icon-only presentation.",
|
|
59
|
+
"Use notification dots as supplemental status and never as the only indication of important information.",
|
|
60
|
+
"When the destination body overflows, it becomes a localized keyboard-focusable scroll region in addition to the roving destination controls.",
|
|
61
|
+
"Keep the standard dashboard/settings switch and user/account entry available in the footer."
|
|
62
|
+
],
|
|
63
|
+
"states": [
|
|
64
|
+
"Render exactly one PanelNav per application; it owns primary application destinations and no other navigation role.",
|
|
65
|
+
"The host router owns the current route; prefer currentUrl for active destination matching and use activeId only when route matching is unavailable.",
|
|
66
|
+
"Use anchor mode for native links or routers that intercept anchors; use event mode when the host performs navigation from dsNavSelect.",
|
|
67
|
+
"Groups visually organize top-level destinations only and must not be interpreted as nested or expandable navigation.",
|
|
68
|
+
"Dashboard presents day-to-day operational destinations; Settings presents administrative and configuration destinations.",
|
|
69
|
+
"The standard footer switches the entire destination collection and navStyle between Dashboard and Settings and exposes the user/account entry.",
|
|
70
|
+
"The host owns the external user menu opened from dsNavUserAction.",
|
|
71
|
+
"On desktop, allow user-controlled expand and collapse and persist the preference with storageKey when cross-session persistence is appropriate."
|
|
72
|
+
],
|
|
73
|
+
"responsiveBehavior": [
|
|
74
|
+
"At 1200px and wider, default to expanded, allow the user to expand or collapse, and restore the locally persisted desktop preference when available.",
|
|
75
|
+
"From 768px through 1199px, present PanelNav permanently collapsed and do not allow user expansion.",
|
|
76
|
+
"Below 768px, the consuming application replaces PanelNav with a dedicated mobile navigation component; PanelNav does not own mobile navigation.",
|
|
77
|
+
"AppShell permanently owns responsive composition: it determines shell mode, enforces tablet collapse, and replaces PanelNav with future mobile navigation below 768px.",
|
|
78
|
+
"When the viewport is below breakpoint, PanelNav locks collapsed, disables its header toggle affordance, and preserves the persisted desktop collapse preference.",
|
|
79
|
+
"Collapsed presentation must preserve destination ordering, active state, notification status, and accessible names."
|
|
80
|
+
],
|
|
81
|
+
"frameworkCaveats": {
|
|
82
|
+
"customElements": [
|
|
83
|
+
"Assign groups as a JavaScript property for structured data; a JSON string attribute is also supported.",
|
|
84
|
+
"Set nav-style before the element connects when first-paint style must match the current route."
|
|
85
|
+
],
|
|
86
|
+
"react": [
|
|
87
|
+
"Pass groups as an array through the generated React wrapper and synchronize currentUrl with the host router.",
|
|
88
|
+
"AppShell owns mobile replacement; PanelNav enforces its configured breakpoint expansion lock."
|
|
89
|
+
],
|
|
90
|
+
"angular": [
|
|
91
|
+
"Import DsPanelNav from @ds-mo/ui/angular/ds-panel-nav and bind groups as a property.",
|
|
92
|
+
"Bind both the nav-style attribute and navStyle property when first-paint route styling must be deterministic.",
|
|
93
|
+
"AppShell owns mobile replacement; PanelNav enforces its configured breakpoint expansion lock."
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
"references": [
|
|
97
|
+
{
|
|
98
|
+
"label": "Executable Storybook examples",
|
|
99
|
+
"path": "src/wc/components/PanelNav/PanelNav.stories.ts"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"label": "Framework and routing integration",
|
|
103
|
+
"path": "docs/framework-integration.md"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"label": "Application shell composition",
|
|
107
|
+
"path": "agent/patterns/application-shell/pattern.agent.json"
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
},
|
|
111
|
+
"api": {
|
|
112
|
+
"props": {
|
|
113
|
+
"accountLabel": {
|
|
114
|
+
"type": "string",
|
|
115
|
+
"resolvedType": "string",
|
|
116
|
+
"attribute": "account-label",
|
|
117
|
+
"default": "'Account'",
|
|
118
|
+
"required": false,
|
|
119
|
+
"mutable": false
|
|
120
|
+
},
|
|
121
|
+
"activeId": {
|
|
122
|
+
"type": "string",
|
|
123
|
+
"resolvedType": "string",
|
|
124
|
+
"attribute": "active-id",
|
|
125
|
+
"default": "''",
|
|
126
|
+
"required": false,
|
|
127
|
+
"mutable": false,
|
|
128
|
+
"description": "ID of the currently active/selected nav item. Overridden by `currentUrl` matching when set."
|
|
129
|
+
},
|
|
130
|
+
"breakpoint": {
|
|
131
|
+
"type": "number",
|
|
132
|
+
"resolvedType": "number",
|
|
133
|
+
"attribute": "breakpoint",
|
|
134
|
+
"default": "0",
|
|
135
|
+
"required": false,
|
|
136
|
+
"mutable": false,
|
|
137
|
+
"description": "Viewport width (px) below which the nav locks in icon-only mode. 0 = disabled."
|
|
138
|
+
},
|
|
139
|
+
"collapseNavigationLabel": {
|
|
140
|
+
"type": "string",
|
|
141
|
+
"resolvedType": "string",
|
|
142
|
+
"attribute": "collapse-navigation-label",
|
|
143
|
+
"default": "'Collapse navigation'",
|
|
144
|
+
"required": false,
|
|
145
|
+
"mutable": false
|
|
146
|
+
},
|
|
147
|
+
"collapsed": {
|
|
148
|
+
"type": "boolean",
|
|
149
|
+
"resolvedType": "boolean",
|
|
150
|
+
"attribute": "collapsed",
|
|
151
|
+
"default": "false",
|
|
152
|
+
"required": false,
|
|
153
|
+
"mutable": true,
|
|
154
|
+
"description": "Whether the nav is in collapsed (icon-only) state.\nSet `storageKey` to persist across reloads. `dsNavToggle` still fires on change."
|
|
155
|
+
},
|
|
156
|
+
"currentUrl": {
|
|
157
|
+
"type": "string",
|
|
158
|
+
"resolvedType": "string",
|
|
159
|
+
"attribute": "current-url",
|
|
160
|
+
"default": "''",
|
|
161
|
+
"required": false,
|
|
162
|
+
"mutable": false,
|
|
163
|
+
"description": "Current route URL (e.g. `window.location.pathname` or the router's active URL).\nWhen set the component derives the active item by matching item `href` values\nagainst this string (longest segment-boundary prefix wins), overriding `activeId`."
|
|
164
|
+
},
|
|
165
|
+
"dashboardLabel": {
|
|
166
|
+
"type": "string",
|
|
167
|
+
"resolvedType": "string",
|
|
168
|
+
"attribute": "dashboard-label",
|
|
169
|
+
"default": "'Dashboard'",
|
|
170
|
+
"required": false,
|
|
171
|
+
"mutable": false
|
|
172
|
+
},
|
|
173
|
+
"dashboardNavigationLabel": {
|
|
174
|
+
"type": "string",
|
|
175
|
+
"resolvedType": "string",
|
|
176
|
+
"attribute": "dashboard-navigation-label",
|
|
177
|
+
"default": "'Dashboard navigation'",
|
|
178
|
+
"required": false,
|
|
179
|
+
"mutable": false
|
|
180
|
+
},
|
|
181
|
+
"disableViewTransition": {
|
|
182
|
+
"type": "boolean",
|
|
183
|
+
"resolvedType": "boolean",
|
|
184
|
+
"attribute": "disable-view-transition",
|
|
185
|
+
"default": "false",
|
|
186
|
+
"required": false,
|
|
187
|
+
"mutable": false,
|
|
188
|
+
"description": "When `true`, style changes apply synchronously — host app owns view transitions."
|
|
189
|
+
},
|
|
190
|
+
"expandNavigationLabel": {
|
|
191
|
+
"type": "string",
|
|
192
|
+
"resolvedType": "string",
|
|
193
|
+
"attribute": "expand-navigation-label",
|
|
194
|
+
"default": "'Expand navigation'",
|
|
195
|
+
"required": false,
|
|
196
|
+
"mutable": false
|
|
197
|
+
},
|
|
198
|
+
"groups": {
|
|
199
|
+
"type": "string | PanelNavGroup[]",
|
|
200
|
+
"resolvedType": "PanelNavGroup[] | string",
|
|
201
|
+
"attribute": "groups",
|
|
202
|
+
"default": "'[]'",
|
|
203
|
+
"required": false,
|
|
204
|
+
"mutable": false,
|
|
205
|
+
"description": "Nav groups — set via JS property (`el.groups = [...]`) or JSON string attribute."
|
|
206
|
+
},
|
|
207
|
+
"navStyle": {
|
|
208
|
+
"type": "NavChromeStyle",
|
|
209
|
+
"resolvedType": "\"dashboard\" | \"settings\"",
|
|
210
|
+
"attribute": "nav-style",
|
|
211
|
+
"default": "'dashboard'",
|
|
212
|
+
"required": false,
|
|
213
|
+
"mutable": false,
|
|
214
|
+
"description": "Style slot: `dashboard` or `settings`. Colors match for now;\nclass hooks reserved for texture/glyph layers. Property: `navStyle`. Attribute: `nav-style`."
|
|
215
|
+
},
|
|
216
|
+
"navigationItemsLabel": {
|
|
217
|
+
"type": "string",
|
|
218
|
+
"resolvedType": "string",
|
|
219
|
+
"attribute": "navigation-items-label",
|
|
220
|
+
"default": "'Navigation items'",
|
|
221
|
+
"required": false,
|
|
222
|
+
"mutable": false,
|
|
223
|
+
"description": "Localized name for the body when overflow makes it a keyboard-scrollable region."
|
|
224
|
+
},
|
|
225
|
+
"routerMode": {
|
|
226
|
+
"type": "PanelNavRouterMode",
|
|
227
|
+
"resolvedType": "\"anchor\" | \"event\"",
|
|
228
|
+
"attribute": "router-mode",
|
|
229
|
+
"default": "'anchor'",
|
|
230
|
+
"required": false,
|
|
231
|
+
"mutable": false,
|
|
232
|
+
"description": "How items with `href` render:\n- `anchor` (default): native `<a href>` — works with routers that intercept anchors.\n- `event`: always `<button>`; host handles navigation via `dsNavSelect`."
|
|
233
|
+
},
|
|
234
|
+
"settingsLabel": {
|
|
235
|
+
"type": "string",
|
|
236
|
+
"resolvedType": "string",
|
|
237
|
+
"attribute": "settings-label",
|
|
238
|
+
"default": "'Settings'",
|
|
239
|
+
"required": false,
|
|
240
|
+
"mutable": false
|
|
241
|
+
},
|
|
242
|
+
"settingsNavigationLabel": {
|
|
243
|
+
"type": "string",
|
|
244
|
+
"resolvedType": "string",
|
|
245
|
+
"attribute": "settings-navigation-label",
|
|
246
|
+
"default": "'Settings navigation'",
|
|
247
|
+
"required": false,
|
|
248
|
+
"mutable": false
|
|
249
|
+
},
|
|
250
|
+
"storageKey": {
|
|
251
|
+
"type": "string",
|
|
252
|
+
"resolvedType": "string",
|
|
253
|
+
"attribute": "storage-key",
|
|
254
|
+
"default": "''",
|
|
255
|
+
"required": false,
|
|
256
|
+
"mutable": false,
|
|
257
|
+
"description": "`localStorage` key used to persist the collapsed state across page loads.\nWhen set, collapsed state is restored on mount and written on each toggle."
|
|
258
|
+
},
|
|
259
|
+
"userInitial": {
|
|
260
|
+
"type": "string",
|
|
261
|
+
"resolvedType": "string",
|
|
262
|
+
"attribute": "user-initial",
|
|
263
|
+
"default": "''",
|
|
264
|
+
"required": false,
|
|
265
|
+
"mutable": false,
|
|
266
|
+
"description": "Single character shown in the collapsed avatar"
|
|
267
|
+
},
|
|
268
|
+
"userName": {
|
|
269
|
+
"type": "string",
|
|
270
|
+
"resolvedType": "string",
|
|
271
|
+
"attribute": "user-name",
|
|
272
|
+
"default": "''",
|
|
273
|
+
"required": false,
|
|
274
|
+
"mutable": false,
|
|
275
|
+
"description": "Display name for the footer user section"
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
"events": [
|
|
279
|
+
{
|
|
280
|
+
"name": "dsChromeTransitionEnd",
|
|
281
|
+
"detail": "ChromeTransitionDetail",
|
|
282
|
+
"bubbles": true,
|
|
283
|
+
"cancelable": true,
|
|
284
|
+
"composed": true
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
"name": "dsChromeTransitionStart",
|
|
288
|
+
"detail": "ChromeTransitionDetail",
|
|
289
|
+
"bubbles": true,
|
|
290
|
+
"cancelable": true,
|
|
291
|
+
"composed": true,
|
|
292
|
+
"description": "Bubbling lifecycle — `ds-app-shell` pauses chrome metrics during width motion."
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
"name": "dsNavFooterAction",
|
|
296
|
+
"detail": "void",
|
|
297
|
+
"bubbles": true,
|
|
298
|
+
"cancelable": true,
|
|
299
|
+
"composed": true,
|
|
300
|
+
"description": "Emitted when the footer left button (gear / dashboard) is clicked."
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
"name": "dsNavSelect",
|
|
304
|
+
"detail": "string",
|
|
305
|
+
"bubbles": true,
|
|
306
|
+
"cancelable": true,
|
|
307
|
+
"composed": true,
|
|
308
|
+
"description": "Emitted when a nav item is clicked. Detail = the item's `id`."
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"name": "dsNavToggle",
|
|
312
|
+
"detail": "boolean",
|
|
313
|
+
"bubbles": true,
|
|
314
|
+
"cancelable": true,
|
|
315
|
+
"composed": true,
|
|
316
|
+
"description": "Emitted when the collapse toggle is clicked. Detail = new collapsed state."
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
"name": "dsNavUserAction",
|
|
320
|
+
"detail": "PanelNavUserActionDetail",
|
|
321
|
+
"bubbles": true,
|
|
322
|
+
"cancelable": true,
|
|
323
|
+
"composed": true,
|
|
324
|
+
"description": "Emitted when the footer user button is clicked. Detail includes the anchor for `ds-menu`."
|
|
325
|
+
}
|
|
326
|
+
],
|
|
327
|
+
"methods": [
|
|
328
|
+
{
|
|
329
|
+
"name": "toggleCollapsed",
|
|
330
|
+
"signature": "toggleCollapsed() => Promise<void>",
|
|
331
|
+
"description": "Toggle the desktop preference. No-op while the breakpoint locks the panel collapsed."
|
|
332
|
+
}
|
|
333
|
+
],
|
|
334
|
+
"slots": []
|
|
335
|
+
},
|
|
336
|
+
"props": {
|
|
337
|
+
"accountLabel": {
|
|
338
|
+
"type": "string",
|
|
339
|
+
"resolvedType": "string",
|
|
340
|
+
"attribute": "account-label",
|
|
341
|
+
"default": "'Account'",
|
|
342
|
+
"required": false,
|
|
343
|
+
"mutable": false
|
|
344
|
+
},
|
|
345
|
+
"activeId": {
|
|
346
|
+
"type": "string",
|
|
347
|
+
"resolvedType": "string",
|
|
348
|
+
"attribute": "active-id",
|
|
349
|
+
"default": "''",
|
|
350
|
+
"required": false,
|
|
351
|
+
"mutable": false,
|
|
352
|
+
"description": "ID of the currently active/selected nav item. Overridden by `currentUrl` matching when set."
|
|
353
|
+
},
|
|
354
|
+
"breakpoint": {
|
|
355
|
+
"type": "number",
|
|
356
|
+
"resolvedType": "number",
|
|
357
|
+
"attribute": "breakpoint",
|
|
358
|
+
"default": "0",
|
|
359
|
+
"required": false,
|
|
360
|
+
"mutable": false,
|
|
361
|
+
"description": "Viewport width (px) below which the nav locks in icon-only mode. 0 = disabled."
|
|
362
|
+
},
|
|
363
|
+
"collapseNavigationLabel": {
|
|
364
|
+
"type": "string",
|
|
365
|
+
"resolvedType": "string",
|
|
366
|
+
"attribute": "collapse-navigation-label",
|
|
367
|
+
"default": "'Collapse navigation'",
|
|
368
|
+
"required": false,
|
|
369
|
+
"mutable": false
|
|
370
|
+
},
|
|
371
|
+
"collapsed": {
|
|
372
|
+
"type": "boolean",
|
|
373
|
+
"resolvedType": "boolean",
|
|
374
|
+
"attribute": "collapsed",
|
|
375
|
+
"default": "false",
|
|
376
|
+
"required": false,
|
|
377
|
+
"mutable": true,
|
|
378
|
+
"description": "Whether the nav is in collapsed (icon-only) state.\nSet `storageKey` to persist across reloads. `dsNavToggle` still fires on change."
|
|
379
|
+
},
|
|
380
|
+
"currentUrl": {
|
|
381
|
+
"type": "string",
|
|
382
|
+
"resolvedType": "string",
|
|
383
|
+
"attribute": "current-url",
|
|
384
|
+
"default": "''",
|
|
385
|
+
"required": false,
|
|
386
|
+
"mutable": false,
|
|
387
|
+
"description": "Current route URL (e.g. `window.location.pathname` or the router's active URL).\nWhen set the component derives the active item by matching item `href` values\nagainst this string (longest segment-boundary prefix wins), overriding `activeId`."
|
|
388
|
+
},
|
|
389
|
+
"dashboardLabel": {
|
|
390
|
+
"type": "string",
|
|
391
|
+
"resolvedType": "string",
|
|
392
|
+
"attribute": "dashboard-label",
|
|
393
|
+
"default": "'Dashboard'",
|
|
394
|
+
"required": false,
|
|
395
|
+
"mutable": false
|
|
396
|
+
},
|
|
397
|
+
"dashboardNavigationLabel": {
|
|
398
|
+
"type": "string",
|
|
399
|
+
"resolvedType": "string",
|
|
400
|
+
"attribute": "dashboard-navigation-label",
|
|
401
|
+
"default": "'Dashboard navigation'",
|
|
402
|
+
"required": false,
|
|
403
|
+
"mutable": false
|
|
404
|
+
},
|
|
405
|
+
"disableViewTransition": {
|
|
406
|
+
"type": "boolean",
|
|
407
|
+
"resolvedType": "boolean",
|
|
408
|
+
"attribute": "disable-view-transition",
|
|
409
|
+
"default": "false",
|
|
410
|
+
"required": false,
|
|
411
|
+
"mutable": false,
|
|
412
|
+
"description": "When `true`, style changes apply synchronously — host app owns view transitions."
|
|
413
|
+
},
|
|
414
|
+
"expandNavigationLabel": {
|
|
415
|
+
"type": "string",
|
|
416
|
+
"resolvedType": "string",
|
|
417
|
+
"attribute": "expand-navigation-label",
|
|
418
|
+
"default": "'Expand navigation'",
|
|
419
|
+
"required": false,
|
|
420
|
+
"mutable": false
|
|
421
|
+
},
|
|
422
|
+
"groups": {
|
|
423
|
+
"type": "string | PanelNavGroup[]",
|
|
424
|
+
"resolvedType": "PanelNavGroup[] | string",
|
|
425
|
+
"attribute": "groups",
|
|
426
|
+
"default": "'[]'",
|
|
427
|
+
"required": false,
|
|
428
|
+
"mutable": false,
|
|
429
|
+
"description": "Nav groups — set via JS property (`el.groups = [...]`) or JSON string attribute."
|
|
430
|
+
},
|
|
431
|
+
"navStyle": {
|
|
432
|
+
"type": "NavChromeStyle",
|
|
433
|
+
"resolvedType": "\"dashboard\" | \"settings\"",
|
|
434
|
+
"attribute": "nav-style",
|
|
435
|
+
"default": "'dashboard'",
|
|
436
|
+
"required": false,
|
|
437
|
+
"mutable": false,
|
|
438
|
+
"description": "Style slot: `dashboard` or `settings`. Colors match for now;\nclass hooks reserved for texture/glyph layers. Property: `navStyle`. Attribute: `nav-style`."
|
|
439
|
+
},
|
|
440
|
+
"navigationItemsLabel": {
|
|
441
|
+
"type": "string",
|
|
442
|
+
"resolvedType": "string",
|
|
443
|
+
"attribute": "navigation-items-label",
|
|
444
|
+
"default": "'Navigation items'",
|
|
445
|
+
"required": false,
|
|
446
|
+
"mutable": false,
|
|
447
|
+
"description": "Localized name for the body when overflow makes it a keyboard-scrollable region."
|
|
448
|
+
},
|
|
449
|
+
"routerMode": {
|
|
450
|
+
"type": "PanelNavRouterMode",
|
|
451
|
+
"resolvedType": "\"anchor\" | \"event\"",
|
|
452
|
+
"attribute": "router-mode",
|
|
453
|
+
"default": "'anchor'",
|
|
454
|
+
"required": false,
|
|
455
|
+
"mutable": false,
|
|
456
|
+
"description": "How items with `href` render:\n- `anchor` (default): native `<a href>` — works with routers that intercept anchors.\n- `event`: always `<button>`; host handles navigation via `dsNavSelect`."
|
|
457
|
+
},
|
|
458
|
+
"settingsLabel": {
|
|
459
|
+
"type": "string",
|
|
460
|
+
"resolvedType": "string",
|
|
461
|
+
"attribute": "settings-label",
|
|
462
|
+
"default": "'Settings'",
|
|
463
|
+
"required": false,
|
|
464
|
+
"mutable": false
|
|
465
|
+
},
|
|
466
|
+
"settingsNavigationLabel": {
|
|
467
|
+
"type": "string",
|
|
468
|
+
"resolvedType": "string",
|
|
469
|
+
"attribute": "settings-navigation-label",
|
|
470
|
+
"default": "'Settings navigation'",
|
|
471
|
+
"required": false,
|
|
472
|
+
"mutable": false
|
|
473
|
+
},
|
|
474
|
+
"storageKey": {
|
|
475
|
+
"type": "string",
|
|
476
|
+
"resolvedType": "string",
|
|
477
|
+
"attribute": "storage-key",
|
|
478
|
+
"default": "''",
|
|
479
|
+
"required": false,
|
|
480
|
+
"mutable": false,
|
|
481
|
+
"description": "`localStorage` key used to persist the collapsed state across page loads.\nWhen set, collapsed state is restored on mount and written on each toggle."
|
|
482
|
+
},
|
|
483
|
+
"userInitial": {
|
|
484
|
+
"type": "string",
|
|
485
|
+
"resolvedType": "string",
|
|
486
|
+
"attribute": "user-initial",
|
|
487
|
+
"default": "''",
|
|
488
|
+
"required": false,
|
|
489
|
+
"mutable": false,
|
|
490
|
+
"description": "Single character shown in the collapsed avatar"
|
|
491
|
+
},
|
|
492
|
+
"userName": {
|
|
493
|
+
"type": "string",
|
|
494
|
+
"resolvedType": "string",
|
|
495
|
+
"attribute": "user-name",
|
|
496
|
+
"default": "''",
|
|
497
|
+
"required": false,
|
|
498
|
+
"mutable": false,
|
|
499
|
+
"description": "Display name for the footer user section"
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
"events": [
|
|
503
|
+
{
|
|
504
|
+
"name": "dsChromeTransitionEnd",
|
|
505
|
+
"detail": "ChromeTransitionDetail",
|
|
506
|
+
"bubbles": true,
|
|
507
|
+
"cancelable": true,
|
|
508
|
+
"composed": true
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
"name": "dsChromeTransitionStart",
|
|
512
|
+
"detail": "ChromeTransitionDetail",
|
|
513
|
+
"bubbles": true,
|
|
514
|
+
"cancelable": true,
|
|
515
|
+
"composed": true,
|
|
516
|
+
"description": "Bubbling lifecycle — `ds-app-shell` pauses chrome metrics during width motion."
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
"name": "dsNavFooterAction",
|
|
520
|
+
"detail": "void",
|
|
521
|
+
"bubbles": true,
|
|
522
|
+
"cancelable": true,
|
|
523
|
+
"composed": true,
|
|
524
|
+
"description": "Emitted when the footer left button (gear / dashboard) is clicked."
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
"name": "dsNavSelect",
|
|
528
|
+
"detail": "string",
|
|
529
|
+
"bubbles": true,
|
|
530
|
+
"cancelable": true,
|
|
531
|
+
"composed": true,
|
|
532
|
+
"description": "Emitted when a nav item is clicked. Detail = the item's `id`."
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
"name": "dsNavToggle",
|
|
536
|
+
"detail": "boolean",
|
|
537
|
+
"bubbles": true,
|
|
538
|
+
"cancelable": true,
|
|
539
|
+
"composed": true,
|
|
540
|
+
"description": "Emitted when the collapse toggle is clicked. Detail = new collapsed state."
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
"name": "dsNavUserAction",
|
|
544
|
+
"detail": "PanelNavUserActionDetail",
|
|
545
|
+
"bubbles": true,
|
|
546
|
+
"cancelable": true,
|
|
547
|
+
"composed": true,
|
|
548
|
+
"description": "Emitted when the footer user button is clicked. Detail includes the anchor for `ds-menu`."
|
|
549
|
+
}
|
|
550
|
+
],
|
|
551
|
+
"methods": [
|
|
552
|
+
{
|
|
553
|
+
"name": "toggleCollapsed",
|
|
554
|
+
"signature": "toggleCollapsed() => Promise<void>",
|
|
555
|
+
"description": "Toggle the desktop preference. No-op while the breakpoint locks the panel collapsed."
|
|
556
|
+
}
|
|
557
|
+
],
|
|
558
|
+
"slots": [],
|
|
559
|
+
"exports": {
|
|
560
|
+
"customElement": "ds-panel-nav",
|
|
561
|
+
"react": "DsPanelNav",
|
|
562
|
+
"angular": "DsPanelNav"
|
|
563
|
+
},
|
|
564
|
+
"consumption": {
|
|
565
|
+
"install": "npm install @ds-mo/ui @ds-mo/tokens @ds-mo/icons",
|
|
566
|
+
"cssSetup": "import '@ds-mo/tokens';\nimport '@ds-mo/tokens/reset';\nimport '@ds-mo/tokens/globals';",
|
|
567
|
+
"customElements": {
|
|
568
|
+
"import": "import '@ds-mo/ui/dist/components/ds-panel-nav.js';",
|
|
569
|
+
"example": "<ds-panel-nav></ds-panel-nav>"
|
|
570
|
+
},
|
|
571
|
+
"react": {
|
|
572
|
+
"import": "import { DsPanelNav } from '@ds-mo/ui/react';",
|
|
573
|
+
"example": "<DsPanelNav />"
|
|
574
|
+
},
|
|
575
|
+
"angular": {
|
|
576
|
+
"import": "import { DsPanelNav } from '@ds-mo/ui/angular/ds-panel-nav';",
|
|
577
|
+
"example": "<ds-panel-nav></ds-panel-nav>"
|
|
578
|
+
},
|
|
579
|
+
"peerDependencies": {
|
|
580
|
+
"required": [
|
|
581
|
+
"@ds-mo/tokens ^3.1.1",
|
|
582
|
+
"@ds-mo/icons ^6.2.0"
|
|
583
|
+
],
|
|
584
|
+
"frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
},
|
|
588
|
+
"dependencies": [
|
|
589
|
+
"@ds-mo/ui",
|
|
590
|
+
"@ds-mo/tokens",
|
|
591
|
+
"@ds-mo/icons"
|
|
592
|
+
],
|
|
593
|
+
"registryDependencies": [
|
|
594
|
+
"badge",
|
|
595
|
+
"button-unfilled",
|
|
596
|
+
"icon",
|
|
597
|
+
"text",
|
|
598
|
+
"tooltip"
|
|
599
|
+
],
|
|
600
|
+
"files": [
|
|
601
|
+
{
|
|
602
|
+
"path": "src/wc/components/PanelNav/PanelNav.css",
|
|
603
|
+
"content": "@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n\n/* ── Theme variables ──────────────────────────────────────────────────────── */\n\n.panel-nav {\n /* ─ Sizes ─ */\n --_nav-width: var(--dimension-panel-width-2xs);\n --_nav-width-collapsed: var(--dimension-size-600);\n --_nav-item-height: var(--dimension-size-400, 32px);\n --_nav-padding-x: var(--dimension-space-100, 8px);\n --_nav-padding-y: var(--dimension-space-100, 8px);\n /* gear button outer width = padding*2 + icon */\n --_nav-footer-btn-w: calc(var(--dimension-space-075, 6px) * 2 + var(--dimension-iconography-md, 20px));\n\n /* ─ Motion ─ */\n /* --ds-panel-nav-speed: unitless multiplier; override in Storybook to slow-mo (e.g. 10 = 10×).\n *-dur vars are duration-only (needed for calc); *-motion vars add easing for use in transitions. */\n --_nav-speed: var(--ds-panel-nav-speed, 1);\n --_nav-expand-dur: calc(var(--_nav-speed) * var(--effect-animation-duration-medium-1, 300ms));\n --_nav-expand-motion: var(--_nav-expand-dur) var(--effect-animation-easing-ease-in-out, ease);\n --_nav-label-fade-dur: calc(var(--_nav-speed) * var(--effect-animation-duration-medium-1, 300ms));\n --_nav-label-fade-motion: var(--_nav-label-fade-dur) var(--effect-animation-easing-ease-in-out, ease);\n --_nav-label-fade-out-dur: calc(var(--_nav-speed) * var(--effect-animation-duration-medium-1, 300ms));\n --_nav-label-fade-out-motion: var(--_nav-label-fade-out-dur) var(--effect-animation-easing-ease-in-out, ease);\n\n /* ─ Colours – default chrome (app surfaces) ─ */\n --_nav-bg: var(--color-background-secondary);\n --_nav-border: var(--color-border-tertiary);\n --_nav-divider: var(--color-border-tertiary);\n --_nav-fg-primary: var(--color-foreground-primary);\n --_nav-fg-secondary: var(--color-foreground-secondary);\n --_nav-fg-tertiary: var(--color-foreground-tertiary);\n --_nav-hover: var(--color-interaction-hover);\n --_nav-pressed: var(--color-interaction-pressed);\n --_nav-focus: var(--color-interaction-focus);\n --_nav-dot: var(--color-foreground-bold-brand);\n --ds-interaction-hover: var(--_nav-hover);\n --ds-interaction-pressed: var(--_nav-pressed);\n --ds-focus-ring-color: var(--_nav-focus);\n}\n\n/* ── Default chrome overrides ────────────────────────────────────────────── */\n\n/* Shell-owned chrome (bg + wash) — panel is a transparent frame. */\n:host-context(ds-app-shell:not([gradient-preset='none'])) .panel-nav {\n background-color: transparent;\n}\n\n/* Style slots — same app tokens for now; hook texture/glyph layers on these classes later. */\n.panel-nav--dashboard,\n.panel-nav--settings {\n --_nav-bg: var(--color-background-secondary);\n --_nav-border: var(--color-border-tertiary);\n --_nav-divider: var(--color-border-tertiary);\n --_nav-fg-primary: var(--color-foreground-primary);\n --_nav-fg-secondary: var(--color-foreground-secondary);\n --_nav-fg-tertiary: var(--color-foreground-tertiary);\n --_nav-hover: var(--color-interaction-hover);\n --_nav-pressed: var(--color-interaction-pressed);\n --_nav-focus: var(--color-interaction-focus);\n --_nav-dot: var(--color-foreground-bold-brand);\n --ds-interaction-hover: var(--_nav-hover);\n --ds-interaction-pressed: var(--_nav-pressed);\n --ds-focus-ring-color: var(--_nav-focus);\n}\n\n/* ── Shell ───────────────────────────────────────────────────────────────── */\n\n.panel-nav {\n display: flex;\n flex-direction: column;\n position: relative;\n width: var(--_nav-width);\n min-width: var(--_nav-width);\n height: 100%;\n background-color: var(--_nav-bg);\n border-right: var(--dimension-stroke-width-012, 1px) solid var(--_nav-border);\n box-sizing: border-box;\n overflow: visible;\n transition:\n width var(--_nav-expand-motion),\n min-width var(--_nav-expand-motion),\n background-color var(--effect-motion-short-3, 200ms ease),\n border-color var(--effect-motion-short-3, 200ms ease);\n font-family: var(--typography-font-family, system-ui);\n isolation: isolate;\n}\n\n.panel-nav__header,\n.panel-nav__body,\n.panel-nav__footer {\n position: relative;\n z-index: 3;\n}\n\n.panel-nav--collapsed {\n width: var(--_nav-width-collapsed);\n min-width: var(--_nav-width-collapsed);\n}\n\n/* ── Header ──────────────────────────────────────────────────────────────── */\n\n.panel-nav__header {\n display: flex;\n align-items: center;\n height: var(--dimension-size-600, 48px);\n padding: var(--dimension-space-100, 8px);\n flex-shrink: 0;\n box-sizing: border-box;\n border-bottom: var(--dimension-stroke-width-012, 1px) solid var(--_nav-divider);\n transition: border-color var(--effect-motion-short-3, 200ms ease);\n}\n\n.panel-nav__header-btn {\n all: unset;\n /* Re-assert after unset — required for `.ds-interaction-fill` stacking. */\n position: relative;\n z-index: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--dimension-space-075, 6px);\n border-radius: var(--dimension-radius-025, 2px);\n cursor: pointer;\n color: var(--_nav-fg-primary);\n flex-shrink: 0;\n box-sizing: border-box;\n transition: color var(--effect-motion-short-3, 200ms ease);\n /* Hover/press: `.ds-interaction-fill` (tokens from --_nav-* via --ds-interaction-*). */\n}\n\n.panel-nav--breakpoint-locked .panel-nav__header-btn {\n cursor: default;\n}\n\n/* Logo — visible by default; instant swap to toggle on hover (no crossfade overlap) */\n.panel-nav__header-logo {\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n z-index: 2;\n opacity: 1;\n}\n\n.panel-nav__m-mark {\n width: var(--dimension-iconography-md, 20px);\n height: var(--dimension-iconography-md, 20px);\n display: block;\n}\n\n/* Toggle icon — stacked on logo; under hover wash (::after z-index 3) */\n.panel-nav__header-toggle {\n display: flex;\n align-items: center;\n justify-content: center;\n position: absolute;\n inset: 0;\n z-index: 2;\n opacity: 0;\n pointer-events: none;\n}\n\n.panel-nav:not(.panel-nav--breakpoint-locked) .panel-nav__header-btn:hover .panel-nav__header-logo { opacity: 0; }\n.panel-nav:not(.panel-nav--breakpoint-locked) .panel-nav__header-btn:hover .panel-nav__header-toggle { opacity: 1; }\n\n/* ── Scrollable body ─────────────────────────────────────────────────────── */\n\n.panel-nav__body {\n flex: 1;\n overflow-y: auto;\n overflow-x: hidden;\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-200, 16px);\n padding: var(--dimension-space-100, 8px);\n box-sizing: border-box;\n scrollbar-width: none;\n}\n\n.panel-nav__body::-webkit-scrollbar { display: none; }\n\n/* ── Group ───────────────────────────────────────────────────────────────── */\n\n.panel-nav__group {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050, 4px);\n}\n\n/*\n * Section headers — typography via ds-text (text-caption + emphasis).\n * Element selector so display:flex beats Text's inline-block, and padding\n * isn't clobbered by sharing `.panel-nav__item-label-text` on the same host.\n */\nds-text.panel-nav__group-label {\n display: flex;\n align-items: center;\n box-sizing: border-box;\n text-transform: uppercase;\n color: var(--_nav-fg-secondary);\n user-select: none;\n height: var(--dimension-size-300, 24px);\n /* Former nested pad: group-label (075) + item-label-text (025). */\n padding: 0 calc(var(--dimension-space-075, 6px) + var(--dimension-space-025, 2px));\n white-space: nowrap;\n overflow: hidden;\n max-height: var(--dimension-size-300, 24px);\n opacity: 1;\n transition:\n opacity var(--_nav-label-fade-motion) 0ms,\n max-height var(--_nav-label-fade-motion) 0ms,\n padding var(--_nav-label-fade-motion) 0ms,\n color var(--effect-motion-short-3, 200ms ease);\n}\n\n/* ── Item ────────────────────────────────────────────────────────────────── */\n\n.panel-nav__item {\n all: unset;\n /* Re-assert after unset — required for `.ds-interaction-fill` stacking. */\n position: relative;\n z-index: 0;\n display: flex;\n align-items: center;\n gap: var(--dimension-space-050, 4px);\n padding: 0 var(--dimension-space-075, 6px);\n height: var(--dimension-size-400, 32px);\n border-radius: var(--dimension-radius-025, 2px);\n cursor: pointer;\n color: var(--_nav-fg-secondary);\n box-sizing: border-box;\n width: 100%;\n user-select: none;\n transition: color var(--effect-motion-short-1, 50ms ease-in-out);\n /* Hover/press: `.ds-interaction-fill` — no --selected (icon/type weight only). */\n}\n\n/* ── Active item ─────────────────────────────────────────────────────────── */\n\n.panel-nav__item--active {\n color: var(--_nav-fg-primary);\n /* Bold typography via ds-text (text-body-medium + emphasis) on the label */\n}\n\n/* ── Item icon ───────────────────────────────────────────────────────────── */\n\n.panel-nav__item-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--dimension-iconography-md, 20px);\n height: var(--dimension-iconography-md, 20px);\n position: relative;\n z-index: 2;\n}\n\n/* ── Item label ──────────────────────────────────────────────────────────── */\n\n.panel-nav__item-label {\n position: relative;\n z-index: 2;\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n height: var(--typography-lineheight-md, 20px);\n opacity: 1;\n /* Gradient wipe: mask sweeps left-to-right on expand, right-to-left on collapse */\n /* stylelint-disable-next-line color-named -- mask luminance stops, not theme colors */\n mask-image: linear-gradient(to right, black 50%, transparent 100%);\n /* stylelint-disable-next-line color-named -- mask luminance stops, not theme colors */\n -webkit-mask-image: linear-gradient(to right, black 50%, transparent 100%);\n mask-size: 200% 100%;\n -webkit-mask-size: 200% 100%;\n mask-position: 0% 0;\n -webkit-mask-position: 0% 0;\n transition:\n opacity var(--_nav-label-fade-motion) 0ms,\n mask-position var(--_nav-label-fade-motion) 0ms,\n -webkit-mask-position var(--_nav-label-fade-motion) 0ms;\n}\n\n.panel-nav__item-label-text {\n padding: 0 var(--dimension-space-025, 2px);\n}\n\n/* ── Item notification dot — 20px suffix zone matches the icon geometry ──── */\n\n.panel-nav__item-dot-box {\n position: absolute;\n z-index: 2;\n right: var(--dimension-space-075, 6px);\n top: var(--dimension-space-075, 6px);\n display: flex;\n align-items: center;\n justify-content: center;\n width: var(--dimension-iconography-md, 20px);\n height: var(--dimension-iconography-md, 20px);\n pointer-events: none;\n}\n\n.panel-nav__item-dot {\n transition:\n transform var(--_nav-expand-motion);\n}\n\n.panel-nav--collapsed .panel-nav__item-dot {\n transform: translate(\n calc(var(--dimension-space-075, 6px) + var(--dimension-stroke-width-012, 1px)),\n calc(0px - var(--dimension-space-075, 6px) - var(--dimension-stroke-width-012, 1px))\n );\n}\n\n.panel-nav--animating .panel-nav__item-label {\n text-overflow: clip;\n}\n\n.panel-nav--collapsed .panel-nav__item-label {\n opacity: 0;\n pointer-events: none;\n text-overflow: clip;\n mask-position: 100% 0;\n -webkit-mask-position: 100% 0;\n transition:\n opacity var(--_nav-label-fade-out-motion) 0ms,\n mask-position var(--_nav-label-fade-out-motion) 0ms,\n -webkit-mask-position var(--_nav-label-fade-out-motion) 0ms;\n}\n\n.panel-nav--collapsed ds-text.panel-nav__group-label {\n opacity: 0;\n max-height: 0;\n padding: 0 calc(var(--dimension-space-075, 6px) + var(--dimension-space-025, 2px));\n pointer-events: none;\n transition:\n opacity var(--_nav-label-fade-out-motion) 0ms,\n max-height var(--_nav-label-fade-out-motion) 0ms,\n padding var(--_nav-label-fade-out-motion) 0ms,\n color var(--effect-motion-short-3, 200ms ease);\n}\n\n/* ── Footer ──────────────────────────────────────────────────────────────── */\n\n.panel-nav__footer {\n position: relative;\n flex-shrink: 0;\n /* single row: padding*2 + item */\n height: calc(var(--_nav-padding-y) * 2 + var(--_nav-item-height));\n transition: height var(--_nav-expand-motion);\n}\n\n.panel-nav--collapsed .panel-nav__footer {\n /* two rows: padding*2 + item*2 + gap */\n height: calc(var(--_nav-padding-y) * 2 + var(--_nav-item-height) * 2 + var(--dimension-space-050, 4px));\n}\n\n/* ── Footer icon button (gear / dashboard) ───────────────────────────────── */\n\n.panel-nav__footer-btn {\n position: absolute;\n left: var(--_nav-padding-x);\n top: var(--_nav-padding-y);\n --ds-button-unfilled-fg: var(--_nav-fg-secondary);\n --ds-button-unfilled-fg-active: var(--_nav-fg-primary);\n --ds-interaction-hover: var(--_nav-hover);\n --ds-interaction-pressed: var(--_nav-pressed);\n --ds-focus-ring-color: var(--_nav-focus);\n}\n\n/* ── Footer user — absolutely positioned, slides from row to below gear ───── */\n\n.panel-nav__footer-user {\n position: absolute;\n /* row position: right of gear button */\n left: calc(var(--_nav-padding-x) + var(--_nav-footer-btn-w) + var(--dimension-space-050, 4px));\n right: calc(var(--_nav-padding-x) - var(--dimension-stroke-width-012, 1px));\n top: var(--_nav-padding-y);\n width: auto;\n min-width: 0;\n transition:\n left var(--_nav-expand-motion),\n right var(--_nav-expand-motion),\n top var(--_nav-expand-motion),\n color var(--effect-motion-short-3, 200ms ease);\n}\n\n.panel-nav--collapsed .panel-nav__footer-user {\n /* column position: full-width row below gear */\n left: var(--_nav-padding-x);\n top: calc(var(--_nav-padding-y) + var(--_nav-item-height) + var(--dimension-space-050, 4px));\n}\n\n/* Label: absolute so it stays in place and only fades — no mask slide */\n.panel-nav__footer-user-label {\n text-align: right;\n position: absolute;\n left: var(--dimension-space-075, 6px);\n right: calc(var(--dimension-space-075, 6px) + var(--dimension-iconography-md, 20px) + var(--dimension-space-050, 4px));\n top: 50%;\n transform: translateY(-50%);\n flex: none;\n height: var(--typography-lineheight-md, 20px);\n mask-image: none;\n -webkit-mask-image: none;\n transition: opacity var(--_nav-label-fade-motion);\n}\n\n.panel-nav--collapsed .panel-nav__footer-user-label {\n opacity: 0;\n pointer-events: none;\n mask-position: 0% 0;\n -webkit-mask-position: 0% 0;\n}\n\n/* ── Footer user icon — cross-fades chevron ↔ circle+initial ────────────── */\n\n/* Absolute — right-anchored in expanded, left-anchored in collapsed to match regular item icon position */\n.panel-nav__footer-user-icon {\n position: absolute;\n left: calc(100% - var(--dimension-iconography-md, 20px) - var(--dimension-space-075, 6px));\n top: 0;\n bottom: 0;\n height: auto;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: left var(--_nav-expand-motion);\n}\n\n.panel-nav--collapsed .panel-nav__footer-user-icon {\n left: var(--dimension-space-075, 6px);\n}\n\n.panel-nav__footer-icon-expanded,\n.panel-nav__footer-icon-collapsed {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.panel-nav__footer-icon-expanded {\n opacity: 1;\n transition: opacity var(--_nav-label-fade-motion);\n}\n\n.panel-nav__footer-icon-collapsed {\n opacity: 0;\n transition: opacity var(--_nav-label-fade-motion);\n}\n\n.panel-nav--collapsed .panel-nav__footer-icon-expanded {\n opacity: 0;\n transition: opacity var(--_nav-label-fade-out-motion);\n}\n\n.panel-nav--collapsed .panel-nav__footer-icon-collapsed {\n opacity: 1;\n transition: opacity var(--_nav-label-fade-out-motion);\n}\n\n/* ── User initial — overlaid on the circle icon ─────────────────────────── */\n\n/*\n * Center the caption glyph on the Circle icon. Don't use inset:0 + flex — the\n * inner `.ds-text__element { width: 100% }` stretches and left-aligns the letter.\n * Translate-centering keeps the complete caption metric box on the icon center;\n * the caption variant remains the sole owner of its line-height.\n * (Do not set `inset: auto` after top/left — it resets them.)\n */\nds-text.panel-nav__user-initial,\nds-text.panel-nav__user-initial.ds-text--caption {\n position: absolute;\n top: 50%;\n left: 50%;\n right: auto;\n bottom: auto;\n transform: translate(-50%, -50%);\n display: block;\n width: max-content;\n max-width: none;\n min-width: 0;\n text-align: center;\n color: var(--_nav-fg-secondary);\n pointer-events: none;\n transition: color var(--effect-motion-short-3, 200ms ease);\n}\n\n/* ── Drag-to-resize handle ───────────────────────────────────────────────── */\n\n/* The resize overlay line — extends past the right edge; keep above workspace content */\n.panel-nav__resize-handle {\n position: absolute;\n top: 0;\n /* Centered on the right edge: --dimension-space-050 outside + inside = --dimension-space-100 hit zone */\n right: var(--dimension-space-n050, -4px);\n bottom: 0;\n width: var(--dimension-space-100, 8px);\n z-index: 20;\n cursor: ew-resize;\n touch-action: none;\n /* Needs a real background for Safari hit-testing */\n background: transparent;\n}\n\n/* Safari requires non-transparent fill for hit testing */\n.panel-nav__resize-handle::before {\n content: '';\n position: absolute;\n inset: 0;\n /* stylelint-disable-next-line color-named -- near-invisible hit-test fill; not a theme color */\n background: rgba(0, 0, 0, 0.001);\n}\n\n/* Resize overlay line */\n.panel-nav__resize-handle::after {\n content: '';\n position: absolute;\n top: 0;\n bottom: 0;\n left: 50%;\n transform: translateX(-50%);\n width: var(--dimension-stroke-width-025, 2px);\n background: var(--color-foreground-primary);\n opacity: 0;\n pointer-events: none;\n transition: opacity var(--effect-motion-short-2, 100ms ease-in-out);\n}\n\n.panel-nav__resize-handle--overlay::after {\n opacity: 1;\n}\n\n/* ── Reduced motion ──────────────────────────────────────────────────────── */\n\n@media (prefers-reduced-motion: reduce) {\n .panel-nav,\n .panel-nav *,\n .panel-nav *::before,\n .panel-nav *::after,\n .panel-nav__resize-handle,\n .panel-nav__resize-handle::before,\n .panel-nav__resize-handle::after {\n /* Near-instant — browsers treat 0 as \"no transition\" and can skip the end state. */\n transition-duration: var(--effect-animation-duration-instant) !important;\n animation-duration: var(--effect-animation-duration-instant) !important;\n transition-delay: var(--effect-animation-duration-instant) !important;\n }\n}\n",
|
|
604
|
+
"type": "registry:ui"
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
"path": "src/wc/components/PanelNav/PanelNav.tsx",
|
|
608
|
+
"content": "import { Component, Prop, Event, EventEmitter, Watch, State, Element, Method, h, Host } from '@stencil/core';\nimport type { ChromeTransitionDetail } from '../../shell/chrome-transition';\nimport type { NavChromeStyle } from '../../shell/nav-chrome';\nimport {\n deriveActiveIdFromUrl,\n panelNavWidthTransitionMs,\n parsePanelNavGroups,\n resolvePanelNavDisableVt,\n resolvePanelNavStyle,\n resolvePanelNavToggle,\n shouldResyncPanelNavGroups,\n shouldResyncPanelNavStyle,\n} from './panel-nav-utils';\nimport {\n PANEL_NAV_USER_MENU_ANCHOR_ID,\n PANEL_NAV_USER_MENU_PLACEMENT,\n type PanelNavGroup,\n type PanelNavItem,\n type PanelNavRouterMode,\n type PanelNavUserActionDetail,\n} from './panel-nav-types';\nimport { scrollEdgeFadeClassMap } from '../../utils/scroll-edge-fade';\nimport { prefersReducedMotion } from '../../utils/resolve-css-time-ms';\n\n@Component({\n tag: 'ds-panel-nav',\n styleUrl: 'PanelNav.css',\n styleUrls: ['../../utils/scroll-edge-fade.css'],\n scoped: true,\n})\nexport class PanelNav {\n /** Style slot: `dashboard` or `settings`. Colors match for now;\n * class hooks reserved for texture/glyph layers. Property: `navStyle`. Attribute: `nav-style`. */\n @Prop({ attribute: 'nav-style', reflect: true }) navStyle: NavChromeStyle = 'dashboard';\n\n /** When `true`, style changes apply synchronously — host app owns view transitions. */\n @Prop() disableViewTransition: boolean = false;\n\n /** Nav groups — set via JS property (`el.groups = [...]`) or JSON string attribute. */\n @Prop() groups: string | PanelNavGroup[] = '[]';\n\n /** How items with `href` render:\n * - `anchor` (default): native `<a href>` — works with routers that intercept anchors.\n * - `event`: always `<button>`; host handles navigation via `dsNavSelect`. */\n @Prop() routerMode: PanelNavRouterMode = 'anchor';\n\n /** ID of the currently active/selected nav item. Overridden by `currentUrl` matching when set. */\n @Prop() activeId: string = '';\n\n /** Whether the nav is in collapsed (icon-only) state.\n * Set `storageKey` to persist across reloads. `dsNavToggle` still fires on change. */\n @Prop({ mutable: true }) collapsed: boolean = false;\n\n /** Viewport width (px) below which the nav locks in icon-only mode. 0 = disabled. */\n @Prop() breakpoint: number = 0;\n\n /** `localStorage` key used to persist the collapsed state across page loads.\n * When set, collapsed state is restored on mount and written on each toggle. */\n @Prop() storageKey: string = '';\n\n /** Current route URL (e.g. `window.location.pathname` or the router's active URL).\n * When set the component derives the active item by matching item `href` values\n * against this string (longest segment-boundary prefix wins), overriding `activeId`. */\n @Prop() currentUrl: string = '';\n\n /** Display name for the footer user section */\n @Prop() userName: string = '';\n\n /** Single character shown in the collapsed avatar */\n @Prop() userInitial: string = '';\n\n @Prop() dashboardLabel: string = 'Dashboard';\n @Prop() settingsLabel: string = 'Settings';\n @Prop() accountLabel: string = 'Account';\n @Prop() dashboardNavigationLabel: string = 'Dashboard navigation';\n @Prop() settingsNavigationLabel: string = 'Settings navigation';\n @Prop() expandNavigationLabel: string = 'Expand navigation';\n @Prop() collapseNavigationLabel: string = 'Collapse navigation';\n /** Localized name for the body when overflow makes it a keyboard-scrollable region. */\n @Prop() navigationItemsLabel: string = 'Navigation items';\n\n /** Emitted when a nav item is clicked. Detail = the item's `id`. */\n @Event() dsNavSelect!: EventEmitter<string>;\n\n /** Emitted when the collapse toggle is clicked. Detail = new collapsed state. */\n @Event() dsNavToggle!: EventEmitter<boolean>;\n\n /** Bubbling lifecycle — `ds-app-shell` pauses chrome metrics during width motion. */\n @Event({ bubbles: true, composed: true })\n dsChromeTransitionStart!: EventEmitter<ChromeTransitionDetail>;\n\n @Event({ bubbles: true, composed: true })\n dsChromeTransitionEnd!: EventEmitter<ChromeTransitionDetail>;\n\n /** Emitted when the footer left button (gear / dashboard) is clicked. */\n @Event() dsNavFooterAction!: EventEmitter<void>;\n\n /** Emitted when the footer user button is clicked. Detail includes the anchor for `ds-menu`. */\n @Event() dsNavUserAction!: EventEmitter<PanelNavUserActionDetail>;\n\n @Element() el!: HTMLElement;\n\n @State() private renderedStyle: NavChromeStyle = 'dashboard';\n @State() private parsedGroups: PanelNavGroup[] = [];\n @State() private isAnimating = false;\n @State() private rovingIndex: number = 0;\n @State() private viewportNarrow: boolean = false;\n @State() private urlDerivedActiveId: string = '';\n @State() private bodyScrollable = false;\n\n private transitionCompletionHandler?: (e: TransitionEvent) => void;\n private transitionFallbackTimer: number | null = null;\n private resizeObserver?: ResizeObserver;\n private scrollRegionObserver?: ResizeObserver;\n private bodyEl?: HTMLElement;\n\n // Drag-to-resize state (not @State — no re-render needed)\n private isDragging = false;\n private dragStartX = 0;\n private didSnap = false;\n private lastDeltaX = 0;\n private edgeOverlayTimer: number | null = null;\n private globalMouseMoveHandler?: (e: MouseEvent) => void;\n private globalMouseUpHandler?: () => void;\n\n @State() private showEdgeOverlay = false;\n\n private static readonly HOST_PROP_SYNC_BUDGET = 8;\n\n /** The ID that should be treated as active. `currentUrl` matching takes precedence\n * over `activeId` when both are present. */\n private get effectiveActiveId(): string {\n return this.urlDerivedActiveId || this.activeId;\n }\n\n private get effectiveDisableViewTransition(): boolean {\n return resolvePanelNavDisableVt(\n this.disableViewTransition,\n this.el.getAttribute('disable-view-transition'),\n );\n }\n\n @Watch('collapsed')\n onCollapsedChange(_next: boolean, prev: boolean | undefined) {\n if (prev === undefined) return;\n this.startCollapseAnimation();\n }\n\n @Watch('viewportNarrow')\n onViewportNarrowChange(next: boolean, prev: boolean | undefined) {\n if (prev === undefined) return;\n if (next && this.rovingIndex === 0) {\n this.rovingIndex = this.getFirstRovingIndex();\n }\n this.startCollapseAnimation();\n }\n\n @Watch('breakpoint')\n onBreakpointChange() {\n this.disconnectResizeObserver();\n if (this.breakpoint > 0) this.connectResizeObserver();\n }\n\n @Watch('groups')\n onGroupsChange(val: string | PanelNavGroup[]) {\n this.parsedGroups = parsePanelNavGroups(val);\n this.rovingIndex = 0;\n this.syncActiveFromUrl();\n }\n\n @Watch('activeId')\n @Watch('urlDerivedActiveId')\n onActiveIdChange() {\n /* Active route updates aria-current only — roving tab stop stays user-controlled. */\n }\n\n @Watch('currentUrl')\n onCurrentUrlChange() {\n this.syncActiveFromUrl();\n }\n\n @Watch('navStyle')\n onNavStyleChange(newVal: NavChromeStyle) {\n this.renderedStyle = newVal;\n }\n\n componentWillLoad() {\n this.renderedStyle = resolvePanelNavStyle(this.navStyle, this.el.getAttribute('nav-style'));\n if (this.storageKey) {\n try {\n const stored = localStorage.getItem(this.storageKey);\n if (stored !== null) this.collapsed = stored === 'true';\n } catch { /* localStorage unavailable */ }\n }\n this.onGroupsChange(this.groups);\n this.syncActiveFromUrl();\n }\n\n componentDidLoad() {\n this.syncHostPropsIfNeeded();\n this.scheduleDeferredHostPropSync();\n if (this.breakpoint > 0) this.connectResizeObserver();\n this.connectScrollRegionObserver();\n this.updateBodyScrollable();\n }\n\n componentDidRender() {\n this.updateBodyScrollable();\n }\n\n disconnectedCallback() {\n this.disconnectResizeObserver();\n this.disconnectScrollRegionObserver();\n this.clearCollapseAnimationCompletion(\n this.el.querySelector('.panel-nav') as HTMLElement | null,\n );\n this.clearEdgeOverlayTimer();\n if (this.globalMouseMoveHandler) {\n window.removeEventListener('mousemove', this.globalMouseMoveHandler);\n }\n if (this.globalMouseUpHandler) {\n window.removeEventListener('mouseup', this.globalMouseUpHandler);\n }\n }\n\n private startCollapseAnimation() {\n if (this.isAnimating) return;\n this.isAnimating = true;\n this.dsChromeTransitionStart.emit({ source: 'panel-nav' });\n const panel = this.el.querySelector('.panel-nav') as HTMLElement | null;\n const widthBefore = panel?.getBoundingClientRect().width ?? 0;\n this.clearCollapseAnimationCompletion(panel);\n this.transitionCompletionHandler = (e: TransitionEvent) => {\n if (e.propertyName === 'width' || e.propertyName === 'min-width') {\n this.finishCollapseAnimation(panel);\n }\n };\n panel?.addEventListener('transitionend', this.transitionCompletionHandler);\n panel?.addEventListener('transitioncancel', this.transitionCompletionHandler);\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n if (!this.isAnimating) return;\n const widthAfter = panel?.getBoundingClientRect().width ?? 0;\n if (Math.abs(widthAfter - widthBefore) < 1) {\n this.finishCollapseAnimation(panel);\n return;\n }\n if (!panel) {\n this.finishCollapseAnimation(panel);\n return;\n }\n\n const fallbackMs = panelNavWidthTransitionMs(getComputedStyle(panel));\n if (fallbackMs <= 0) {\n this.finishCollapseAnimation(panel);\n return;\n }\n this.transitionFallbackTimer = window.setTimeout(() => {\n this.finishCollapseAnimation(panel);\n }, fallbackMs);\n });\n });\n }\n\n private finishCollapseAnimation(panel: HTMLElement | null) {\n const wasAnimating = this.isAnimating;\n this.clearCollapseAnimationCompletion(panel);\n if (!wasAnimating) return;\n this.isAnimating = false;\n this.dsChromeTransitionEnd.emit({ source: 'panel-nav' });\n }\n\n private clearCollapseAnimationCompletion(panel: HTMLElement | null) {\n if (this.transitionCompletionHandler) {\n panel?.removeEventListener('transitionend', this.transitionCompletionHandler);\n panel?.removeEventListener('transitioncancel', this.transitionCompletionHandler);\n this.transitionCompletionHandler = undefined;\n }\n if (this.transitionFallbackTimer !== null) {\n window.clearTimeout(this.transitionFallbackTimer);\n this.transitionFallbackTimer = null;\n }\n }\n\n private connectResizeObserver() {\n this.viewportNarrow = window.innerWidth < this.breakpoint;\n this.resizeObserver = new ResizeObserver(() => {\n this.viewportNarrow = window.innerWidth < this.breakpoint;\n });\n this.resizeObserver.observe(document.documentElement);\n }\n\n private disconnectResizeObserver() {\n this.resizeObserver?.disconnect();\n this.resizeObserver = undefined;\n }\n\n private connectScrollRegionObserver() {\n this.disconnectScrollRegionObserver();\n if (!this.bodyEl || typeof ResizeObserver === 'undefined') return;\n this.scrollRegionObserver = new ResizeObserver(() => this.updateBodyScrollable());\n this.scrollRegionObserver.observe(this.bodyEl);\n }\n\n private disconnectScrollRegionObserver() {\n this.scrollRegionObserver?.disconnect();\n this.scrollRegionObserver = undefined;\n }\n\n private updateBodyScrollable() {\n const next = Boolean(this.bodyEl && this.bodyEl.scrollHeight > this.bodyEl.clientHeight + 1);\n if (next !== this.bodyScrollable) this.bodyScrollable = next;\n }\n\n /** Derive active ID from the current URL by matching item `href` values.\n * Uses longest segment-boundary prefix match. */\n private syncActiveFromUrl() {\n if (!this.currentUrl) {\n this.urlDerivedActiveId = '';\n return;\n }\n this.urlDerivedActiveId = deriveActiveIdFromUrl(this.currentUrl, this.getAllItems());\n }\n\n /** Re-parse props assigned by the host after componentWillLoad (Angular ngAfterViewInit). */\n private syncHostPropsIfNeeded() {\n if (shouldResyncPanelNavStyle(this.renderedStyle, this.navStyle)) {\n this.renderedStyle = this.navStyle;\n }\n\n if (shouldResyncPanelNavGroups(this.parsedGroups, this.groups)) {\n this.onGroupsChange(this.groups);\n } else if (this.currentUrl) {\n this.syncActiveFromUrl();\n }\n }\n\n /** Poll across animation frames — host props may land without triggering @Watch. */\n private scheduleDeferredHostPropSync() {\n let remaining = PanelNav.HOST_PROP_SYNC_BUDGET;\n const tick = () => {\n this.syncHostPropsIfNeeded();\n if (--remaining > 0) {\n requestAnimationFrame(tick);\n }\n };\n queueMicrotask(tick);\n }\n\n /** Centralised toggle: updates the desktop preference unless breakpoint-locked. */\n private applyToggle(next: boolean) {\n if (this.viewportNarrow) return;\n this.collapsed = next;\n if (this.storageKey) {\n try { localStorage.setItem(this.storageKey, String(next)); } catch { /* unavailable */ }\n }\n this.dsNavToggle.emit(next);\n }\n\n private getAllItems(): PanelNavItem[] {\n return this.parsedGroups.flatMap(g => g.items);\n }\n\n private getFirstRovingIndex(): number {\n return this.viewportNarrow ? 1 : 0;\n }\n\n private getFooterRovingIndex(): number {\n return 1 + this.getAllItems().length;\n }\n\n private getUserRovingIndex(): number {\n return this.getFooterRovingIndex() + 1;\n }\n\n private getRovingElement(index: number): HTMLElement | null {\n const itemCount = this.getAllItems().length;\n if (index === 0) {\n return this.viewportNarrow ? null : this.el.querySelector('.panel-nav__header-btn');\n }\n if (index >= 1 && index <= itemCount) {\n const items = this.el.querySelectorAll<HTMLElement>('.panel-nav__body .panel-nav__item');\n return items[index - 1] ?? null;\n }\n if (index === this.getFooterRovingIndex()) {\n return this.el.querySelector('.panel-nav__footer-btn .button-unfilled');\n }\n if (index === this.getUserRovingIndex()) {\n return this.el.querySelector('.panel-nav__footer-user');\n }\n return null;\n }\n\n private focusRovingAt(index: number) {\n this.rovingIndex = index;\n this.getRovingElement(index)?.focus({ preventScroll: true });\n }\n\n private activateRovingIndex(index: number) {\n const itemCount = this.getAllItems().length;\n const items = this.getAllItems();\n if (index === 0) {\n this.handleToggle();\n return;\n }\n if (index >= 1 && index <= itemCount) {\n this.handleItemClick(items[index - 1].id);\n return;\n }\n if (index === this.getFooterRovingIndex()) {\n this.handleFooterAction();\n return;\n }\n if (index === this.getUserRovingIndex()) {\n const anchor = this.el.querySelector(`#${PANEL_NAV_USER_MENU_ANCHOR_ID}`) as HTMLElement | null;\n if (anchor) this.dsNavUserAction.emit({ anchor, menuPlacement: PANEL_NAV_USER_MENU_PLACEMENT });\n }\n }\n\n private handleRovingKeyDown(e: KeyboardEvent, index: number) {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n this.activateRovingIndex(index);\n return;\n }\n\n const footerIdx = this.getFooterRovingIndex();\n const userIdx = this.getUserRovingIndex();\n const firstIdx = this.getFirstRovingIndex();\n let next: number | undefined;\n\n switch (e.key) {\n case 'ArrowDown':\n if (index === userIdx) return;\n if (index === footerIdx) next = userIdx;\n else next = index + 1;\n break;\n case 'ArrowUp':\n if (index === firstIdx) return;\n if (index === userIdx) next = footerIdx;\n else if (index === footerIdx) next = footerIdx - 1;\n else next = index - 1;\n break;\n case 'ArrowRight':\n if (index === footerIdx) next = userIdx;\n else return;\n break;\n case 'ArrowLeft':\n if (index === userIdx) next = footerIdx;\n else return;\n break;\n case 'Home':\n e.preventDefault();\n next = firstIdx;\n break;\n case 'End':\n e.preventDefault();\n next = userIdx;\n break;\n default:\n return;\n }\n\n if (next === undefined || next === index) return;\n e.preventDefault();\n this.focusRovingAt(next);\n }\n\n private handleItemClick(id: string) {\n this.dsNavSelect.emit(id);\n }\n\n private handleToggle() {\n const next = resolvePanelNavToggle(this.collapsed, this.viewportNarrow);\n if (next !== null) this.applyToggle(next);\n }\n\n /** Toggle the desktop preference. No-op while the breakpoint locks the panel collapsed. */\n @Method()\n async toggleCollapsed() {\n this.handleToggle();\n }\n\n private handleFooterAction() {\n this.dsNavFooterAction.emit();\n }\n\n private handleUserAction(e: MouseEvent) {\n const anchor = e.currentTarget as HTMLElement;\n this.dsNavUserAction.emit({ anchor, menuPlacement: PANEL_NAV_USER_MENU_PLACEMENT });\n }\n\n private clearEdgeOverlayTimer() {\n if (this.edgeOverlayTimer !== null) {\n window.clearTimeout(this.edgeOverlayTimer);\n this.edgeOverlayTimer = null;\n }\n }\n\n private handleResizeHandleMouseEnter() {\n this.clearEdgeOverlayTimer();\n if (prefersReducedMotion()) {\n this.showEdgeOverlay = true;\n return;\n }\n this.edgeOverlayTimer = window.setTimeout(() => {\n this.showEdgeOverlay = true;\n this.edgeOverlayTimer = null;\n }, 500);\n }\n\n private handleResizeHandleMouseLeave() {\n if (this.isDragging) return;\n this.clearEdgeOverlayTimer();\n this.showEdgeOverlay = false;\n }\n\n private handleResizeHandleMouseDown(e: MouseEvent) {\n e.preventDefault();\n e.stopPropagation();\n\n const wasCollapsed = this.collapsed || this.viewportNarrow;\n this.isDragging = true;\n this.dragStartX = e.clientX;\n this.didSnap = false;\n this.lastDeltaX = 0;\n this.clearEdgeOverlayTimer();\n this.showEdgeOverlay = false;\n\n document.body.style.cursor = 'ew-resize';\n document.body.style.userSelect = 'none';\n\n const onMove = (ev: MouseEvent) => {\n const deltaX = ev.clientX - this.dragStartX;\n this.lastDeltaX = Math.abs(deltaX);\n if (this.didSnap) return;\n if (!wasCollapsed && deltaX < -8) {\n this.applyToggle(true);\n this.didSnap = true;\n } else if (wasCollapsed && deltaX > 8) {\n this.applyToggle(false);\n this.didSnap = true;\n }\n };\n\n const onUp = () => {\n if (!this.didSnap && this.lastDeltaX < 3) {\n this.applyToggle(!wasCollapsed);\n }\n this.isDragging = false;\n this.showEdgeOverlay = false;\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n window.removeEventListener('mousemove', onMove);\n window.removeEventListener('mouseup', onUp);\n this.globalMouseMoveHandler = undefined;\n this.globalMouseUpHandler = undefined;\n };\n\n this.globalMouseMoveHandler = onMove;\n this.globalMouseUpHandler = onUp;\n window.addEventListener('mousemove', onMove);\n window.addEventListener('mouseup', onUp);\n }\n\n private renderFooterAction(isDashboardChrome: boolean) {\n // Short chrome labels — same string for tip + aria (not \"Open/Go to …\").\n const footerLabel = isDashboardChrome ? this.settingsLabel : this.dashboardLabel;\n return (\n <ds-tooltip label={footerLabel} side=\"right\" size=\"sm\">\n <ds-button-unfilled variant=\"icon\"\n class=\"panel-nav__footer-btn\"\n icon={isDashboardChrome ? 'Gear' : 'Dashboard'}\n activeFill={false}\n hasBorder={false}\n focusTabIndex={this.rovingIndex === this.getFooterRovingIndex() ? 0 : -1}\n aria-label={footerLabel}\n onDsClick={() => this.handleFooterAction()}\n onKeyDown={(e: KeyboardEvent) => this.handleRovingKeyDown(e, this.getFooterRovingIndex())}\n onFocusin={() => { this.rovingIndex = this.getFooterRovingIndex(); }}\n />\n </ds-tooltip>\n );\n }\n\n private renderFooterUser(collapsed: boolean, userName: string, userInitial: string) {\n // Always wrap so collapse morph CSS can transition on a stable button node.\n // Tip only when collapsed (empty label → ds-tooltip skips show).\n const tipLabel = collapsed ? this.accountLabel : '';\n const userButton = (\n <button\n type=\"button\"\n id={PANEL_NAV_USER_MENU_ANCHOR_ID}\n class=\"panel-nav__item panel-nav__footer-user ds-focus-ring-inset ds-interaction-fill\"\n tabIndex={this.rovingIndex === this.getUserRovingIndex() ? 0 : -1}\n aria-label={this.accountLabel}\n onClick={(e) => this.handleUserAction(e)}\n onKeyDown={(e: KeyboardEvent) => this.handleRovingKeyDown(e, this.getUserRovingIndex())}\n onFocus={() => { this.rovingIndex = this.getUserRovingIndex(); }}\n >\n <ds-text\n class=\"panel-nav__item-label panel-nav__footer-user-label panel-nav__item-label-text\"\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis\n color=\"inherit\"\n >\n {userName}\n </ds-text>\n <span class=\"panel-nav__item-icon panel-nav__footer-user-icon\" aria-hidden=\"true\">\n <span class=\"panel-nav__footer-icon-expanded\">\n <ds-icon name=\"ChevronUpDown\" size=\"md\" color=\"inherit\" />\n </span>\n <span class=\"panel-nav__footer-icon-collapsed\">\n <ds-icon name=\"Circle\" size=\"md\" color=\"inherit\" />\n <ds-text\n class=\"panel-nav__user-initial\"\n as=\"span\"\n variant=\"text-caption\"\n emphasis\n align=\"center\"\n color=\"inherit\"\n >\n {userInitial}\n </ds-text>\n </span>\n </span>\n </button>\n );\n\n return (\n <ds-tooltip label={tipLabel} side=\"right\" size=\"sm\">\n {userButton}\n </ds-tooltip>\n );\n }\n\n private renderNavItem(item: PanelNavItem, idx: number, collapsed: boolean) {\n const isActive = item.id === this.effectiveActiveId;\n const rovingPosition = idx + 1;\n\n const itemContent = [\n <span class=\"panel-nav__item-icon\">\n <ds-icon name={item.icon} size=\"md\" color=\"inherit\" />\n </span>,\n <ds-text\n class=\"panel-nav__item-label panel-nav__item-label-text\"\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis={isActive}\n color=\"inherit\"\n >\n {item.label}\n </ds-text>,\n item.dot && (\n <span class=\"panel-nav__item-dot-box\" aria-hidden=\"true\">\n <ds-badge\n class=\"panel-nav__item-dot\"\n variant=\"dot\"\n hasRing={collapsed}\n label=\"\"\n />\n </span>\n ),\n ];\n\n const sharedProps = {\n class: {\n 'panel-nav__item': true,\n 'panel-nav__item--active': isActive,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n },\n 'aria-current': isActive ? ('page' as const) : undefined,\n tabIndex: rovingPosition === this.rovingIndex ? 0 : -1,\n onClick: () => this.handleItemClick(item.id),\n onKeyDown: (e: KeyboardEvent) => this.handleRovingKeyDown(e, rovingPosition),\n onFocus: () => { this.rovingIndex = rovingPosition; },\n };\n\n const useAnchor = this.routerMode === 'anchor' && item.href;\n const control = useAnchor\n ? <a {...sharedProps} href={item.href}>{itemContent}</a>\n : <button {...sharedProps} type=\"button\">{itemContent}</button>;\n\n // Always wrap so label fade CSS can transition on a stable item node.\n // Tip only when collapsed (empty label → ds-tooltip skips show).\n return (\n <ds-tooltip label={collapsed ? item.label : ''} side=\"right\" size=\"sm\">\n {control}\n </ds-tooltip>\n );\n }\n\n render() {\n const chromeStyle = this.renderedStyle;\n const isDashboardChrome = chromeStyle === 'dashboard';\n const isDashboard = this.navStyle === 'dashboard';\n const collapsed = this.collapsed || this.viewportNarrow;\n const { userName, userInitial } = this;\n\n const navCls: Record<string, boolean> = {\n 'panel-nav': true,\n 'panel-nav--dashboard': isDashboard,\n 'panel-nav--settings': !isDashboard,\n 'panel-nav--collapsed': collapsed,\n 'panel-nav--breakpoint-locked': this.viewportNarrow,\n 'panel-nav--animating': this.isAnimating,\n };\n\n return (\n <Host style={{ display: 'block', position: 'relative' }}>\n <nav\n class={navCls}\n aria-label={isDashboardChrome ? this.dashboardNavigationLabel : this.settingsNavigationLabel}\n >\n\n {/* ── Header: Motive logo, reveals collapse toggle on hover ── */}\n <div class=\"panel-nav__header\">\n <button\n type=\"button\"\n class=\"panel-nav__header-btn ds-focus-ring-inset ds-interaction-fill\"\n disabled={this.viewportNarrow}\n tabIndex={!this.viewportNarrow && this.rovingIndex === 0 ? 0 : -1}\n onClick={() => this.handleToggle()}\n onKeyDown={(e: KeyboardEvent) => this.handleRovingKeyDown(e, 0)}\n onFocus={() => { this.rovingIndex = 0; }}\n aria-label={collapsed ? this.expandNavigationLabel : this.collapseNavigationLabel}\n aria-expanded={collapsed ? 'false' : 'true'}\n >\n {/* Motive M mark — fades out on hover to reveal collapse toggle */}\n <span class=\"panel-nav__header-logo\" aria-hidden=\"true\">\n <svg class=\"panel-nav__m-mark\" viewBox=\"0 0 20 20\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" focusable=\"false\" aria-hidden=\"true\">\n <path d=\"M11.1159 4.31537H7.67021L2.53401 13.0703H0V15.6875H4.02716L8.24319 8.49978V15.6846H11.6342L15.8289 8.47829V15.6846H18.7122V4.3125H15.2559L11.1159 11.3648V4.31537Z\" fill=\"currentColor\"/>\n </svg>\n </span>\n {/* Collapse / expand icon — revealed on hover via CSS */}\n <span class=\"panel-nav__header-toggle\" aria-hidden=\"true\">\n <ds-icon\n name={collapsed ? 'LeftExpandB' : 'LeftCollapseB'}\n size=\"md\"\n color=\"inherit\"\n />\n </span>\n </button>\n </div>\n\n {/* ── Scrollable body ── */}\n <div\n class={{\n 'panel-nav__body': true,\n 'ds-focus-ring': this.bodyScrollable,\n ...scrollEdgeFadeClassMap({ edges: 'bottom' }),\n }}\n ref={element => {\n const next = (element as HTMLElement) ?? undefined;\n if (next === this.bodyEl) return;\n this.bodyEl = next;\n this.connectScrollRegionObserver();\n }}\n role={this.bodyScrollable ? 'region' : undefined}\n aria-label={this.bodyScrollable ? this.navigationItemsLabel : undefined}\n tabIndex={this.bodyScrollable ? 0 : undefined}\n >\n {(() => {\n let flatIdx = 0;\n return this.parsedGroups.map(group => (\n <div class=\"panel-nav__group\">\n {group.label && (\n <ds-text\n class=\"panel-nav__group-label\"\n as=\"span\"\n variant=\"text-caption\"\n emphasis\n color=\"inherit\"\n >\n {group.label}\n </ds-text>\n )}\n {group.items.map(item => this.renderNavItem(item, flatIdx++, collapsed))}\n </div>\n ));\n })()}\n </div>\n\n {/* ── Footer ── */}\n <div class=\"panel-nav__footer\">\n {this.renderFooterAction(isDashboardChrome)}\n {this.renderFooterUser(collapsed, userName, userInitial)}\n </div>\n\n </nav>\n\n {/* Drag-to-resize handle — always rendered, hidden only when auto-collapsed by breakpoint */}\n {!this.viewportNarrow && (\n <div\n class={{\n 'panel-nav__resize-handle': true,\n 'panel-nav__resize-handle--overlay': this.showEdgeOverlay,\n }}\n onMouseEnter={() => this.handleResizeHandleMouseEnter()}\n onMouseLeave={() => this.handleResizeHandleMouseLeave()}\n onMouseDown={(e: MouseEvent) => this.handleResizeHandleMouseDown(e)}\n aria-hidden=\"true\"\n />\n )}\n </Host>\n );\n }\n}\n",
|
|
609
|
+
"type": "registry:ui"
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
"path": "src/wc/components/PanelNav/panel-nav-types.ts",
|
|
613
|
+
"content": "import type { MenuPlacement } from '../Menu/menu-placement';\nimport { PANEL_NAV_USER_MENU_PLACEMENT } from '../Menu/menu-placement';\nimport type { NavChromeStyle } from '../../shell/nav-chrome';\n\nexport type { NavChromeStyle };\nexport type PanelNavRouterMode = 'anchor' | 'event';\n\nexport interface PanelNavItem {\n id: string;\n icon: string;\n label: string;\n /** Show a notification dot badge on the item */\n dot?: boolean;\n /** Route path used for `currentUrl` matching. In `anchor` mode also sets `<a href>`.\n * In `event` mode navigation is delegated to the host via `dsNavSelect`. */\n href?: string;\n}\n\nexport interface PanelNavGroup {\n id?: string;\n label?: string;\n items: PanelNavItem[];\n}\n\n/** Stable `id` on the footer user button — use with `ds-menu` `anchor-id`. */\nexport const PANEL_NAV_USER_MENU_ANCHOR_ID = 'ds-panel-nav-user-menu-anchor';\n\n/** Detail for `dsNavUserAction` — anchor element for an external `ds-menu`. */\nexport interface PanelNavUserActionDetail {\n anchor: HTMLElement;\n /** Recommended `ds-menu` placement — spread onto external user menu instance. */\n menuPlacement: MenuPlacement;\n}\n\nexport { PANEL_NAV_USER_MENU_PLACEMENT };\n",
|
|
614
|
+
"type": "registry:ui"
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
"path": "src/wc/components/PanelNav/panel-nav-utils.ts",
|
|
618
|
+
"content": "import type { PanelNavGroup, PanelNavItem } from './panel-nav-types';\nimport { parseCssTimeMs } from '../../utils/resolve-css-time-ms';\nimport {\n NAV_STYLE_HINT_ATTR,\n clearNavStyleHint,\n readNavStyleAttr,\n resolveNavChromeStyle,\n setNavStyleHint,\n shouldResyncNavChromeStyle,\n} from '../../shell/nav-chrome';\n\nexport type { NavChromeStyle } from '../../shell/nav-chrome';\nexport {\n NAV_STYLE_HINT_ATTR,\n readNavStyleAttr,\n setNavStyleHint,\n clearNavStyleHint,\n resolveNavChromeStyle as resolvePanelNavStyle,\n shouldResyncNavChromeStyle as shouldResyncPanelNavStyle,\n};\n\n/** Whether `path` matches `href` at a segment boundary (exact or child path). */\nexport function hrefMatchesPath(path: string, href: string): boolean {\n return path === href || path.startsWith(`${href}/`);\n}\n\n/** Derive the active nav item id from a URL using longest segment-boundary prefix match. */\nexport function deriveActiveIdFromUrl(path: string, items: PanelNavItem[]): string {\n if (!path) return '';\n\n let bestId = '';\n let bestLen = 0;\n\n for (const item of items) {\n if (!item.href) continue;\n if (hrefMatchesPath(path, item.href) && item.href.length > bestLen) {\n bestId = item.id;\n bestLen = item.href.length;\n }\n }\n\n return bestId;\n}\n\n/** Total nav item count across all groups. */\nexport function countPanelNavItems(groups: PanelNavGroup[]): number {\n return groups.reduce((sum, g) => sum + g.items.length, 0);\n}\n\n/** Resolve a user toggle without mutating the persisted desktop preference while breakpoint-locked. */\nexport function resolvePanelNavToggle(\n collapsed: boolean,\n viewportNarrow: boolean,\n): boolean | null {\n return viewportNarrow ? null : !collapsed;\n}\n\nexport interface PanelNavTransitionStyle {\n transitionProperty: string;\n transitionDuration: string;\n transitionDelay: string;\n}\n\nfunction parseCssTimeList(value: string): number[] {\n return value.split(',').map(item => parseCssTimeMs(item.trim(), 0));\n}\n\n/**\n * Maximum computed time for PanelNav's width transition. CSS repeats shorter\n * duration/delay lists to match the property list, so mirror that behavior.\n */\nexport function panelNavWidthTransitionMs(style: PanelNavTransitionStyle): number {\n const properties = style.transitionProperty.split(',').map(item => item.trim());\n const durations = parseCssTimeList(style.transitionDuration);\n const delays = parseCssTimeList(style.transitionDelay);\n\n return properties.reduce((max, property, index) => {\n if (property !== 'all' && property !== 'width' && property !== 'min-width') return max;\n const duration = durations[index % durations.length] ?? 0;\n const delay = delays[index % delays.length] ?? 0;\n return Math.max(max, duration + delay, 0);\n }, 0);\n}\n\n/** Parse `groups` from either a JSON attribute string or a JS property array. */\nexport function parsePanelNavGroups(groups: string | unknown): PanelNavGroup[] {\n if (Array.isArray(groups)) return groups;\n if (typeof groups === 'string') {\n try {\n const parsed = JSON.parse(groups);\n return Array.isArray(parsed) ? parsed : [];\n } catch {\n return [];\n }\n }\n return [];\n}\n\n/** Resolve whether VT is disabled from prop and/or host attribute. */\nexport function resolvePanelNavDisableVt(prop: boolean, attr: string | null): boolean {\n if (prop) return true;\n if (attr === null) return false;\n return attr !== 'false';\n}\n\n/** True when host `groups` has items but internal parsed state is still empty. */\nexport function shouldResyncPanelNavGroups(\n parsedGroups: PanelNavGroup[],\n groups: string | unknown,\n): boolean {\n return (\n countPanelNavItems(parsedGroups) === 0 &&\n countPanelNavItems(parsePanelNavGroups(groups)) > 0\n );\n}\n",
|
|
619
|
+
"type": "registry:ui"
|
|
620
|
+
}
|
|
621
|
+
]
|
|
622
|
+
}
|