@ds-mo/ui 9.2.0 → 9.2.1

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 (149) hide show
  1. package/README.md +0 -79
  2. package/dist/.build-stamp +1 -1
  3. package/dist/agent-patterns.json +1 -1
  4. package/dist/agent.json +17 -1
  5. package/dist/angular/ds-avatar.d.ts +1 -1
  6. package/dist/angular/ds-avatar.js +3 -3
  7. package/dist/angular/ds-conversation-list-item.d.ts +1 -1
  8. package/dist/angular/ds-conversation-list-item.js +3 -3
  9. package/dist/angular/ds-menu.d.ts +6 -1
  10. package/dist/angular/ds-menu.js +5 -2
  11. package/dist/angular/ds-panel-nav.d.ts +1 -1
  12. package/dist/angular/ds-panel-nav.js +3 -3
  13. package/dist/components/ds-agent-activity.js +1 -1
  14. package/dist/components/ds-agent-response.js +1 -1
  15. package/dist/components/ds-agent-response.js.map +1 -1
  16. package/dist/components/ds-agent-source-list.js +1 -1
  17. package/dist/components/ds-agent-tool-call.js +1 -1
  18. package/dist/components/ds-attachment-list.js +1 -1
  19. package/dist/components/ds-avatar.js +1 -1
  20. package/dist/components/ds-avatar.js.map +1 -1
  21. package/dist/components/ds-bar-nav.js +1 -1
  22. package/dist/components/ds-bar-nav.js.map +1 -1
  23. package/dist/components/ds-button-filled.js +1 -1
  24. package/dist/components/ds-button-unfilled.js +1 -1
  25. package/dist/components/ds-card-data-viz-donut.js +1 -1
  26. package/dist/components/ds-card-setting.js +1 -1
  27. package/dist/components/ds-card-shell-data-viz.js +1 -1
  28. package/dist/components/ds-card.js +1 -1
  29. package/dist/components/ds-chart-legend.js +1 -1
  30. package/dist/components/ds-checkbox.js +1 -1
  31. package/dist/components/ds-chip.js +1 -1
  32. package/dist/components/ds-code-block.js +1 -1
  33. package/dist/components/ds-conversation-list-item.js +1 -1
  34. package/dist/components/ds-conversation-list-item.js.map +1 -1
  35. package/dist/components/ds-conversation-list-section.js +1 -1
  36. package/dist/components/ds-conversation-list-section.js.map +1 -1
  37. package/dist/components/ds-conversation-list.js +1 -1
  38. package/dist/components/ds-conversation-list.js.map +1 -1
  39. package/dist/components/ds-divider.js +1 -1
  40. package/dist/components/ds-empty-state.js +1 -1
  41. package/dist/components/ds-icon.js +1 -1
  42. package/dist/components/ds-input.js +1 -1
  43. package/dist/components/ds-markdown.js +1 -1
  44. package/dist/components/ds-menu.js +1 -1
  45. package/dist/components/ds-message-bubble.js +1 -1
  46. package/dist/components/ds-message-composer.js +1 -1
  47. package/dist/components/ds-message-composer.js.map +1 -1
  48. package/dist/components/ds-message-scroller.js +1 -1
  49. package/dist/components/ds-message-scroller.js.map +1 -1
  50. package/dist/components/ds-message.js +1 -1
  51. package/dist/components/ds-panel-nav.js +1 -1
  52. package/dist/components/ds-panel-nav.js.map +1 -1
  53. package/dist/components/ds-panel-tool-header.js +1 -1
  54. package/dist/components/ds-panel-tool-search.js +1 -1
  55. package/dist/components/ds-panel-tools.js +1 -1
  56. package/dist/components/ds-panel-tools.js.map +1 -1
  57. package/dist/components/ds-select-multi.js +1 -1
  58. package/dist/components/ds-select.js +1 -1
  59. package/dist/components/ds-slider.js +1 -1
  60. package/dist/components/ds-slider.js.map +1 -1
  61. package/dist/components/ds-switch.js +1 -1
  62. package/dist/components/ds-tab-group.js +1 -1
  63. package/dist/components/ds-tag.js +1 -1
  64. package/dist/components/ds-tag.js.map +1 -1
  65. package/dist/components/{p-Di-ThR90.js → p-BHpWYKtP.js} +2 -2
  66. package/dist/components/p-BHpWYKtP.js.map +1 -0
  67. package/dist/components/p-BRfOCc_h.js +2 -0
  68. package/dist/components/p-BRfOCc_h.js.map +1 -0
  69. package/dist/components/{p-BEQB8EnB.js → p-CBYuvZKM.js} +2 -2
  70. package/dist/components/{p-BFWC17em.js → p-CJlgONwR.js} +2 -2
  71. package/dist/components/p-CWNdl52Y.js +2 -0
  72. package/dist/components/p-CWNdl52Y.js.map +1 -0
  73. package/dist/components/{p-BZAEZENv.js → p-CblAABEo.js} +2 -2
  74. package/dist/components/{p--KbMQblZ.js → p-CeFBA7Dx.js} +2 -2
  75. package/dist/components/p-CiKGqsA2.js +2 -0
  76. package/dist/components/{p-CnM9SW_8.js.map → p-CiKGqsA2.js.map} +1 -1
  77. package/dist/components/{p-2br9YDbG.js → p-Ci_nVk5P.js} +2 -2
  78. package/dist/components/p-Cijzn3YQ.js +2 -0
  79. package/dist/components/p-Cijzn3YQ.js.map +1 -0
  80. package/dist/components/p-DFxqk71w.js +2 -0
  81. package/dist/components/p-DFxqk71w.js.map +1 -0
  82. package/dist/components/{p-C0sq948W.js → p-DaQ0ZE_V.js} +2 -2
  83. package/dist/components/{p-t9Ei1kHP.js → p-DiooIO2B.js} +2 -2
  84. package/dist/components/{p-DIsDiGcp.js → p-Dj5MejHv.js} +2 -2
  85. package/dist/components/p-Dj5MejHv.js.map +1 -0
  86. package/dist/components/{p-Bo5wikia.js → p-DnMCRAwx.js} +2 -2
  87. package/dist/components/{p-CZMEhdlA.js → p-Dst-OliP.js} +2 -2
  88. package/dist/components/p-Dst-OliP.js.map +1 -0
  89. package/dist/components/p-T0Mzhv3-.js +2 -0
  90. package/dist/components/p-T0Mzhv3-.js.map +1 -0
  91. package/dist/components/{p-Bx1G7P6W.js → p-kezp2_Xu.js} +2 -2
  92. package/dist/components/{p-Di4HBckb.js → p-xB0GPG3-.js} +2 -2
  93. package/dist/docs/components.json +291 -11
  94. package/dist/mcp-data/registry/agent-response.json +1 -1
  95. package/dist/mcp-data/registry/avatar.json +19 -1
  96. package/dist/mcp-data/registry/bar-nav.json +1 -1
  97. package/dist/mcp-data/registry/button-unfilled.json +2 -2
  98. package/dist/mcp-data/registry/code-block.json +1 -1
  99. package/dist/mcp-data/registry/conversation-list-item.json +20 -2
  100. package/dist/mcp-data/registry/conversation-list-section.json +1 -1
  101. package/dist/mcp-data/registry/conversation-list.json +1 -1
  102. package/dist/mcp-data/registry/markdown.json +1 -1
  103. package/dist/mcp-data/registry/menu.json +17 -1
  104. package/dist/mcp-data/registry/message-bubble.json +1 -1
  105. package/dist/mcp-data/registry/message-composer.json +1 -1
  106. package/dist/mcp-data/registry/message-scroller.json +2 -2
  107. package/dist/mcp-data/registry/message.json +2 -2
  108. package/dist/mcp-data/registry/panel-nav.json +20 -2
  109. package/dist/mcp-data/registry/panel-tool-header.json +1 -1
  110. package/dist/mcp-data/registry/panel-tools.json +1 -1
  111. package/dist/mcp-data/registry/registry.json +16 -0
  112. package/dist/mcp-data/registry/slider.json +1 -1
  113. package/dist/mcp-data/registry/switch.json +1 -1
  114. package/dist/mcp-data/registry/tag.json +2 -2
  115. package/dist/react/ds-menu.d.ts +1 -0
  116. package/dist/react/ds-menu.js +1 -0
  117. package/dist/types/components/Avatar/Avatar.d.ts +3 -0
  118. package/dist/types/components/BarNav/BarNav.d.ts +2 -0
  119. package/dist/types/components/ButtonUnfilled/ButtonUnfilled.d.ts +2 -0
  120. package/dist/types/components/ConversationListItem/ConversationListItem.d.ts +2 -0
  121. package/dist/types/components/Menu/Menu.d.ts +6 -0
  122. package/dist/types/components/MessageScroller/MessageScroller.d.ts +4 -0
  123. package/dist/types/components/PanelNav/PanelNav.d.ts +2 -0
  124. package/dist/types/components.d.ts +44 -6
  125. package/package.json +8 -11
  126. package/dist/components/p-41UAx2eq.js +0 -2
  127. package/dist/components/p-41UAx2eq.js.map +0 -1
  128. package/dist/components/p-B7NbIYjS.js +0 -2
  129. package/dist/components/p-B7NbIYjS.js.map +0 -1
  130. package/dist/components/p-CC1C8s6m.js +0 -2
  131. package/dist/components/p-CC1C8s6m.js.map +0 -1
  132. package/dist/components/p-CZMEhdlA.js.map +0 -1
  133. package/dist/components/p-CnM9SW_8.js +0 -2
  134. package/dist/components/p-CuKn9I5f.js +0 -2
  135. package/dist/components/p-CuKn9I5f.js.map +0 -1
  136. package/dist/components/p-DFMM60My.js +0 -2
  137. package/dist/components/p-DFMM60My.js.map +0 -1
  138. package/dist/components/p-DIsDiGcp.js.map +0 -1
  139. package/dist/components/p-Di-ThR90.js.map +0 -1
  140. /package/dist/components/{p-BEQB8EnB.js.map → p-CBYuvZKM.js.map} +0 -0
  141. /package/dist/components/{p-BFWC17em.js.map → p-CJlgONwR.js.map} +0 -0
  142. /package/dist/components/{p-BZAEZENv.js.map → p-CblAABEo.js.map} +0 -0
  143. /package/dist/components/{p--KbMQblZ.js.map → p-CeFBA7Dx.js.map} +0 -0
  144. /package/dist/components/{p-2br9YDbG.js.map → p-Ci_nVk5P.js.map} +0 -0
  145. /package/dist/components/{p-C0sq948W.js.map → p-DaQ0ZE_V.js.map} +0 -0
  146. /package/dist/components/{p-t9Ei1kHP.js.map → p-DiooIO2B.js.map} +0 -0
  147. /package/dist/components/{p-Bo5wikia.js.map → p-DnMCRAwx.js.map} +0 -0
  148. /package/dist/components/{p-Bx1G7P6W.js.map → p-kezp2_Xu.js.map} +0 -0
  149. /package/dist/components/{p-Di4HBckb.js.map → p-xB0GPG3-.js.map} +0 -0
@@ -118,6 +118,15 @@
118
118
  "required": false,
119
119
  "mutable": false
120
120
  },
121
+ "accountMenuExpanded": {
122
+ "type": "boolean",
123
+ "resolvedType": "boolean",
124
+ "attribute": "account-menu-expanded",
125
+ "default": "false",
126
+ "required": false,
127
+ "mutable": false,
128
+ "description": "Keep the account-menu trigger visually active while its popup is open or closing."
129
+ },
121
130
  "activeId": {
122
131
  "type": "string",
123
132
  "resolvedType": "string",
@@ -342,6 +351,15 @@
342
351
  "required": false,
343
352
  "mutable": false
344
353
  },
