@ds-mo/ui 14.28.0 → 14.29.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.
Files changed (210) hide show
  1. package/dist/.package-ready.json +1 -1
  2. package/dist/agent-patterns.json +27 -17
  3. package/dist/agent.json +148 -25
  4. package/dist/angular/ds-button-filled.d.ts +1 -1
  5. package/dist/angular/ds-button-filled.js +3 -3
  6. package/dist/angular/ds-button-unfilled.d.ts +1 -1
  7. package/dist/angular/ds-button-unfilled.js +3 -3
  8. package/dist/angular/ds-panel-nav.d.ts +1 -1
  9. package/dist/angular/ds-panel-nav.js +3 -3
  10. package/dist/angular/ds-panel-tool-header.d.ts +1 -1
  11. package/dist/angular/ds-panel-tool-header.js +3 -3
  12. package/dist/angular/ds-panel-tool-search.d.ts +1 -1
  13. package/dist/angular/ds-panel-tool-search.js +3 -3
  14. package/dist/angular/ds-table.d.ts +1 -1
  15. package/dist/angular/ds-table.js +3 -3
  16. package/dist/angular/ds-tag.d.ts +1 -1
  17. package/dist/angular/ds-tag.js +3 -3
  18. package/dist/components/ds-agent-questionnaire.js +1 -1
  19. package/dist/components/ds-agent-response.js +1 -1
  20. package/dist/components/ds-banner.js +1 -1
  21. package/dist/components/ds-bar-nav.js +1 -1
  22. package/dist/components/ds-bar-title.js +1 -1
  23. package/dist/components/ds-bar-workflow.js +1 -1
  24. package/dist/components/ds-button-filled.js +1 -1
  25. package/dist/components/ds-button-unfilled.js +1 -1
  26. package/dist/components/ds-card-chart.js +1 -1
  27. package/dist/components/ds-card-navigation.js +1 -1
  28. package/dist/components/ds-card-navigation.js.map +1 -1
  29. package/dist/components/ds-card-overview.js +1 -1
  30. package/dist/components/ds-card-overview.js.map +1 -1
  31. package/dist/components/ds-card-setting.js +1 -1
  32. package/dist/components/ds-chart-legend.js +1 -1
  33. package/dist/components/ds-chart-legend.js.map +1 -1
  34. package/dist/components/ds-checkbox.js +1 -1
  35. package/dist/components/ds-chip.js +1 -1
  36. package/dist/components/ds-code-block.js +1 -1
  37. package/dist/components/ds-conversation-list-item.js +1 -1
  38. package/dist/components/ds-conversation-list-item.js.map +1 -1
  39. package/dist/components/ds-filter-menu.js +1 -1
  40. package/dist/components/ds-input.js +1 -1
  41. package/dist/components/ds-markdown.js +1 -1
  42. package/dist/components/ds-menu.js +1 -1
  43. package/dist/components/ds-message-actions.js +1 -1
  44. package/dist/components/ds-message-composer.js +1 -1
  45. package/dist/components/ds-message-scroller.js +1 -1
  46. package/dist/components/ds-mobile-bar-nav.js +1 -1
  47. package/dist/components/ds-mobile-header.js +1 -1
  48. package/dist/components/ds-mobile-section-switcher.js +1 -1
  49. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  50. package/dist/components/ds-modal.js +1 -1
  51. package/dist/components/ds-pagination.js +1 -1
  52. package/dist/components/ds-panel-nav.js +1 -1
  53. package/dist/components/ds-panel-sub-nav.js +1 -1
  54. package/dist/components/ds-panel-sub-nav.js.map +1 -1
  55. package/dist/components/ds-panel-tool-header.js +1 -1
  56. package/dist/components/ds-panel-tool-search.js +1 -1
  57. package/dist/components/ds-panel-tool-search.js.map +1 -1
  58. package/dist/components/ds-panel-tools.js +1 -1
  59. package/dist/components/ds-radio.js +1 -1
  60. package/dist/components/ds-select.js +1 -1
  61. package/dist/components/ds-shell-app.js +1 -1
  62. package/dist/components/ds-shell-app.js.map +1 -1
  63. package/dist/components/ds-shell-tools.js +1 -1
  64. package/dist/components/ds-tab-group.js +1 -1
  65. package/dist/components/ds-table-filter.js +1 -1
  66. package/dist/components/ds-table-group.js +1 -1
  67. package/dist/components/ds-table-group.js.map +1 -1
  68. package/dist/components/ds-table-saved-views.js +1 -1
  69. package/dist/components/ds-table-saved-views.js.map +1 -1
  70. package/dist/components/ds-table-search.js +1 -1
  71. package/dist/components/ds-table-search.js.map +1 -1
  72. package/dist/components/ds-table-sort.js +1 -1
  73. package/dist/components/ds-table-sort.js.map +1 -1
  74. package/dist/components/ds-table.js +1 -1
  75. package/dist/components/ds-table.js.map +1 -1
  76. package/dist/components/ds-tag.js +1 -1
  77. package/dist/components/ds-textarea.js +1 -1
  78. package/dist/components/ds-textarea.js.map +1 -1
  79. package/dist/components/ds-toast.js +1 -1
  80. package/dist/components/p--yvQzZoO.js +2 -0
  81. package/dist/components/p--yvQzZoO.js.map +1 -0
  82. package/dist/components/p-B5AZd41L.js +2 -0
  83. package/dist/components/p-B5AZd41L.js.map +1 -0
  84. package/dist/components/p-B5eMVBlz.js +2 -0
  85. package/dist/components/p-B5eMVBlz.js.map +1 -0
  86. package/dist/components/p-BGnRFtBa.js +2 -0
  87. package/dist/components/p-BGnRFtBa.js.map +1 -0
  88. package/dist/components/p-BIDGQ4tc.js +2 -0
  89. package/dist/components/p-BIDGQ4tc.js.map +1 -0
  90. package/dist/components/p-BZDQLjE0.js +2 -0
  91. package/dist/components/p-BZDQLjE0.js.map +1 -0
  92. package/dist/components/{p-DmYZLH_L.js → p-BdC7wxhu.js} +2 -2
  93. package/dist/components/{p-DmYZLH_L.js.map → p-BdC7wxhu.js.map} +1 -1
  94. package/dist/components/p-BfVlFinS.js +2 -0
  95. package/dist/components/{p-WM1GTEun.js.map → p-BfVlFinS.js.map} +1 -1
  96. package/dist/components/p-BmRHJ_jo.js +2 -0
  97. package/dist/components/p-BmRHJ_jo.js.map +1 -0
  98. package/dist/components/{p-CxbGZJBU.js → p-BwAWZAHk.js} +2 -2
  99. package/dist/components/{p-8atLxerP.js → p-By5TDnbF.js} +2 -2
  100. package/dist/components/{p-BdXUd-tK.js → p-CDXcINTO.js} +2 -2
  101. package/dist/components/{p-BdXUd-tK.js.map → p-CDXcINTO.js.map} +1 -1
  102. package/dist/components/{p-DGHeKNfG.js → p-CE8gsv7V.js} +2 -2
  103. package/dist/components/p-CLcwh1kq.js +2 -0
  104. package/dist/components/p-CLcwh1kq.js.map +1 -0
  105. package/dist/components/p-CcK3UiTu.js +2 -0
  106. package/dist/components/{p-NB-SoVpo.js.map → p-CcK3UiTu.js.map} +1 -1
  107. package/dist/components/p-Cr5vn5JO.js +2 -0
  108. package/dist/components/{p-CsW_2w-s.js.map → p-Cr5vn5JO.js.map} +1 -1
  109. package/dist/components/{p-CW7JhWlS.js → p-Crd3PEUx.js} +2 -2
  110. package/dist/components/{p-CW7JhWlS.js.map → p-Crd3PEUx.js.map} +1 -1
  111. package/dist/components/{p-Kp8ifB1g.js → p-Ct90k-nz.js} +2 -2
  112. package/dist/components/{p-C82vesh_.js → p-Cx2dWjaW.js} +2 -2
  113. package/dist/components/p-CyoLh8su.js +2 -0
  114. package/dist/components/p-CyoLh8su.js.map +1 -0
  115. package/dist/components/p-CysCSinT.js +2 -0
  116. package/dist/components/{p-BdU6FPu4.js.map → p-CysCSinT.js.map} +1 -1
  117. package/dist/components/p-CzbsTZt7.js.map +1 -1
  118. package/dist/components/p-D1pPq-y0.js +2 -0
  119. package/dist/components/{p-BWDbER5T.js.map → p-D1pPq-y0.js.map} +1 -1
  120. package/dist/components/{p-B3iy9Rl_.js → p-DF8W85Tz.js} +2 -2
  121. package/dist/components/p-GbtafFds.js +2 -0
  122. package/dist/components/p-GbtafFds.js.map +1 -0
  123. package/dist/components/{p-B_NN7k8H.js → p-K39w53Bw.js} +2 -2
  124. package/dist/components/p-K39w53Bw.js.map +1 -0
  125. package/dist/components/p-frKsmPQn.js +2 -0
  126. package/dist/components/{p-CvB76TJn.js.map → p-frKsmPQn.js.map} +1 -1
  127. package/dist/docs/components.json +188 -6
  128. package/dist/lib/components/ButtonFilled/ButtonFilled.d.ts +11 -3
  129. package/dist/lib/components/PanelTools/panel-tools-types.d.ts +2 -0
  130. package/dist/lib/components/ShellApp/shell-app-types.d.ts +2 -0
  131. package/dist/lib/shell/index.js.map +1 -1
  132. package/dist/lib/utils/choice-list.d.ts +2 -0
  133. package/dist/mcp-data/registry/bar-nav.json +7 -2
  134. package/dist/mcp-data/registry/bar-title.json +8 -3
  135. package/dist/mcp-data/registry/button-filled.json +26 -5
  136. package/dist/mcp-data/registry/button-unfilled.json +27 -6
  137. package/dist/mcp-data/registry/filter-menu.json +7 -2
  138. package/dist/mcp-data/registry/mobile-section-switcher.json +8 -3
  139. package/dist/mcp-data/registry/panel-nav.json +27 -6
  140. package/dist/mcp-data/registry/panel-tool-header.json +23 -2
  141. package/dist/mcp-data/registry/panel-tool-search.json +23 -2
  142. package/dist/mcp-data/registry/panel-tools.json +7 -2
  143. package/dist/mcp-data/registry/registry.json +147 -24
  144. package/dist/mcp-data/registry/select.json +8 -3
  145. package/dist/mcp-data/registry/shell-app.json +8 -3
  146. package/dist/mcp-data/registry/shell-tools.json +7 -2
  147. package/dist/mcp-data/registry/table-group.json +7 -2
  148. package/dist/mcp-data/registry/table-saved-views.json +7 -2
  149. package/dist/mcp-data/registry/table-search.json +7 -2
  150. package/dist/mcp-data/registry/table-sort.json +7 -2
  151. package/dist/mcp-data/registry/table.json +24 -3
  152. package/dist/mcp-data/registry/tag.json +24 -3
  153. package/dist/styles/table.css +1 -0
  154. package/dist/types/components/BarTitle/BarTitle.d.ts +3 -0
  155. package/dist/types/components/ButtonFilled/ButtonFilled.d.ts +11 -3
  156. package/dist/types/components/ButtonUnfilled/ButtonUnfilled.d.ts +11 -5
  157. package/dist/types/components/MobileSectionSwitcher/MobileSectionSwitcher.d.ts +2 -0
  158. package/dist/types/components/PanelNav/PanelNav.d.ts +4 -1
  159. package/dist/types/components/PanelToolHeader/PanelToolHeader.d.ts +2 -0
  160. package/dist/types/components/PanelToolSearch/PanelToolSearch.d.ts +2 -0
  161. package/dist/types/components/PanelTools/panel-tools-types.d.ts +2 -0
  162. package/dist/types/components/ShellApp/ShellApp.d.ts +1 -0
  163. package/dist/types/components/ShellApp/shell-app-types.d.ts +2 -0
  164. package/dist/types/components/Table/Table.d.ts +10 -0
  165. package/dist/types/components/Table/table-row-view.d.ts +6 -1
  166. package/dist/types/components/TableSort/TableSort.d.ts +1 -0
  167. package/dist/types/components/Tag/Tag.d.ts +3 -0
  168. package/dist/types/components.d.ts +69 -6
  169. package/dist/types/utils/choice-list.d.ts +2 -0
  170. package/dist/vue/ds-button-filled.js +1 -0
  171. package/dist/vue/ds-button-unfilled.js +1 -0
  172. package/dist/vue/ds-panel-nav.js +1 -0
  173. package/dist/vue/ds-panel-tool-header.js +1 -0
  174. package/dist/vue/ds-panel-tool-search.js +1 -0
  175. package/dist/vue/ds-table.js +1 -0
  176. package/dist/vue/ds-tag.js +1 -0
  177. package/package.json +1 -1
  178. package/dist/components/p-6iF6gdoc.js +0 -2
  179. package/dist/components/p-6iF6gdoc.js.map +0 -1
  180. package/dist/components/p-BWDbER5T.js +0 -2
  181. package/dist/components/p-B_NN7k8H.js.map +0 -1
  182. package/dist/components/p-BdU6FPu4.js +0 -2
  183. package/dist/components/p-Bj018KMC.js +0 -2
  184. package/dist/components/p-Bj018KMC.js.map +0 -1
  185. package/dist/components/p-BxLF8b8a.js +0 -2
  186. package/dist/components/p-BxLF8b8a.js.map +0 -1
  187. package/dist/components/p-C1BJgnHY.js +0 -2
  188. package/dist/components/p-C1BJgnHY.js.map +0 -1
  189. package/dist/components/p-C78uTz-n.js +0 -2
  190. package/dist/components/p-C78uTz-n.js.map +0 -1
  191. package/dist/components/p-COn7l__5.js +0 -2
  192. package/dist/components/p-COn7l__5.js.map +0 -1
  193. package/dist/components/p-CsW_2w-s.js +0 -2
  194. package/dist/components/p-CvB76TJn.js +0 -2
  195. package/dist/components/p-D4-RkuER.js +0 -2
  196. package/dist/components/p-D4-RkuER.js.map +0 -1
  197. package/dist/components/p-DB_tlJ2d.js +0 -2
  198. package/dist/components/p-DB_tlJ2d.js.map +0 -1
  199. package/dist/components/p-NB-SoVpo.js +0 -2
  200. package/dist/components/p-P9My22Z1.js +0 -2
  201. package/dist/components/p-P9My22Z1.js.map +0 -1
  202. package/dist/components/p-WM1GTEun.js +0 -2
  203. package/dist/components/p-oAbq-DLk.js +0 -2
  204. package/dist/components/p-oAbq-DLk.js.map +0 -1
  205. /package/dist/components/{p-CxbGZJBU.js.map → p-BwAWZAHk.js.map} +0 -0
  206. /package/dist/components/{p-8atLxerP.js.map → p-By5TDnbF.js.map} +0 -0
  207. /package/dist/components/{p-DGHeKNfG.js.map → p-CE8gsv7V.js.map} +0 -0
  208. /package/dist/components/{p-Kp8ifB1g.js.map → p-Ct90k-nz.js.map} +0 -0
  209. /package/dist/components/{p-C82vesh_.js.map → p-Cx2dWjaW.js.map} +0 -0
  210. /package/dist/components/{p-B3iy9Rl_.js.map → p-DF8W85Tz.js.map} +0 -0
@@ -39,7 +39,8 @@
39
39
  "Dividers group menu items but never participate in previous or next resolution.",
40
40
  "The anchored local-view menu retains its neighboring-direction hints; the primary-page sheet uses an unfilled perimeter trigger without hints.",
41
41
  "A sheet trigger can combine stable page identity and the current local view, separated by a decorative middle dot. The whole control opens only the supplied local destinations; long page names truncate before crowding out the active view.",
42
- "Sheet selection emits navigation intent immediately while exit motion runs independently, so the controlled title can update without waiting for dismissal. Removing the owner or hiding its header closes the top layer without leaving the application blocked."
42
+ "Sheet selection emits navigation intent immediately while exit motion runs independently, so the controlled title can update without waiting for dismissal. Removing the owner or hiding its header closes the top layer without leaving the application blocked.",
43
+ "The trigger automatically retains the shared surface-open pressed wash through menu or sheet exit. Selecting a destination remains separate from owning the open surface."
43
44
  ],
44
45
  "responsiveBehavior": [
45
46
  "Use only in the dedicated mobile header presentation."
@@ -48,6 +49,10 @@
48
49
  {
49
50
  "label": "Storybook",
50
51
  "path": "src/wc/components/MobileSectionSwitcher/MobileSectionSwitcher.stories.ts"
52
+ },
53
+ {
54
+ "label": "Surface ownership and trigger state",
55
+ "path": "src/docs/SurfaceOwnership.mdx"
51
56
  }
52
57
  ]
53
58
  },