354
+ "accountMenuExpanded": {
355
+ "type": "boolean",
356
+ "resolvedType": "boolean",
357
+ "attribute": "account-menu-expanded",
358
+ "default": "false",
359
+ "required": false,
360
+ "mutable": false,
361
+ "description": "Keep the account-menu trigger visually active while its popup is open or closing."
362
+ },
345
363
  "activeId": {
346
364
  "type": "string",
347
365
  "resolvedType": "string",
@@ -600,12 +618,12 @@
600
618
  "files": [
601
619
  {
602
620
  "path": "src/wc/components/PanelNav/PanelNav.css",
603
- "content": "@import '../../utils/control-density.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n\n/* ── Theme variables ──────────────────────────────────────────────────────── */\n\n.panel-nav {\n /* ─ Sizes ─ */\n --_nav-width: var(--dimension-panel-width-2xs);\n --_nav-width-collapsed: var(--dimension-size-600);\n --_nav-item-height: var(--dimension-size-400, 32px);\n --_nav-padding-x: var(--dimension-space-100, 8px);\n --_nav-padding-y: var(--dimension-space-100, 8px);\n /* gear button outer width = padding*2 + icon */\n --_nav-footer-btn-w: calc(var(--dimension-space-075, 6px) * 2 + var(--dimension-iconography-md, 20px));\n\n /* ─ Motion ─ */\n /* --ds-panel-nav-speed: unitless multiplier; override in Storybook to slow-mo (e.g. 10 = 10×).\n *-dur vars are duration-only (needed for calc); *-motion vars add easing for use in transitions. */\n --_nav-speed: var(--ds-panel-nav-speed, 1);\n --_nav-expand-dur: calc(var(--_nav-speed) * var(--effect-animation-duration-medium-1, 300ms));\n --_nav-expand-motion: var(--_nav-expand-dur) var(--effect-animation-easing-ease-in-out, ease);\n --_nav-label-fade-dur: calc(var(--_nav-speed) * var(--effect-animation-duration-medium-1, 300ms));\n --_nav-label-fade-motion: var(--_nav-label-fade-dur) var(--effect-animation-easing-ease-in-out, ease);\n --_nav-label-fade-out-dur: calc(var(--_nav-speed) * var(--effect-animation-duration-medium-1, 300ms));\n --_nav-label-fade-out-motion: var(--_nav-label-fade-out-dur) var(--effect-animation-easing-ease-in-out, ease);\n\n /* ─ Colours – default chrome (app surfaces) ─ */\n --_nav-bg: var(--color-background-secondary);\n --_nav-border: var(--color-border-tertiary);\n --_nav-divider: var(--color-border-tertiary);\n --_nav-fg-primary: var(--color-foreground-primary);\n --_nav-fg-secondary: var(--color-foreground-secondary);\n --_nav-fg-tertiary: var(--color-foreground-tertiary);\n --_nav-hover: var(--color-interaction-hover);\n --_nav-pressed: var(--color-interaction-pressed);\n --_nav-focus: var(--color-interaction-focus);\n --_nav-dot: var(--color-foreground-bold-brand);\n --ds-interaction-hover: var(--_nav-hover);\n --ds-interaction-pressed: var(--_nav-pressed);\n --ds-focus-ring-color: var(--_nav-focus);\n}\n\n/* ── Default chrome overrides ────────────────────────────────────────────── */\n\n/* Shell-owned chrome (bg + wash) — panel is a transparent frame. */\n:host-context(ds-app-shell:not([gradient-preset='none'])) .panel-nav {\n background-color: transparent;\n}\n\n/* Style slots — same app tokens for now; hook texture/glyph layers on these classes later. */\n.panel-nav--dashboard,\n.panel-nav--settings {\n --_nav-bg: var(--color-background-secondary);\n --_nav-border: var(--color-border-tertiary);\n --_nav-divider: var(--color-border-tertiary);\n --_nav-fg-primary: var(--color-foreground-primary);\n --_nav-fg-secondary: var(--color-foreground-secondary);\n --_nav-fg-tertiary: var(--color-foreground-tertiary);\n --_nav-hover: var(--color-interaction-hover);\n --_nav-pressed: var(--color-interaction-pressed);\n --_nav-focus: var(--color-interaction-focus);\n --_nav-dot: var(--color-foreground-bold-brand);\n --ds-interaction-hover: var(--_nav-hover);\n --ds-interaction-pressed: var(--_nav-pressed);\n --ds-focus-ring-color: var(--_nav-focus);\n}\n\n/* ── Shell ───────────────────────────────────────────────────────────────── */\n\n.panel-nav {\n display: flex;\n flex-direction: column;\n position: relative;\n width: var(--_nav-width);\n min-width: var(--_nav-width);\n height: 100%;\n background-color: var(--_nav-bg);\n border-right: var(--dimension-stroke-width-012, 1px) solid var(--_nav-border);\n box-sizing: border-box;\n overflow: visible;\n transition:\n width var(--_nav-expand-motion),\n min-width var(--_nav-expand-motion),\n background-color var(--effect-motion-short-3, 200ms ease),\n border-color var(--effect-motion-short-3, 200ms ease);\n font-family: var(--typography-font-family, system-ui);\n isolation: isolate;\n}\n\n.panel-nav__header,\n.panel-nav__body,\n.panel-nav__footer {\n position: relative;\n z-index: 3;\n}\n\n.panel-nav--collapsed {\n width: var(--_nav-width-collapsed);\n min-width: var(--_nav-width-collapsed);\n}\n\n/* ── Header ──────────────────────────────────────────────────────────────── */\n\n.panel-nav__header {\n display: flex;\n align-items: center;\n height: var(--dimension-size-600, 48px);\n padding: var(--dimension-space-100, 8px);\n flex-shrink: 0;\n box-sizing: border-box;\n border-bottom: var(--dimension-stroke-width-012, 1px) solid var(--_nav-divider);\n transition: border-color var(--effect-motion-short-3, 200ms ease);\n}\n\n.panel-nav__header-btn {\n all: unset;\n /* Re-assert after unset — required for `.ds-interaction-fill` stacking. */\n position: relative;\n z-index: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--dimension-space-075, 6px);\n border-radius: var(--dimension-radius-025, 2px);\n cursor: pointer;\n color: var(--_nav-fg-primary);\n flex-shrink: 0;\n box-sizing: border-box;\n transition: color var(--effect-motion-short-3, 200ms ease);\n /* Hover/press: `.ds-interaction-fill` (tokens from --_nav-* via --ds-interaction-*). */\n}\n\n.panel-nav--breakpoint-locked .panel-nav__header-btn {\n cursor: default;\n}\n\n/* Logo — visible by default; instant swap to toggle on hover (no crossfade overlap) */\n.panel-nav__header-logo {\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n z-index: 2;\n opacity: 1;\n}\n\n.panel-nav__m-mark {\n width: var(--dimension-iconography-md, 20px);\n height: var(--dimension-iconography-md, 20px);\n display: block;\n}\n\n/* Toggle icon — stacked on logo; under hover wash (::after z-index 3) */\n.panel-nav__header-toggle {\n display: flex;\n align-items: center;\n justify-content: center;\n position: absolute;\n inset: 0;\n z-index: 2;\n opacity: 0;\n pointer-events: none;\n}\n\n.panel-nav:not(.panel-nav--breakpoint-locked) .panel-nav__header-btn:hover .panel-nav__header-logo { opacity: 0; }\n.panel-nav:not(.panel-nav--breakpoint-locked) .panel-nav__header-btn:hover .panel-nav__header-toggle { opacity: 1; }\n\n/* ── Scrollable body ─────────────────────────────────────────────────────── */\n\n.panel-nav__body {\n flex: 1;\n overflow-y: auto;\n overflow-x: hidden;\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-200, 16px);\n padding: var(--dimension-space-100, 8px);\n box-sizing: border-box;\n scrollbar-width: none;\n}\n\n.panel-nav__body::-webkit-scrollbar { display: none; }\n\n/* ── Group ───────────────────────────────────────────────────────────────── */\n\n.panel-nav__group {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050, 4px);\n}\n\n/*\n * Section headers — typography via ds-text (text-caption + emphasis).\n * Element selector so display:flex beats Text's inline-block, and padding\n * isn't clobbered by sharing `.panel-nav__item-label-text` on the same host.\n */\nds-text.panel-nav__group-label {\n display: flex;\n align-items: center;\n box-sizing: border-box;\n text-transform: uppercase;\n color: var(--_nav-fg-secondary);\n user-select: none;\n height: var(--dimension-size-300, 24px);\n /* Former nested pad: group-label (075) + item-label-text (025). */\n padding: 0 calc(var(--dimension-space-075, 6px) + var(--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(--dimension-radius-025, 2px);\n cursor: pointer;\n color: var(--_nav-fg-secondary);\n box-sizing: border-box;\n width: 100%;\n user-select: none;\n transition: color var(--effect-motion-short-1, 50ms ease-in-out);\n /* Hover/press: `.ds-interaction-fill` — no --selected (icon/type weight only). */\n}\n\n/* ── Active item ─────────────────────────────────────────────────────────── */\n\n.panel-nav__item--active {\n color: var(--_nav-fg-primary);\n /* Bold typography via ds-text (text-body-medium + emphasis) on the label */\n}\n\n/* ── Item icon ───────────────────────────────────────────────────────────── */\n\n.panel-nav__item-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--dimension-iconography-md, 20px);\n height: var(--dimension-iconography-md, 20px);\n position: relative;\n z-index: 2;\n}\n\n/* ── Item label ──────────────────────────────────────────────────────────── */\n\n.panel-nav__item-label {\n position: relative;\n z-index: 2;\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n height: var(--typography-lineheight-md, 20px);\n opacity: 1;\n /* Gradient wipe: mask sweeps left-to-right on expand, right-to-left on collapse */\n /* stylelint-disable-next-line color-named -- mask luminance stops, not theme colors */\n mask-image: linear-gradient(to right, black 50%, transparent 100%);\n /* stylelint-disable-next-line color-named -- mask luminance stops, not theme colors */\n -webkit-mask-image: linear-gradient(to right, black 50%, transparent 100%);\n mask-size: 200% 100%;\n -webkit-mask-size: 200% 100%;\n mask-position: 0% 0;\n -webkit-mask-position: 0% 0;\n transition:\n opacity var(--_nav-label-fade-motion) 0ms,\n mask-position var(--_nav-label-fade-motion) 0ms,\n -webkit-mask-position var(--_nav-label-fade-motion) 0ms;\n}\n\n.panel-nav__item-label-text {\n padding: 0 var(--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:\n transform var(--_nav-expand-motion);\n}\n\n.panel-nav--collapsed .panel-nav__item-dot {\n transform: translate(\n calc(var(--dimension-space-075, 6px) + var(--dimension-stroke-width-012, 1px)),\n calc(0px - var(--dimension-space-075, 6px) - var(--dimension-stroke-width-012, 1px))\n );\n}\n\n.panel-nav--animating .panel-nav__item-label {\n text-overflow: clip;\n}\n\n.panel-nav--collapsed .panel-nav__item-label {\n opacity: 0;\n pointer-events: none;\n text-overflow: clip;\n mask-position: 100% 0;\n -webkit-mask-position: 100% 0;\n transition:\n opacity var(--_nav-label-fade-out-motion) 0ms,\n mask-position var(--_nav-label-fade-out-motion) 0ms,\n -webkit-mask-position var(--_nav-label-fade-out-motion) 0ms;\n}\n\n.panel-nav--collapsed ds-text.panel-nav__group-label {\n opacity: 0;\n max-height: 0;\n padding: 0 calc(var(--dimension-space-075, 6px) + var(--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(var(--_nav-padding-y) * 2 + var(--_nav-item-height) * 2 + var(--dimension-space-050, 4px));\n}\n\n/* ── Footer icon button (gear / dashboard) ───────────────────────────────── */\n\n.panel-nav__footer-btn {\n position: absolute;\n left: var(--_nav-padding-x);\n top: var(--_nav-padding-y);\n --ds-button-unfilled-fg: var(--_nav-fg-secondary);\n --ds-button-unfilled-fg-active: var(--_nav-fg-primary);\n --ds-interaction-hover: var(--_nav-hover);\n --ds-interaction-pressed: var(--_nav-pressed);\n --ds-focus-ring-color: var(--_nav-focus);\n}\n\n/* ── Footer user — absolutely positioned, slides from row to below gear ───── */\n\n.panel-nav__footer-user {\n position: absolute;\n /* 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 color var(--effect-motion-short-3, 200ms ease);\n}\n\n.panel-nav--collapsed .panel-nav__footer-user {\n /* Compact column keeps the intentionally tighter 4px vertical gap. */\n left: var(--_nav-padding-x);\n top: calc(var(--_nav-padding-y) + var(--_nav-item-height) + var(--dimension-space-050, 4px));\n}\n\n/* Label: absolute so it stays in place and only fades — no mask slide */\n.panel-nav__footer-user-label {\n text-align: right;\n position: absolute;\n left: var(--dimension-space-075, 6px);\n right: calc(var(--dimension-space-075, 6px) + var(--dimension-iconography-md, 20px) + var(--dimension-space-050, 4px));\n top: 50%;\n transform: translateY(-50%);\n flex: none;\n height: var(--typography-lineheight-md, 20px);\n mask-image: none;\n -webkit-mask-image: none;\n transition: opacity var(--_nav-label-fade-motion);\n}\n\n.panel-nav--collapsed .panel-nav__footer-user-label {\n opacity: 0;\n pointer-events: none;\n mask-position: 0% 0;\n -webkit-mask-position: 0% 0;\n}\n\n/* ── Footer user icon — cross-fades chevron ↔ circle+initial ────────────── */\n\n/* Absolute — right-anchored in expanded, left-anchored in collapsed to match regular item icon position */\n.panel-nav__footer-user-icon {\n position: absolute;\n left: calc(100% - var(--dimension-iconography-md, 20px) - var(--dimension-space-075, 6px));\n top: 0;\n bottom: 0;\n height: auto;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: left var(--_nav-expand-motion);\n}\n\n.panel-nav--collapsed .panel-nav__footer-user-icon {\n left: var(--dimension-space-075, 6px);\n}\n\n.panel-nav__footer-icon-expanded,\n.panel-nav__footer-icon-collapsed {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.panel-nav__footer-icon-expanded {\n opacity: 1;\n transition: opacity var(--_nav-label-fade-motion);\n}\n\n.panel-nav__footer-icon-collapsed {\n opacity: 0;\n transition: opacity var(--_nav-label-fade-motion);\n}\n\n.panel-nav--collapsed .panel-nav__footer-icon-expanded {\n opacity: 0;\n transition: opacity var(--_nav-label-fade-out-motion);\n}\n\n.panel-nav--collapsed .panel-nav__footer-icon-collapsed {\n opacity: 1;\n transition: opacity var(--_nav-label-fade-out-motion);\n}\n\n/* ── User initial — overlaid on the circle icon ─────────────────────────── */\n\n/*\n * Center the caption glyph on the Circle icon. Don't use inset:0 + flex — the\n * inner `.ds-text__element { width: 100% }` stretches and left-aligns the letter.\n * Translate-centering keeps the complete caption metric box on the icon center;\n * the caption variant remains the sole owner of its line-height.\n * (Do not set `inset: auto` after top/left — it resets them.)\n */\nds-text.panel-nav__user-initial,\nds-text.panel-nav__user-initial.ds-text--caption {\n position: absolute;\n top: 50%;\n left: 50%;\n right: auto;\n bottom: auto;\n transform: translate(-50%, -50%);\n display: block;\n width: max-content;\n max-width: none;\n min-width: 0;\n text-align: center;\n color: var(--_nav-fg-secondary);\n pointer-events: none;\n transition: color var(--effect-motion-short-3, 200ms ease);\n}\n\n/* ── Drag-to-resize handle ───────────────────────────────────────────────── */\n\n/* The resize overlay line — extends past the right edge; keep above workspace content */\n.panel-nav__resize-handle {\n position: absolute;\n top: 0;\n /* Centered on the right edge: --dimension-space-050 outside + inside = --dimension-space-100 hit zone */\n right: var(--dimension-space-n050, -4px);\n bottom: 0;\n width: var(--dimension-space-100, 8px);\n z-index: 20;\n cursor: ew-resize;\n touch-action: none;\n /* Needs a real background for Safari hit-testing */\n background: transparent;\n}\n\n/* Safari requires non-transparent fill for hit testing */\n.panel-nav__resize-handle::before {\n content: '';\n position: absolute;\n inset: 0;\n /* stylelint-disable-next-line color-named -- near-invisible hit-test fill; not a theme color */\n background: rgba(0, 0, 0, 0.001);\n}\n\n/* Resize overlay line */\n.panel-nav__resize-handle::after {\n content: '';\n position: absolute;\n top: 0;\n bottom: 0;\n left: 50%;\n transform: translateX(-50%);\n width: var(--dimension-stroke-width-025, 2px);\n background: var(--color-foreground-primary);\n opacity: 0;\n pointer-events: none;\n transition: opacity var(--effect-motion-short-2, 100ms ease-in-out);\n}\n\n.panel-nav__resize-handle--overlay::after {\n opacity: 1;\n}\n\n/* ── Reduced motion ──────────────────────────────────────────────────────── */\n\n@media (prefers-reduced-motion: reduce) {\n .panel-nav,\n .panel-nav *,\n .panel-nav *::before,\n .panel-nav *::after,\n .panel-nav__resize-handle,\n .panel-nav__resize-handle::before,\n .panel-nav__resize-handle::after {\n /* Near-instant — browsers treat 0 as \"no transition\" and can skip the end state. */\n transition-duration: var(--effect-animation-duration-instant) !important;\n animation-duration: var(--effect-animation-duration-instant) !important;\n transition-delay: var(--effect-animation-duration-instant) !important;\n }\n}\n",
621
+ "content": "@import '../../utils/control-density.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n\n/* ── Theme variables ──────────────────────────────────────────────────────── */\n\n.panel-nav {\n /* ─ Sizes ─ */\n --_nav-width: var(--dimension-panel-width-2xs);\n --_nav-width-collapsed: var(--dimension-size-600);\n --_nav-item-height: var(--dimension-size-400, 32px);\n --_nav-padding-x: var(--dimension-space-100, 8px);\n --_nav-padding-y: var(--dimension-space-100, 8px);\n /* gear button outer width = padding*2 + icon */\n --_nav-footer-btn-w: calc(var(--dimension-space-075, 6px) * 2 + var(--dimension-iconography-md, 20px));\n\n /* ─ Motion ─ */\n /* --ds-panel-nav-speed: unitless multiplier; override in Storybook to slow-mo (e.g. 10 = 10×).\n *-dur vars are duration-only (needed for calc); *-motion vars add easing for use in transitions. */\n --_nav-speed: var(--ds-panel-nav-speed, 1);\n --_nav-expand-dur: calc(var(--_nav-speed) * var(--effect-animation-duration-medium-1, 300ms));\n --_nav-expand-motion: var(--_nav-expand-dur) var(--effect-animation-easing-ease-in-out, ease);\n --_nav-label-fade-dur: calc(var(--_nav-speed) * var(--effect-animation-duration-medium-1, 300ms));\n --_nav-label-fade-motion: var(--_nav-label-fade-dur) var(--effect-animation-easing-ease-in-out, ease);\n --_nav-label-fade-out-dur: calc(var(--_nav-speed) * var(--effect-animation-duration-medium-1, 300ms));\n --_nav-label-fade-out-motion: var(--_nav-label-fade-out-dur) var(--effect-animation-easing-ease-in-out, ease);\n\n /* ─ Colours – default chrome (app surfaces) ─ */\n --_nav-bg: var(--color-background-secondary);\n --_nav-border: var(--color-border-tertiary);\n --_nav-divider: var(--color-border-tertiary);\n --_nav-fg-primary: var(--color-foreground-primary);\n --_nav-fg-secondary: var(--color-foreground-secondary);\n --_nav-fg-tertiary: var(--color-foreground-tertiary);\n --_nav-hover: var(--color-interaction-hover);\n --_nav-pressed: var(--color-interaction-pressed);\n --_nav-focus: var(--color-interaction-focus);\n --_nav-dot: var(--color-foreground-bold-brand);\n --ds-interaction-hover: var(--_nav-hover);\n --ds-interaction-pressed: var(--_nav-pressed);\n --ds-focus-ring-color: var(--_nav-focus);\n}\n\n/* ── Default chrome overrides ────────────────────────────────────────────── */\n\n/* Shell-owned chrome (bg + wash) — panel is a transparent frame. */\n:host-context(ds-app-shell:not([gradient-preset='none'])) .panel-nav {\n background-color: transparent;\n}\n\n/* Style slots — same app tokens for now; hook texture/glyph layers on these classes later. */\n.panel-nav--dashboard,\n.panel-nav--settings {\n --_nav-bg: var(--color-background-secondary);\n --_nav-border: var(--color-border-tertiary);\n --_nav-divider: var(--color-border-tertiary);\n --_nav-fg-primary: var(--color-foreground-primary);\n --_nav-fg-secondary: var(--color-foreground-secondary);\n --_nav-fg-tertiary: var(--color-foreground-tertiary);\n --_nav-hover: var(--color-interaction-hover);\n --_nav-pressed: var(--color-interaction-pressed);\n --_nav-focus: var(--color-interaction-focus);\n --_nav-dot: var(--color-foreground-bold-brand);\n --ds-interaction-hover: var(--_nav-hover);\n --ds-interaction-pressed: var(--_nav-pressed);\n --ds-focus-ring-color: var(--_nav-focus);\n}\n\n/* ── Shell ───────────────────────────────────────────────────────────────── */\n\n.panel-nav {\n display: flex;\n flex-direction: column;\n position: relative;\n width: var(--_nav-width);\n min-width: var(--_nav-width);\n height: 100%;\n background-color: var(--_nav-bg);\n border-right: var(--dimension-stroke-width-012, 1px) solid var(--_nav-border);\n box-sizing: border-box;\n overflow: visible;\n transition:\n width var(--_nav-expand-motion),\n min-width var(--_nav-expand-motion),\n background-color var(--effect-motion-short-3, 200ms ease),\n border-color var(--effect-motion-short-3, 200ms ease);\n font-family: var(--typography-font-family, system-ui);\n isolation: isolate;\n}\n\n.panel-nav__header,\n.panel-nav__body,\n.panel-nav__footer {\n position: relative;\n z-index: 3;\n}\n\n.panel-nav--collapsed {\n width: var(--_nav-width-collapsed);\n min-width: var(--_nav-width-collapsed);\n}\n\n/* ── Header ──────────────────────────────────────────────────────────────── */\n\n.panel-nav__header {\n display: flex;\n align-items: center;\n height: var(--dimension-size-600, 48px);\n padding: var(--dimension-space-100, 8px);\n flex-shrink: 0;\n box-sizing: border-box;\n border-bottom: var(--dimension-stroke-width-012, 1px) solid var(--_nav-divider);\n transition: border-color var(--effect-motion-short-3, 200ms ease);\n}\n\n.panel-nav__header-btn {\n all: unset;\n /* Re-assert after unset — required for `.ds-interaction-fill` stacking. */\n position: relative;\n z-index: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--dimension-space-075, 6px);\n border-radius: var(--dimension-radius-025, 2px);\n cursor: pointer;\n color: var(--_nav-fg-primary);\n flex-shrink: 0;\n box-sizing: border-box;\n transition: color var(--effect-motion-short-3, 200ms ease);\n /* Hover/press: `.ds-interaction-fill` (tokens from --_nav-* via --ds-interaction-*). */\n}\n\n.panel-nav--breakpoint-locked .panel-nav__header-btn {\n cursor: default;\n}\n\n/* Logo — visible by default; instant swap to toggle on hover (no crossfade overlap) */\n.panel-nav__header-logo {\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n z-index: 2;\n opacity: 1;\n}\n\n.panel-nav__m-mark {\n width: var(--dimension-iconography-md, 20px);\n height: var(--dimension-iconography-md, 20px);\n display: block;\n}\n\n/* Toggle icon — stacked on logo; under hover wash (::after z-index 3) */\n.panel-nav__header-toggle {\n display: flex;\n align-items: center;\n justify-content: center;\n position: absolute;\n inset: 0;\n z-index: 2;\n opacity: 0;\n pointer-events: none;\n}\n\n.panel-nav:not(.panel-nav--breakpoint-locked) .panel-nav__header-btn:hover .panel-nav__header-logo { opacity: 0; }\n.panel-nav:not(.panel-nav--breakpoint-locked) .panel-nav__header-btn:hover .panel-nav__header-toggle { opacity: 1; }\n\n/* ── Scrollable body ─────────────────────────────────────────────────────── */\n\n.panel-nav__body {\n flex: 1;\n overflow-y: auto;\n overflow-x: hidden;\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-200, 16px);\n padding: var(--dimension-space-100, 8px);\n box-sizing: border-box;\n scrollbar-width: none;\n}\n\n.panel-nav__body::-webkit-scrollbar { display: none; }\n\n/* ── Group ───────────────────────────────────────────────────────────────── */\n\n.panel-nav__group {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050, 4px);\n}\n\n/*\n * Section headers — typography via ds-text (text-caption + emphasis).\n * Element selector so display:flex beats Text's inline-block, and padding\n * isn't clobbered by sharing `.panel-nav__item-label-text` on the same host.\n */\nds-text.panel-nav__group-label {\n display: flex;\n align-items: center;\n box-sizing: border-box;\n text-transform: uppercase;\n color: var(--_nav-fg-secondary);\n user-select: none;\n height: var(--dimension-size-300, 24px);\n /* Former nested pad: group-label (075) + item-label-text (025). */\n padding: 0 calc(var(--dimension-space-075, 6px) + var(--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(--dimension-radius-025, 2px);\n cursor: pointer;\n color: var(--_nav-fg-secondary);\n box-sizing: border-box;\n width: 100%;\n user-select: none;\n transition: color var(--effect-motion-short-1, 50ms ease-in-out);\n /* Hover/press: `.ds-interaction-fill` — no --selected (icon/type weight only). */\n}\n\n/* ── Active item ─────────────────────────────────────────────────────────── */\n\n.panel-nav__item--active {\n color: var(--_nav-fg-primary);\n /* Bold typography via ds-text (text-body-medium + emphasis) on the label */\n}\n\n/* ── Item icon ───────────────────────────────────────────────────────────── */\n\n.panel-nav__item-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--dimension-iconography-md, 20px);\n height: var(--dimension-iconography-md, 20px);\n position: relative;\n z-index: 2;\n}\n\n/* ── Item label ──────────────────────────────────────────────────────────── */\n\n.panel-nav__item-label {\n position: relative;\n z-index: 2;\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n height: var(--typography-lineheight-md, 20px);\n opacity: 1;\n /* Gradient wipe: mask sweeps left-to-right on expand, right-to-left on collapse */\n /* stylelint-disable-next-line color-named -- mask luminance stops, not theme colors */\n mask-image: linear-gradient(to right, black 50%, transparent 100%);\n /* stylelint-disable-next-line color-named -- mask luminance stops, not theme colors */\n -webkit-mask-image: linear-gradient(to right, black 50%, transparent 100%);\n mask-size: 200% 100%;\n -webkit-mask-size: 200% 100%;\n mask-position: 0% 0;\n -webkit-mask-position: 0% 0;\n transition:\n opacity var(--_nav-label-fade-motion) 0ms,\n mask-position var(--_nav-label-fade-motion) 0ms,\n -webkit-mask-position var(--_nav-label-fade-motion) 0ms;\n}\n\n.panel-nav__item-label-text {\n padding: 0 var(--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:\n transform var(--_nav-expand-motion);\n}\n\n.panel-nav--collapsed .panel-nav__item-dot {\n transform: translate(\n calc(var(--dimension-space-075, 6px) + var(--dimension-stroke-width-012, 1px)),\n calc(0px - var(--dimension-space-075, 6px) - var(--dimension-stroke-width-012, 1px))\n );\n}\n\n.panel-nav--animating .panel-nav__item-label {\n text-overflow: clip;\n}\n\n.panel-nav--collapsed .panel-nav__item-label {\n opacity: 0;\n pointer-events: none;\n text-overflow: clip;\n mask-position: 100% 0;\n -webkit-mask-position: 100% 0;\n transition:\n opacity var(--_nav-label-fade-out-motion) 0ms,\n mask-position var(--_nav-label-fade-out-motion) 0ms,\n -webkit-mask-position var(--_nav-label-fade-out-motion) 0ms;\n}\n\n.panel-nav--collapsed ds-text.panel-nav__group-label {\n opacity: 0;\n max-height: 0;\n padding: 0 calc(var(--dimension-space-075, 6px) + var(--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(var(--_nav-padding-y) * 2 + var(--_nav-item-height) * 2 + var(--dimension-space-050, 4px));\n}\n\n/* ── Footer icon button (gear / dashboard) ───────────────────────────────── */\n\n.panel-nav__footer-btn {\n position: absolute;\n left: var(--_nav-padding-x);\n top: var(--_nav-padding-y);\n --ds-button-unfilled-fg: var(--_nav-fg-secondary);\n --ds-button-unfilled-fg-active: var(--_nav-fg-primary);\n --ds-interaction-hover: var(--_nav-hover);\n --ds-interaction-pressed: var(--_nav-pressed);\n --ds-focus-ring-color: var(--_nav-focus);\n}\n\n/* ── Footer user — absolutely positioned, slides from row to below gear ───── */\n\n.panel-nav__footer-user {\n position: absolute;\n /* 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.panel-nav__footer-user--menu-expanded:hover:not(:disabled)::after {\n background: var(--_nav-pressed);\n}\n\n.panel-nav__footer-user--menu-expanded {\n color: var(--_nav-fg-primary);\n}\n\n.panel-nav--collapsed .panel-nav__footer-user {\n /* Compact column keeps the intentionally tighter 4px vertical gap. */\n left: var(--_nav-padding-x);\n top: calc(var(--_nav-padding-y) + var(--_nav-item-height) + var(--dimension-space-050, 4px));\n}\n\n/* Label: absolute so it stays in place and only fades — no mask slide */\n.panel-nav__footer-user-label {\n text-align: right;\n position: absolute;\n left: var(--dimension-space-075, 6px);\n right: calc(var(--dimension-space-075, 6px) + var(--dimension-iconography-md, 20px) + var(--dimension-space-050, 4px));\n top: 50%;\n transform: translateY(-50%);\n flex: none;\n height: var(--typography-lineheight-md, 20px);\n mask-image: none;\n -webkit-mask-image: none;\n transition: opacity var(--_nav-label-fade-motion);\n}\n\n.panel-nav--collapsed .panel-nav__footer-user-label {\n opacity: 0;\n pointer-events: none;\n mask-position: 0% 0;\n -webkit-mask-position: 0% 0;\n}\n\n/* ── Footer user icon — cross-fades chevron ↔ circle+initial ────────────── */\n\n/* Absolute — right-anchored in expanded, left-anchored in collapsed to match regular item icon position */\n.panel-nav__footer-user-icon {\n position: absolute;\n left: calc(100% - var(--dimension-iconography-md, 20px) - var(--dimension-space-075, 6px));\n top: 0;\n bottom: 0;\n height: auto;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: left var(--_nav-expand-motion);\n}\n\n.panel-nav--collapsed .panel-nav__footer-user-icon {\n left: var(--dimension-space-075, 6px);\n}\n\n.panel-nav__footer-icon-expanded,\n.panel-nav__footer-icon-collapsed {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.panel-nav__footer-icon-expanded {\n opacity: 1;\n transition: opacity var(--_nav-label-fade-motion);\n}\n\n.panel-nav__footer-icon-collapsed {\n opacity: 0;\n transition: opacity var(--_nav-label-fade-motion);\n}\n\n.panel-nav--collapsed .panel-nav__footer-icon-expanded {\n opacity: 0;\n transition: opacity var(--_nav-label-fade-out-motion);\n}\n\n.panel-nav--collapsed .panel-nav__footer-icon-collapsed {\n opacity: 1;\n transition: opacity var(--_nav-label-fade-out-motion);\n}\n\n/* ── User initial — overlaid on the circle icon ─────────────────────────── */\n\n/*\n * Center the caption glyph on the Circle icon. Don't use inset:0 + flex — the\n * inner `.ds-text__element { width: 100% }` stretches and left-aligns the letter.\n * Translate-centering keeps the complete caption metric box on the icon center;\n * the caption variant remains the sole owner of its line-height.\n * (Do not set `inset: auto` after top/left — it resets them.)\n */\nds-text.panel-nav__user-initial,\nds-text.panel-nav__user-initial.ds-text--caption {\n position: absolute;\n top: 50%;\n left: 50%;\n right: auto;\n bottom: auto;\n transform: translate(-50%, -50%);\n display: block;\n width: max-content;\n max-width: none;\n min-width: 0;\n text-align: center;\n color: 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",
604
622
  "type": "registry:ui"
605
623
  },
606
624
  {
607
625
  "path": "src/wc/components/PanelNav/PanelNav.tsx",
608
- "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 deriveActiveIdFromUrl,\n panelNavWidthTransitionMs,\n parsePanelNavGroups,\n resolvePanelNavDisableVt,\n resolvePanelNavStyle,\n resolvePanelNavToggle,\n shouldResyncPanelNavGroups,\n shouldResyncPanelNavStyle,\n} from './panel-nav-utils';\nimport {\n PANEL_NAV_USER_MENU_ANCHOR_ID,\n PANEL_NAV_USER_MENU_PLACEMENT,\n type PanelNavGroup,\n type PanelNavItem,\n type PanelNavRouterMode,\n type PanelNavUserActionDetail,\n} from './panel-nav-types';\nimport { scrollEdgeFadeClassMap } from '../../utils/scroll-edge-fade';\nimport { prefersReducedMotion } from '../../utils/resolve-css-time-ms';\n\n@Component({\n tag: 'ds-panel-nav',\n styleUrl: 'PanelNav.css',\n styleUrls: ['../../utils/scroll-edge-fade.css'],\n scoped: true,\n})\nexport class PanelNav {\n /** Style slot: `dashboard` or `settings`. Colors match for now;\n * class hooks reserved for texture/glyph layers. Property: `navStyle`. Attribute: `nav-style`. */\n @Prop({ attribute: 'nav-style', reflect: true }) navStyle: NavChromeStyle = 'dashboard';\n\n /** When `true`, style changes apply synchronously — host app owns view transitions. */\n @Prop() disableViewTransition: boolean = false;\n\n /** Nav groups — set via JS property (`el.groups = [...]`) or JSON string attribute. */\n @Prop() groups: string | PanelNavGroup[] = '[]';\n\n /** How items with `href` render:\n * - `anchor` (default): native `<a href>` — works with routers that intercept anchors.\n * - `event`: always `<button>`; host handles navigation via `dsNavSelect`. */\n @Prop() routerMode: PanelNavRouterMode = 'anchor';\n\n /** ID of the currently active/selected nav item. Overridden by `currentUrl` matching when set. */\n @Prop() activeId: string = '';\n\n /** Whether the nav is in collapsed (icon-only) state.\n * Set `storageKey` to persist across reloads. `dsNavToggle` still fires on change. */\n @Prop({ mutable: true }) collapsed: boolean = false;\n\n /** Viewport width (px) below which the nav locks in icon-only mode. 0 = disabled. */\n @Prop() breakpoint: number = 0;\n\n /** `localStorage` key used to persist the collapsed state across page loads.\n * When set, collapsed state is restored on mount and written on each toggle. */\n @Prop() storageKey: string = '';\n\n /** Current route URL (e.g. `window.location.pathname` or the router's active URL).\n * When set the component derives the active item by matching item `href` values\n * against this string (longest segment-boundary prefix wins), overriding `activeId`. */\n @Prop() currentUrl: string = '';\n\n /** Display name for the footer user section */\n @Prop() userName: string = '';\n\n /** Single character shown in the collapsed avatar */\n @Prop() userInitial: string = '';\n\n @Prop() dashboardLabel: string = 'Dashboard';\n @Prop() settingsLabel: string = 'Settings';\n @Prop() accountLabel: string = 'Account';\n @Prop() dashboardNavigationLabel: string = 'Dashboard navigation';\n @Prop() settingsNavigationLabel: string = 'Settings navigation';\n @Prop() expandNavigationLabel: string = 'Expand navigation';\n @Prop() collapseNavigationLabel: string = 'Collapse navigation';\n /** Localized name for the body when overflow makes it a keyboard-scrollable region. */\n @Prop() navigationItemsLabel: string = 'Navigation items';\n\n /** Emitted when a nav item is clicked. Detail = the item's `id`. */\n @Event() dsNavSelect!: EventEmitter<string>;\n\n /** Emitted when the collapse toggle is clicked. Detail = new collapsed state. */\n @Event() dsNavToggle!: EventEmitter<boolean>;\n\n /** Bubbling lifecycle — `ds-app-shell` pauses chrome metrics during width motion. */\n @Event({ bubbles: true, composed: true })\n dsChromeTransitionStart!: EventEmitter<ChromeTransitionDetail>;\n\n @Event({ bubbles: true, composed: true })\n dsChromeTransitionEnd!: EventEmitter<ChromeTransitionDetail>;\n\n /** Emitted when the footer left button (gear / dashboard) is clicked. */\n @Event() dsNavFooterAction!: EventEmitter<void>;\n\n /** Emitted when the footer user button is clicked. Detail includes the anchor for `ds-menu`. */\n @Event() dsNavUserAction!: EventEmitter<PanelNavUserActionDetail>;\n\n @Element() el!: HTMLElement;\n\n @State() private renderedStyle: NavChromeStyle = 'dashboard';\n @State() private parsedGroups: PanelNavGroup[] = [];\n @State() private isAnimating = false;\n @State() private rovingIndex: number = 0;\n @State() private viewportNarrow: boolean = false;\n @State() private urlDerivedActiveId: string = '';\n @State() private bodyScrollable = false;\n\n private transitionCompletionHandler?: (e: TransitionEvent) => void;\n private transitionFallbackTimer: number | null = null;\n private resizeObserver?: ResizeObserver;\n private scrollRegionObserver?: ResizeObserver;\n private bodyEl?: HTMLElement;\n private initialRenderComplete = false;\n\n // Drag-to-resize state (not @State — no re-render needed)\n private isDragging = false;\n private dragStartX = 0;\n private didSnap = false;\n private lastDeltaX = 0;\n private edgeOverlayTimer: number | null = null;\n private globalMouseMoveHandler?: (e: MouseEvent) => void;\n private globalMouseUpHandler?: () => void;\n\n @State() private showEdgeOverlay = false;\n\n private static readonly HOST_PROP_SYNC_BUDGET = 8;\n\n /** The ID that should be treated as active. `currentUrl` matching takes precedence\n * over `activeId` when both are present. */\n private get effectiveActiveId(): string {\n return this.urlDerivedActiveId || this.activeId;\n }\n\n private get effectiveDisableViewTransition(): boolean {\n return resolvePanelNavDisableVt(\n this.disableViewTransition,\n this.el.getAttribute('disable-view-transition')\n );\n }\n\n @Watch('collapsed')\n onCollapsedChange(_next: boolean, prev: boolean | undefined) {\n if (prev === undefined || !this.initialRenderComplete) return;\n this.startCollapseAnimation();\n }\n\n @Watch('viewportNarrow')\n onViewportNarrowChange(next: boolean, prev: boolean | undefined) {\n if (prev === undefined || !this.initialRenderComplete) return;\n if (next && this.rovingIndex === 0) {\n this.rovingIndex = this.getFirstRovingIndex();\n }\n this.startCollapseAnimation();\n }\n\n @Watch('breakpoint')\n onBreakpointChange() {\n this.disconnectResizeObserver();\n if (this.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 }\n\n @Watch('activeId')\n @Watch('urlDerivedActiveId')\n onActiveIdChange() {\n /* Active route updates aria-current only — roving tab stop stays user-controlled. */\n }\n\n @Watch('currentUrl')\n onCurrentUrlChange() {\n this.syncActiveFromUrl();\n }\n\n @Watch('navStyle')\n onNavStyleChange(newVal: NavChromeStyle) {\n this.renderedStyle = newVal;\n }\n\n componentWillLoad() {\n this.renderedStyle = resolvePanelNavStyle(this.navStyle, this.el.getAttribute('nav-style'));\n 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 }\n\n disconnectedCallback() {\n this.disconnectResizeObserver();\n this.disconnectScrollRegionObserver();\n this.clearCollapseAnimationCompletion(\n this.el.querySelector('.panel-nav') as HTMLElement | null\n );\n this.clearEdgeOverlayTimer();\n if (this.globalMouseMoveHandler) {\n window.removeEventListener('mousemove', this.globalMouseMoveHandler);\n }\n if (this.globalMouseUpHandler) {\n window.removeEventListener('mouseup', this.globalMouseUpHandler);\n }\n }\n\n private startCollapseAnimation() {\n if (this.isAnimating) return;\n this.isAnimating = true;\n this.dsChromeTransitionStart.emit({ source: 'panel-nav' });\n const panel = this.el.querySelector('.panel-nav') as HTMLElement | null;\n const widthBefore = panel?.getBoundingClientRect().width ?? 0;\n this.clearCollapseAnimationCompletion(panel);\n this.transitionCompletionHandler = (e: TransitionEvent) => {\n if (e.propertyName === 'width' || e.propertyName === 'min-width') {\n this.finishCollapseAnimation(panel);\n }\n };\n panel?.addEventListener('transitionend', this.transitionCompletionHandler);\n panel?.addEventListener('transitioncancel', this.transitionCompletionHandler);\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n if (!this.isAnimating) return;\n const widthAfter = panel?.getBoundingClientRect().width ?? 0;\n if (Math.abs(widthAfter - widthBefore) < 1) {\n this.finishCollapseAnimation(panel);\n return;\n }\n if (!panel) {\n this.finishCollapseAnimation(panel);\n return;\n }\n\n const fallbackMs = panelNavWidthTransitionMs(getComputedStyle(panel));\n if (fallbackMs <= 0) {\n this.finishCollapseAnimation(panel);\n return;\n }\n this.transitionFallbackTimer = window.setTimeout(() => {\n this.finishCollapseAnimation(panel);\n }, fallbackMs);\n });\n });\n }\n\n private finishCollapseAnimation(panel: HTMLElement | null) {\n const wasAnimating = this.isAnimating;\n this.clearCollapseAnimationCompletion(panel);\n if (!wasAnimating) return;\n this.isAnimating = false;\n this.dsChromeTransitionEnd.emit({ source: 'panel-nav' });\n }\n\n private clearCollapseAnimationCompletion(panel: HTMLElement | null) {\n if (this.transitionCompletionHandler) {\n panel?.removeEventListener('transitionend', this.transitionCompletionHandler);\n panel?.removeEventListener('transitioncancel', this.transitionCompletionHandler);\n this.transitionCompletionHandler = undefined;\n }\n if (this.transitionFallbackTimer !== null) {\n window.clearTimeout(this.transitionFallbackTimer);\n this.transitionFallbackTimer = null;\n }\n }\n\n private 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 return;\n }\n this.urlDerivedActiveId = deriveActiveIdFromUrl(this.currentUrl, this.getAllItems());\n }\n\n /** Re-parse props assigned by the host after componentWillLoad (Angular ngAfterViewInit). */\n private syncHostPropsIfNeeded() {\n if (shouldResyncPanelNavStyle(this.renderedStyle, this.navStyle)) {\n this.renderedStyle = this.navStyle;\n }\n\n if (shouldResyncPanelNavGroups(this.parsedGroups, this.groups)) {\n this.onGroupsChange(this.groups);\n } else if (this.currentUrl) {\n this.syncActiveFromUrl();\n }\n }\n\n /** Poll across animation frames — host props may land without triggering @Watch. */\n private scheduleDeferredHostPropSync() {\n let remaining = PanelNav.HOST_PROP_SYNC_BUDGET;\n const tick = () => {\n this.syncHostPropsIfNeeded();\n if (--remaining > 0) {\n requestAnimationFrame(tick);\n }\n };\n queueMicrotask(tick);\n }\n\n /** Centralised toggle: updates the desktop preference unless breakpoint-locked. */\n private applyToggle(next: boolean) {\n if (this.viewportNarrow) return;\n this.collapsed = next;\n if (this.storageKey) {\n try {\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 getFirstRovingIndex(): number {\n return this.viewportNarrow ? 1 : 0;\n }\n\n private getFooterRovingIndex(): number {\n return 1 + this.getAllItems().length;\n }\n\n private getUserRovingIndex(): number {\n return this.getFooterRovingIndex() + 1;\n }\n\n private getRovingElement(index: number): HTMLElement | null {\n const itemCount = this.getAllItems().length;\n if (index === 0) {\n return this.viewportNarrow ? null : this.el.querySelector('.panel-nav__header-btn');\n }\n if (index >= 1 && index <= itemCount) {\n const items = this.el.querySelectorAll<HTMLElement>('.panel-nav__body .panel-nav__item');\n return items[index - 1] ?? null;\n }\n if (index === this.getFooterRovingIndex()) {\n return this.el.querySelector('.panel-nav__footer-btn .button-unfilled');\n }\n if (index === this.getUserRovingIndex()) {\n return this.el.querySelector('.panel-nav__footer-user');\n }\n return null;\n }\n\n private focusRovingAt(index: number) {\n this.rovingIndex = index;\n this.getRovingElement(index)?.focus({ preventScroll: true });\n }\n\n private activateRovingIndex(index: number) {\n const itemCount = this.getAllItems().length;\n const items = this.getAllItems();\n if (index === 0) {\n this.handleToggle();\n return;\n }\n if (index >= 1 && index <= itemCount) {\n this.handleItemClick(items[index - 1].id);\n return;\n }\n if (index === this.getFooterRovingIndex()) {\n this.handleFooterAction();\n return;\n }\n if (index === this.getUserRovingIndex()) {\n const anchor = this.el.querySelector(\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);\n return;\n }\n\n const footerIdx = this.getFooterRovingIndex();\n const userIdx = this.getUserRovingIndex();\n const firstIdx = this.getFirstRovingIndex();\n let next: number | undefined;\n\n switch (e.key) {\n case 'ArrowDown':\n if (index === userIdx) return;\n if (index === footerIdx) next = userIdx;\n else next = index + 1;\n break;\n case 'ArrowUp':\n if (index === firstIdx) return;\n if (index === userIdx) next = footerIdx;\n else if (index === footerIdx) next = footerIdx - 1;\n else next = index - 1;\n break;\n case 'ArrowRight':\n if (index === footerIdx) next = userIdx;\n else return;\n break;\n case 'ArrowLeft':\n if (index === userIdx) next = footerIdx;\n else return;\n break;\n case 'Home':\n e.preventDefault();\n next = firstIdx;\n break;\n case 'End':\n e.preventDefault();\n next = userIdx;\n break;\n default:\n return;\n }\n\n if (next === undefined || next === index) return;\n e.preventDefault();\n this.focusRovingAt(next);\n }\n\n private handleItemClick(id: string) {\n this.dsNavSelect.emit(id);\n }\n\n private handleToggle() {\n const next = resolvePanelNavToggle(this.collapsed, this.viewportNarrow);\n if (next !== null) this.applyToggle(next);\n }\n\n /** Toggle the desktop preference. No-op while the breakpoint locks the panel collapsed. */\n @Method()\n async toggleCollapsed() {\n this.handleToggle();\n }\n\n private handleFooterAction() {\n this.dsNavFooterAction.emit();\n }\n\n private handleUserAction(e: MouseEvent) {\n const anchor = e.currentTarget as HTMLElement;\n this.dsNavUserAction.emit({ anchor, menuPlacement: PANEL_NAV_USER_MENU_PLACEMENT });\n }\n\n private clearEdgeOverlayTimer() {\n if (this.edgeOverlayTimer !== null) {\n window.clearTimeout(this.edgeOverlayTimer);\n this.edgeOverlayTimer = null;\n }\n }\n\n private handleResizeHandleMouseEnter() {\n this.clearEdgeOverlayTimer();\n if (prefersReducedMotion()) {\n this.showEdgeOverlay = true;\n return;\n }\n this.edgeOverlayTimer = window.setTimeout(() => {\n this.showEdgeOverlay = true;\n this.edgeOverlayTimer = null;\n }, 500);\n }\n\n private handleResizeHandleMouseLeave() {\n if (this.isDragging) return;\n this.clearEdgeOverlayTimer();\n this.showEdgeOverlay = false;\n }\n\n private handleResizeHandleMouseDown(e: MouseEvent) {\n e.preventDefault();\n e.stopPropagation();\n\n const wasCollapsed = this.collapsed || this.viewportNarrow;\n this.isDragging = true;\n this.dragStartX = e.clientX;\n this.didSnap = false;\n this.lastDeltaX = 0;\n this.clearEdgeOverlayTimer();\n this.showEdgeOverlay = false;\n\n document.body.style.cursor = 'ew-resize';\n document.body.style.userSelect = 'none';\n\n const onMove = (ev: MouseEvent) => {\n const deltaX = ev.clientX - this.dragStartX;\n this.lastDeltaX = Math.abs(deltaX);\n if (this.didSnap) return;\n if (!wasCollapsed && deltaX < -8) {\n this.applyToggle(true);\n this.didSnap = true;\n } else if (wasCollapsed && deltaX > 8) {\n this.applyToggle(false);\n this.didSnap = true;\n }\n };\n\n const onUp = () => {\n if (!this.didSnap && this.lastDeltaX < 3) {\n this.applyToggle(!wasCollapsed);\n }\n this.isDragging = false;\n this.showEdgeOverlay = false;\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n window.removeEventListener('mousemove', onMove);\n window.removeEventListener('mouseup', onUp);\n this.globalMouseMoveHandler = undefined;\n this.globalMouseUpHandler = undefined;\n };\n\n this.globalMouseMoveHandler = onMove;\n this.globalMouseUpHandler = onUp;\n window.addEventListener('mousemove', onMove);\n window.addEventListener('mouseup', onUp);\n }\n\n private renderFooterAction(isDashboardChrome: boolean) {\n // Short chrome labels — same string for tip + aria (not \"Open/Go to …\").\n const footerLabel = isDashboardChrome ? this.settingsLabel : this.dashboardLabel;\n return (\n <ds-tooltip label={footerLabel} side=\"right\" size=\"sm\">\n <ds-button-unfilled\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=\"panel-nav__item panel-nav__footer-user ds-focus-ring-inset ds-interaction-fill\"\n tabIndex={this.rovingIndex === this.getUserRovingIndex() ? 0 : -1}\n aria-label={this.accountLabel}\n onClick={e => this.handleUserAction(e)}\n onKeyDown={(e: KeyboardEvent) => this.handleRovingKeyDown(e, this.getUserRovingIndex())}\n onFocus={() => {\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 >\n {userName}\n </ds-text>\n <span class=\"panel-nav__item-icon panel-nav__footer-user-icon\" aria-hidden=\"true\">\n <span class=\"panel-nav__footer-icon-expanded\">\n <ds-icon name=\"ChevronUpDown\" size=\"md\" color=\"inherit\" />\n </span>\n <span class=\"panel-nav__footer-icon-collapsed\">\n <ds-icon name=\"Circle\" size=\"md\" color=\"inherit\" />\n <ds-text\n class=\"panel-nav__user-initial\"\n as=\"span\"\n variant=\"text-caption\"\n emphasis\n align=\"center\"\n color=\"inherit\"\n >\n {userInitial}\n </ds-text>\n </span>\n </span>\n </button>\n );\n\n return (\n <ds-tooltip label={tipLabel} side=\"right\" size=\"sm\">\n {userButton}\n </ds-tooltip>\n );\n }\n\n private renderNavItem(item: PanelNavItem, idx: number, collapsed: boolean) {\n const isActive = item.id === this.effectiveActiveId;\n const rovingPosition = idx + 1;\n\n const itemContent = [\n <span class=\"panel-nav__item-icon\">\n <ds-icon name={item.icon} size=\"md\" color=\"inherit\" />\n </span>,\n <ds-text\n class=\"panel-nav__item-label panel-nav__item-label-text ds-control--md\"\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis={isActive}\n color=\"inherit\"\n >\n {item.label}\n </ds-text>,\n item.dot && (\n <span class=\"panel-nav__item-dot-box\" aria-hidden=\"true\">\n <ds-badge class=\"panel-nav__item-dot\" variant=\"dot\" hasRing={collapsed} label=\"\" />\n </span>\n ),\n ];\n\n const sharedProps = {\n class: {\n 'panel-nav__item': true,\n 'panel-nav__item--active': isActive,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n },\n 'aria-current': isActive ? ('page' as const) : undefined,\n tabIndex: rovingPosition === this.rovingIndex ? 0 : -1,\n onClick: () => this.handleItemClick(item.id),\n onKeyDown: (e: KeyboardEvent) => this.handleRovingKeyDown(e, rovingPosition),\n onFocus: () => {\n this.rovingIndex = rovingPosition;\n },\n };\n\n const useAnchor = this.routerMode === 'anchor' && item.href;\n const control = useAnchor ? (\n <a {...sharedProps} href={item.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 render() {\n const chromeStyle = this.renderedStyle;\n const isDashboardChrome = chromeStyle === 'dashboard';\n const isDashboard = this.navStyle === 'dashboard';\n const collapsed = this.collapsed || this.viewportNarrow;\n const { userName, userInitial } = this;\n\n const navCls: Record<string, boolean> = {\n 'panel-nav': true,\n 'panel-nav--dashboard': isDashboard,\n 'panel-nav--settings': !isDashboard,\n 'panel-nav--collapsed': collapsed,\n 'panel-nav--breakpoint-locked': this.viewportNarrow,\n 'panel-nav--animating': this.isAnimating,\n };\n\n return (\n <Host style={{ display: 'block', position: 'relative' }}>\n <nav\n class={navCls}\n aria-label={\n isDashboardChrome ? this.dashboardNavigationLabel : this.settingsNavigationLabel\n }\n >\n {/* ── Header: Motive logo, reveals collapse toggle on hover ── */}\n <div class=\"panel-nav__header\">\n <button\n type=\"button\"\n class=\"panel-nav__header-btn ds-focus-ring-inset ds-interaction-fill\"\n disabled={this.viewportNarrow}\n tabIndex={!this.viewportNarrow && this.rovingIndex === 0 ? 0 : -1}\n onClick={() => this.handleToggle()}\n onKeyDown={(e: KeyboardEvent) => this.handleRovingKeyDown(e, 0)}\n onFocus={() => {\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-focus-ring': this.bodyScrollable,\n ...scrollEdgeFadeClassMap({ edges: 'bottom' }),\n }}\n ref={element => {\n const next = (element as HTMLElement) ?? undefined;\n if (next === this.bodyEl) return;\n this.bodyEl = next;\n this.connectScrollRegionObserver();\n }}\n role={this.bodyScrollable ? 'region' : undefined}\n aria-label={this.bodyScrollable ? this.navigationItemsLabel : undefined}\n tabIndex={this.bodyScrollable ? 0 : undefined}\n >\n {(() => {\n let flatIdx = 0;\n return this.parsedGroups.map(group => (\n <div class=\"panel-nav__group\">\n {group.label && (\n <ds-text\n class=\"panel-nav__group-label 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 => this.renderNavItem(item, flatIdx++, collapsed))}\n </div>\n ));\n })()}\n </div>\n\n {/* ── Footer ── */}\n <div class=\"panel-nav__footer\">\n {this.renderFooterAction(isDashboardChrome)}\n {this.renderFooterUser(collapsed, userName, userInitial)}\n </div>\n </nav>\n\n {/* Drag-to-resize handle — always rendered, hidden only when auto-collapsed by breakpoint */}\n {!this.viewportNarrow && (\n <div\n class={{\n 'panel-nav__resize-handle': true,\n 'panel-nav__resize-handle--overlay': this.showEdgeOverlay,\n }}\n onMouseEnter={() => this.handleResizeHandleMouseEnter()}\n onMouseLeave={() => this.handleResizeHandleMouseLeave()}\n onMouseDown={(e: MouseEvent) => this.handleResizeHandleMouseDown(e)}\n aria-hidden=\"true\"\n />\n )}\n </Host>\n );\n }\n}\n",
626
+ "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 deriveActiveIdFromUrl,\n panelNavWidthTransitionMs,\n parsePanelNavGroups,\n resolvePanelNavDisableVt,\n resolvePanelNavStyle,\n resolvePanelNavToggle,\n shouldResyncPanelNavGroups,\n shouldResyncPanelNavStyle,\n} from './panel-nav-utils';\nimport {\n PANEL_NAV_USER_MENU_ANCHOR_ID,\n PANEL_NAV_USER_MENU_PLACEMENT,\n type PanelNavGroup,\n type PanelNavItem,\n type PanelNavRouterMode,\n type PanelNavUserActionDetail,\n} from './panel-nav-types';\nimport { scrollEdgeFadeClassMap } from '../../utils/scroll-edge-fade';\nimport { prefersReducedMotion } from '../../utils/resolve-css-time-ms';\n\n@Component({\n tag: 'ds-panel-nav',\n styleUrl: 'PanelNav.css',\n styleUrls: ['../../utils/scroll-edge-fade.css'],\n scoped: true,\n})\nexport class PanelNav {\n /** Style slot: `dashboard` or `settings`. Colors match for now;\n * class hooks reserved for texture/glyph layers. Property: `navStyle`. Attribute: `nav-style`. */\n @Prop({ attribute: 'nav-style', reflect: true }) navStyle: NavChromeStyle = 'dashboard';\n\n /** When `true`, style changes apply synchronously — host app owns view transitions. */\n @Prop() disableViewTransition: boolean = false;\n\n /** Nav groups — set via JS property (`el.groups = [...]`) or JSON string attribute. */\n @Prop() groups: string | PanelNavGroup[] = '[]';\n\n /** How items with `href` render:\n * - `anchor` (default): native `<a href>` — works with routers that intercept anchors.\n * - `event`: always `<button>`; host handles navigation via `dsNavSelect`. */\n @Prop() routerMode: PanelNavRouterMode = 'anchor';\n\n /** ID of the currently active/selected nav item. Overridden by `currentUrl` matching when set. */\n @Prop() activeId: string = '';\n\n /** Whether the nav is in collapsed (icon-only) state.\n * Set `storageKey` to persist across reloads. `dsNavToggle` still fires on change. */\n @Prop({ mutable: true }) collapsed: boolean = false;\n\n /** Viewport width (px) below which the nav locks in icon-only mode. 0 = disabled. */\n @Prop() breakpoint: number = 0;\n\n /** `localStorage` key used to persist the collapsed state across page loads.\n * When set, collapsed state is restored on mount and written on each toggle. */\n @Prop() storageKey: string = '';\n\n /** Current route URL (e.g. `window.location.pathname` or the router's active URL).\n * When set the component derives the active item by matching item `href` values\n * against this string (longest segment-boundary prefix wins), overriding `activeId`. */\n @Prop() currentUrl: string = '';\n\n /** Display name for the footer user section */\n @Prop() userName: string = '';\n\n /** Single character shown in the collapsed avatar */\n @Prop() userInitial: string = '';\n\n @Prop() dashboardLabel: string = 'Dashboard';\n @Prop() settingsLabel: string = 'Settings';\n @Prop() accountLabel: string = 'Account';\n /** Keep the account-menu trigger visually active 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 the collapse toggle is clicked. Detail = new collapsed state. */\n @Event() dsNavToggle!: EventEmitter<boolean>;\n\n /** Bubbling lifecycle — `ds-app-shell` pauses chrome metrics during width motion. */\n @Event({ bubbles: true, composed: true })\n dsChromeTransitionStart!: EventEmitter<ChromeTransitionDetail>;\n\n @Event({ bubbles: true, composed: true })\n dsChromeTransitionEnd!: EventEmitter<ChromeTransitionDetail>;\n\n /** Emitted when the footer left button (gear / dashboard) is clicked. */\n @Event() dsNavFooterAction!: EventEmitter<void>;\n\n /** Emitted when the footer user button is clicked. Detail includes the anchor for `ds-menu`. */\n @Event() dsNavUserAction!: EventEmitter<PanelNavUserActionDetail>;\n\n @Element() el!: HTMLElement;\n\n @State() private renderedStyle: NavChromeStyle = 'dashboard';\n @State() private parsedGroups: PanelNavGroup[] = [];\n @State() private isAnimating = false;\n @State() private rovingIndex: number = 0;\n @State() private viewportNarrow: boolean = false;\n @State() private urlDerivedActiveId: string = '';\n @State() private bodyScrollable = false;\n\n private transitionCompletionHandler?: (e: TransitionEvent) => void;\n private transitionFallbackTimer: number | null = null;\n private resizeObserver?: ResizeObserver;\n private scrollRegionObserver?: ResizeObserver;\n private bodyEl?: HTMLElement;\n private initialRenderComplete = false;\n\n // Drag-to-resize state (not @State — no re-render needed)\n private isDragging = false;\n private dragStartX = 0;\n private didSnap = false;\n private lastDeltaX = 0;\n private edgeOverlayTimer: number | null = null;\n private globalMouseMoveHandler?: (e: MouseEvent) => void;\n private globalMouseUpHandler?: () => void;\n\n @State() private showEdgeOverlay = false;\n\n private static readonly HOST_PROP_SYNC_BUDGET = 8;\n\n /** The ID that should be treated as active. `currentUrl` matching takes precedence\n * over `activeId` when both are present. */\n private get effectiveActiveId(): string {\n return this.urlDerivedActiveId || this.activeId;\n }\n\n private get effectiveDisableViewTransition(): boolean {\n return resolvePanelNavDisableVt(\n this.disableViewTransition,\n this.el.getAttribute('disable-view-transition')\n );\n }\n\n @Watch('collapsed')\n onCollapsedChange(_next: boolean, prev: boolean | undefined) {\n if (prev === undefined || !this.initialRenderComplete) return;\n this.startCollapseAnimation();\n }\n\n @Watch('viewportNarrow')\n onViewportNarrowChange(next: boolean, prev: boolean | undefined) {\n if (prev === undefined || !this.initialRenderComplete) return;\n if (next && this.rovingIndex === 0) {\n this.rovingIndex = this.getFirstRovingIndex();\n }\n this.startCollapseAnimation();\n }\n\n @Watch('breakpoint')\n onBreakpointChange() {\n this.disconnectResizeObserver();\n if (this.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 }\n\n @Watch('activeId')\n @Watch('urlDerivedActiveId')\n onActiveIdChange() {\n /* Active route updates aria-current only — roving tab stop stays user-controlled. */\n }\n\n @Watch('currentUrl')\n onCurrentUrlChange() {\n this.syncActiveFromUrl();\n }\n\n @Watch('navStyle')\n onNavStyleChange(newVal: NavChromeStyle) {\n this.renderedStyle = newVal;\n }\n\n componentWillLoad() {\n this.renderedStyle = resolvePanelNavStyle(this.navStyle, this.el.getAttribute('nav-style'));\n 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 }\n\n disconnectedCallback() {\n this.disconnectResizeObserver();\n this.disconnectScrollRegionObserver();\n this.clearCollapseAnimationCompletion(\n this.el.querySelector('.panel-nav') as HTMLElement | null\n );\n this.clearEdgeOverlayTimer();\n if (this.globalMouseMoveHandler) {\n window.removeEventListener('mousemove', this.globalMouseMoveHandler);\n }\n if (this.globalMouseUpHandler) {\n window.removeEventListener('mouseup', this.globalMouseUpHandler);\n }\n }\n\n private startCollapseAnimation() {\n if (this.isAnimating) return;\n this.isAnimating = true;\n this.dsChromeTransitionStart.emit({ source: 'panel-nav' });\n const panel = this.el.querySelector('.panel-nav') as HTMLElement | null;\n const widthBefore = panel?.getBoundingClientRect().width ?? 0;\n this.clearCollapseAnimationCompletion(panel);\n this.transitionCompletionHandler = (e: TransitionEvent) => {\n if (e.propertyName === 'width' || e.propertyName === 'min-width') {\n this.finishCollapseAnimation(panel);\n }\n };\n panel?.addEventListener('transitionend', this.transitionCompletionHandler);\n panel?.addEventListener('transitioncancel', this.transitionCompletionHandler);\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n if (!this.isAnimating) return;\n const widthAfter = panel?.getBoundingClientRect().width ?? 0;\n if (Math.abs(widthAfter - widthBefore) < 1) {\n this.finishCollapseAnimation(panel);\n return;\n }\n if (!panel) {\n this.finishCollapseAnimation(panel);\n return;\n }\n\n const fallbackMs = panelNavWidthTransitionMs(getComputedStyle(panel));\n if (fallbackMs <= 0) {\n this.finishCollapseAnimation(panel);\n return;\n }\n this.transitionFallbackTimer = window.setTimeout(() => {\n this.finishCollapseAnimation(panel);\n }, fallbackMs);\n });\n });\n }\n\n private finishCollapseAnimation(panel: HTMLElement | null) {\n const wasAnimating = this.isAnimating;\n this.clearCollapseAnimationCompletion(panel);\n if (!wasAnimating) return;\n this.isAnimating = false;\n this.dsChromeTransitionEnd.emit({ source: 'panel-nav' });\n }\n\n private clearCollapseAnimationCompletion(panel: HTMLElement | null) {\n if (this.transitionCompletionHandler) {\n panel?.removeEventListener('transitionend', this.transitionCompletionHandler);\n panel?.removeEventListener('transitioncancel', this.transitionCompletionHandler);\n this.transitionCompletionHandler = undefined;\n }\n if (this.transitionFallbackTimer !== null) {\n window.clearTimeout(this.transitionFallbackTimer);\n this.transitionFallbackTimer = null;\n }\n }\n\n private 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 return;\n }\n this.urlDerivedActiveId = deriveActiveIdFromUrl(this.currentUrl, this.getAllItems());\n }\n\n /** Re-parse props assigned by the host after componentWillLoad (Angular ngAfterViewInit). */\n private syncHostPropsIfNeeded() {\n if (shouldResyncPanelNavStyle(this.renderedStyle, this.navStyle)) {\n this.renderedStyle = this.navStyle;\n }\n\n if (shouldResyncPanelNavGroups(this.parsedGroups, this.groups)) {\n this.onGroupsChange(this.groups);\n } else if (this.currentUrl) {\n this.syncActiveFromUrl();\n }\n }\n\n /** Poll across animation frames — host props may land without triggering @Watch. */\n private scheduleDeferredHostPropSync() {\n let remaining = PanelNav.HOST_PROP_SYNC_BUDGET;\n const tick = () => {\n this.syncHostPropsIfNeeded();\n if (--remaining > 0) {\n requestAnimationFrame(tick);\n }\n };\n queueMicrotask(tick);\n }\n\n /** Centralised toggle: updates the desktop preference unless breakpoint-locked. */\n private applyToggle(next: boolean) {\n if (this.viewportNarrow) return;\n this.collapsed = next;\n if (this.storageKey) {\n try {\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 getFirstRovingIndex(): number {\n return this.viewportNarrow ? 1 : 0;\n }\n\n private getFooterRovingIndex(): number {\n return 1 + this.getAllItems().length;\n }\n\n private getUserRovingIndex(): number {\n return this.getFooterRovingIndex() + 1;\n }\n\n private getRovingElement(index: number): HTMLElement | null {\n const itemCount = this.getAllItems().length;\n if (index === 0) {\n return this.viewportNarrow ? null : this.el.querySelector('.panel-nav__header-btn');\n }\n if (index >= 1 && index <= itemCount) {\n const items = this.el.querySelectorAll<HTMLElement>('.panel-nav__body .panel-nav__item');\n return items[index - 1] ?? null;\n }\n if (index === this.getFooterRovingIndex()) {\n return this.el.querySelector('.panel-nav__footer-btn .button-unfilled');\n }\n if (index === this.getUserRovingIndex()) {\n return this.el.querySelector('.panel-nav__footer-user');\n }\n return null;\n }\n\n private focusRovingAt(index: number) {\n this.rovingIndex = index;\n this.getRovingElement(index)?.focus({ preventScroll: true });\n }\n\n private activateRovingIndex(index: number) {\n const itemCount = this.getAllItems().length;\n const items = this.getAllItems();\n if (index === 0) {\n this.handleToggle();\n return;\n }\n if (index >= 1 && index <= itemCount) {\n this.handleItemClick(items[index - 1].id);\n return;\n }\n if (index === this.getFooterRovingIndex()) {\n this.handleFooterAction();\n return;\n }\n if (index === this.getUserRovingIndex()) {\n const anchor = this.el.querySelector(\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);\n return;\n }\n\n const footerIdx = this.getFooterRovingIndex();\n const userIdx = this.getUserRovingIndex();\n const firstIdx = this.getFirstRovingIndex();\n let next: number | undefined;\n\n switch (e.key) {\n case 'ArrowDown':\n if (index === userIdx) return;\n if (index === footerIdx) next = userIdx;\n else next = index + 1;\n break;\n case 'ArrowUp':\n if (index === firstIdx) return;\n if (index === userIdx) next = footerIdx;\n else if (index === footerIdx) next = footerIdx - 1;\n else next = index - 1;\n break;\n case 'ArrowRight':\n if (index === footerIdx) next = userIdx;\n else return;\n break;\n case 'ArrowLeft':\n if (index === userIdx) next = footerIdx;\n else return;\n break;\n case 'Home':\n e.preventDefault();\n next = firstIdx;\n break;\n case 'End':\n e.preventDefault();\n next = userIdx;\n break;\n default:\n return;\n }\n\n if (next === undefined || next === index) return;\n e.preventDefault();\n this.focusRovingAt(next);\n }\n\n private handleItemClick(id: string) {\n this.dsNavSelect.emit(id);\n }\n\n private handleToggle() {\n const next = resolvePanelNavToggle(this.collapsed, this.viewportNarrow);\n if (next !== null) this.applyToggle(next);\n }\n\n /** Toggle the desktop preference. No-op while the breakpoint locks the panel collapsed. */\n @Method()\n async toggleCollapsed() {\n this.handleToggle();\n }\n\n private handleFooterAction() {\n this.dsNavFooterAction.emit();\n }\n\n private handleUserAction(e: MouseEvent) {\n const anchor = e.currentTarget as HTMLElement;\n this.dsNavUserAction.emit({ anchor, menuPlacement: PANEL_NAV_USER_MENU_PLACEMENT });\n }\n\n private clearEdgeOverlayTimer() {\n if (this.edgeOverlayTimer !== null) {\n window.clearTimeout(this.edgeOverlayTimer);\n this.edgeOverlayTimer = null;\n }\n }\n\n private handleResizeHandleMouseEnter() {\n this.clearEdgeOverlayTimer();\n if (prefersReducedMotion()) {\n this.showEdgeOverlay = true;\n return;\n }\n this.edgeOverlayTimer = window.setTimeout(() => {\n this.showEdgeOverlay = true;\n this.edgeOverlayTimer = null;\n }, 500);\n }\n\n private handleResizeHandleMouseLeave() {\n if (this.isDragging) return;\n this.clearEdgeOverlayTimer();\n this.showEdgeOverlay = false;\n }\n\n private handleResizeHandleMouseDown(e: MouseEvent) {\n e.preventDefault();\n e.stopPropagation();\n\n const wasCollapsed = this.collapsed || this.viewportNarrow;\n this.isDragging = true;\n this.dragStartX = e.clientX;\n this.didSnap = false;\n this.lastDeltaX = 0;\n this.clearEdgeOverlayTimer();\n this.showEdgeOverlay = false;\n\n document.body.style.cursor = 'ew-resize';\n document.body.style.userSelect = 'none';\n\n const onMove = (ev: MouseEvent) => {\n const deltaX = ev.clientX - this.dragStartX;\n this.lastDeltaX = Math.abs(deltaX);\n if (this.didSnap) return;\n if (!wasCollapsed && deltaX < -8) {\n this.applyToggle(true);\n this.didSnap = true;\n } else if (wasCollapsed && deltaX > 8) {\n this.applyToggle(false);\n this.didSnap = true;\n }\n };\n\n const onUp = () => {\n if (!this.didSnap && this.lastDeltaX < 3) {\n this.applyToggle(!wasCollapsed);\n }\n this.isDragging = false;\n this.showEdgeOverlay = false;\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n window.removeEventListener('mousemove', onMove);\n window.removeEventListener('mouseup', onUp);\n this.globalMouseMoveHandler = undefined;\n this.globalMouseUpHandler = undefined;\n };\n\n this.globalMouseMoveHandler = onMove;\n this.globalMouseUpHandler = onUp;\n window.addEventListener('mousemove', onMove);\n window.addEventListener('mouseup', onUp);\n }\n\n private renderFooterAction(isDashboardChrome: boolean) {\n // Short chrome labels — same string for tip + aria (not \"Open/Go to …\").\n const footerLabel = isDashboardChrome ? this.settingsLabel : this.dashboardLabel;\n return (\n <ds-tooltip label={footerLabel} side=\"right\" size=\"sm\">\n <ds-button-unfilled\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__item--active': this.accountMenuExpanded,\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 >\n {userName}\n </ds-text>\n <span class=\"panel-nav__item-icon panel-nav__footer-user-icon\" aria-hidden=\"true\">\n <span class=\"panel-nav__footer-icon-expanded\">\n <ds-icon name=\"ChevronUpDown\" size=\"md\" color=\"inherit\" />\n </span>\n <span class=\"panel-nav__footer-icon-collapsed\">\n <ds-icon name=\"Circle\" size=\"md\" color=\"inherit\" />\n <ds-text\n class=\"panel-nav__user-initial\"\n as=\"span\"\n variant=\"text-caption\"\n emphasis\n align=\"center\"\n color=\"inherit\"\n >\n {userInitial}\n </ds-text>\n </span>\n </span>\n </button>\n );\n\n return (\n <ds-tooltip label={tipLabel} side=\"right\" size=\"sm\">\n {userButton}\n </ds-tooltip>\n );\n }\n\n private renderNavItem(item: PanelNavItem, idx: number, collapsed: boolean) {\n const isActive = item.id === this.effectiveActiveId;\n const rovingPosition = idx + 1;\n\n const itemContent = [\n <span class=\"panel-nav__item-icon\">\n <ds-icon name={item.icon} size=\"md\" color=\"inherit\" />\n </span>,\n <ds-text\n class=\"panel-nav__item-label panel-nav__item-label-text ds-control--md\"\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis={isActive}\n color=\"inherit\"\n >\n {item.label}\n </ds-text>,\n item.dot && (\n <span class=\"panel-nav__item-dot-box\" aria-hidden=\"true\">\n <ds-badge class=\"panel-nav__item-dot\" variant=\"dot\" hasRing={collapsed} label=\"\" />\n </span>\n ),\n ];\n\n const sharedProps = {\n class: {\n 'panel-nav__item': true,\n 'panel-nav__item--active': isActive,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n },\n 'aria-current': isActive ? ('page' as const) : undefined,\n tabIndex: rovingPosition === this.rovingIndex ? 0 : -1,\n onClick: () => this.handleItemClick(item.id),\n onKeyDown: (e: KeyboardEvent) => this.handleRovingKeyDown(e, rovingPosition),\n onFocus: () => {\n this.rovingIndex = rovingPosition;\n },\n };\n\n const useAnchor = this.routerMode === 'anchor' && item.href;\n const control = useAnchor ? (\n <a {...sharedProps} href={item.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 render() {\n const chromeStyle = this.renderedStyle;\n const isDashboardChrome = chromeStyle === 'dashboard';\n const isDashboard = this.navStyle === 'dashboard';\n const collapsed = this.collapsed || this.viewportNarrow;\n const { userName, userInitial } = this;\n\n const navCls: Record<string, boolean> = {\n 'panel-nav': true,\n 'panel-nav--dashboard': isDashboard,\n 'panel-nav--settings': !isDashboard,\n 'panel-nav--collapsed': collapsed,\n 'panel-nav--breakpoint-locked': this.viewportNarrow,\n 'panel-nav--animating': this.isAnimating,\n };\n\n return (\n <Host style={{ display: 'block', position: 'relative' }}>\n <nav\n class={navCls}\n aria-label={\n isDashboardChrome ? this.dashboardNavigationLabel : this.settingsNavigationLabel\n }\n >\n {/* ── Header: Motive logo, reveals collapse toggle on hover ── */}\n <div class=\"panel-nav__header\">\n <button\n type=\"button\"\n class=\"panel-nav__header-btn ds-focus-ring-inset ds-interaction-fill\"\n disabled={this.viewportNarrow}\n tabIndex={!this.viewportNarrow && this.rovingIndex === 0 ? 0 : -1}\n onClick={() => this.handleToggle()}\n onKeyDown={(e: KeyboardEvent) => this.handleRovingKeyDown(e, 0)}\n onFocus={() => {\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-focus-ring': this.bodyScrollable,\n ...scrollEdgeFadeClassMap({ edges: 'bottom' }),\n }}\n ref={element => {\n const next = (element as HTMLElement) ?? undefined;\n if (next === this.bodyEl) return;\n this.bodyEl = next;\n this.connectScrollRegionObserver();\n }}\n role={this.bodyScrollable ? 'region' : undefined}\n aria-label={this.bodyScrollable ? this.navigationItemsLabel : undefined}\n tabIndex={this.bodyScrollable ? 0 : undefined}\n >\n {(() => {\n let flatIdx = 0;\n return this.parsedGroups.map(group => (\n <div class=\"panel-nav__group\">\n {group.label && (\n <ds-text\n class=\"panel-nav__group-label 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 => this.renderNavItem(item, flatIdx++, collapsed))}\n </div>\n ));\n })()}\n </div>\n\n {/* ── Footer ── */}\n <div class=\"panel-nav__footer\">\n {this.renderFooterAction(isDashboardChrome)}\n {this.renderFooterUser(collapsed, userName, userInitial)}\n </div>\n </nav>\n\n {/* Drag-to-resize handle — always rendered, hidden only when auto-collapsed by breakpoint */}\n {!this.viewportNarrow && (\n <div\n class={{\n 'panel-nav__resize-handle': true,\n 'panel-nav__resize-handle--overlay': this.showEdgeOverlay,\n }}\n onMouseEnter={() => this.handleResizeHandleMouseEnter()}\n onMouseLeave={() => this.handleResizeHandleMouseLeave()}\n onMouseDown={(e: MouseEvent) => this.handleResizeHandleMouseDown(e)}\n aria-hidden=\"true\"\n />\n )}\n </Host>\n );\n }\n}\n",
609
627
  "type": "registry:ui"
610
628
  },
611
629
  {
@@ -335,7 +335,7 @@
335
335
  },
336
336
  {
337
337
  "path": "src/wc/components/PanelToolHeader/PanelToolHeader.tsx",
338
- "content": "import { Component, Element, Event, EventEmitter, h, Host, Method, Prop } from '@stencil/core';\nimport type { PanelToolsHeaderAction } from '../PanelTools/panel-tools-types';\n\n@Component({\n tag: 'ds-panel-tool-header',\n styleUrl: 'PanelToolHeader.css',\n scoped: true,\n})\nexport class PanelToolHeader {\n @Element() el!: HTMLElement;\n\n /** Visible heading for the active tool view. */\n @Prop() heading: string = '';\n /** Show the leading Back action. */\n @Prop() showBack: boolean = false;\n /** Canonical icon for the leading navigation or dismiss action. */\n @Prop() backIcon: string = 'ChevronLeft';\n /** Accessible name for the leading navigation or dismiss action. */\n @Prop() backAriaLabel: string = 'Back';\n /** Show the trailing options-menu trigger. */\n @Prop() showMenu: boolean = true;\n /** Accessible name for the options-menu trigger. */\n @Prop() menuAriaLabel: string = 'More options';\n /** Stable id placed on the menu trigger host for ds-menu anchoring. */\n @Prop() menuTriggerId: string = '';\n /** Id of the menu controlled by the trigger. */\n @Prop() menuControls: string | undefined;\n /** Whether the controlled menu is open. */\n @Prop() menuExpanded: boolean = false;\n /** Trailing application-owned icon actions. When present, these replace the legacy menu action. */\n @Prop() actions: PanelToolsHeaderAction[] = [];\n\n @Event() dsBack!: EventEmitter<MouseEvent>;\n @Event() dsMenuToggle!: EventEmitter<MouseEvent>;\n @Event() dsAction!: EventEmitter<{ id: string; originalEvent: MouseEvent }>;\n\n @Method()\n async focusMenuTrigger() {\n const trigger = this.el.querySelector(\n 'ds-button-unfilled.panel-tool-header__menu, ds-button-unfilled[data-header-action-id=\"menu\"]'\n ) as (HTMLElement & { setFocus?: () => Promise<void> }) | null;\n await trigger?.setFocus?.();\n }\n\n render() {\n const authoredActions = this.actions.length > 0;\n const actions: PanelToolsHeaderAction[] = authoredActions\n ? this.actions\n : this.showMenu\n ? [\n {\n id: 'menu',\n icon: 'Ellipses',\n ariaLabel: this.menuAriaLabel,\n triggerId: this.menuTriggerId,\n controls: this.menuControls,\n expanded: this.menuExpanded,\n haspopup: 'menu',\n },\n ]\n : [];\n\n return (\n <Host>\n <header class=\"panel-tool-header\">\n <div class=\"panel-tool-header__leading\">\n {this.showBack ? (\n <ds-button-unfilled\n class=\"panel-tool-header__back\"\n variant=\"icon\"\n icon={this.backIcon}\n size=\"md\"\n aria-label={this.backAriaLabel}\n activeFill={false}\n hasBorder={false}\n onDsClick={(event: CustomEvent<MouseEvent>) => this.dsBack.emit(event.detail)}\n />\n ) : null}\n </div>\n <ds-text\n class=\"panel-tool-header__heading ds-control--md\"\n as=\"h2\"\n variant=\"text-body-medium\"\n emphasis\n color=\"primary\"\n lineTruncation={1}\n >\n {this.heading}\n </ds-text>\n <div class=\"panel-tool-header__trailing\">\n {actions.map(action => (\n <ds-button-unfilled\n key={action.id}\n id={action.triggerId || undefined}\n data-header-action-id={action.id}\n class={{\n 'panel-tool-header__action': true,\n 'panel-tool-header__menu': action.id === 'menu',\n }}\n variant=\"icon\"\n icon={action.icon}\n size=\"md\"\n aria-label={action.ariaLabel}\n haspopup={action.haspopup}\n controls={action.controls}\n expanded={action.expanded}\n pressed={action.pressed}\n isInactive={action.isInactive}\n activeFill={false}\n hasBorder={false}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n if (!authoredActions && action.id === 'menu')\n this.dsMenuToggle.emit(event.detail);\n this.dsAction.emit({ id: action.id, originalEvent: event.detail });\n }}\n />\n ))}\n </div>\n </header>\n </Host>\n );\n }\n}\n",
338
+ "content": "import { Component, Element, Event, EventEmitter, h, Host, Method, Prop } from '@stencil/core';\nimport type { PanelToolsHeaderAction } from '../PanelTools/panel-tools-types';\n\n@Component({\n tag: 'ds-panel-tool-header',\n styleUrl: 'PanelToolHeader.css',\n scoped: true,\n})\nexport class PanelToolHeader {\n @Element() el!: HTMLElement;\n\n /** Visible heading for the active tool view. */\n @Prop() heading: string = '';\n /** Show the leading Back action. */\n @Prop() showBack: boolean = false;\n /** Canonical icon for the leading navigation or dismiss action. */\n @Prop() backIcon: string = 'ChevronLeft';\n /** Accessible name for the leading navigation or dismiss action. */\n @Prop() backAriaLabel: string = 'Back';\n /** Show the trailing options-menu trigger. */\n @Prop() showMenu: boolean = true;\n /** Accessible name for the options-menu trigger. */\n @Prop() menuAriaLabel: string = 'More options';\n /** Stable id placed on the menu trigger host for ds-menu anchoring. */\n @Prop() menuTriggerId: string = '';\n /** Id of the menu controlled by the trigger. */\n @Prop() menuControls: string | undefined;\n /** Whether the controlled menu is open. */\n @Prop() menuExpanded: boolean = false;\n /** Trailing application-owned icon actions. When present, these replace the legacy menu action. */\n @Prop() actions: PanelToolsHeaderAction[] = [];\n\n @Event() dsBack!: EventEmitter<MouseEvent>;\n @Event() dsMenuToggle!: EventEmitter<MouseEvent>;\n @Event() dsAction!: EventEmitter<{ id: string; originalEvent: MouseEvent }>;\n\n @Method()\n async focusMenuTrigger() {\n const trigger = this.el.querySelector(\n 'ds-button-unfilled.panel-tool-header__menu, ds-button-unfilled[data-header-action-id=\"menu\"]'\n ) as (HTMLElement & { setFocus?: () => Promise<void> }) | null;\n await trigger?.setFocus?.();\n }\n\n render() {\n const authoredActions = this.actions.length > 0;\n const actions: PanelToolsHeaderAction[] = authoredActions\n ? this.actions\n : this.showMenu\n ? [\n {\n id: 'menu',\n icon: 'Ellipses',\n ariaLabel: this.menuAriaLabel,\n triggerId: this.menuTriggerId,\n controls: this.menuControls,\n expanded: this.menuExpanded,\n haspopup: 'menu',\n },\n ]\n : [];\n\n return (\n <Host>\n <header class=\"panel-tool-header\">\n <div class=\"panel-tool-header__leading\">\n {this.showBack ? (\n <ds-button-unfilled\n class=\"panel-tool-header__back\"\n variant=\"icon\"\n icon={this.backIcon}\n size=\"md\"\n aria-label={this.backAriaLabel}\n activeFill={false}\n hasBorder={false}\n onDsClick={(event: CustomEvent<MouseEvent>) => this.dsBack.emit(event.detail)}\n />\n ) : null}\n </div>\n <ds-text\n class=\"panel-tool-header__heading ds-control--md\"\n as=\"h2\"\n variant=\"text-body-medium\"\n emphasis\n color=\"primary\"\n lineTruncation={1}\n >\n {this.heading}\n </ds-text>\n <div class=\"panel-tool-header__trailing\">\n {actions.map(action => (\n <ds-button-unfilled\n key={action.id}\n id={action.triggerId || undefined}\n data-header-action-id={action.id}\n class={{\n 'panel-tool-header__action': true,\n 'panel-tool-header__menu': action.id === 'menu',\n }}\n variant=\"icon\"\n icon={action.icon}\n size=\"md\"\n aria-label={action.ariaLabel}\n haspopup={action.haspopup}\n controls={action.controls}\n expanded={action.expanded}\n pressed={action.pressed}\n isActive={!!action.expanded}\n isInactive={action.isInactive}\n activeFill={false}\n hasBorder={false}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n if (!authoredActions && action.id === 'menu')\n this.dsMenuToggle.emit(event.detail);\n this.dsAction.emit({ id: action.id, originalEvent: event.detail });\n }}\n />\n ))}\n </div>\n </header>\n </Host>\n );\n }\n}\n",
339
339
  "type": "registry:ui"
340
340
  }
341
341
  ]
@@ -492,7 +492,7 @@
492
492
  "files": [
493
493
  {
494
494
  "path": "src/wc/components/PanelTools/PanelTools.css",
495
- "content": "@import '../../utils/control-density.css';\n\n:host {\n display: block;\n flex: 0 0 auto;\n box-sizing: border-box;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n --_panel-tools-rail-width: var(--dimension-size-600);\n --_panel-tools-drawer-width: var(--ds-shell-panel-tools-width, var(--dimension-panel-width-xs));\n --_panel-tools-border: var(--dimension-stroke-width-012, 1px) solid var(--color-border-tertiary);\n /* Match panel-nav expand timing (medium-1); easing differs by direction. */\n --_panel-tools-expand-dur: var(--effect-animation-duration-medium-1, 300ms);\n}\n\n.panel-tools__layout {\n display: flex;\n flex-direction: row-reverse;\n align-items: stretch;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n}\n\n/* Clip frame — only max-width animates (slide reveal, same as pre-rail :host). */\n.panel-tools__drawer {\n flex: 0 0 auto;\n max-width: 0;\n min-width: 0;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n box-sizing: border-box;\n transition: none;\n}\n\n:host(.panel-tools--ready) .panel-tools__drawer {\n transition: max-width var(--_panel-tools-expand-dur)\n var(--effect-animation-easing-ease-in-out, ease);\n}\n\n:host(.panel-tools--ready.panel-tools--motion-opening) .panel-tools__drawer {\n transition: max-width var(--_panel-tools-expand-dur)\n var(--effect-animation-easing-ease-out, ease-out);\n}\n\n:host(.panel-tools--ready.panel-tools--motion-closing) .panel-tools__drawer {\n transition: max-width var(--_panel-tools-expand-dur)\n var(--effect-animation-easing-ease-in, ease-in);\n}\n\n:host(.panel-tools--open) .panel-tools__drawer {\n max-width: var(--_panel-tools-drawer-width);\n}\n\n.panel-tools__drawer--visible {\n border-left: var(--_panel-tools-border);\n}\n\n/* Fixed-width surface — AppShell supplies the standard 300px tools width. */\n.panel-tools__drawer-surface {\n display: flex;\n flex-direction: column;\n width: var(--_panel-tools-drawer-width);\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n background-color: transparent;\n}\n\n.panel-tools__full-views,\n.panel-tools__full-view {\n flex: 1 1 auto;\n min-height: 0;\n overflow: hidden;\n}\n\n.panel-tools__full-view {\n height: 100%;\n}\n\n.panel-tools__full-view {\n display: none;\n}\n.panel-tools__full-view--active {\n display: block;\n}\n\n:host([presentation='fullscreen']) {\n height: 100vh;\n inset: 0;\n position: fixed;\n width: 100vw;\n z-index: var(--dimension-z-index-modal);\n}\n\n:host([presentation='fullscreen']) .panel-tools__rail {\n display: none;\n}\n:host([presentation='fullscreen']) .panel-tools__drawer {\n max-width: 100vw;\n transition: none;\n width: 100vw;\n}\n:host([presentation='fullscreen']) .panel-tools__drawer-surface {\n background-color: transparent;\n width: 100vw;\n}\n\n:host(.panel-tools--presentation-snap) .panel-tools__drawer {\n transition: none;\n}\n\n.panel-tools__body {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n position: relative;\n}\n\n/* Skip paint while fully closed; cleared during open/close motion for WYSIWYG slide. */\n:host(.panel-tools--drawer-resting) .panel-tools__body {\n content-visibility: auto;\n contain-intrinsic-size: auto 200px;\n}\n\n.panel-tools__view {\n display: none;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n padding: var(--dimension-space-200);\n color: var(--color-foreground-secondary);\n font-family: var(--typography-font-family);\n font-size: var(--typography-fontsize-md);\n line-height: var(--typography-lineheight-md);\n font-weight: var(--typography-weight-regular);\n letter-spacing: var(--typography-letterspacing-negative-half);\n}\n\n.panel-tools__view ::slotted(*) {\n margin: 0;\n color: var(--color-foreground-secondary);\n font-family: var(--typography-font-family);\n font-size: var(--typography-fontsize-md);\n line-height: var(--typography-lineheight-md);\n font-weight: var(--typography-weight-regular);\n letter-spacing: var(--typography-letterspacing-negative-half);\n}\n\n.panel-tools__view--active {\n display: block;\n}\n\n/* Rail — mirrors collapsed panel-nav width, padding, and item rhythm. */\n.panel-tools__rail {\n flex: 0 0 var(--_panel-tools-rail-width);\n width: var(--_panel-tools-rail-width);\n min-width: var(--_panel-tools-rail-width);\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n height: 100%;\n min-height: 0;\n border-left: var(--_panel-tools-border);\n background-color: transparent;\n}\n\n.panel-tools__rail-header {\n display: flex;\n align-items: center;\n justify-content: center;\n height: var(--dimension-size-600);\n padding: var(--dimension-space-100);\n flex-shrink: 0;\n box-sizing: border-box;\n border-bottom: var(--_panel-tools-border);\n}\n\n.panel-tools__rail-body {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050);\n padding: var(--dimension-space-100);\n box-sizing: border-box;\n scrollbar-width: none;\n}\n\n.panel-tools__rail-body::-webkit-scrollbar {\n display: none;\n}\n\n/* Flush to the bottom of the rail — mirrors Search header pin. */\n.panel-tools__rail-footer {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n margin-top: auto;\n padding: var(--dimension-space-100);\n box-sizing: border-box;\n}\n\n.panel-tools__rail-action {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: var(--ds-control-height);\n flex-shrink: 0;\n}\n\n@media (prefers-reduced-motion: reduce) {\n :host(.panel-tools--ready.panel-tools--motion-opening) .panel-tools__drawer,\n :host(.panel-tools--ready.panel-tools--motion-closing) .panel-tools__drawer {\n transition: none;\n }\n}\n",
495
+ "content": "@import '../../utils/control-density.css';\n\n:host {\n display: block;\n flex: 0 0 auto;\n box-sizing: border-box;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n --_panel-tools-rail-width: var(--dimension-size-600);\n --_panel-tools-drawer-width: var(--ds-shell-panel-tools-width, var(--dimension-panel-width-xs));\n --_panel-tools-border: var(--dimension-stroke-width-012, 1px) solid var(--color-border-tertiary);\n /* Match panel-nav expand timing (medium-1); easing differs by direction. */\n --_panel-tools-expand-dur: var(--effect-animation-duration-medium-1, 300ms);\n}\n\n.panel-tools__layout {\n display: flex;\n flex-direction: row-reverse;\n align-items: stretch;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n}\n\n/* Clip frame — only max-width animates (slide reveal, same as pre-rail :host). */\n.panel-tools__drawer {\n flex: 0 0 auto;\n max-width: 0;\n min-width: 0;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n box-sizing: border-box;\n transition: none;\n}\n\n:host(.panel-tools--ready) .panel-tools__drawer {\n transition: max-width var(--_panel-tools-expand-dur)\n var(--effect-animation-easing-ease-in-out, ease);\n}\n\n:host(.panel-tools--ready.panel-tools--motion-opening) .panel-tools__drawer {\n transition: max-width var(--_panel-tools-expand-dur)\n var(--effect-animation-easing-ease-out, ease-out);\n}\n\n:host(.panel-tools--ready.panel-tools--motion-closing) .panel-tools__drawer {\n transition: max-width var(--_panel-tools-expand-dur)\n var(--effect-animation-easing-ease-in, ease-in);\n}\n\n:host(.panel-tools--open) .panel-tools__drawer {\n max-width: var(--_panel-tools-drawer-width);\n}\n\n.panel-tools__drawer--visible {\n border-left: var(--_panel-tools-border);\n}\n\n/* Fixed-width surface — AppShell supplies the standard 300px tools width. */\n.panel-tools__drawer-surface {\n display: flex;\n flex-direction: column;\n width: var(--_panel-tools-drawer-width);\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n background-color: transparent;\n}\n\n.panel-tools__full-views,\n.panel-tools__full-view {\n flex: 1 1 auto;\n min-height: 0;\n overflow: hidden;\n}\n\n.panel-tools__full-view {\n height: 100%;\n}\n\n.panel-tools__full-view {\n display: none;\n}\n.panel-tools__full-view--active {\n display: block;\n}\n\n:host([presentation='fullscreen']) {\n height: 100vh;\n inset: 0;\n position: fixed;\n width: 100vw;\n z-index: var(--dimension-z-index-modal);\n}\n\n:host([presentation='fullscreen']) .panel-tools__rail {\n display: none;\n}\n:host([presentation='fullscreen']) .panel-tools__drawer {\n max-width: 100vw;\n transition: none;\n width: 100vw;\n}\n:host([presentation='fullscreen']) .panel-tools__drawer--visible {\n border-left: none;\n}\n:host([presentation='fullscreen']) .panel-tools__drawer-surface {\n background-color: transparent;\n width: 100vw;\n}\n\n:host(.panel-tools--presentation-snap) .panel-tools__drawer {\n transition: none;\n}\n\n.panel-tools__body {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n position: relative;\n}\n\n/* Skip paint while fully closed; cleared during open/close motion for WYSIWYG slide. */\n:host(.panel-tools--drawer-resting) .panel-tools__body {\n content-visibility: auto;\n contain-intrinsic-size: auto 200px;\n}\n\n.panel-tools__view {\n display: none;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n padding: var(--dimension-space-200);\n color: var(--color-foreground-secondary);\n font-family: var(--typography-font-family);\n font-size: var(--typography-fontsize-md);\n line-height: var(--typography-lineheight-md);\n font-weight: var(--typography-weight-regular);\n letter-spacing: var(--typography-letterspacing-negative-half);\n}\n\n.panel-tools__view ::slotted(*) {\n margin: 0;\n color: var(--color-foreground-secondary);\n font-family: var(--typography-font-family);\n font-size: var(--typography-fontsize-md);\n line-height: var(--typography-lineheight-md);\n font-weight: var(--typography-weight-regular);\n letter-spacing: var(--typography-letterspacing-negative-half);\n}\n\n.panel-tools__view--active {\n display: block;\n}\n\n/* Rail — mirrors collapsed panel-nav width, padding, and item rhythm. */\n.panel-tools__rail {\n flex: 0 0 var(--_panel-tools-rail-width);\n width: var(--_panel-tools-rail-width);\n min-width: var(--_panel-tools-rail-width);\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n height: 100%;\n min-height: 0;\n border-left: var(--_panel-tools-border);\n background-color: transparent;\n}\n\n.panel-tools__rail-header {\n display: flex;\n align-items: center;\n justify-content: center;\n height: var(--dimension-size-600);\n padding: var(--dimension-space-100);\n flex-shrink: 0;\n box-sizing: border-box;\n border-bottom: var(--_panel-tools-border);\n}\n\n.panel-tools__rail-body {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050);\n padding: var(--dimension-space-100);\n box-sizing: border-box;\n scrollbar-width: none;\n}\n\n.panel-tools__rail-body::-webkit-scrollbar {\n display: none;\n}\n\n/* Flush to the bottom of the rail — mirrors Search header pin. */\n.panel-tools__rail-footer {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n margin-top: auto;\n padding: var(--dimension-space-100);\n box-sizing: border-box;\n}\n\n.panel-tools__rail-action {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: var(--ds-control-height);\n flex-shrink: 0;\n}\n\n@media (prefers-reduced-motion: reduce) {\n :host(.panel-tools--ready.panel-tools--motion-opening) .panel-tools__drawer,\n :host(.panel-tools--ready.panel-tools--motion-closing) .panel-tools__drawer {\n transition: none;\n }\n}\n",
496
496
  "type": "registry:ui"
497
497
  },
498
498
  {
@@ -601,6 +601,10 @@
601
601
  "type": "string",
602
602
  "required": false
603
603
  },
604
+ "iconColor": {
605
+ "type": "IconColor",
606
+ "required": false
607
+ },
604
608
  "label": {
605
609
  "type": "string | undefined",
606
610
  "required": false
@@ -2289,6 +2293,10 @@
2289
2293
  ]
2290
2294
  },
2291
2295
  "props": {
2296
+ "actionsOpen": {
2297
+ "type": "boolean",
2298
+ "required": false
2299
+ },
2292
2300
  "conversationId": {
2293
2301
  "type": "string",
2294
2302
  "required": false
@@ -3440,6 +3448,10 @@
3440
3448
  }
3441
3449
  },
3442
3450
  "events": [
3451
+ {
3452
+ "name": "dsAfterClose",
3453
+ "detail": "void"
3454
+ },
3443
3455
  {
3444
3456
  "name": "dsClose",
3445
3457
  "detail": "void"
@@ -4000,6 +4012,10 @@
4000
4012
  "type": "string",
4001
4013
  "required": false
4002
4014
  },
4015
+ "accountMenuExpanded": {
4016
+ "type": "boolean",
4017
+ "required": false
4018
+ },
4003
4019
  "activeId": {
4004
4020
  "type": "string",
4005
4021
  "required": false
@@ -645,7 +645,7 @@
645
645
  "files": [
646
646
  {
647
647
  "path": "src/wc/components/Slider/Slider.css",
648
- "content": "@import '../../utils/control-inactive.css';\n\n:host {\n --ds-slider-control-size: var(--dimension-size-400);\n --ds-slider-thumb-size: var(--dimension-size-200);\n --ds-slider-track-size: var(--dimension-size-100);\n --ds-slider-track-stroke: var(--dimension-stroke-width-015);\n --ds-slider-thumb-stroke: var(--dimension-stroke-width-018);\n --ds-slider-vertical-length: calc(var(--dimension-size-800) * 3);\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n gap: var(--dimension-space-050);\n width: 100%;\n color: var(--color-foreground-primary);\n}\n\n:host(.slider--sm) {\n --ds-slider-control-size: var(--dimension-size-300);\n --ds-slider-thumb-size: var(--dimension-size-150);\n --ds-slider-track-size: var(--dimension-size-075);\n --ds-slider-track-stroke: var(--dimension-stroke-width-012);\n}\n\n:host(.slider--xs) {\n --ds-slider-control-size: var(--dimension-size-200);\n --ds-slider-thumb-size: var(--dimension-size-100);\n --ds-slider-track-size: var(--dimension-size-050);\n --ds-slider-track-stroke: calc(var(--dimension-stroke-width-base) * 3 / 32);\n}\n\n:host(.slider--vertical) {\n width: fit-content;\n min-width: var(--dimension-form-width-xs);\n}\n\n.slider__label-row {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: var(--dimension-space-100);\n min-width: 0;\n}\n\n.slider__label {\n min-width: 0;\n}\n\n.slider__value {\n flex-shrink: 0;\n}\n\n.slider__control {\n position: relative;\n width: 100%;\n height: var(--ds-slider-control-size);\n border-radius: var(--dimension-radius-half);\n cursor: pointer;\n touch-action: none;\n user-select: none;\n}\n\n:host(.slider--vertical) .slider__control {\n align-self: center;\n width: var(--ds-slider-control-size);\n height: var(--ds-slider-vertical-length);\n}\n\n:host(.slider--readonly) .slider__control {\n cursor: default;\n}\n\n/* Inactive: `.ds-control-inactive` (25% opacity) on the host. */\n:host(.ds-control-inactive) .slider__control {\n cursor: not-allowed;\n}\n\n.slider__rail {\n position: absolute;\n top: 50%;\n right: 0;\n left: 0;\n height: var(--ds-slider-track-size);\n overflow: hidden;\n border-radius: var(--dimension-radius-025);\n background-color: transparent;\n box-shadow: inset 0 0 0 var(--ds-slider-track-stroke) var(--color-foreground-tertiary);\n transform: translateY(-50%);\n pointer-events: none;\n}\n\n:host(.slider--thumb-center) .slider__rail {\n right: calc(var(--ds-slider-thumb-size) / 2);\n left: calc(var(--ds-slider-thumb-size) / 2);\n}\n\n.slider__indicator {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: calc(\n var(--ds-slider-thumb-size) / 2 +\n (var(--ds-slider-end-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size))\n );\n border-radius: inherit;\n background-color: var(--color-background-bold-brand);\n}\n\n:host(.slider--range) .slider__indicator {\n left: calc(\n var(--ds-slider-thumb-size) / 2 +\n (var(--ds-slider-start-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size))\n );\n width: calc(\n ((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) / 100) *\n (100% - var(--ds-slider-thumb-size))\n );\n}\n\n:host(.slider--thumb-center) .slider__indicator {\n left: calc(var(--ds-slider-start-pct, 0) * 1%);\n width: calc((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) * 1%);\n}\n\n.slider__thumb {\n position: absolute;\n top: 50%;\n left: calc((var(--ds-slider-thumb-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size)));\n width: var(--ds-slider-thumb-size);\n height: var(--ds-slider-thumb-size);\n transform: translateY(-50%);\n z-index: 1;\n}\n\n.slider__thumb--active {\n z-index: 2;\n}\n\n.slider__input {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n padding: 0;\n border: 0;\n opacity: 0;\n appearance: none;\n pointer-events: none;\n z-index: 2;\n}\n\n.slider__thumb-visual {\n position: absolute;\n inset: 0;\n box-sizing: border-box;\n overflow: visible;\n border-radius: var(--dimension-radius-025);\n background-color: var(--color-background-primary);\n box-shadow: inset 0 0 0 var(--ds-slider-thumb-stroke) var(--color-border-bold-brand);\n pointer-events: none;\n transition:\n background-color var(--effect-motion-short-2),\n box-shadow var(--effect-motion-short-2);\n}\n\n.slider__thumb-wash {\n position: absolute;\n inset: 0;\n border-radius: inherit;\n background-color: transparent;\n transition: background-color var(--effect-motion-short-2);\n}\n\n:host(:not(.slider--readonly):not(.ds-control-inactive)) .slider__thumb:hover .slider__thumb-wash {\n background-color: var(--color-interaction-hover);\n}\n\n:host([data-dragging]:not(.slider--readonly):not(.ds-control-inactive))\n .slider__thumb--active .slider__thumb-wash {\n background-color: var(--color-interaction-pressed);\n}\n\n.slider__input:focus-visible + .slider__thumb-visual,\n:host([data-review-focus]) .slider__thumb[data-index='0'] .slider__thumb-visual {\n outline: var(--dimension-stroke-width-025) solid var(--color-interaction-focus);\n outline-offset: var(--dimension-space-025);\n}\n\n:host(.slider--vertical) .slider__rail {\n top: 0;\n right: auto;\n bottom: 0;\n left: 50%;\n width: var(--ds-slider-track-size);\n height: auto;\n transform: translateX(-50%);\n}\n\n:host(.slider--vertical.slider--thumb-center) .slider__rail {\n top: calc(var(--ds-slider-thumb-size) / 2);\n bottom: calc(var(--ds-slider-thumb-size) / 2);\n}\n\n:host(.slider--vertical) .slider__indicator {\n top: auto;\n right: 0;\n bottom: 0;\n left: 0;\n width: auto;\n height: calc(\n var(--ds-slider-thumb-size) / 2 +\n (var(--ds-slider-end-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size))\n );\n}\n\n:host(.slider--vertical.slider--range) .slider__indicator {\n bottom: calc(\n var(--ds-slider-thumb-size) / 2 +\n (var(--ds-slider-start-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size))\n );\n height: calc(\n ((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) / 100) *\n (100% - var(--ds-slider-thumb-size))\n );\n}\n\n:host(.slider--vertical.slider--thumb-center) .slider__indicator {\n bottom: calc(var(--ds-slider-start-pct, 0) * 1%);\n height: calc((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) * 1%);\n}\n\n:host(.slider--vertical) .slider__thumb {\n top: auto;\n bottom: calc((var(--ds-slider-thumb-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size)));\n left: 50%;\n transform: translateX(-50%);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .slider__thumb-visual,\n .slider__thumb-wash {\n transition: none;\n }\n}\n",
648
+ "content": "@import '../../utils/control-inactive.css';\n\n:host {\n --ds-slider-control-size: var(--dimension-size-400);\n --ds-slider-thumb-size: var(--dimension-size-200);\n --ds-slider-track-size: var(--dimension-size-100);\n --ds-slider-track-stroke: var(--dimension-stroke-width-012);\n --ds-slider-vertical-length: calc(var(--dimension-size-800) * 3);\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n gap: var(--dimension-space-050);\n width: 100%;\n color: var(--color-foreground-primary);\n}\n\n:host(.slider--sm) {\n --ds-slider-control-size: var(--dimension-size-300);\n --ds-slider-thumb-size: var(--dimension-size-150);\n --ds-slider-track-size: var(--dimension-size-075);\n}\n\n:host(.slider--xs) {\n --ds-slider-control-size: var(--dimension-size-200);\n --ds-slider-thumb-size: var(--dimension-size-100);\n --ds-slider-track-size: var(--dimension-size-050);\n}\n\n:host(.slider--vertical) {\n width: fit-content;\n min-width: var(--dimension-form-width-xs);\n}\n\n.slider__label-row {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: var(--dimension-space-100);\n min-width: 0;\n}\n\n.slider__label {\n min-width: 0;\n}\n\n.slider__value {\n flex-shrink: 0;\n}\n\n.slider__control {\n position: relative;\n width: 100%;\n height: var(--ds-slider-control-size);\n border-radius: var(--dimension-radius-half);\n cursor: pointer;\n touch-action: none;\n user-select: none;\n}\n\n:host(.slider--vertical) .slider__control {\n align-self: center;\n width: var(--ds-slider-control-size);\n height: var(--ds-slider-vertical-length);\n}\n\n:host(.slider--readonly) .slider__control {\n cursor: default;\n}\n\n/* Inactive: `.ds-control-inactive` (25% opacity) on the host. */\n:host(.ds-control-inactive) .slider__control {\n cursor: not-allowed;\n}\n\n.slider__rail {\n position: absolute;\n top: 50%;\n right: 0;\n left: 0;\n height: var(--ds-slider-track-size);\n overflow: hidden;\n border-radius: var(--dimension-radius-025);\n background-color: transparent;\n box-shadow: inset 0 0 0 var(--ds-slider-track-stroke) var(--color-border-secondary);\n transform: translateY(-50%);\n pointer-events: none;\n}\n\n:host(.slider--thumb-center) .slider__rail {\n right: calc(var(--ds-slider-thumb-size) / 2);\n left: calc(var(--ds-slider-thumb-size) / 2);\n}\n\n.slider__indicator {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: calc(\n var(--ds-slider-thumb-size) / 2 +\n (var(--ds-slider-end-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size))\n );\n border-radius: inherit;\n background-color: var(--color-background-bold-brand);\n}\n\n:host(.slider--range) .slider__indicator {\n left: calc(\n var(--ds-slider-thumb-size) / 2 +\n (var(--ds-slider-start-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size))\n );\n width: calc(\n ((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) / 100) *\n (100% - var(--ds-slider-thumb-size))\n );\n}\n\n:host(.slider--thumb-center) .slider__indicator {\n left: calc(var(--ds-slider-start-pct, 0) * 1%);\n width: calc((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) * 1%);\n}\n\n.slider__thumb {\n position: absolute;\n top: 50%;\n left: calc((var(--ds-slider-thumb-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size)));\n width: var(--ds-slider-thumb-size);\n height: var(--ds-slider-thumb-size);\n transform: translateY(-50%);\n z-index: 1;\n}\n\n.slider__thumb--active {\n z-index: 2;\n}\n\n.slider__input {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n padding: 0;\n border: 0;\n opacity: 0;\n appearance: none;\n pointer-events: none;\n z-index: 2;\n}\n\n.slider__thumb-visual {\n position: absolute;\n inset: 0;\n box-sizing: border-box;\n overflow: visible;\n border-radius: var(--dimension-radius-025);\n background-color: var(--color-background-primary);\n box-shadow: var(--effect-elevation-elevated-sm);\n pointer-events: none;\n transition:\n background-color var(--effect-motion-short-2),\n box-shadow var(--effect-motion-short-2);\n}\n\n.slider__thumb-wash {\n position: absolute;\n inset: 0;\n border-radius: inherit;\n background-color: transparent;\n transition: background-color var(--effect-motion-short-2);\n}\n\n:host(:not(.slider--readonly):not(.ds-control-inactive)) .slider__thumb:hover .slider__thumb-wash {\n background-color: var(--color-interaction-hover);\n}\n\n:host([data-dragging]:not(.slider--readonly):not(.ds-control-inactive))\n .slider__thumb--active .slider__thumb-wash {\n background-color: var(--color-interaction-pressed);\n}\n\n.slider__input:focus-visible + .slider__thumb-visual,\n:host([data-review-focus]) .slider__thumb[data-index='0'] .slider__thumb-visual {\n outline: var(--dimension-stroke-width-025) solid var(--color-interaction-focus);\n outline-offset: var(--dimension-space-025);\n}\n\n:host(.slider--vertical) .slider__rail {\n top: 0;\n right: auto;\n bottom: 0;\n left: 50%;\n width: var(--ds-slider-track-size);\n height: auto;\n transform: translateX(-50%);\n}\n\n:host(.slider--vertical.slider--thumb-center) .slider__rail {\n top: calc(var(--ds-slider-thumb-size) / 2);\n bottom: calc(var(--ds-slider-thumb-size) / 2);\n}\n\n:host(.slider--vertical) .slider__indicator {\n top: auto;\n right: 0;\n bottom: 0;\n left: 0;\n width: auto;\n height: calc(\n var(--ds-slider-thumb-size) / 2 +\n (var(--ds-slider-end-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size))\n );\n}\n\n:host(.slider--vertical.slider--range) .slider__indicator {\n bottom: calc(\n var(--ds-slider-thumb-size) / 2 +\n (var(--ds-slider-start-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size))\n );\n height: calc(\n ((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) / 100) *\n (100% - var(--ds-slider-thumb-size))\n );\n}\n\n:host(.slider--vertical.slider--thumb-center) .slider__indicator {\n bottom: calc(var(--ds-slider-start-pct, 0) * 1%);\n height: calc((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) * 1%);\n}\n\n:host(.slider--vertical) .slider__thumb {\n top: auto;\n bottom: calc((var(--ds-slider-thumb-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size)));\n left: 50%;\n transform: translateX(-50%);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .slider__thumb-visual,\n .slider__thumb-wash {\n transition: none;\n }\n}\n",
649
649
  "type": "registry:ui"
650
650
  },
651
651
  {
@@ -349,7 +349,7 @@
349
349
  "files": [
350
350
  {
351
351
  "path": "src/wc/components/Switch/Switch.css",
352
- "content": "@import '../../utils/focus-ring.css';\n@import '../../utils/control-inactive.css';\n\n:host {\n --ds-switch-width: var(--dimension-size-400);\n --ds-switch-height: var(--dimension-size-250);\n --ds-switch-thumb-size: var(--dimension-size-150);\n --ds-switch-gap: var(--dimension-space-050);\n --ds-switch-stroke: var(--dimension-stroke-width-015);\n --ds-switch-inset: var(--ds-switch-gap);\n --ds-switch-thumb-hover: var(--color-interaction-hover);\n --ds-switch-thumb-pressed: var(--color-interaction-pressed);\n --ds-switch-travel: calc(\n var(--ds-switch-width) - var(--ds-switch-thumb-size) - var(--ds-switch-inset) -\n var(--ds-switch-inset)\n );\n position: relative;\n display: inline-flex;\n width: var(--ds-switch-width);\n height: var(--ds-switch-height);\n padding: 0;\n border: none;\n border-radius: var(--dimension-radius-half);\n box-sizing: border-box;\n flex-shrink: 0;\n overflow: visible;\n cursor: pointer;\n user-select: none;\n outline: none;\n background-color: transparent;\n box-shadow: inset 0 0 0 var(--ds-switch-stroke) var(--color-foreground-tertiary);\n transition:\n background-color var(--effect-motion-short-3),\n box-shadow var(--effect-motion-short-3);\n}\n\n:host(.switch--sm) {\n --ds-switch-width: var(--dimension-size-300);\n --ds-switch-height: var(--dimension-size-200);\n --ds-switch-thumb-size: calc(var(--dimension-size-base) * 5 / 4);\n --ds-switch-gap: calc(var(--dimension-space-025) + var(--dimension-space-012));\n --ds-switch-stroke: var(--dimension-stroke-width-012);\n}\n\n:host(.switch--xs) {\n --ds-switch-width: var(--dimension-size-250);\n --ds-switch-height: var(--dimension-size-150);\n --ds-switch-thumb-size: var(--dimension-size-100);\n --ds-switch-gap: var(--dimension-space-025);\n --ds-switch-stroke: calc(var(--dimension-stroke-width-base) * 3 / 32);\n}\n\n:host(.checked) {\n background-color: var(--color-background-bold-brand);\n box-shadow: none;\n}\n\n:host(.switch--readonly),\n:host(.switch--presentation) {\n cursor: default;\n}\n\n:host(.switch--presentation) {\n pointer-events: none;\n}\n\n/* Inactive: `.ds-control-inactive` (25% opacity). */\n\n.thumb {\n position: absolute;\n top: 50%;\n left: var(--ds-switch-inset);\n width: var(--ds-switch-thumb-size);\n height: var(--ds-switch-thumb-size);\n border-radius: var(--dimension-radius-half);\n background-color: var(--color-foreground-tertiary);\n box-shadow: none;\n transform: translateY(-50%);\n transition:\n background-color var(--effect-motion-short-3),\n transform var(--effect-motion-short-3);\n pointer-events: none;\n}\n\n.thumb::after {\n content: '';\n position: absolute;\n inset: 0;\n border-radius: inherit;\n background: transparent;\n pointer-events: none;\n}\n\n:host(:hover:not(:disabled):not(.ds-control-inactive):not(.switch--readonly):not(.switch--presentation))\n .thumb::after {\n background: var(--ds-switch-thumb-hover);\n}\n\n:host(:active:not(:disabled):not(.ds-control-inactive):not(.switch--readonly):not(.switch--presentation))\n .thumb::after {\n background: var(--ds-switch-thumb-pressed);\n}\n\n:host(.checked) .thumb {\n background-color: var(--color-background-primary);\n box-shadow: none;\n transform: translate3d(var(--ds-switch-travel), -50%, 0);\n}\n\n@media (prefers-reduced-motion: reduce) {\n :host {\n transition: none;\n }\n\n .thumb {\n transition: none;\n }\n}\n",
352
+ "content": "@import '../../utils/focus-ring.css';\n@import '../../utils/control-inactive.css';\n\n:host {\n --ds-switch-width: var(--dimension-size-400);\n --ds-switch-height: var(--dimension-size-250);\n --ds-switch-thumb-size: var(--dimension-size-150);\n --ds-switch-gap: var(--dimension-space-050);\n --ds-switch-stroke: var(--dimension-stroke-width-012);\n --ds-switch-inset: var(--ds-switch-gap);\n --ds-switch-thumb-hover: var(--color-interaction-hover);\n --ds-switch-thumb-pressed: var(--color-interaction-pressed);\n --ds-switch-travel: calc(\n var(--ds-switch-width) - var(--ds-switch-thumb-size) - var(--ds-switch-inset) -\n var(--ds-switch-inset)\n );\n position: relative;\n display: inline-flex;\n width: var(--ds-switch-width);\n height: var(--ds-switch-height);\n padding: 0;\n border: none;\n border-radius: var(--dimension-radius-half);\n box-sizing: border-box;\n flex-shrink: 0;\n overflow: visible;\n cursor: pointer;\n user-select: none;\n outline: none;\n background-color: transparent;\n box-shadow: inset 0 0 0 var(--ds-switch-stroke) var(--color-border-secondary);\n transition:\n background-color var(--effect-motion-short-3),\n box-shadow var(--effect-motion-short-3);\n}\n\n:host(.switch--sm) {\n --ds-switch-width: var(--dimension-size-300);\n --ds-switch-height: var(--dimension-size-200);\n --ds-switch-thumb-size: calc(var(--dimension-size-base) * 5 / 4);\n --ds-switch-gap: calc(var(--dimension-space-025) + var(--dimension-space-012));\n}\n\n:host(.switch--xs) {\n --ds-switch-width: var(--dimension-size-250);\n --ds-switch-height: var(--dimension-size-150);\n --ds-switch-thumb-size: var(--dimension-size-100);\n --ds-switch-gap: var(--dimension-space-025);\n}\n\n:host(.checked) {\n background-color: var(--color-background-bold-brand);\n box-shadow: none;\n}\n\n:host(.switch--readonly),\n:host(.switch--presentation) {\n cursor: default;\n}\n\n:host(.switch--presentation) {\n pointer-events: none;\n}\n\n/* Inactive: `.ds-control-inactive` (25% opacity). */\n\n.thumb {\n position: absolute;\n top: 50%;\n left: var(--ds-switch-inset);\n width: var(--ds-switch-thumb-size);\n height: var(--ds-switch-thumb-size);\n border-radius: var(--dimension-radius-half);\n background-color: var(--color-foreground-tertiary);\n box-shadow: none;\n transform: translateY(-50%);\n transition:\n background-color var(--effect-motion-short-3),\n transform var(--effect-motion-short-3);\n pointer-events: none;\n}\n\n.thumb::after {\n content: '';\n position: absolute;\n inset: 0;\n border-radius: inherit;\n background: transparent;\n pointer-events: none;\n}\n\n:host(:hover:not(:disabled):not(.ds-control-inactive):not(.switch--readonly):not(.switch--presentation))\n .thumb::after {\n background: var(--ds-switch-thumb-hover);\n}\n\n:host(:active:not(:disabled):not(.ds-control-inactive):not(.switch--readonly):not(.switch--presentation))\n .thumb::after {\n background: var(--ds-switch-thumb-pressed);\n}\n\n:host(.checked) .thumb {\n background-color: var(--color-background-primary);\n box-shadow: none;\n transform: translate3d(var(--ds-switch-travel), -50%, 0);\n}\n\n@media (prefers-reduced-motion: reduce) {\n :host {\n transition: none;\n }\n\n .thumb {\n transition: none;\n }\n}\n",
353
353
  "type": "registry:ui"
354
354
  },
355
355
  {
@@ -340,12 +340,12 @@
340
340
  "files": [
341
341
  {
342
342
  "path": "src/wc/components/Tag/Tag.css",
343
- "content": "@import '../../utils/tag-chip.css';\n\n/* ── Intent × Contrast — strong ─────────────────────────────────────────── */\n\n:host(.tag--intent-neutral.tag--contrast-strong) { background-color: var(--color-background-strong-neutral); color: var(--color-foreground-medium-neutral); }\n:host(.tag--intent-brand.tag--contrast-strong) { background-color: var(--color-background-strong-brand); color: var(--color-foreground-on-strong-background-primary); }\n:host(.tag--intent-ai.tag--contrast-strong) { background-color: var(--color-background-strong-ai); color: var(--color-foreground-medium-ai); }\n:host(.tag--intent-negative.tag--contrast-strong) { background-color: var(--color-background-strong-negative); color: var(--color-foreground-medium-negative); }\n:host(.tag--intent-warning.tag--contrast-strong) { background-color: var(--color-background-strong-warning); color: var(--color-foreground-medium-warning); }\n:host(.tag--intent-caution.tag--contrast-strong) { background-color: var(--color-background-strong-caution); color: var(--color-foreground-medium-caution); }\n:host(.tag--intent-positive.tag--contrast-strong) { background-color: var(--color-background-strong-positive); color: var(--color-foreground-medium-positive); }\n\n/* ── Intent × Contrast — bold ───────────────────────────────────────────── */\n\n:host(.tag--intent-neutral.tag--contrast-bold) { background-color: var(--color-background-bold-neutral); color: var(--color-foreground-on-bold-background-primary); }\n:host(.tag--intent-brand.tag--contrast-bold) { background-color: var(--color-background-bold-brand); color: var(--color-foreground-on-bold-background-primary); }\n:host(.tag--intent-ai.tag--contrast-bold) { background-color: var(--color-background-bold-ai); color: var(--color-foreground-on-bold-background-primary); }\n:host(.tag--intent-negative.tag--contrast-bold) { background-color: var(--color-background-bold-negative); color: var(--color-foreground-on-bold-background-primary); }\n:host(.tag--intent-warning.tag--contrast-bold) { background-color: var(--color-background-bold-warning); color: var(--color-foreground-on-bold-background-primary); }\n:host(.tag--intent-caution.tag--contrast-bold) { background-color: var(--color-background-bold-caution); color: var(--color-foreground-on-bold-background-primary); }\n:host(.tag--intent-positive.tag--contrast-bold) { background-color: var(--color-background-bold-positive); color: var(--color-foreground-on-bold-background-primary); }\n\n/* ── Intent × Contrast — medium ─────────────────────────────────────────── */\n\n:host(.tag--intent-neutral.tag--contrast-medium) { background-color: var(--color-background-medium-neutral); color: var(--color-foreground-strong-neutral); }\n:host(.tag--intent-brand.tag--contrast-medium) { background-color: var(--color-background-medium-brand); color: var(--color-foreground-on-medium-background-primary); }\n:host(.tag--intent-ai.tag--contrast-medium) { background-color: var(--color-background-medium-ai); color: var(--color-foreground-strong-ai); }\n:host(.tag--intent-negative.tag--contrast-medium) { background-color: var(--color-background-medium-negative); color: var(--color-foreground-strong-negative); }\n:host(.tag--intent-warning.tag--contrast-medium) { background-color: var(--color-background-medium-warning); color: var(--color-foreground-strong-warning); }\n:host(.tag--intent-caution.tag--contrast-medium) { background-color: var(--color-background-medium-caution); color: var(--color-foreground-strong-caution); }\n:host(.tag--intent-positive.tag--contrast-medium) { background-color: var(--color-background-medium-positive); color: var(--color-foreground-strong-positive); }\n\n/* ── Intent × Contrast — faint ──────────────────────────────────────────── */\n\n:host(.tag--intent-neutral.tag--contrast-faint) { background-color: var(--color-background-faint-neutral); color: var(--color-foreground-bold-neutral); }\n:host(.tag--intent-brand.tag--contrast-faint) { background-color: var(--color-background-faint-brand); color: var(--color-foreground-bold-brand); }\n:host(.tag--intent-ai.tag--contrast-faint) { background-color: var(--color-background-faint-ai); color: var(--color-foreground-bold-ai); }\n:host(.tag--intent-negative.tag--contrast-faint) { background-color: var(--color-background-faint-negative); color: var(--color-foreground-bold-negative); }\n:host(.tag--intent-warning.tag--contrast-faint) { background-color: var(--color-background-faint-warning); color: var(--color-foreground-bold-warning); }\n:host(.tag--intent-caution.tag--contrast-faint) { background-color: var(--color-background-faint-caution); color: var(--color-foreground-bold-caution); }\n:host(.tag--intent-positive.tag--contrast-faint) { background-color: var(--color-background-faint-positive); color: var(--color-foreground-bold-positive); }\n\n/* ── Interactive menu trigger ───────────────────────────────────────────── */\n\n:host(.tag--interactive) {\n padding: 0;\n gap: 0;\n}\n\n.tag__button {\n all: unset;\n position: relative;\n z-index: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n min-width: 0;\n max-width: 100%;\n height: 100%;\n padding: 0 var(--ds-control-padding-inline);\n gap: var(--ds-control-gap);\n border-radius: inherit;\n color: inherit;\n cursor: pointer;\n}\n\n.tag__button:disabled {\n cursor: not-allowed;\n}\n\n/* Hover/press wash stays above the Tag's semantic background. */\n\n/* ── Icons ──────────────────────────────────────────────────────────────── */\n\n.tag__prefix-icon,\n.tag__suffix-icon {\n position: relative;\n z-index: 2;\n display: inline-flex;\n flex-shrink: 0;\n width: var(--ds-control-icon);\n height: var(--ds-control-icon);\n}\n",
343
+ "content": "@import '../../utils/tag-chip.css';\n\n/* ── Intent × Contrast — strong ─────────────────────────────────────────── */\n\n:host(.tag--intent-neutral.tag--contrast-strong) { background-color: var(--color-background-strong-neutral); color: var(--color-foreground-medium-neutral); }\n:host(.tag--intent-brand.tag--contrast-strong) { background-color: var(--color-background-strong-brand); color: var(--color-foreground-on-strong-background-primary); }\n:host(.tag--intent-ai.tag--contrast-strong) { background-color: var(--color-background-strong-ai); color: var(--color-foreground-medium-ai); }\n:host(.tag--intent-negative.tag--contrast-strong) { background-color: var(--color-background-strong-negative); color: var(--color-foreground-medium-negative); }\n:host(.tag--intent-warning.tag--contrast-strong) { background-color: var(--color-background-strong-warning); color: var(--color-foreground-medium-warning); }\n:host(.tag--intent-caution.tag--contrast-strong) { background-color: var(--color-background-strong-caution); color: var(--color-foreground-medium-caution); }\n:host(.tag--intent-positive.tag--contrast-strong) { background-color: var(--color-background-strong-positive); color: var(--color-foreground-medium-positive); }\n\n/* ── Intent × Contrast — bold ───────────────────────────────────────────── */\n\n:host(.tag--intent-neutral.tag--contrast-bold) { background-color: var(--color-background-bold-neutral); color: var(--color-foreground-on-bold-background-primary); }\n:host(.tag--intent-brand.tag--contrast-bold) { background-color: var(--color-background-bold-brand); color: var(--color-foreground-on-bold-background-primary); }\n:host(.tag--intent-ai.tag--contrast-bold) { background-color: var(--color-background-bold-ai); color: var(--color-foreground-on-bold-background-primary); }\n:host(.tag--intent-negative.tag--contrast-bold) { background-color: var(--color-background-bold-negative); color: var(--color-foreground-on-bold-background-primary); }\n:host(.tag--intent-warning.tag--contrast-bold) { background-color: var(--color-background-bold-warning); color: var(--color-foreground-on-bold-background-primary); }\n:host(.tag--intent-caution.tag--contrast-bold) { background-color: var(--color-background-bold-caution); color: var(--color-foreground-on-bold-background-primary); }\n:host(.tag--intent-positive.tag--contrast-bold) { background-color: var(--color-background-bold-positive); color: var(--color-foreground-on-bold-background-primary); }\n\n/* ── Intent × Contrast — medium ─────────────────────────────────────────── */\n\n:host(.tag--intent-neutral.tag--contrast-medium) { background-color: var(--color-background-medium-neutral); color: var(--color-foreground-strong-neutral); }\n:host(.tag--intent-brand.tag--contrast-medium) { background-color: var(--color-background-medium-brand); color: var(--color-foreground-on-medium-background-primary); }\n:host(.tag--intent-ai.tag--contrast-medium) { background-color: var(--color-background-medium-ai); color: var(--color-foreground-strong-ai); }\n:host(.tag--intent-negative.tag--contrast-medium) { background-color: var(--color-background-medium-negative); color: var(--color-foreground-strong-negative); }\n:host(.tag--intent-warning.tag--contrast-medium) { background-color: var(--color-background-medium-warning); color: var(--color-foreground-strong-warning); }\n:host(.tag--intent-caution.tag--contrast-medium) { background-color: var(--color-background-medium-caution); color: var(--color-foreground-strong-caution); }\n:host(.tag--intent-positive.tag--contrast-medium) { background-color: var(--color-background-medium-positive); color: var(--color-foreground-strong-positive); }\n\n/* ── Intent × Contrast — faint ──────────────────────────────────────────── */\n\n:host(.tag--intent-neutral.tag--contrast-faint) { background-color: var(--color-background-faint-neutral); color: var(--color-foreground-bold-neutral); }\n:host(.tag--intent-brand.tag--contrast-faint) { background-color: var(--color-background-faint-brand); color: var(--color-foreground-bold-brand); }\n:host(.tag--intent-ai.tag--contrast-faint) { background-color: var(--color-background-faint-ai); color: var(--color-foreground-bold-ai); }\n:host(.tag--intent-negative.tag--contrast-faint) { background-color: var(--color-background-faint-negative); color: var(--color-foreground-bold-negative); }\n:host(.tag--intent-warning.tag--contrast-faint) { background-color: var(--color-background-faint-warning); color: var(--color-foreground-bold-warning); }\n:host(.tag--intent-caution.tag--contrast-faint) { background-color: var(--color-background-faint-caution); color: var(--color-foreground-bold-caution); }\n:host(.tag--intent-positive.tag--contrast-faint) { background-color: var(--color-background-faint-positive); color: var(--color-foreground-bold-positive); }\n\n/* ── Interactive menu trigger ───────────────────────────────────────────── */\n\n:host(.tag--interactive) {\n padding: 0;\n gap: 0;\n}\n\n.tag__button {\n all: unset;\n position: relative;\n z-index: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n min-width: 0;\n max-width: 100%;\n height: 100%;\n padding: 0 var(--ds-control-padding-inline);\n gap: var(--ds-control-gap);\n border-radius: inherit;\n color: inherit;\n cursor: pointer;\n}\n\n.tag__button:disabled {\n cursor: not-allowed;\n}\n\n.tag__button--expanded::after {\n background: var(--ds-interaction-pressed);\n}\n\n/* Hover/press wash stays above the Tag's semantic background. */\n\n/* ── Icons ──────────────────────────────────────────────────────────────── */\n\n.tag__prefix-icon,\n.tag__suffix-icon {\n position: relative;\n z-index: 2;\n display: inline-flex;\n flex-shrink: 0;\n width: var(--ds-control-icon);\n height: var(--ds-control-icon);\n}\n",
344
344
  "type": "registry:ui"
345
345
  },
346
346
  {
347
347
  "path": "src/wc/components/Tag/Tag.tsx",
348
- "content": "import { Component, Event, EventEmitter, Prop, h, Host } from '@stencil/core';\nimport { CONTROL_TEXT_VARIANT } from '../../utils';\n\nexport type TagIntent = 'neutral' | 'brand' | 'ai' | 'negative' | 'warning' | 'caution' | 'positive';\nexport type TagContrast = 'strong' | 'bold' | 'medium' | 'faint';\nexport type TagSize = 'md' | 'sm' | 'xs';\n\n/**\n * `ds-icon` size prop matching control-density icon metrics\n * (md→20 / sm→16 / xs→12 via `--dimension-iconography-*`).\n */\nconst ICON_SIZE: Record<TagSize, 'md' | 'sm' | 'xs'> = {\n md: 'md',\n sm: 'sm',\n xs: 'xs',\n};\n\n@Component({\n tag: 'ds-tag',\n styleUrl: 'Tag.css',\n scoped: true,\n})\nexport class Tag {\n @Prop() label!: string;\n /** Canonical icon name rendered before the visible label. */\n @Prop() icon: string = '';\n @Prop() intent: TagIntent = 'neutral';\n @Prop() contrast: TagContrast = 'faint';\n @Prop() size: TagSize = 'md';\n @Prop() rounded: boolean = false;\n @Prop() maxWidth: string | number | undefined;\n /** Render a menu-trigger button with a fixed ChevronUpDown suffix. */\n @Prop() interactive: boolean = false;\n /** Controlled open state for the menu triggered by an interactive Tag. */\n @Prop() expanded: boolean = false;\n /** ID of the menu controlled by an interactive Tag. */\n @Prop() ariaControls: string | undefined;\n /** Disable an interactive Tag and apply the shared inactive treatment. */\n @Prop() isInactive: boolean = false;\n\n /** Fired when the interactive Tag button is activated. */\n @Event() dsClick!: EventEmitter<MouseEvent>;\n\n private handleClick = (event: MouseEvent) => {\n if (!this.interactive || this.isInactive) return;\n this.dsClick.emit(event);\n };\n\n render() {\n const textVariant = CONTROL_TEXT_VARIANT[this.size];\n const iconSize = ICON_SIZE[this.size];\n\n const maxWidthStyle = this.maxWidth != null\n ? { maxWidth: typeof this.maxWidth === 'number' ? `${this.maxWidth}px` : this.maxWidth }\n : undefined;\n const hostClass = {\n 'tag': true,\n [`tag--intent-${this.intent}`]: true,\n [`tag--contrast-${this.contrast}`]: true,\n [`tag--size-${this.size}`]: true,\n 'ds-control--md': this.size === 'md',\n 'ds-control--sm': this.size === 'sm',\n 'ds-control--xs': this.size === 'xs',\n 'tag--rounded': this.rounded,\n 'tag--interactive': this.interactive,\n 'ds-control-inactive': this.interactive && this.isInactive,\n };\n const content = [\n this.icon && (\n <ds-icon\n class=\"tag__prefix-icon\"\n name={this.icon}\n size={iconSize}\n color=\"inherit\"\n ></ds-icon>\n ),\n <ds-text class=\"tag__label\" as=\"span\" variant={textVariant} color=\"inherit\">\n {this.label}\n </ds-text>,\n ];\n\n if (!this.interactive) {\n return <Host class={hostClass} style={maxWidthStyle}>{content}</Host>;\n }\n\n return (\n <Host class={hostClass} style={maxWidthStyle}>\n <button\n type=\"button\"\n class=\"tag__button ds-interaction-fill ds-focus-ring-inset\"\n aria-haspopup=\"menu\"\n aria-expanded={this.expanded ? 'true' : 'false'}\n aria-controls={this.ariaControls}\n disabled={this.isInactive || undefined}\n onClick={this.handleClick}\n >\n {content}\n <ds-icon\n class=\"tag__suffix-icon\"\n name=\"ChevronUpDown\"\n size={iconSize}\n color=\"inherit\"\n ></ds-icon>\n </button>\n </Host>\n );\n }\n}\n",
348
+ "content": "import { Component, Event, EventEmitter, Prop, h, Host } from '@stencil/core';\nimport { CONTROL_TEXT_VARIANT } from '../../utils';\n\nexport type TagIntent = 'neutral' | 'brand' | 'ai' | 'negative' | 'warning' | 'caution' | 'positive';\nexport type TagContrast = 'strong' | 'bold' | 'medium' | 'faint';\nexport type TagSize = 'md' | 'sm' | 'xs';\n\n/**\n * `ds-icon` size prop matching control-density icon metrics\n * (md→20 / sm→16 / xs→12 via `--dimension-iconography-*`).\n */\nconst ICON_SIZE: Record<TagSize, 'md' | 'sm' | 'xs'> = {\n md: 'md',\n sm: 'sm',\n xs: 'xs',\n};\n\n@Component({\n tag: 'ds-tag',\n styleUrl: 'Tag.css',\n scoped: true,\n})\nexport class Tag {\n @Prop() label!: string;\n /** Canonical icon name rendered before the visible label. */\n @Prop() icon: string = '';\n @Prop() intent: TagIntent = 'neutral';\n @Prop() contrast: TagContrast = 'faint';\n @Prop() size: TagSize = 'md';\n @Prop() rounded: boolean = false;\n @Prop() maxWidth: string | number | undefined;\n /** Render a menu-trigger button with a fixed ChevronUpDown suffix. */\n @Prop() interactive: boolean = false;\n /** Controlled open state for the menu triggered by an interactive Tag. */\n @Prop() expanded: boolean = false;\n /** ID of the menu controlled by an interactive Tag. */\n @Prop() ariaControls: string | undefined;\n /** Disable an interactive Tag and apply the shared inactive treatment. */\n @Prop() isInactive: boolean = false;\n\n /** Fired when the interactive Tag button is activated. */\n @Event() dsClick!: EventEmitter<MouseEvent>;\n\n private handleClick = (event: MouseEvent) => {\n if (!this.interactive || this.isInactive) return;\n this.dsClick.emit(event);\n };\n\n render() {\n const textVariant = CONTROL_TEXT_VARIANT[this.size];\n const iconSize = ICON_SIZE[this.size];\n\n const maxWidthStyle = this.maxWidth != null\n ? { maxWidth: typeof this.maxWidth === 'number' ? `${this.maxWidth}px` : this.maxWidth }\n : undefined;\n const hostClass = {\n 'tag': true,\n [`tag--intent-${this.intent}`]: true,\n [`tag--contrast-${this.contrast}`]: true,\n [`tag--size-${this.size}`]: true,\n 'ds-control--md': this.size === 'md',\n 'ds-control--sm': this.size === 'sm',\n 'ds-control--xs': this.size === 'xs',\n 'tag--rounded': this.rounded,\n 'tag--interactive': this.interactive,\n 'ds-control-inactive': this.interactive && this.isInactive,\n };\n const content = [\n this.icon && (\n <ds-icon\n class=\"tag__prefix-icon\"\n name={this.icon}\n size={iconSize}\n color=\"inherit\"\n ></ds-icon>\n ),\n <ds-text class=\"tag__label\" as=\"span\" variant={textVariant} color=\"inherit\">\n {this.label}\n </ds-text>,\n ];\n\n if (!this.interactive) {\n return <Host class={hostClass} style={maxWidthStyle}>{content}</Host>;\n }\n\n return (\n <Host class={hostClass} style={maxWidthStyle}>\n <button\n type=\"button\"\n class={{\n 'tag__button': true,\n 'tag__button--expanded': this.expanded,\n 'ds-interaction-fill': true,\n 'ds-focus-ring-inset': true,\n }}\n aria-haspopup=\"menu\"\n aria-expanded={this.expanded ? 'true' : 'false'}\n aria-controls={this.ariaControls}\n disabled={this.isInactive || undefined}\n onClick={this.handleClick}\n >\n {content}\n <ds-icon\n class=\"tag__suffix-icon\"\n name=\"ChevronUpDown\"\n size={iconSize}\n color=\"inherit\"\n ></ds-icon>\n </button>\n </Host>\n );\n }\n}\n",
349
349
  "type": "registry:ui"
350
350
  }
351
351
  ]
@@ -8,6 +8,7 @@ import type { Components } from "@ds-mo/ui/dist/components";
8
8
  import { DsMenu as DsMenuElement } from "@ds-mo/ui/dist/components/ds-menu.js";
9
9
  export type DsMenuEvents = {
10
10
  onDsClose: EventName<DsMenuCustomEvent<void>>;
11
+ onDsAfterClose: EventName<DsMenuCustomEvent<void>>;
11
12
  onDsSelect: EventName<DsMenuCustomEvent<MenuItemData>>;
12
13
  onDsGradientSelect: EventName<DsMenuCustomEvent<ShellGradientPreset>>;
13
14
  onDsSwatchSelect: EventName<DsMenuCustomEvent<string>>;
@@ -9,6 +9,7 @@ export const DsMenu = /*@__PURE__*/ createComponent({
9
9
  react: React,
10
10
  events: {
11
11
  onDsClose: 'dsClose',
12
+ onDsAfterClose: 'dsAfterClose',
12
13
  onDsSelect: 'dsSelect',
13
14
  onDsGradientSelect: 'dsGradientSelect',
14
15
  onDsSwatchSelect: 'dsSwatchSelect'
@@ -1,6 +1,9 @@
1
+ import type { IconColor } from '../Icon/Icon';
1
2
  export declare class Avatar {
2
3
  /** Canonical IcoMo icon name displayed inside the avatar. */
3
4
  icon: string;
5
+ /** Semantic color for the icon. Use primary to surface unread or current identity state. */
6
+ iconColor: IconColor;
4
7
  /** Optional accessible label. Omit when the surrounding content already conveys the meaning. */
5
8
  label: string | undefined;
6
9
  render(): any;
@@ -31,6 +31,7 @@ export declare class BarNav {
31
31
  private hideTabsForDetailRoute;
32
32
  private tabsCollapsed;
33
33
  private menuOpen;
34
+ private menuTriggerActive;
34
35
  /** When false, defer showing expanded tabs or the collapsed trigger until intrinsic width is measured (avoids a one-frame flash of the full tab row on narrow viewports). */
35
36
  private tabLayoutCommitted;
36
37
  private visibleTabs;
@@ -96,6 +97,7 @@ export declare class BarNav {
96
97
  private syncMenuAnchor;
97
98
  private handleMenuSelect;
98
99
  private handleMenuClose;
100
+ private handleMenuAfterClose;
99
101
  /** Batch tabs/basePath/currentUrl updates so URL derivation never runs with a mixed section. */
100
102
  private incomingTabs;
101
103
  private applyHostProps;
@@ -61,6 +61,8 @@ export declare class ButtonUnfilled {
61
61
  private get showIcon();
62
62
  private get showLabel();
63
63
  private get showDot();
64
+ /** An expanded popup trigger is visually active even when selection is controlled separately. */
65
+ private get visuallyActive();
64
66
  /** Knock-out ring: selected fill → active wash; otherwise surface token. */
65
67
  private get dotRing();
66
68
  private get accessibleName();
@@ -12,6 +12,8 @@ export declare class ConversationListItem {
12
12
  state: ConversationItemState;
13
13
  statusLabel: string;
14
14
  unreadCount: number;
15
+ /** Keep the contextual-actions surface visible while its popup is open or closing. */
16
+ actionsOpen: boolean;
15
17
  dsSelect: EventEmitter<{
16
18
  id: string;
17
19
  }>;