@@ -215,12 +220,12 @@
215
220
  "files": [
216
221
  {
217
222
  "path": "src/wc/components/MobileSectionSwitcher/MobileSectionSwitcher.css",
218
- "content": "@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n@import '../../utils/control-density.css';\n\n:host {\n display: block;\n min-width: 0;\n max-width: 100%;\n color: var(--color-foreground-primary);\n}\n\n.mobile-section-switcher {\n appearance: none;\n display: grid;\n grid-template-columns: var(--dimension-size-100) minmax(0, auto) var(--dimension-size-100);\n align-items: center;\n justify-content: center;\n gap: var(--dimension-space-050);\n max-width: 100%;\n min-height: var(--dimension-size-400);\n padding-inline: var(--dimension-space-100);\n border: 0;\n border-radius: var(--dimension-radius-100);\n background: var(--color-background-secondary);\n color: inherit;\n box-sizing: border-box;\n cursor: default;\n}\n\n.mobile-section-switcher--expanded {\n background: var(--color-background-tertiary);\n}\n\n.mobile-section-switcher__label {\n min-width: 0;\n max-width: 100%;\n text-align: center;\n}\n\n.mobile-section-switcher__position {\n width: var(--dimension-size-050);\n height: var(--dimension-size-050);\n border-radius: var(--dimension-radius-half);\n background: currentColor;\n opacity: 0;\n}\n\n.mobile-section-switcher__position--visible {\n opacity: 1;\n}\n\n.mobile-section-switcher--sheet {\n grid-template-columns: minmax(0, auto);\n min-height: var(--dimension-size-500);\n padding-inline: var(--dimension-space-150);\n border-radius: var(--dimension-radius-half);\n background: transparent;\n --ds-interaction-border-width: var(--dimension-stroke-width-012);\n --ds-interaction-border-color: var(--color-border-tertiary);\n}\n\n.mobile-section-switcher--combined {\n display: flex;\n}\n\n.mobile-section-switcher__page-label {\n flex: 0 1 auto;\n min-width: 0;\n}\n\n.mobile-section-switcher__separator {\n flex: 0 0 auto;\n}\n\n.mobile-section-switcher--combined .mobile-section-switcher__label {\n flex: 0 0 auto;\n max-width: 50%;\n}\n\n.mobile-section-sheet {\n position: fixed;\n inset: 0 0 auto;\n width: 100%;\n max-width: none;\n max-height: calc(100dvh - var(--dimension-size-500) - var(--dimension-space-200));\n margin: 0;\n padding: 0;\n border: 0;\n border-radius: 0;\n box-sizing: border-box;\n overflow: hidden;\n background: var(--color-background-primary);\n color: var(--color-foreground-secondary);\n box-shadow: var(--effect-elevation-elevated-floating);\n outline: none;\n}\n\n.mobile-section-sheet[open] {\n display: flex;\n flex-direction: column;\n animation: mobileSectionSheetIn var(--effect-motion-short-2) both;\n}\n\n.mobile-section-sheet::backdrop {\n background: var(--color-background-shade);\n animation: mobileSectionShadeIn var(--effect-motion-short-2) both;\n}\n\n.mobile-section-sheet--closing[open] {\n animation-name: mobileSectionSheetOut;\n}\n\n.mobile-section-sheet--closing::backdrop {\n animation-name: mobileSectionShadeOut;\n}\n\n.mobile-section-sheet__items {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-100);\n min-height: 0;\n padding: var(--dimension-space-100);\n padding-block-start: max(var(--dimension-space-100), env(safe-area-inset-top, 0px));\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n\n.mobile-section-sheet__item {\n appearance: none;\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n height: var(--ds-control-height);\n width: 100%;\n min-width: 0;\n padding: 0 var(--ds-control-padding-inline);\n box-sizing: border-box;\n border: 0;\n border-radius: var(--ds-control-radius);\n background: transparent;\n color: inherit;\n text-align: start;\n}\n\n.mobile-section-sheet__item[aria-current='page'] {\n color: var(--color-foreground-primary);\n}\n\n.mobile-section-sheet__item:disabled {\n color: var(--color-foreground-tertiary);\n}\n\n.mobile-section-sheet__label {\n min-width: 0;\n padding-inline: var(--ds-control-label-inset);\n}\n\n.mobile-section-sheet__divider {\n flex: 0 0 auto;\n height: var(--dimension-stroke-width-012);\n margin-inline: var(--dimension-space-100);\n background: var(--color-border-tertiary);\n}\n\n@keyframes mobileSectionSheetIn {\n from {\n transform: translateY(-100%);\n }\n to {\n transform: translateY(0);\n }\n}\n\n@keyframes mobileSectionSheetOut {\n from {\n transform: translateY(0);\n }\n to {\n transform: translateY(-100%);\n }\n}\n\n@keyframes mobileSectionShadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n@keyframes mobileSectionShadeOut {\n from {\n opacity: 1;\n }\n to {\n opacity: 0;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .mobile-section-sheet[open],\n .mobile-section-sheet::backdrop {\n animation: none;\n }\n}\n",
223
+ "content": "@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n@import '../../utils/control-density.css';\n\n:host {\n display: block;\n min-width: 0;\n max-width: 100%;\n color: var(--color-foreground-primary);\n}\n\n.mobile-section-switcher {\n appearance: none;\n display: grid;\n grid-template-columns: var(--dimension-size-100) minmax(0, auto) var(--dimension-size-100);\n align-items: center;\n justify-content: center;\n gap: var(--dimension-space-050);\n max-width: 100%;\n min-height: var(--dimension-size-400);\n padding-inline: var(--dimension-space-100);\n border: 0;\n border-radius: var(--dimension-radius-100);\n background: var(--color-background-secondary);\n color: inherit;\n box-sizing: border-box;\n cursor: default;\n}\n\n.mobile-section-switcher__label {\n min-width: 0;\n max-width: 100%;\n text-align: center;\n}\n\n.mobile-section-switcher__position {\n width: var(--dimension-size-050);\n height: var(--dimension-size-050);\n border-radius: var(--dimension-radius-half);\n background: currentColor;\n opacity: 0;\n}\n\n.mobile-section-switcher__position--visible {\n opacity: 1;\n}\n\n.mobile-section-switcher--sheet {\n grid-template-columns: minmax(0, auto);\n min-height: var(--dimension-size-500);\n padding-inline: var(--dimension-space-150);\n border-radius: var(--dimension-radius-half);\n background: transparent;\n --ds-interaction-border-width: var(--dimension-stroke-width-012);\n --ds-interaction-border-color: var(--color-border-tertiary);\n}\n\n.mobile-section-switcher--combined {\n display: flex;\n}\n\n.mobile-section-switcher__page-label {\n flex: 0 1 auto;\n min-width: 0;\n}\n\n.mobile-section-switcher__separator {\n flex: 0 0 auto;\n}\n\n.mobile-section-switcher--combined .mobile-section-switcher__label {\n flex: 0 0 auto;\n max-width: 50%;\n}\n\n.mobile-section-sheet {\n position: fixed;\n inset: 0 0 auto;\n width: 100%;\n max-width: none;\n max-height: calc(100dvh - var(--dimension-size-500) - var(--dimension-space-200));\n margin: 0;\n padding: 0;\n border: 0;\n border-radius: 0;\n box-sizing: border-box;\n overflow: hidden;\n background: var(--color-background-primary);\n color: var(--color-foreground-secondary);\n box-shadow: var(--effect-elevation-elevated-floating);\n outline: none;\n}\n\n.mobile-section-sheet[open] {\n display: flex;\n flex-direction: column;\n animation: mobileSectionSheetIn var(--effect-motion-short-2) both;\n}\n\n.mobile-section-sheet::backdrop {\n background: var(--color-background-shade);\n animation: mobileSectionShadeIn var(--effect-motion-short-2) both;\n}\n\n.mobile-section-sheet--closing[open] {\n animation-name: mobileSectionSheetOut;\n}\n\n.mobile-section-sheet--closing::backdrop {\n animation-name: mobileSectionShadeOut;\n}\n\n.mobile-section-sheet__items {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-100);\n min-height: 0;\n padding: var(--dimension-space-100);\n padding-block-start: max(var(--dimension-space-100), env(safe-area-inset-top, 0px));\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n\n.mobile-section-sheet__item {\n appearance: none;\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n height: var(--ds-control-height);\n width: 100%;\n min-width: 0;\n padding: 0 var(--ds-control-padding-inline);\n box-sizing: border-box;\n border: 0;\n border-radius: var(--ds-control-radius);\n background: transparent;\n color: inherit;\n text-align: start;\n}\n\n.mobile-section-sheet__item[aria-current='page'] {\n color: var(--color-foreground-primary);\n}\n\n.mobile-section-sheet__item:disabled {\n color: var(--color-foreground-tertiary);\n}\n\n.mobile-section-sheet__label {\n min-width: 0;\n padding-inline: var(--ds-control-label-inset);\n}\n\n.mobile-section-sheet__divider {\n flex: 0 0 auto;\n height: var(--dimension-stroke-width-012);\n margin-inline: var(--dimension-space-100);\n background: var(--color-border-tertiary);\n}\n\n@keyframes mobileSectionSheetIn {\n from {\n transform: translateY(-100%);\n }\n to {\n transform: translateY(0);\n }\n}\n\n@keyframes mobileSectionSheetOut {\n from {\n transform: translateY(0);\n }\n to {\n transform: translateY(-100%);\n }\n}\n\n@keyframes mobileSectionShadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n@keyframes mobileSectionShadeOut {\n from {\n opacity: 1;\n }\n to {\n opacity: 0;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .mobile-section-sheet[open],\n .mobile-section-sheet::backdrop {\n animation: none;\n }\n}\n",
219
224
  "type": "registry:ui"
220
225
  },
221
226
  {
222
227
  "path": "src/wc/components/MobileSectionSwitcher/MobileSectionSwitcher.tsx",
223
- "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport type { MenuItemData, MenuSection } from '../Menu/menu-types';\nimport {\n getSelectableTabs,\n isTabDivider,\n type TabItem,\n type TabItemTab,\n} from '../TabGroup/tab-item-utils';\nimport { resolveMobileSectionPosition } from './mobile-section-switcher-utils';\nimport { resolveMotionTimeMs, TOKEN_DEFAULTS } from '../../utils';\n\nlet nextMobileSectionSwitcherId = 0;\n\n@Component({\n tag: 'ds-mobile-section-switcher',\n styleUrl: 'MobileSectionSwitcher.css',\n scoped: true,\n})\nexport class MobileSectionSwitcher {\n @Element() el!: HTMLElement;\n\n /** Ordered controlled sections. Divider entries group the popup menu. */\n @Prop() sections: TabItem[] = [];\n /** Controlled selected section id. */\n @Prop() value: string = '';\n /** Accessible name for the section chooser. */\n @Prop() navigationLabel: string = 'Change page section';\n /** Anchored local-view menu or viewport-edge sheet for primary page sections. */\n @Prop() presentation: 'menu' | 'sheet' = 'menu';\n /** Stable page identity shown before the selected section in a sheet trigger. */\n @Prop() pageLabel: string = '';\n\n /** Selection intent. The route, tool, or workflow owner updates `value`. */\n @Event() dsChange!: EventEmitter<string>;\n\n @State() private menuOpen = false;\n @State() private initialFocusVisible = false;\n @State() private sheetClosing = false;\n @State() private focusedSection = '';\n\n private readonly instanceId = nextMobileSectionSwitcherId++;\n private readonly triggerId = `ds-mobile-section-switcher-trigger-${this.instanceId}`;\n private readonly menuId = `ds-mobile-section-switcher-menu-${this.instanceId}`;\n private triggerEl: HTMLButtonElement | null = null;\n private dialogEl: HTMLDialogElement | null = null;\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n private visibilityObserver?: ResizeObserver;\n\n componentDidLoad() {\n this.visibilityObserver = new ResizeObserver(() => {\n if (this.dialogEl?.open && !this.triggerEl?.getClientRects().length) {\n this.finishSheetClose(false);\n }\n });\n this.visibilityObserver.observe(this.el);\n }\n\n componentDidRender() {\n if (!this.dialogEl) return;\n if (this.presentation === 'sheet' && this.menuOpen && !this.dialogEl.open) {\n const initialSection = this.focusedSection;\n this.dialogEl.showModal();\n this.focusSheetItem(initialSection);\n }\n if (this.sheetClosing && this.dialogEl.open && !this.closeTimer) {\n const duration = resolveMotionTimeMs(\n TOKEN_DEFAULTS.motionShort2,\n TOKEN_DEFAULTS.animationDurationShort3\n );\n if (!duration) this.finishSheetClose();\n else this.closeTimer = setTimeout(() => this.finishSheetClose(), duration);\n }\n }\n\n disconnectedCallback() {\n this.visibilityObserver?.disconnect();\n this.finishSheetClose(false);\n }\n\n @Watch('presentation')\n @Watch('value')\n @Watch('pageLabel')\n handleContextChange() {\n this.closeMenu();\n }\n\n private finishSheetClose(restoreFocus = true) {\n if (this.closeTimer) clearTimeout(this.closeTimer);\n this.closeTimer = null;\n const wasOpen = this.dialogEl?.open;\n this.dialogEl?.close();\n this.menuOpen = false;\n this.sheetClosing = false;\n if (wasOpen && restoreFocus && this.triggerEl?.getClientRects().length) this.triggerEl.focus();\n }\n\n private focusSheetItem(id: string) {\n const buttons = Array.from(this.dialogEl?.querySelectorAll<HTMLButtonElement>('button') ?? []);\n const button =\n buttons.find(item => item.value === id && !item.disabled) ??\n buttons.find(item => !item.disabled);\n if (button) {\n this.focusedSection = button.value;\n button.focus();\n } else this.dialogEl?.focus();\n }\n\n private handleSheetKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n event.preventDefault();\n event.stopPropagation();\n this.closeMenu();\n return;\n }\n const sections = this.selectableSections.filter(section => !section.isInactive);\n if (!sections.length) return;\n const index = Math.max(\n 0,\n sections.findIndex(section => section.id === this.focusedSection)\n );\n let next: number;\n if (event.key === 'ArrowDown') next = (index + 1) % sections.length;\n else if (event.key === 'ArrowUp') next = (index + sections.length - 1) % sections.length;\n else if (event.key === 'Home') next = 0;\n else if (event.key === 'End') next = sections.length - 1;\n else if (event.key === 'Tab') next = index;\n else if (\n event.key.length === 1 &&\n !event.ctrlKey &&\n !event.metaKey &&\n !event.altKey &&\n event.key !== ' '\n ) {\n const match = sections.findIndex((_, offset) =>\n sections[(index + offset + 1) % sections.length].label\n .toLowerCase()\n .startsWith(event.key.toLowerCase())\n );\n if (match < 0) return;\n next = (index + match + 1) % sections.length;\n } else return;\n event.preventDefault();\n this.initialFocusVisible = true;\n this.focusSheetItem(sections[next].id);\n };\n\n private get resolvedSections(): TabItem[] {\n return this.sections ?? [];\n }\n\n private get selectableSections(): TabItemTab[] {\n return getSelectableTabs(this.resolvedSections);\n }\n\n private get selectedIndex(): number {\n return resolveMobileSectionPosition(this.resolvedSections, this.value).selectedIndex;\n }\n\n private get selectedSection(): TabItemTab | undefined {\n return this.selectableSections[this.selectedIndex];\n }\n\n private get hasPrevious(): boolean {\n return resolveMobileSectionPosition(this.resolvedSections, this.value).hasPrevious;\n }\n\n private get hasNext(): boolean {\n return resolveMobileSectionPosition(this.resolvedSections, this.value).hasNext;\n }\n\n private get triggerAriaLabel(): string {\n return `${this.presentation === 'sheet' && this.pageLabel ? `${this.pageLabel}. ` : ''}${this.navigationLabel}. Current section: ${this.selectedSection?.label ?? ''}`;\n }\n\n private get menuSections(): MenuSection[] {\n const groups: MenuSection[] = [];\n let items: MenuItemData[] = [];\n const commit = () => {\n if (items.length) groups.push({ items });\n items = [];\n };\n\n for (const section of this.resolvedSections) {\n if (isTabDivider(section)) {\n commit();\n continue;\n }\n items.push({\n label: section.label,\n value: section.id,\n dot: section.dot,\n isSelected: section.id === this.selectedSection?.id,\n isInactive: section.isInactive,\n });\n }\n commit();\n return groups;\n }\n\n @Watch('sections')\n handleSectionsChange() {\n if (this.selectableSections.length <= 1) this.closeMenu();\n else if (this.dialogEl?.open && !this.sheetClosing) this.focusSheetItem(this.focusedSection);\n }\n\n private toggleMenu = (event: MouseEvent) => {\n if (this.selectableSections.length <= 1) return;\n this.initialFocusVisible = event.detail === 0;\n if (this.presentation === 'sheet') {\n if (this.menuOpen) this.closeMenu();\n else {\n this.focusedSection = this.selectedSection?.id ?? '';\n this.menuOpen = true;\n }\n return;\n }\n this.menuOpen = !this.menuOpen;\n };\n\n private closeMenu = () => {\n this.menuOpen = false;\n if (this.dialogEl?.open) this.sheetClosing = true;\n };\n\n private renderSheet() {\n return (\n <dialog\n id={this.menuId}\n ref={element => {\n this.dialogEl = element ?? null;\n }}\n class={{ 'mobile-section-sheet': true, 'mobile-section-sheet--closing': this.sheetClosing }}\n aria-label={this.navigationLabel}\n tabIndex={-1}\n onCancel={event => {\n event.preventDefault();\n this.closeMenu();\n }}\n onKeyDown={this.handleSheetKeyDown}\n onClick={event => {\n if (!this.dialogEl || event.target !== this.dialogEl) return;\n const rect = this.dialogEl.getBoundingClientRect();\n if (\n event.clientY < rect.top ||\n event.clientY > rect.bottom ||\n event.clientX < rect.left ||\n event.clientX > rect.right\n )\n this.closeMenu();\n }}\n >\n <div class=\"mobile-section-sheet__items\" role=\"menu\" aria-label={this.navigationLabel}>\n {this.resolvedSections.map(section =>\n isTabDivider(section) ? (\n <div class=\"mobile-section-sheet__divider\" role=\"separator\" />\n ) : (\n <button\n type=\"button\"\n role=\"menuitem\"\n value={section.id}\n disabled={section.isInactive}\n aria-current={section.id === this.selectedSection?.id ? 'page' : undefined}\n tabIndex={section.id === this.focusedSection ? 0 : -1}\n class={{\n 'mobile-section-sheet__item': true,\n 'ds-control--lg': true,\n 'ds-interaction-fill': true,\n 'ds-focus-ring-inset': true,\n 'ds-focus-ring--visible':\n this.initialFocusVisible && section.id === this.focusedSection,\n }}\n onFocus={() => {\n this.focusedSection = section.id;\n }}\n onClick={() => {\n if (this.sheetClosing || section.isInactive) return;\n this.closeMenu();\n if (section.id !== this.selectedSection?.id) this.dsChange.emit(section.id);\n }}\n >\n <ds-text\n class=\"mobile-section-sheet__label ds-interaction-fill__content\"\n as=\"span\"\n variant=\"text-body-large\"\n color=\"inherit\"\n emphasis={section.id === this.selectedSection?.id}\n lineTruncation={1}\n >\n {section.label}\n </ds-text>\n </button>\n )\n )}\n </div>\n </dialog>\n );\n }\n\n private handleSelect = (event: CustomEvent<MenuItemData>) => {\n const id = String(event.detail.value ?? '');\n const section = this.selectableSections.find(candidate => candidate.id === id);\n if (!section || section.isInactive) return;\n this.closeMenu();\n if (id !== this.selectedSection?.id) this.dsChange.emit(id);\n requestAnimationFrame(() => this.triggerEl?.focus());\n };\n\n render() {\n const selected = this.selectedSection;\n if (!selected) return <Host />;\n const combined = this.presentation === 'sheet' && !!this.pageLabel;\n\n return (\n <Host>\n <button\n ref={element => {\n this.triggerEl = element ?? null;\n }}\n id={this.triggerId}\n class={{\n 'mobile-section-switcher': true,\n 'mobile-section-switcher--expanded': this.menuOpen,\n 'mobile-section-switcher--sheet': this.presentation === 'sheet',\n 'mobile-section-switcher--combined': combined,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n }}\n type=\"button\"\n aria-haspopup={this.presentation === 'sheet' ? 'dialog' : 'menu'}\n aria-controls={this.menuId}\n aria-expanded={String(this.menuOpen)}\n aria-label={this.triggerAriaLabel}\n onClick={this.toggleMenu}\n >\n {this.presentation === 'menu' ? (\n <span\n class={{\n 'mobile-section-switcher__position': true,\n 'mobile-section-switcher__position--visible': this.hasPrevious,\n 'ds-interaction-fill__content': true,\n }}\n aria-hidden=\"true\"\n />\n ) : null}\n {combined\n ? [\n <ds-text\n class=\"mobile-section-switcher__page-label ds-interaction-fill__content\"\n as=\"span\"\n variant=\"text-body-large\"\n emphasis\n color=\"inherit\"\n lineTruncation={1}\n >\n {this.pageLabel}\n </ds-text>,\n <ds-text\n class=\"mobile-section-switcher__separator ds-interaction-fill__content\"\n as=\"span\"\n variant=\"text-body-large\"\n color=\"inherit\"\n aria-hidden=\"true\"\n >\n ·\n </ds-text>,\n ]\n : null}\n <ds-text\n class=\"mobile-section-switcher__label ds-interaction-fill__content\"\n as=\"span\"\n variant={this.presentation === 'sheet' ? 'text-body-large' : 'text-body-medium'}\n emphasis\n color=\"inherit\"\n lineTruncation={1}\n >\n {selected.label}\n </ds-text>\n {this.presentation === 'menu' ? (\n <span\n class={{\n 'mobile-section-switcher__position': true,\n 'mobile-section-switcher__position--visible': this.hasNext,\n 'ds-interaction-fill__content': true,\n }}\n aria-hidden=\"true\"\n />\n ) : null}\n </button>\n\n {this.presentation === 'sheet' || this.sheetClosing ? (\n this.renderSheet()\n ) : this.selectableSections.length > 1 ? (\n <ds-menu\n id={this.menuId}\n anchorId={this.triggerId}\n align=\"center\"\n menuLabel={this.navigationLabel}\n open={this.menuOpen}\n initialFocusVisible={this.initialFocusVisible}\n sections={this.menuSections}\n onDsClose={this.closeMenu}\n onDsSelect={this.handleSelect}\n />\n ) : null}\n </Host>\n );\n }\n}\n",
228
+ "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport type { MenuItemData, MenuSection } from '../Menu/menu-types';\nimport {\n getSelectableTabs,\n isTabDivider,\n type TabItem,\n type TabItemTab,\n} from '../TabGroup/tab-item-utils';\nimport { resolveMobileSectionPosition } from './mobile-section-switcher-utils';\nimport { resolveMotionTimeMs, TOKEN_DEFAULTS } from '../../utils';\n\nlet nextMobileSectionSwitcherId = 0;\n\n@Component({\n tag: 'ds-mobile-section-switcher',\n styleUrl: 'MobileSectionSwitcher.css',\n scoped: true,\n})\nexport class MobileSectionSwitcher {\n @Element() el!: HTMLElement;\n\n /** Ordered controlled sections. Divider entries group the popup menu. */\n @Prop() sections: TabItem[] = [];\n /** Controlled selected section id. */\n @Prop() value: string = '';\n /** Accessible name for the section chooser. */\n @Prop() navigationLabel: string = 'Change page section';\n /** Anchored local-view menu or viewport-edge sheet for primary page sections. */\n @Prop() presentation: 'menu' | 'sheet' = 'menu';\n /** Stable page identity shown before the selected section in a sheet trigger. */\n @Prop() pageLabel: string = '';\n\n /** Selection intent. The route, tool, or workflow owner updates `value`. */\n @Event() dsChange!: EventEmitter<string>;\n\n @State() private menuOpen = false;\n @State() private menuSurfaceOpen = false;\n @State() private initialFocusVisible = false;\n @State() private sheetClosing = false;\n @State() private focusedSection = '';\n\n private readonly instanceId = nextMobileSectionSwitcherId++;\n private readonly triggerId = `ds-mobile-section-switcher-trigger-${this.instanceId}`;\n private readonly menuId = `ds-mobile-section-switcher-menu-${this.instanceId}`;\n private triggerEl: HTMLButtonElement | null = null;\n private dialogEl: HTMLDialogElement | null = null;\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n private visibilityObserver?: ResizeObserver;\n\n componentDidLoad() {\n this.visibilityObserver = new ResizeObserver(() => {\n if (this.dialogEl?.open && !this.triggerEl?.getClientRects().length) {\n this.finishSheetClose(false);\n }\n });\n this.visibilityObserver.observe(this.el);\n }\n\n componentDidRender() {\n if (!this.dialogEl) return;\n if (this.presentation === 'sheet' && this.menuOpen && !this.dialogEl.open) {\n const initialSection = this.focusedSection;\n this.dialogEl.showModal();\n this.focusSheetItem(initialSection);\n }\n if (this.sheetClosing && this.dialogEl.open && !this.closeTimer) {\n const duration = resolveMotionTimeMs(\n TOKEN_DEFAULTS.motionShort2,\n TOKEN_DEFAULTS.animationDurationShort3\n );\n if (!duration) this.finishSheetClose();\n else this.closeTimer = setTimeout(() => this.finishSheetClose(), duration);\n }\n }\n\n disconnectedCallback() {\n this.visibilityObserver?.disconnect();\n this.menuSurfaceOpen = false;\n this.finishSheetClose(false);\n }\n\n @Watch('presentation')\n handlePresentationChange() {\n this.menuSurfaceOpen = false;\n this.closeMenu();\n }\n\n @Watch('value')\n @Watch('pageLabel')\n handleContextChange() {\n this.closeMenu();\n }\n\n private finishSheetClose(restoreFocus = true) {\n if (this.closeTimer) clearTimeout(this.closeTimer);\n this.closeTimer = null;\n const wasOpen = this.dialogEl?.open;\n this.dialogEl?.close();\n this.menuOpen = false;\n this.sheetClosing = false;\n if (wasOpen && restoreFocus && this.triggerEl?.getClientRects().length) this.triggerEl.focus();\n }\n\n private focusSheetItem(id: string) {\n const buttons = Array.from(this.dialogEl?.querySelectorAll<HTMLButtonElement>('button') ?? []);\n const button =\n buttons.find(item => item.value === id && !item.disabled) ??\n buttons.find(item => !item.disabled);\n if (button) {\n this.focusedSection = button.value;\n button.focus();\n } else this.dialogEl?.focus();\n }\n\n private handleSheetKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n event.preventDefault();\n event.stopPropagation();\n this.closeMenu();\n return;\n }\n const sections = this.selectableSections.filter(section => !section.isInactive);\n if (!sections.length) return;\n const index = Math.max(\n 0,\n sections.findIndex(section => section.id === this.focusedSection)\n );\n let next: number;\n if (event.key === 'ArrowDown') next = (index + 1) % sections.length;\n else if (event.key === 'ArrowUp') next = (index + sections.length - 1) % sections.length;\n else if (event.key === 'Home') next = 0;\n else if (event.key === 'End') next = sections.length - 1;\n else if (event.key === 'Tab') next = index;\n else if (\n event.key.length === 1 &&\n !event.ctrlKey &&\n !event.metaKey &&\n !event.altKey &&\n event.key !== ' '\n ) {\n const match = sections.findIndex((_, offset) =>\n sections[(index + offset + 1) % sections.length].label\n .toLowerCase()\n .startsWith(event.key.toLowerCase())\n );\n if (match < 0) return;\n next = (index + match + 1) % sections.length;\n } else return;\n event.preventDefault();\n this.initialFocusVisible = true;\n this.focusSheetItem(sections[next].id);\n };\n\n private get resolvedSections(): TabItem[] {\n return this.sections ?? [];\n }\n\n private get selectableSections(): TabItemTab[] {\n return getSelectableTabs(this.resolvedSections);\n }\n\n private get selectedIndex(): number {\n return resolveMobileSectionPosition(this.resolvedSections, this.value).selectedIndex;\n }\n\n private get selectedSection(): TabItemTab | undefined {\n return this.selectableSections[this.selectedIndex];\n }\n\n private get hasPrevious(): boolean {\n return resolveMobileSectionPosition(this.resolvedSections, this.value).hasPrevious;\n }\n\n private get hasNext(): boolean {\n return resolveMobileSectionPosition(this.resolvedSections, this.value).hasNext;\n }\n\n private get triggerAriaLabel(): string {\n return `${this.presentation === 'sheet' && this.pageLabel ? `${this.pageLabel}. ` : ''}${this.navigationLabel}. Current section: ${this.selectedSection?.label ?? ''}`;\n }\n\n private get menuSections(): MenuSection[] {\n const groups: MenuSection[] = [];\n let items: MenuItemData[] = [];\n const commit = () => {\n if (items.length) groups.push({ items });\n items = [];\n };\n\n for (const section of this.resolvedSections) {\n if (isTabDivider(section)) {\n commit();\n continue;\n }\n items.push({\n label: section.label,\n value: section.id,\n dot: section.dot,\n isSelected: section.id === this.selectedSection?.id,\n isInactive: section.isInactive,\n });\n }\n commit();\n return groups;\n }\n\n @Watch('sections')\n handleSectionsChange() {\n if (this.selectableSections.length <= 1) {\n this.closeMenu();\n this.menuSurfaceOpen = false;\n } else if (this.dialogEl?.open && !this.sheetClosing) this.focusSheetItem(this.focusedSection);\n }\n\n private toggleMenu = (event: MouseEvent) => {\n if (this.selectableSections.length <= 1) return;\n this.initialFocusVisible = event.detail === 0;\n if (this.presentation === 'sheet') {\n if (this.menuOpen) this.closeMenu();\n else {\n this.focusedSection = this.selectedSection?.id ?? '';\n this.menuOpen = true;\n }\n return;\n }\n this.menuOpen = !this.menuOpen;\n if (this.menuOpen) this.menuSurfaceOpen = true;\n };\n\n private closeMenu = () => {\n this.menuOpen = false;\n if (this.dialogEl?.open) this.sheetClosing = true;\n };\n\n private renderSheet() {\n return (\n <dialog\n id={this.menuId}\n ref={element => {\n this.dialogEl = element ?? null;\n }}\n class={{ 'mobile-section-sheet': true, 'mobile-section-sheet--closing': this.sheetClosing }}\n aria-label={this.navigationLabel}\n tabIndex={-1}\n onCancel={event => {\n event.preventDefault();\n this.closeMenu();\n }}\n onKeyDown={this.handleSheetKeyDown}\n onClick={event => {\n if (!this.dialogEl || event.target !== this.dialogEl) return;\n const rect = this.dialogEl.getBoundingClientRect();\n if (\n event.clientY < rect.top ||\n event.clientY > rect.bottom ||\n event.clientX < rect.left ||\n event.clientX > rect.right\n )\n this.closeMenu();\n }}\n >\n <div class=\"mobile-section-sheet__items\" role=\"menu\" aria-label={this.navigationLabel}>\n {this.resolvedSections.map(section =>\n isTabDivider(section) ? (\n <div class=\"mobile-section-sheet__divider\" role=\"separator\" />\n ) : (\n <button\n type=\"button\"\n role=\"menuitem\"\n value={section.id}\n disabled={section.isInactive}\n aria-current={section.id === this.selectedSection?.id ? 'page' : undefined}\n tabIndex={section.id === this.focusedSection ? 0 : -1}\n class={{\n 'mobile-section-sheet__item': true,\n 'ds-control--lg': true,\n 'ds-interaction-fill': true,\n 'ds-focus-ring-inset': true,\n 'ds-focus-ring--visible':\n this.initialFocusVisible && section.id === this.focusedSection,\n }}\n onFocus={() => {\n this.focusedSection = section.id;\n }}\n onClick={() => {\n if (this.sheetClosing || section.isInactive) return;\n this.closeMenu();\n if (section.id !== this.selectedSection?.id) this.dsChange.emit(section.id);\n }}\n >\n <ds-text\n class=\"mobile-section-sheet__label ds-interaction-fill__content\"\n as=\"span\"\n variant=\"text-body-large\"\n color=\"inherit\"\n emphasis={section.id === this.selectedSection?.id}\n lineTruncation={1}\n >\n {section.label}\n </ds-text>\n </button>\n )\n )}\n </div>\n </dialog>\n );\n }\n\n private handleSelect = (event: CustomEvent<MenuItemData>) => {\n const id = String(event.detail.value ?? '');\n const section = this.selectableSections.find(candidate => candidate.id === id);\n if (!section || section.isInactive) return;\n this.closeMenu();\n if (id !== this.selectedSection?.id) this.dsChange.emit(id);\n requestAnimationFrame(() => this.triggerEl?.focus());\n };\n\n render() {\n const selected = this.selectedSection;\n if (!selected) return <Host />;\n const combined = this.presentation === 'sheet' && !!this.pageLabel;\n\n return (\n <Host>\n <button\n ref={element => {\n this.triggerEl = element ?? null;\n }}\n id={this.triggerId}\n class={{\n 'mobile-section-switcher': true,\n 'mobile-section-switcher--expanded': this.menuOpen,\n 'ds-interaction-fill--surface-open':\n this.menuSurfaceOpen || this.menuOpen || this.sheetClosing,\n 'mobile-section-switcher--sheet': this.presentation === 'sheet',\n 'mobile-section-switcher--combined': combined,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n }}\n type=\"button\"\n aria-haspopup={this.presentation === 'sheet' ? 'dialog' : 'menu'}\n aria-controls={this.menuId}\n aria-expanded={String(this.menuOpen)}\n aria-label={this.triggerAriaLabel}\n onClick={this.toggleMenu}\n >\n {this.presentation === 'menu' ? (\n <span\n class={{\n 'mobile-section-switcher__position': true,\n 'mobile-section-switcher__position--visible': this.hasPrevious,\n 'ds-interaction-fill__content': true,\n }}\n aria-hidden=\"true\"\n />\n ) : null}\n {combined\n ? [\n <ds-text\n class=\"mobile-section-switcher__page-label ds-interaction-fill__content\"\n as=\"span\"\n variant=\"text-body-large\"\n emphasis\n color=\"inherit\"\n lineTruncation={1}\n >\n {this.pageLabel}\n </ds-text>,\n <ds-text\n class=\"mobile-section-switcher__separator ds-interaction-fill__content\"\n as=\"span\"\n variant=\"text-body-large\"\n color=\"inherit\"\n aria-hidden=\"true\"\n >\n ·\n </ds-text>,\n ]\n : null}\n <ds-text\n class=\"mobile-section-switcher__label ds-interaction-fill__content\"\n as=\"span\"\n variant={this.presentation === 'sheet' ? 'text-body-large' : 'text-body-medium'}\n emphasis\n color=\"inherit\"\n lineTruncation={1}\n >\n {selected.label}\n </ds-text>\n {this.presentation === 'menu' ? (\n <span\n class={{\n 'mobile-section-switcher__position': true,\n 'mobile-section-switcher__position--visible': this.hasNext,\n 'ds-interaction-fill__content': true,\n }}\n aria-hidden=\"true\"\n />\n ) : null}\n </button>\n\n {this.presentation === 'sheet' || this.sheetClosing ? (\n this.renderSheet()\n ) : this.selectableSections.length > 1 ? (\n <ds-menu\n id={this.menuId}\n anchorId={this.triggerId}\n align=\"center\"\n menuLabel={this.navigationLabel}\n open={this.menuOpen}\n initialFocusVisible={this.initialFocusVisible}\n sections={this.menuSections}\n onDsClose={this.closeMenu}\n onDsAfterClose={() => {\n if (!this.menuOpen) this.menuSurfaceOpen = false;\n }}\n onDsSelect={this.handleSelect}\n />\n ) : null}\n </Host>\n );\n }\n}\n",
224
229
  "type": "registry:ui"
225
230
  },
226
231
  {
@@ -76,9 +76,10 @@
76
76
  "Dashboard presents day-to-day operational destinations; Settings presents administrative and configuration destinations.",
77
77
  "The standard footer switches the entire destination collection and navStyle between Dashboard and Settings and exposes the user/account entry.",
78
78
  "The host owns the external user menu opened from dsNavUserAction.",
79
- "While the external user menu is open or closing, accountMenuExpanded keeps its trigger visibly pressed without applying selected-destination foreground treatment in either expanded or collapsed presentation.",
79
+ "While the external user menu is open or closing, accountMenuSurfaceOpen keeps its trigger visibly pressed without applying selected-destination foreground treatment in either expanded or collapsed presentation.",
80
80
  "Item and header controls inherit the shared md control radius while local shell geometry continues to own collapsed layout and footer animation positioning.",
81
- "On desktop, allow user-controlled expand and collapse and persist the preference with storageKey when cross-session persistence is appropriate."
81
+ "On desktop, allow user-controlled expand and collapse and persist the preference with storageKey when cross-session persistence is appropriate.",
82
+ "Flyout triggers automatically hold the shared surface-open wash through exit; expanded inline branches do not. For the external account surface, the application supplies accountMenuSurfaceOpen for its rendered lifetime and keeps accountMenuExpanded tied to disclosure semantics."
82
83
  ],
83
84
  "responsiveBehavior": [
84
85
  "At 1200px and wider, default to expanded, allow the user to expand or collapse, and restore the locally persisted desktop preference when available.",
@@ -116,6 +117,10 @@
116
117
  {
117
118
  "label": "Application shell composition",
118
119
  "path": "agent/patterns/application-shell/pattern.agent.json"
120
+ },
121
+ {
122
+ "label": "Surface ownership and trigger state",
123
+ "path": "src/docs/SurfaceOwnership.mdx"
119
124
  }
120
125
  ]
121
126
  },
@@ -136,7 +141,15 @@
136
141
  "default": "false",
137
142
  "required": false,
138
143
  "mutable": false,
139
- "description": "Keep the account-menu trigger visually pressed while its popup is open or closing."
144
+ "description": "Account disclosure state. Expanded-derived styling is deprecated; use accountMenuSurfaceOpen for the rendered lifetime."
145
+ },
146
+ "accountMenuSurfaceOpen": {
147
+ "type": "boolean | undefined",
148
+ "resolvedType": "boolean | undefined",
149
+ "attribute": "account-menu-surface-open",
150
+ "required": false,
151
+ "mutable": false,
152
+ "description": "External account surface is visible, including exit motion. Omission\npreserves the deprecated accountMenuExpanded-derived wash."
140
153
  },
141
154
  "activeChildId": {
142
155
  "type": "string",
@@ -395,7 +408,15 @@
395
408
  "default": "false",
396
409
  "required": false,
397
410
  "mutable": false,
398
- "description": "Keep the account-menu trigger visually pressed while its popup is open or closing."
411
+ "description": "Account disclosure state. Expanded-derived styling is deprecated; use accountMenuSurfaceOpen for the rendered lifetime."
412
+ },
413
+ "accountMenuSurfaceOpen": {
414
+ "type": "boolean | undefined",
415
+ "resolvedType": "boolean | undefined",
416
+ "attribute": "account-menu-surface-open",
417
+ "required": false,
418
+ "mutable": false,
419
+ "description": "External account surface is visible, including exit motion. Omission\npreserves the deprecated accountMenuExpanded-derived wash."
399
420
  },
400
421
  "activeChildId": {
401
422
  "type": "string",
@@ -687,12 +708,12 @@
687
708
  "files": [
688
709
  {
689
710
  "path": "src/wc/components/PanelNav/PanelNav.css",
690
- "content": "@import '../../utils/control-density.css';\n@import '../../utils/chrome-layout.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n@import '../../utils/scrollbar-hidden.css';\n@import '../../utils/nav-disclosure.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(\n var(--dimension-space-075, 6px) * 2 + var(--dimension-iconography-md, 20px)\n );\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)\n var(--effect-animation-easing-ease-in-out, ease);\n --_nav-label-fade-out-dur: calc(\n var(--_nav-speed) * var(--effect-animation-duration-medium-1, 300ms)\n );\n --_nav-label-fade-out-motion: var(--_nav-label-fade-out-dur)\n 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-shell-app:not([gradient-preset='none'])) .panel-nav,\n:host-context(ds-shell-app.shell-app--paper-texture) .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-ui, 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 height: var(--dimension-size-600, 48px);\n flex-shrink: 0;\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(--ds-control-radius, var(--dimension-radius-025, 2px));\n cursor: default;\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@media (hover: hover) and (pointer: fine) {\n .panel-nav:not(.panel-nav--breakpoint-locked)\n .panel-nav__header-btn:hover\n .panel-nav__header-logo {\n opacity: 0;\n }\n\n .panel-nav:not(.panel-nav--breakpoint-locked)\n .panel-nav__header-btn:hover\n .panel-nav__header-toggle {\n opacity: 1;\n }\n}\n/* ── Scrollable body ─────────────────────────────────────────────────────── */\n\n.panel-nav__body {\n flex: 1;\n overflow-y: auto;\n overflow-x: hidden;\n}\n\n/* The collapsed rail's outer width includes its 1px divider. Return that pixel\n to the content box so body items match the other 32px rail controls. */\n.panel-nav--collapsed .panel-nav__body {\n padding-inline-end: calc(var(--_nav-padding-x) - var(--dimension-stroke-width-012));\n}\n\n.panel-nav__sections {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-200, 16px);\n width: 100%;\n}\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.panel-nav__branch {\n display: flex;\n flex-direction: column;\n}\n\n.panel-nav__branch-divider-line {\n display: block;\n height: var(--dimension-stroke-width-012);\n margin-inline: var(--dimension-space-075);\n background: var(--_nav-divider);\n}\n\n.panel-nav__branch-divider--before .panel-nav__branch-divider-line {\n margin-block: var(--dimension-space-050) var(--dimension-space-100);\n}\n\n.panel-nav__branch-divider--after .panel-nav__branch-divider-line {\n margin-block: var(--dimension-space-100) var(--dimension-space-050);\n}\n\n.panel-nav__children-accordion,\n.panel-nav__branch-divider {\n --ds-nav-disclosure-duration: var(\n --_panel-nav-nested-section-duration,\n calc(var(--effect-animation-duration-short-3) * 2 + var(--effect-animation-duration-short-2))\n );\n --ds-nav-disclosure-wait: var(\n --_panel-nav-nested-transition-wait,\n calc(var(--effect-animation-duration-short-3) + var(--effect-animation-duration-short-1))\n );\n --ds-nav-disclosure-fade: var(\n --_panel-nav-nested-child-fade-duration,\n var(--ds-nav-disclosure-duration)\n );\n --ds-nav-disclosure-stagger: var(\n --_panel-nav-nested-child-stagger,\n var(--effect-animation-duration-short-1)\n );\n}\n\n.panel-nav__children {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050, 4px);\n min-height: 0;\n overflow: hidden;\n}\n\n/* Margin lives on the first child so closed 0fr accordions still collapse to 0. */\n.panel-nav__children > .panel-nav__child:first-child {\n margin-block-start: 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(--ds-control-label-inset));\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(--ds-control-radius, var(--dimension-radius-025, 2px));\n cursor: default;\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.panel-nav__parent--muted {\n color: var(--_nav-fg-secondary);\n}\n\n.panel-nav__parent--flyout-active {\n --ds-interaction-hover: var(--_nav-pressed);\n}\n\n.panel-nav__parent--flyout-active::after {\n background: var(--_nav-pressed);\n}\n\n.panel-nav__child {\n flex: 0 0 var(--_nav-item-height);\n padding-inline-start: calc(\n var(--dimension-space-075, 6px) + var(--dimension-iconography-md) + var(--dimension-space-050)\n );\n}\n\n.panel-nav__children-accordion:not(.panel-nav__children-accordion--open) .panel-nav__child {\n color: var(--_nav-fg-secondary);\n}\n\n.panel-nav__child-dot {\n position: relative;\n z-index: 2;\n display: flex;\n align-items: center;\n justify-content: center;\n flex: 0 0 var(--dimension-iconography-md);\n width: var(--dimension-iconography-md);\n height: var(--dimension-iconography-md);\n}\n\n.panel-nav__child--inactive {\n color: var(--_nav-fg-tertiary);\n}\n\n.panel-nav__child--inactive::after {\n display: none;\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 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(--ds-control-label-inset);\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: 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.panel-nav--animating .panel-nav__children-accordion {\n transition-duration: var(--_nav-expand-dur);\n transition-delay: var(--effect-animation-duration-instant);\n}\n\n.panel-nav--animating .panel-nav__branch-divider {\n transition-duration: var(--_nav-expand-dur), var(--_nav-expand-dur);\n transition-delay:\n var(--effect-animation-duration-instant), var(--effect-animation-duration-instant);\n}\n\n.panel-nav--collapsed:not(.panel-nav--animating) .panel-nav__children {\n display: none;\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(--ds-control-label-inset));\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(\n var(--_nav-padding-y) * 2 + var(--_nav-item-height) * 2 + var(--dimension-space-050, 4px)\n );\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 /* Expanded row: balanced shell inset with an 8px gear-to-user gap. */\n left: calc(var(--_nav-padding-x) + var(--_nav-footer-btn-w) + var(--dimension-space-100, 8px));\n right: var(--_nav-padding-x);\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}\n\n/* Popup-open state keeps the native footer trigger visibly pressed. */\n.panel-nav__footer-user--menu-expanded::after {\n background: var(--_nav-pressed);\n}\n\n.panel-nav__footer-user--menu-expanded {\n --ds-interaction-hover: var(--_nav-pressed);\n}\n\n.panel-nav--collapsed .panel-nav__footer-user {\n /* Resolve one shared square control while keeping both inline edges animatable. */\n left: var(--_nav-padding-x);\n right: calc(100% - var(--_nav-padding-x) - var(--_nav-item-height));\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(\n var(--dimension-space-075, 6px) + var(--dimension-iconography-md, 20px) +\n var(--dimension-space-050, 4px)\n );\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: inherit;\n pointer-events: none;\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",
711
+ "content": "@import '../../utils/control-density.css';\n@import '../../utils/chrome-layout.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n@import '../../utils/scrollbar-hidden.css';\n@import '../../utils/nav-disclosure.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(\n var(--dimension-space-075, 6px) * 2 + var(--dimension-iconography-md, 20px)\n );\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)\n var(--effect-animation-easing-ease-in-out, ease);\n --_nav-label-fade-out-dur: calc(\n var(--_nav-speed) * var(--effect-animation-duration-medium-1, 300ms)\n );\n --_nav-label-fade-out-motion: var(--_nav-label-fade-out-dur)\n 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-shell-app:not([gradient-preset='none'])) .panel-nav,\n:host-context(ds-shell-app.shell-app--paper-texture) .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-ui, 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 height: var(--dimension-size-600, 48px);\n flex-shrink: 0;\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(--ds-control-radius, var(--dimension-radius-025, 2px));\n cursor: default;\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@media (hover: hover) and (pointer: fine) {\n .panel-nav:not(.panel-nav--breakpoint-locked)\n .panel-nav__header-btn:hover\n .panel-nav__header-logo {\n opacity: 0;\n }\n\n .panel-nav:not(.panel-nav--breakpoint-locked)\n .panel-nav__header-btn:hover\n .panel-nav__header-toggle {\n opacity: 1;\n }\n}\n/* ── Scrollable body ─────────────────────────────────────────────────────── */\n\n.panel-nav__body {\n flex: 1;\n overflow-y: auto;\n overflow-x: hidden;\n}\n\n/* The collapsed rail's outer width includes its 1px divider. Return that pixel\n to the content box so body items match the other 32px rail controls. */\n.panel-nav--collapsed .panel-nav__body {\n padding-inline-end: calc(var(--_nav-padding-x) - var(--dimension-stroke-width-012));\n}\n\n.panel-nav__sections {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-200, 16px);\n width: 100%;\n}\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.panel-nav__branch {\n display: flex;\n flex-direction: column;\n}\n\n.panel-nav__branch-divider-line {\n display: block;\n height: var(--dimension-stroke-width-012);\n margin-inline: var(--dimension-space-075);\n background: var(--_nav-divider);\n}\n\n.panel-nav__branch-divider--before .panel-nav__branch-divider-line {\n margin-block: var(--dimension-space-050) var(--dimension-space-100);\n}\n\n.panel-nav__branch-divider--after .panel-nav__branch-divider-line {\n margin-block: var(--dimension-space-100) var(--dimension-space-050);\n}\n\n.panel-nav__children-accordion,\n.panel-nav__branch-divider {\n --ds-nav-disclosure-duration: var(\n --_panel-nav-nested-section-duration,\n calc(var(--effect-animation-duration-short-3) * 2 + var(--effect-animation-duration-short-2))\n );\n --ds-nav-disclosure-wait: var(\n --_panel-nav-nested-transition-wait,\n calc(var(--effect-animation-duration-short-3) + var(--effect-animation-duration-short-1))\n );\n --ds-nav-disclosure-fade: var(\n --_panel-nav-nested-child-fade-duration,\n var(--ds-nav-disclosure-duration)\n );\n --ds-nav-disclosure-stagger: var(\n --_panel-nav-nested-child-stagger,\n var(--effect-animation-duration-short-1)\n );\n}\n\n.panel-nav__children {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050, 4px);\n min-height: 0;\n overflow: hidden;\n}\n\n/* Margin lives on the first child so closed 0fr accordions still collapse to 0. */\n.panel-nav__children > .panel-nav__child:first-child {\n margin-block-start: 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(--ds-control-label-inset));\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(--ds-control-radius, var(--dimension-radius-025, 2px));\n cursor: default;\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.panel-nav__parent--muted {\n color: var(--_nav-fg-secondary);\n}\n\n.panel-nav__child {\n flex: 0 0 var(--_nav-item-height);\n padding-inline-start: calc(\n var(--dimension-space-075, 6px) + var(--dimension-iconography-md) + var(--dimension-space-050)\n );\n}\n\n.panel-nav__children-accordion:not(.panel-nav__children-accordion--open) .panel-nav__child {\n color: var(--_nav-fg-secondary);\n}\n\n.panel-nav__child-dot {\n position: relative;\n z-index: 2;\n display: flex;\n align-items: center;\n justify-content: center;\n flex: 0 0 var(--dimension-iconography-md);\n width: var(--dimension-iconography-md);\n height: var(--dimension-iconography-md);\n}\n\n.panel-nav__child--inactive {\n color: var(--_nav-fg-tertiary);\n}\n\n.panel-nav__child--inactive::after {\n display: none;\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 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(--ds-control-label-inset);\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: 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.panel-nav--animating .panel-nav__children-accordion {\n transition-duration: var(--_nav-expand-dur);\n transition-delay: var(--effect-animation-duration-instant);\n}\n\n.panel-nav--animating .panel-nav__branch-divider {\n transition-duration: var(--_nav-expand-dur), var(--_nav-expand-dur);\n transition-delay:\n var(--effect-animation-duration-instant), var(--effect-animation-duration-instant);\n}\n\n.panel-nav--collapsed:not(.panel-nav--animating) .panel-nav__children {\n display: none;\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(--ds-control-label-inset));\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(\n var(--_nav-padding-y) * 2 + var(--_nav-item-height) * 2 + var(--dimension-space-050, 4px)\n );\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 /* Expanded row: balanced shell inset with an 8px gear-to-user gap. */\n left: calc(var(--_nav-padding-x) + var(--_nav-footer-btn-w) + var(--dimension-space-100, 8px));\n right: var(--_nav-padding-x);\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}\n\n.panel-nav--collapsed .panel-nav__footer-user {\n /* Resolve one shared square control while keeping both inline edges animatable. */\n left: var(--_nav-padding-x);\n right: calc(100% - var(--_nav-padding-x) - var(--_nav-item-height));\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(\n var(--dimension-space-075, 6px) + var(--dimension-iconography-md, 20px) +\n var(--dimension-space-050, 4px)\n );\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: inherit;\n pointer-events: none;\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",
691
712
  "type": "registry:ui"
692
713
  },
693
714
  {
694
715
  "path": "src/wc/components/PanelNav/PanelNav.tsx",
695
- "content": "import {\n Component,\n Prop,\n Event,\n EventEmitter,\n Watch,\n State,\n Element,\n Method,\n h,\n Host,\n} from '@stencil/core';\nimport type { ChromeTransitionDetail } from '../../shell/chrome-transition';\nimport type { NavChromeStyle } from '../../shell/nav-chrome';\nimport {\n derivePanelNavSelectionFromUrl,\n firstEnabledPanelNavChild,\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 PanelNavChildItem,\n type PanelNavChildSelectDetail,\n type PanelNavGroup,\n type PanelNavItem,\n type PanelNavPresentation,\n type PanelNavRouterMode,\n type PanelNavUserActionDetail,\n} from './panel-nav-types';\nimport { PANEL_NAV_CHILD_MENU_PLACEMENT } from '../Menu/menu-placement';\nimport type { MenuItemData } from '../Menu/menu-types';\nimport { scrollEdgeFadeClassMap } from '../../utils/scroll-edge-fade';\nimport { prefersReducedMotion } from '../../utils/resolve-css-time-ms';\nimport { resolveSafeUrl } from '../../utils/safe-url';\n\ntype PanelNavRovingEntry =\n | { kind: 'parent'; item: PanelNavItem }\n | { kind: 'child'; parent: PanelNavItem; child: PanelNavChildItem };\n\nlet nextPanelNavInstanceId = 0;\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 private readonly instanceId = nextPanelNavInstanceId++;\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 /** Flat primary destinations, or nested child-route disclosure. */\n @Prop({ reflect: true }) presentation: PanelNavPresentation = 'flat';\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 /** ID of the active child route when URL matching is unavailable. */\n @Prop() activeChildId: 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 /** Keep the account-menu trigger visually pressed while its popup is open or closing. */\n @Prop() accountMenuExpanded: boolean = false;\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 a nested child route is selected. */\n @Event() dsNavChildSelect!: EventEmitter<PanelNavChildSelectDetail>;\n\n /** Emitted when the collapse toggle is clicked. Detail = new collapsed state. */\n @Event() dsNavToggle!: EventEmitter<boolean>;\n\n /** Bubbling lifecycle — `ds-shell-app` 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 urlDerivedActiveChildId: string = '';\n @State() private bodyScrollable = false;\n @State() private expandedParentId: string = '';\n @State() private inlineChildrenExpansionReady = true;\n @State() private flyoutParentId: string = '';\n @State() private flyoutOpen = false;\n @State() private flyoutInitialFocusVisible = 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 private initialRenderComplete = false;\n private inlineChildrenExpansionFrame: number | null = null;\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 effectiveActiveChildId(): string {\n return this.urlDerivedActiveChildId || this.activeChildId;\n }\n\n private get collapsedPresentation(): boolean {\n return this.collapsed || this.viewportNarrow;\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 || !this.initialRenderComplete) return;\n this.stageInlineChildrenExpansion(\n prev && !next && !this.viewportNarrow && this.presentation === 'nested'\n );\n this.flyoutParentId = '';\n this.flyoutOpen = false;\n this.syncExpandedParent();\n this.rovingIndex = this.getFirstRovingIndex();\n this.startCollapseAnimation();\n }\n\n @Watch('viewportNarrow')\n onViewportNarrowChange(next: boolean, prev: boolean | undefined) {\n if (prev === undefined || !this.initialRenderComplete) return;\n this.stageInlineChildrenExpansion(\n prev && !next && !this.collapsed && this.presentation === 'nested'\n );\n this.flyoutParentId = '';\n this.flyoutOpen = false;\n if (next || this.rovingIndex > this.getVisibleRovingEntries().length) {\n this.rovingIndex = this.getFirstRovingIndex();\n }\n this.startCollapseAnimation();\n }\n\n @Watch('breakpoint')\n onBreakpointChange() {\n this.disconnectResizeObserver();\n if (this.effectiveBreakpoint() > 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 this.syncExpandedParent();\n }\n\n @Watch('presentation')\n onPresentationChange() {\n this.stageInlineChildrenExpansion(false);\n this.flyoutParentId = '';\n this.flyoutOpen = false;\n this.syncExpandedParent();\n this.rovingIndex = this.getFirstRovingIndex();\n }\n\n @Watch('activeId')\n @Watch('activeChildId')\n @Watch('urlDerivedActiveId')\n @Watch('urlDerivedActiveChildId')\n onActiveIdChange() {\n this.syncExpandedParent();\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 this.viewportNarrow = this.isViewportNarrow();\n if (this.storageKey) {\n try {\n const stored = localStorage.getItem(this.storageKey);\n if (stored !== null) this.collapsed = stored === 'true';\n } catch {\n /* localStorage unavailable */\n }\n }\n this.onGroupsChange(this.groups);\n this.syncActiveFromUrl();\n }\n\n componentDidLoad() {\n this.syncHostPropsIfNeeded();\n this.scheduleDeferredHostPropSync();\n if (this.effectiveBreakpoint() > 0) this.connectResizeObserver();\n this.connectScrollRegionObserver();\n this.updateBodyScrollable();\n this.initialRenderComplete = true;\n }\n\n componentDidRender() {\n this.updateBodyScrollable();\n this.scheduleInlineChildrenExpansion();\n }\n\n disconnectedCallback() {\n this.disconnectResizeObserver();\n this.disconnectScrollRegionObserver();\n this.clearInlineChildrenExpansionFrame();\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 this.clearCollapseAnimationCompletion(panel);\n this.transitionCompletionHandler = (e: TransitionEvent) => {\n if (e.target === panel && (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 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 stageInlineChildrenExpansion(stage: boolean) {\n this.clearInlineChildrenExpansionFrame();\n this.inlineChildrenExpansionReady = !stage;\n }\n\n private scheduleInlineChildrenExpansion() {\n if (this.inlineChildrenExpansionReady || this.collapsedPresentation) return;\n this.clearInlineChildrenExpansionFrame();\n this.inlineChildrenExpansionFrame = requestAnimationFrame(() => {\n this.inlineChildrenExpansionFrame = null;\n if (this.collapsedPresentation) return;\n\n // Commit the newly mounted accordion's closed geometry before opening it.\n // Without this read, the browser can insert it directly at 1fr and skip motion.\n this.el.querySelector<HTMLElement>('.panel-nav__children-accordion')?.getBoundingClientRect();\n this.inlineChildrenExpansionReady = true;\n });\n }\n\n private clearInlineChildrenExpansionFrame() {\n if (this.inlineChildrenExpansionFrame === null) return;\n cancelAnimationFrame(this.inlineChildrenExpansionFrame);\n this.inlineChildrenExpansionFrame = null;\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 effectiveBreakpoint(): number {\n if (this.breakpoint > 0) return this.breakpoint;\n const attributeValue = Number(this.el.getAttribute('breakpoint') ?? 0);\n return Number.isFinite(attributeValue) && attributeValue > 0 ? attributeValue : 0;\n }\n\n private isViewportNarrow(): boolean {\n const breakpoint = this.effectiveBreakpoint();\n return typeof window !== 'undefined' && breakpoint > 0 && window.innerWidth < breakpoint;\n }\n\n private connectResizeObserver() {\n this.viewportNarrow = this.isViewportNarrow();\n this.resizeObserver = new ResizeObserver(() => {\n this.viewportNarrow = this.isViewportNarrow();\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 this.urlDerivedActiveChildId = '';\n return;\n }\n const selection = derivePanelNavSelectionFromUrl(this.currentUrl, this.getAllItems());\n this.urlDerivedActiveId = selection.parentId;\n this.urlDerivedActiveChildId = selection.childId;\n }\n\n private syncExpandedParent() {\n const active = this.getAllItems().find(item => item.id === this.effectiveActiveId);\n this.expandedParentId =\n this.presentation === 'nested' && active?.children?.length ? active.id : '';\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 {\n localStorage.setItem(this.storageKey, String(next));\n } catch {\n /* unavailable */\n }\n }\n this.dsNavToggle.emit(next);\n }\n\n private getAllItems(): PanelNavItem[] {\n return this.parsedGroups.flatMap(g => g.items);\n }\n\n private getVisibleRovingEntries(): PanelNavRovingEntry[] {\n return this.getAllItems().flatMap<PanelNavRovingEntry>(item => {\n const parent: PanelNavRovingEntry = { kind: 'parent', item };\n if (\n this.presentation !== 'nested' ||\n this.collapsedPresentation ||\n item.id !== this.expandedParentId ||\n !item.children?.length\n ) {\n return [parent];\n }\n return [\n parent,\n ...item.children\n .filter(child => !child.isInactive)\n .map<PanelNavRovingEntry>(child => ({ kind: 'child', parent: item, child })),\n ];\n });\n }\n\n private getFirstRovingIndex(): number {\n return this.viewportNarrow ? 1 : 0;\n }\n\n private getFooterRovingIndex(): number {\n return 1 + this.getVisibleRovingEntries().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.getVisibleRovingEntries().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>(\n '.panel-nav__body [data-panel-nav-roving]'\n );\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, keyboard = false) {\n const entries = this.getVisibleRovingEntries();\n const itemCount = entries.length;\n if (index === 0) {\n this.handleToggle();\n return;\n }\n if (index >= 1 && index <= itemCount) {\n const entry = entries[index - 1];\n if (!entry) return;\n if (entry.kind === 'child') {\n this.emitChildSelect(entry.parent, entry.child);\n } else if (entry.item.children?.length) {\n this.activateParent(entry.item, keyboard);\n } else {\n this.handleItemClick(entry.item.id);\n }\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(\n `#${PANEL_NAV_USER_MENU_ANCHOR_ID}`\n ) as HTMLElement | null;\n if (anchor)\n 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, true);\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 emitChildSelect(parent: PanelNavItem, child: PanelNavChildItem) {\n if (child.isInactive) return;\n this.dsNavChildSelect.emit({\n parentId: parent.id,\n childId: child.id,\n href: child.href,\n });\n }\n\n private followFlyoutAnchor(child: PanelNavChildItem) {\n const href = resolveSafeUrl(child.href);\n if (this.routerMode !== 'anchor' || !href) return;\n const anchor = this.el.ownerDocument.createElement('a');\n anchor.href = href;\n anchor.hidden = true;\n this.el.append(anchor);\n anchor.click();\n anchor.remove();\n }\n\n private activateParent(parent: PanelNavItem, keyboard = false) {\n if (this.presentation === 'nested') {\n this.expandedParentId = parent.id;\n if (this.collapsedPresentation) {\n if (this.flyoutParentId === parent.id && this.flyoutOpen) {\n this.closeFlyout();\n return;\n }\n this.flyoutInitialFocusVisible = keyboard;\n this.flyoutParentId = parent.id;\n this.flyoutOpen = true;\n return;\n }\n }\n\n const child = firstEnabledPanelNavChild(parent.children);\n if (child) this.emitChildSelect(parent, child);\n }\n\n private parentAnchorId(parentId: string): string {\n return `panel-nav-parent-${this.instanceId}-${parentId.replace(/[^a-zA-Z0-9_-]/g, '-')}`;\n }\n\n private childListId(parentId: string): string {\n return `panel-nav-children-${this.instanceId}-${parentId.replace(/[^a-zA-Z0-9_-]/g, '-')}`;\n }\n\n private closeFlyout = () => {\n this.flyoutOpen = false;\n this.flyoutInitialFocusVisible = false;\n };\n\n private handleFlyoutAfterClose = () => {\n if (!this.flyoutOpen) this.flyoutParentId = '';\n };\n\n private handleFlyoutSelect = (event: CustomEvent<MenuItemData>) => {\n const parent = this.getAllItems().find(item => item.id === this.flyoutParentId);\n const child = parent?.children?.find(item => item.id === event.detail?.value);\n if (!parent || !child) return;\n this.emitChildSelect(parent, child);\n this.followFlyoutAnchor(child);\n this.closeFlyout();\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\n 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={() => {\n this.rovingIndex = this.getFooterRovingIndex();\n }}\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={{\n 'panel-nav__item': true,\n 'panel-nav__footer-user': true,\n 'panel-nav__footer-user--menu-expanded': this.accountMenuExpanded,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n }}\n tabIndex={this.rovingIndex === this.getUserRovingIndex() ? 0 : -1}\n aria-label={this.accountLabel}\n aria-haspopup=\"menu\"\n aria-expanded={String(this.accountMenuExpanded)}\n onClick={e => this.handleUserAction(e)}\n onKeyDown={(e: KeyboardEvent) => this.handleRovingKeyDown(e, this.getUserRovingIndex())}\n onFocus={() => {\n this.rovingIndex = this.getUserRovingIndex();\n }}\n >\n <ds-text\n class=\"panel-nav__item-label panel-nav__footer-user-label panel-nav__item-label-text ds-control--md\"\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis\n color=\"inherit\"\n lineTruncation={1}\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 renderParentItem(item: PanelNavItem, rovingPosition: number, collapsed: boolean) {\n const isActive = item.id === this.effectiveActiveId;\n const hasChildRoutes = (item.children?.length ?? 0) > 0;\n const disclosure = this.presentation === 'nested' && hasChildRoutes;\n const expanded = disclosure\n ? collapsed\n ? this.flyoutParentId === item.id\n : this.expandedParentId === item.id\n : false;\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 ds-control--md\"\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis={isActive}\n color=\"inherit\"\n lineTruncation={1}\n >\n {item.label}\n </ds-text>,\n item.dot && (\n <span class=\"panel-nav__item-dot-box\" aria-hidden=\"true\">\n <ds-badge class=\"panel-nav__item-dot\" variant=\"dot\" hasRing={collapsed} label=\"\" />\n </span>\n ),\n ];\n\n const sharedProps = {\n id: disclosure ? this.parentAnchorId(item.id) : undefined,\n class: {\n 'panel-nav__item': true,\n 'panel-nav__parent': disclosure,\n 'panel-nav__parent--expanded': expanded,\n 'panel-nav__parent--flyout-active': collapsed && this.flyoutParentId === item.id,\n 'panel-nav__parent--muted':\n this.presentation === 'nested' &&\n !collapsed &&\n this.expandedParentId !== '' &&\n this.expandedParentId !== item.id,\n 'panel-nav__item--active': isActive,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n },\n 'aria-current': !disclosure && isActive ? ('page' as const) : undefined,\n 'aria-expanded': disclosure ? String(expanded) : undefined,\n 'aria-controls': disclosure && expanded ? this.childListId(item.id) : undefined,\n 'aria-haspopup': disclosure && collapsed ? ('menu' as const) : undefined,\n 'data-panel-nav-roving': '',\n tabIndex: rovingPosition === this.rovingIndex ? 0 : -1,\n onClick: (event: MouseEvent) =>\n hasChildRoutes\n ? this.activateParent(item, event.detail === 0)\n : this.handleItemClick(item.id),\n onKeyDown: (e: KeyboardEvent) => this.handleRovingKeyDown(e, rovingPosition),\n onFocus: () => {\n this.rovingIndex = rovingPosition;\n },\n };\n\n const href = resolveSafeUrl(\n hasChildRoutes ? firstEnabledPanelNavChild(item.children)?.href : item.href\n );\n const useAnchor = this.routerMode === 'anchor' && href && !(disclosure && collapsed);\n const control = useAnchor ? (\n <a {...sharedProps} href={href}>\n {itemContent}\n </a>\n ) : (\n <button {...sharedProps} type=\"button\">\n {itemContent}\n </button>\n );\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 private renderChildItem(\n parent: PanelNavItem,\n child: PanelNavChildItem,\n rovingPosition: number | undefined,\n childIndex: number,\n childReverseIndex: number\n ) {\n const isActive =\n parent.id === this.effectiveActiveId && child.id === this.effectiveActiveChildId;\n const inactive = child.isInactive === true;\n const content = [\n <ds-text\n class=\"panel-nav__item-label panel-nav__item-label-text ds-control--md\"\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis={isActive}\n color=\"inherit\"\n lineTruncation={1}\n >\n {child.label}\n </ds-text>,\n child.dot && (\n <span class=\"panel-nav__child-dot\" aria-hidden=\"true\">\n <ds-badge variant=\"dot\" label=\"\" />\n </span>\n ),\n ];\n const sharedProps = {\n class: {\n 'panel-nav__item': true,\n 'panel-nav__child': true,\n 'ds-nav-disclosure__item': true,\n 'panel-nav__child--active': isActive,\n 'panel-nav__child--inactive': inactive,\n 'panel-nav__item--active': isActive,\n 'ds-focus-ring-inset': !inactive,\n 'ds-interaction-fill': !inactive,\n },\n 'aria-current': isActive ? ('page' as const) : undefined,\n 'aria-disabled': inactive ? 'true' : undefined,\n 'data-panel-nav-roving': inactive || rovingPosition === undefined ? undefined : '',\n style: {\n '--_panel-nav-child-index': String(childIndex),\n '--_panel-nav-child-reverse-index': String(childReverseIndex),\n '--ds-nav-disclosure-index': String(childIndex),\n '--ds-nav-disclosure-reverse-index': String(childReverseIndex),\n },\n tabIndex: !inactive && rovingPosition === this.rovingIndex ? 0 : -1,\n onClick: () => this.emitChildSelect(parent, child),\n onKeyDown: (event: KeyboardEvent) =>\n rovingPosition === undefined ? undefined : this.handleRovingKeyDown(event, rovingPosition),\n onFocus: () => {\n if (rovingPosition !== undefined) this.rovingIndex = rovingPosition;\n },\n };\n const href = resolveSafeUrl(child.href);\n const useAnchor = !inactive && this.routerMode === 'anchor' && href;\n\n return useAnchor ? (\n <a {...sharedProps} href={href}>\n {content}\n </a>\n ) : (\n <button {...sharedProps} type=\"button\" disabled={inactive}>\n {content}\n </button>\n );\n }\n\n private renderFlyout() {\n const parent = this.getAllItems().find(item => item.id === this.flyoutParentId);\n if (!parent?.children?.length) return null;\n return (\n <ds-menu\n id={this.childListId(parent.id)}\n open={this.flyoutOpen}\n side={PANEL_NAV_CHILD_MENU_PLACEMENT.side}\n align={PANEL_NAV_CHILD_MENU_PLACEMENT.align}\n anchorAlignment={PANEL_NAV_CHILD_MENU_PLACEMENT.anchorAlignment}\n sideOffset={PANEL_NAV_CHILD_MENU_PLACEMENT.sideOffset}\n alignOffset={PANEL_NAV_CHILD_MENU_PLACEMENT.alignOffset}\n anchorId={this.parentAnchorId(parent.id)}\n selectionMode=\"single\"\n menuLabel={`${parent.label} sections`}\n initialFocusVisible={this.flyoutInitialFocusVisible}\n sections={[\n {\n items: parent.children.map(child => ({\n label: child.label,\n value: child.id,\n dot: child.dot,\n isInactive: child.isInactive,\n isSelected:\n parent.id === this.effectiveActiveId && child.id === this.effectiveActiveChildId,\n })),\n },\n ]}\n onDsSelect={this.handleFlyoutSelect}\n onDsClose={this.closeFlyout}\n onDsAfterClose={this.handleFlyoutAfterClose}\n />\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 'ds-control--md': 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={\n isDashboardChrome ? this.dashboardNavigationLabel : this.settingsNavigationLabel\n }\n >\n {/* ── Header: Motive logo, reveals collapse toggle on hover ── */}\n <div class=\"panel-nav__header ds-chrome-row ds-chrome-space--md\">\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={() => {\n this.rovingIndex = 0;\n }}\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\n class=\"panel-nav__m-mark\"\n viewBox=\"0 0 20 20\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <path\n 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\"\n fill=\"currentColor\"\n />\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-chrome-column': true,\n 'ds-chrome-space--md': true,\n 'ds-scrollbar-hidden': 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 <div class=\"panel-nav__sections\">\n {(() => {\n let rovingPosition = 1;\n return this.parsedGroups.map(group => (\n <div class=\"panel-nav__group\" key={group.id ?? group.label}>\n {group.label && (\n <ds-text\n class=\"panel-nav__group-label ds-control--md\"\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, itemIndex) => {\n const parentPosition = rovingPosition++;\n const expanded =\n !collapsed &&\n this.inlineChildrenExpansionReady &&\n item.id === this.expandedParentId &&\n (item.children?.length ?? 0) > 0;\n const hasGroupSiblings = group.items.length > 1;\n const hasDividerBefore = hasGroupSiblings && itemIndex > 0;\n const hasDividerAfter =\n hasGroupSiblings && itemIndex < group.items.length - 1;\n const showBranchDividers = expanded && hasGroupSiblings;\n const hasInlineChildren =\n this.presentation === 'nested' &&\n (!collapsed || this.isAnimating) &&\n (item.children?.length ?? 0) > 0;\n const children = hasInlineChildren\n ? item.children?.map((child, childIndex) => {\n const childPosition =\n !expanded || child.isInactive ? undefined : rovingPosition++;\n return this.renderChildItem(\n item,\n child,\n childPosition,\n childIndex,\n (item.children?.length ?? 0) - childIndex - 1\n );\n })\n : null;\n return (\n <div class=\"panel-nav__branch\" key={item.id}>\n {hasDividerBefore ? (\n <div\n key={`${item.id}-divider-before`}\n class={{\n 'panel-nav__branch-divider': true,\n 'panel-nav__branch-divider--before': true,\n 'panel-nav__branch-divider--open': showBranchDividers,\n 'ds-nav-disclosure-divider': true,\n 'ds-nav-disclosure-divider--open': showBranchDividers,\n }}\n aria-hidden=\"true\"\n >\n <div class=\"panel-nav__branch-divider-clip ds-nav-disclosure-divider__clip\">\n <span class=\"panel-nav__branch-divider-line\"></span>\n </div>\n </div>\n ) : null}\n {this.renderParentItem(item, parentPosition, collapsed)}\n {hasInlineChildren ? (\n <div\n key={`${item.id}-children`}\n class={{\n 'panel-nav__children-accordion': true,\n 'panel-nav__children-accordion--open': expanded,\n 'ds-nav-disclosure': true,\n 'ds-nav-disclosure--open': expanded,\n }}\n aria-hidden={expanded ? undefined : 'true'}\n inert={expanded ? undefined : true}\n >\n <div\n id={this.childListId(item.id)}\n class=\"panel-nav__children\"\n role=\"group\"\n aria-labelledby={this.parentAnchorId(item.id)}\n >\n {children}\n </div>\n </div>\n ) : null}\n {hasDividerAfter ? (\n <div\n key={`${item.id}-divider-after`}\n class={{\n 'panel-nav__branch-divider': true,\n 'panel-nav__branch-divider--after': true,\n 'panel-nav__branch-divider--open': showBranchDividers,\n 'ds-nav-disclosure-divider': true,\n 'ds-nav-disclosure-divider--open': showBranchDividers,\n }}\n aria-hidden=\"true\"\n >\n <div class=\"panel-nav__branch-divider-clip ds-nav-disclosure-divider__clip\">\n <span class=\"panel-nav__branch-divider-line\"></span>\n </div>\n </div>\n ) : null}\n </div>\n );\n })}\n </div>\n ));\n })()}\n </div>\n </div>\n\n {/* ── Footer ── */}\n <div class=\"panel-nav__footer\">\n {this.renderFooterAction(isDashboardChrome)}\n {this.renderFooterUser(collapsed, userName, userInitial)}\n </div>\n </nav>\n\n {this.renderFlyout()}\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",
716
+ "content": "import {\n Component,\n Prop,\n Event,\n EventEmitter,\n Watch,\n State,\n Element,\n Method,\n h,\n Host,\n} from '@stencil/core';\nimport type { ChromeTransitionDetail } from '../../shell/chrome-transition';\nimport type { NavChromeStyle } from '../../shell/nav-chrome';\nimport {\n derivePanelNavSelectionFromUrl,\n firstEnabledPanelNavChild,\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 PanelNavChildItem,\n type PanelNavChildSelectDetail,\n type PanelNavGroup,\n type PanelNavItem,\n type PanelNavPresentation,\n type PanelNavRouterMode,\n type PanelNavUserActionDetail,\n} from './panel-nav-types';\nimport { PANEL_NAV_CHILD_MENU_PLACEMENT } from '../Menu/menu-placement';\nimport type { MenuItemData } from '../Menu/menu-types';\nimport { scrollEdgeFadeClassMap } from '../../utils/scroll-edge-fade';\nimport { prefersReducedMotion } from '../../utils/resolve-css-time-ms';\nimport { resolveSafeUrl } from '../../utils/safe-url';\n\ntype PanelNavRovingEntry =\n | { kind: 'parent'; item: PanelNavItem }\n | { kind: 'child'; parent: PanelNavItem; child: PanelNavChildItem };\n\nlet nextPanelNavInstanceId = 0;\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 private readonly instanceId = nextPanelNavInstanceId++;\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 /** Flat primary destinations, or nested child-route disclosure. */\n @Prop({ reflect: true }) presentation: PanelNavPresentation = 'flat';\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 /** ID of the active child route when URL matching is unavailable. */\n @Prop() activeChildId: 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 /** Account disclosure state. Expanded-derived styling is deprecated; use accountMenuSurfaceOpen for the rendered lifetime. */\n @Prop() accountMenuExpanded: boolean = false;\n /** External account surface is visible, including exit motion. Omission\n * preserves the deprecated accountMenuExpanded-derived wash. */\n @Prop() accountMenuSurfaceOpen: boolean | undefined;\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 a nested child route is selected. */\n @Event() dsNavChildSelect!: EventEmitter<PanelNavChildSelectDetail>;\n\n /** Emitted when the collapse toggle is clicked. Detail = new collapsed state. */\n @Event() dsNavToggle!: EventEmitter<boolean>;\n\n /** Bubbling lifecycle — `ds-shell-app` 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 urlDerivedActiveChildId: string = '';\n @State() private bodyScrollable = false;\n @State() private expandedParentId: string = '';\n @State() private inlineChildrenExpansionReady = true;\n @State() private flyoutParentId: string = '';\n @State() private flyoutOpen = false;\n @State() private flyoutInitialFocusVisible = 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 private initialRenderComplete = false;\n private inlineChildrenExpansionFrame: number | null = null;\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 effectiveActiveChildId(): string {\n return this.urlDerivedActiveChildId || this.activeChildId;\n }\n\n private get collapsedPresentation(): boolean {\n return this.collapsed || this.viewportNarrow;\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 || !this.initialRenderComplete) return;\n this.stageInlineChildrenExpansion(\n prev && !next && !this.viewportNarrow && this.presentation === 'nested'\n );\n this.flyoutParentId = '';\n this.flyoutOpen = false;\n this.syncExpandedParent();\n this.rovingIndex = this.getFirstRovingIndex();\n this.startCollapseAnimation();\n }\n\n @Watch('viewportNarrow')\n onViewportNarrowChange(next: boolean, prev: boolean | undefined) {\n if (prev === undefined || !this.initialRenderComplete) return;\n this.stageInlineChildrenExpansion(\n prev && !next && !this.collapsed && this.presentation === 'nested'\n );\n this.flyoutParentId = '';\n this.flyoutOpen = false;\n if (next || this.rovingIndex > this.getVisibleRovingEntries().length) {\n this.rovingIndex = this.getFirstRovingIndex();\n }\n this.startCollapseAnimation();\n }\n\n @Watch('breakpoint')\n onBreakpointChange() {\n this.disconnectResizeObserver();\n if (this.effectiveBreakpoint() > 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 this.syncExpandedParent();\n }\n\n @Watch('presentation')\n onPresentationChange() {\n this.stageInlineChildrenExpansion(false);\n this.flyoutParentId = '';\n this.flyoutOpen = false;\n this.syncExpandedParent();\n this.rovingIndex = this.getFirstRovingIndex();\n }\n\n @Watch('activeId')\n @Watch('activeChildId')\n @Watch('urlDerivedActiveId')\n @Watch('urlDerivedActiveChildId')\n onActiveIdChange() {\n this.syncExpandedParent();\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 this.viewportNarrow = this.isViewportNarrow();\n if (this.storageKey) {\n try {\n const stored = localStorage.getItem(this.storageKey);\n if (stored !== null) this.collapsed = stored === 'true';\n } catch {\n /* localStorage unavailable */\n }\n }\n this.onGroupsChange(this.groups);\n this.syncActiveFromUrl();\n }\n\n componentDidLoad() {\n this.syncHostPropsIfNeeded();\n this.scheduleDeferredHostPropSync();\n if (this.effectiveBreakpoint() > 0) this.connectResizeObserver();\n this.connectScrollRegionObserver();\n this.updateBodyScrollable();\n this.initialRenderComplete = true;\n }\n\n componentDidRender() {\n this.updateBodyScrollable();\n this.scheduleInlineChildrenExpansion();\n }\n\n disconnectedCallback() {\n this.disconnectResizeObserver();\n this.disconnectScrollRegionObserver();\n this.clearInlineChildrenExpansionFrame();\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 this.clearCollapseAnimationCompletion(panel);\n this.transitionCompletionHandler = (e: TransitionEvent) => {\n if (e.target === panel && (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 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 stageInlineChildrenExpansion(stage: boolean) {\n this.clearInlineChildrenExpansionFrame();\n this.inlineChildrenExpansionReady = !stage;\n }\n\n private scheduleInlineChildrenExpansion() {\n if (this.inlineChildrenExpansionReady || this.collapsedPresentation) return;\n this.clearInlineChildrenExpansionFrame();\n this.inlineChildrenExpansionFrame = requestAnimationFrame(() => {\n this.inlineChildrenExpansionFrame = null;\n if (this.collapsedPresentation) return;\n\n // Commit the newly mounted accordion's closed geometry before opening it.\n // Without this read, the browser can insert it directly at 1fr and skip motion.\n this.el.querySelector<HTMLElement>('.panel-nav__children-accordion')?.getBoundingClientRect();\n this.inlineChildrenExpansionReady = true;\n });\n }\n\n private clearInlineChildrenExpansionFrame() {\n if (this.inlineChildrenExpansionFrame === null) return;\n cancelAnimationFrame(this.inlineChildrenExpansionFrame);\n this.inlineChildrenExpansionFrame = null;\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 effectiveBreakpoint(): number {\n if (this.breakpoint > 0) return this.breakpoint;\n const attributeValue = Number(this.el.getAttribute('breakpoint') ?? 0);\n return Number.isFinite(attributeValue) && attributeValue > 0 ? attributeValue : 0;\n }\n\n private isViewportNarrow(): boolean {\n const breakpoint = this.effectiveBreakpoint();\n return typeof window !== 'undefined' && breakpoint > 0 && window.innerWidth < breakpoint;\n }\n\n private connectResizeObserver() {\n this.viewportNarrow = this.isViewportNarrow();\n this.resizeObserver = new ResizeObserver(() => {\n this.viewportNarrow = this.isViewportNarrow();\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 this.urlDerivedActiveChildId = '';\n return;\n }\n const selection = derivePanelNavSelectionFromUrl(this.currentUrl, this.getAllItems());\n this.urlDerivedActiveId = selection.parentId;\n this.urlDerivedActiveChildId = selection.childId;\n }\n\n private syncExpandedParent() {\n const active = this.getAllItems().find(item => item.id === this.effectiveActiveId);\n this.expandedParentId =\n this.presentation === 'nested' && active?.children?.length ? active.id : '';\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 {\n localStorage.setItem(this.storageKey, String(next));\n } catch {\n /* unavailable */\n }\n }\n this.dsNavToggle.emit(next);\n }\n\n private getAllItems(): PanelNavItem[] {\n return this.parsedGroups.flatMap(g => g.items);\n }\n\n private getVisibleRovingEntries(): PanelNavRovingEntry[] {\n return this.getAllItems().flatMap<PanelNavRovingEntry>(item => {\n const parent: PanelNavRovingEntry = { kind: 'parent', item };\n if (\n this.presentation !== 'nested' ||\n this.collapsedPresentation ||\n item.id !== this.expandedParentId ||\n !item.children?.length\n ) {\n return [parent];\n }\n return [\n parent,\n ...item.children\n .filter(child => !child.isInactive)\n .map<PanelNavRovingEntry>(child => ({ kind: 'child', parent: item, child })),\n ];\n });\n }\n\n private getFirstRovingIndex(): number {\n return this.viewportNarrow ? 1 : 0;\n }\n\n private getFooterRovingIndex(): number {\n return 1 + this.getVisibleRovingEntries().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.getVisibleRovingEntries().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>(\n '.panel-nav__body [data-panel-nav-roving]'\n );\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, keyboard = false) {\n const entries = this.getVisibleRovingEntries();\n const itemCount = entries.length;\n if (index === 0) {\n this.handleToggle();\n return;\n }\n if (index >= 1 && index <= itemCount) {\n const entry = entries[index - 1];\n if (!entry) return;\n if (entry.kind === 'child') {\n this.emitChildSelect(entry.parent, entry.child);\n } else if (entry.item.children?.length) {\n this.activateParent(entry.item, keyboard);\n } else {\n this.handleItemClick(entry.item.id);\n }\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(\n `#${PANEL_NAV_USER_MENU_ANCHOR_ID}`\n ) as HTMLElement | null;\n if (anchor)\n 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, true);\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 emitChildSelect(parent: PanelNavItem, child: PanelNavChildItem) {\n if (child.isInactive) return;\n this.dsNavChildSelect.emit({\n parentId: parent.id,\n childId: child.id,\n href: child.href,\n });\n }\n\n private followFlyoutAnchor(child: PanelNavChildItem) {\n const href = resolveSafeUrl(child.href);\n if (this.routerMode !== 'anchor' || !href) return;\n const anchor = this.el.ownerDocument.createElement('a');\n anchor.href = href;\n anchor.hidden = true;\n this.el.append(anchor);\n anchor.click();\n anchor.remove();\n }\n\n private activateParent(parent: PanelNavItem, keyboard = false) {\n if (this.presentation === 'nested') {\n this.expandedParentId = parent.id;\n if (this.collapsedPresentation) {\n if (this.flyoutParentId === parent.id && this.flyoutOpen) {\n this.closeFlyout();\n return;\n }\n this.flyoutInitialFocusVisible = keyboard;\n this.flyoutParentId = parent.id;\n this.flyoutOpen = true;\n return;\n }\n }\n\n const child = firstEnabledPanelNavChild(parent.children);\n if (child) this.emitChildSelect(parent, child);\n }\n\n private parentAnchorId(parentId: string): string {\n return `panel-nav-parent-${this.instanceId}-${parentId.replace(/[^a-zA-Z0-9_-]/g, '-')}`;\n }\n\n private childListId(parentId: string): string {\n return `panel-nav-children-${this.instanceId}-${parentId.replace(/[^a-zA-Z0-9_-]/g, '-')}`;\n }\n\n private closeFlyout = () => {\n this.flyoutOpen = false;\n this.flyoutInitialFocusVisible = false;\n };\n\n private handleFlyoutAfterClose = () => {\n if (!this.flyoutOpen) this.flyoutParentId = '';\n };\n\n private handleFlyoutSelect = (event: CustomEvent<MenuItemData>) => {\n const parent = this.getAllItems().find(item => item.id === this.flyoutParentId);\n const child = parent?.children?.find(item => item.id === event.detail?.value);\n if (!parent || !child) return;\n this.emitChildSelect(parent, child);\n this.followFlyoutAnchor(child);\n this.closeFlyout();\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\n 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={() => {\n this.rovingIndex = this.getFooterRovingIndex();\n }}\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={{\n 'panel-nav__item': true,\n 'panel-nav__footer-user': true,\n 'panel-nav__footer-user--menu-expanded': this.accountMenuExpanded,\n 'ds-interaction-fill--surface-open':\n this.accountMenuSurfaceOpen ?? this.accountMenuExpanded,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n }}\n tabIndex={this.rovingIndex === this.getUserRovingIndex() ? 0 : -1}\n aria-label={this.accountLabel}\n aria-haspopup=\"menu\"\n aria-expanded={String(this.accountMenuExpanded)}\n onClick={e => this.handleUserAction(e)}\n onKeyDown={(e: KeyboardEvent) => this.handleRovingKeyDown(e, this.getUserRovingIndex())}\n onFocus={() => {\n this.rovingIndex = this.getUserRovingIndex();\n }}\n >\n <ds-text\n class=\"panel-nav__item-label panel-nav__footer-user-label panel-nav__item-label-text ds-control--md\"\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis\n color=\"inherit\"\n lineTruncation={1}\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 renderParentItem(item: PanelNavItem, rovingPosition: number, collapsed: boolean) {\n const isActive = item.id === this.effectiveActiveId;\n const hasChildRoutes = (item.children?.length ?? 0) > 0;\n const disclosure = this.presentation === 'nested' && hasChildRoutes;\n const expanded = disclosure\n ? collapsed\n ? this.flyoutParentId === item.id\n : this.expandedParentId === item.id\n : false;\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 ds-control--md\"\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis={isActive}\n color=\"inherit\"\n lineTruncation={1}\n >\n {item.label}\n </ds-text>,\n item.dot && (\n <span class=\"panel-nav__item-dot-box\" aria-hidden=\"true\">\n <ds-badge class=\"panel-nav__item-dot\" variant=\"dot\" hasRing={collapsed} label=\"\" />\n </span>\n ),\n ];\n\n const sharedProps = {\n id: disclosure ? this.parentAnchorId(item.id) : undefined,\n class: {\n 'panel-nav__item': true,\n 'panel-nav__parent': disclosure,\n 'panel-nav__parent--expanded': expanded,\n 'panel-nav__parent--flyout-active': collapsed && this.flyoutParentId === item.id,\n 'ds-interaction-fill--surface-open': collapsed && this.flyoutParentId === item.id,\n 'panel-nav__parent--muted':\n this.presentation === 'nested' &&\n !collapsed &&\n this.expandedParentId !== '' &&\n this.expandedParentId !== item.id,\n 'panel-nav__item--active': isActive,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n },\n 'aria-current': !disclosure && isActive ? ('page' as const) : undefined,\n 'aria-expanded': disclosure ? String(expanded) : undefined,\n 'aria-controls': disclosure && expanded ? this.childListId(item.id) : undefined,\n 'aria-haspopup': disclosure && collapsed ? ('menu' as const) : undefined,\n 'data-panel-nav-roving': '',\n tabIndex: rovingPosition === this.rovingIndex ? 0 : -1,\n onClick: (event: MouseEvent) =>\n hasChildRoutes\n ? this.activateParent(item, event.detail === 0)\n : this.handleItemClick(item.id),\n onKeyDown: (e: KeyboardEvent) => this.handleRovingKeyDown(e, rovingPosition),\n onFocus: () => {\n this.rovingIndex = rovingPosition;\n },\n };\n\n const href = resolveSafeUrl(\n hasChildRoutes ? firstEnabledPanelNavChild(item.children)?.href : item.href\n );\n const useAnchor = this.routerMode === 'anchor' && href && !(disclosure && collapsed);\n const control = useAnchor ? (\n <a {...sharedProps} href={href}>\n {itemContent}\n </a>\n ) : (\n <button {...sharedProps} type=\"button\">\n {itemContent}\n </button>\n );\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 private renderChildItem(\n parent: PanelNavItem,\n child: PanelNavChildItem,\n rovingPosition: number | undefined,\n childIndex: number,\n childReverseIndex: number\n ) {\n const isActive =\n parent.id === this.effectiveActiveId && child.id === this.effectiveActiveChildId;\n const inactive = child.isInactive === true;\n const content = [\n <ds-text\n class=\"panel-nav__item-label panel-nav__item-label-text ds-control--md\"\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis={isActive}\n color=\"inherit\"\n lineTruncation={1}\n >\n {child.label}\n </ds-text>,\n child.dot && (\n <span class=\"panel-nav__child-dot\" aria-hidden=\"true\">\n <ds-badge variant=\"dot\" label=\"\" />\n </span>\n ),\n ];\n const sharedProps = {\n class: {\n 'panel-nav__item': true,\n 'panel-nav__child': true,\n 'ds-nav-disclosure__item': true,\n 'panel-nav__child--active': isActive,\n 'panel-nav__child--inactive': inactive,\n 'panel-nav__item--active': isActive,\n 'ds-focus-ring-inset': !inactive,\n 'ds-interaction-fill': !inactive,\n },\n 'aria-current': isActive ? ('page' as const) : undefined,\n 'aria-disabled': inactive ? 'true' : undefined,\n 'data-panel-nav-roving': inactive || rovingPosition === undefined ? undefined : '',\n style: {\n '--_panel-nav-child-index': String(childIndex),\n '--_panel-nav-child-reverse-index': String(childReverseIndex),\n '--ds-nav-disclosure-index': String(childIndex),\n '--ds-nav-disclosure-reverse-index': String(childReverseIndex),\n },\n tabIndex: !inactive && rovingPosition === this.rovingIndex ? 0 : -1,\n onClick: () => this.emitChildSelect(parent, child),\n onKeyDown: (event: KeyboardEvent) =>\n rovingPosition === undefined ? undefined : this.handleRovingKeyDown(event, rovingPosition),\n onFocus: () => {\n if (rovingPosition !== undefined) this.rovingIndex = rovingPosition;\n },\n };\n const href = resolveSafeUrl(child.href);\n const useAnchor = !inactive && this.routerMode === 'anchor' && href;\n\n return useAnchor ? (\n <a {...sharedProps} href={href}>\n {content}\n </a>\n ) : (\n <button {...sharedProps} type=\"button\" disabled={inactive}>\n {content}\n </button>\n );\n }\n\n private renderFlyout() {\n const parent = this.getAllItems().find(item => item.id === this.flyoutParentId);\n if (!parent?.children?.length) return null;\n return (\n <ds-menu\n id={this.childListId(parent.id)}\n open={this.flyoutOpen}\n side={PANEL_NAV_CHILD_MENU_PLACEMENT.side}\n align={PANEL_NAV_CHILD_MENU_PLACEMENT.align}\n anchorAlignment={PANEL_NAV_CHILD_MENU_PLACEMENT.anchorAlignment}\n sideOffset={PANEL_NAV_CHILD_MENU_PLACEMENT.sideOffset}\n alignOffset={PANEL_NAV_CHILD_MENU_PLACEMENT.alignOffset}\n anchorId={this.parentAnchorId(parent.id)}\n selectionMode=\"single\"\n menuLabel={`${parent.label} sections`}\n initialFocusVisible={this.flyoutInitialFocusVisible}\n sections={[\n {\n items: parent.children.map(child => ({\n label: child.label,\n value: child.id,\n dot: child.dot,\n isInactive: child.isInactive,\n isSelected:\n parent.id === this.effectiveActiveId && child.id === this.effectiveActiveChildId,\n })),\n },\n ]}\n onDsSelect={this.handleFlyoutSelect}\n onDsClose={this.closeFlyout}\n onDsAfterClose={this.handleFlyoutAfterClose}\n />\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 'ds-control--md': 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={\n isDashboardChrome ? this.dashboardNavigationLabel : this.settingsNavigationLabel\n }\n >\n {/* ── Header: Motive logo, reveals collapse toggle on hover ── */}\n <div class=\"panel-nav__header ds-chrome-row ds-chrome-space--md\">\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={() => {\n this.rovingIndex = 0;\n }}\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\n class=\"panel-nav__m-mark\"\n viewBox=\"0 0 20 20\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <path\n 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\"\n fill=\"currentColor\"\n />\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-chrome-column': true,\n 'ds-chrome-space--md': true,\n 'ds-scrollbar-hidden': 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 <div class=\"panel-nav__sections\">\n {(() => {\n let rovingPosition = 1;\n return this.parsedGroups.map(group => (\n <div class=\"panel-nav__group\" key={group.id ?? group.label}>\n {group.label && (\n <ds-text\n class=\"panel-nav__group-label ds-control--md\"\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, itemIndex) => {\n const parentPosition = rovingPosition++;\n const expanded =\n !collapsed &&\n this.inlineChildrenExpansionReady &&\n item.id === this.expandedParentId &&\n (item.children?.length ?? 0) > 0;\n const hasGroupSiblings = group.items.length > 1;\n const hasDividerBefore = hasGroupSiblings && itemIndex > 0;\n const hasDividerAfter =\n hasGroupSiblings && itemIndex < group.items.length - 1;\n const showBranchDividers = expanded && hasGroupSiblings;\n const hasInlineChildren =\n this.presentation === 'nested' &&\n (!collapsed || this.isAnimating) &&\n (item.children?.length ?? 0) > 0;\n const children = hasInlineChildren\n ? item.children?.map((child, childIndex) => {\n const childPosition =\n !expanded || child.isInactive ? undefined : rovingPosition++;\n return this.renderChildItem(\n item,\n child,\n childPosition,\n childIndex,\n (item.children?.length ?? 0) - childIndex - 1\n );\n })\n : null;\n return (\n <div class=\"panel-nav__branch\" key={item.id}>\n {hasDividerBefore ? (\n <div\n key={`${item.id}-divider-before`}\n class={{\n 'panel-nav__branch-divider': true,\n 'panel-nav__branch-divider--before': true,\n 'panel-nav__branch-divider--open': showBranchDividers,\n 'ds-nav-disclosure-divider': true,\n 'ds-nav-disclosure-divider--open': showBranchDividers,\n }}\n aria-hidden=\"true\"\n >\n <div class=\"panel-nav__branch-divider-clip ds-nav-disclosure-divider__clip\">\n <span class=\"panel-nav__branch-divider-line\"></span>\n </div>\n </div>\n ) : null}\n {this.renderParentItem(item, parentPosition, collapsed)}\n {hasInlineChildren ? (\n <div\n key={`${item.id}-children`}\n class={{\n 'panel-nav__children-accordion': true,\n 'panel-nav__children-accordion--open': expanded,\n 'ds-nav-disclosure': true,\n 'ds-nav-disclosure--open': expanded,\n }}\n aria-hidden={expanded ? undefined : 'true'}\n inert={expanded ? undefined : true}\n >\n <div\n id={this.childListId(item.id)}\n class=\"panel-nav__children\"\n role=\"group\"\n aria-labelledby={this.parentAnchorId(item.id)}\n >\n {children}\n </div>\n </div>\n ) : null}\n {hasDividerAfter ? (\n <div\n key={`${item.id}-divider-after`}\n class={{\n 'panel-nav__branch-divider': true,\n 'panel-nav__branch-divider--after': true,\n 'panel-nav__branch-divider--open': showBranchDividers,\n 'ds-nav-disclosure-divider': true,\n 'ds-nav-disclosure-divider--open': showBranchDividers,\n }}\n aria-hidden=\"true\"\n >\n <div class=\"panel-nav__branch-divider-clip ds-nav-disclosure-divider__clip\">\n <span class=\"panel-nav__branch-divider-line\"></span>\n </div>\n </div>\n ) : null}\n </div>\n );\n })}\n </div>\n ));\n })()}\n </div>\n </div>\n\n {/* ── Footer ── */}\n <div class=\"panel-nav__footer\">\n {this.renderFooterAction(isDashboardChrome)}\n {this.renderFooterUser(collapsed, userName, userInitial)}\n </div>\n </nav>\n\n {this.renderFlyout()}\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",
696
717
  "type": "registry:ui"
697
718
  },
698
719
  {