@ds-mo/ui 12.6.0 → 12.6.2

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 (144) hide show
  1. package/dist/.build-stamp +1 -1
  2. package/dist/agent-patterns.json +1 -1
  3. package/dist/agent.json +10 -7
  4. package/dist/components/ds-agent-activity.js +1 -1
  5. package/dist/components/ds-agent-response.js +1 -1
  6. package/dist/components/ds-agent-source-list.js +1 -1
  7. package/dist/components/ds-agent-tool-call.js +1 -1
  8. package/dist/components/ds-bar-nav.js +1 -1
  9. package/dist/components/ds-bar-nav.js.map +1 -1
  10. package/dist/components/ds-bar-title.js +1 -1
  11. package/dist/components/ds-bar-title.js.map +1 -1
  12. package/dist/components/ds-bar-workflow.js +1 -1
  13. package/dist/components/ds-breadcrumb.js +1 -1
  14. package/dist/components/ds-button-filled.js +1 -1
  15. package/dist/components/ds-button-unfilled.js +1 -1
  16. package/dist/components/ds-card-data-viz-bar.js +1 -1
  17. package/dist/components/ds-card-data-viz-donut.js +1 -1
  18. package/dist/components/ds-card-data-viz-line.js +1 -1
  19. package/dist/components/ds-card-setting.js +1 -1
  20. package/dist/components/ds-chart-legend.js +1 -1
  21. package/dist/components/ds-chart-legend.js.map +1 -1
  22. package/dist/components/ds-checkbox.js +1 -1
  23. package/dist/components/ds-chip.js +1 -1
  24. package/dist/components/ds-chip.js.map +1 -1
  25. package/dist/components/ds-code-block.js +1 -1
  26. package/dist/components/ds-conversation-list-item.js +1 -1
  27. package/dist/components/ds-conversation-list-item.js.map +1 -1
  28. package/dist/components/ds-input.js +1 -1
  29. package/dist/components/ds-input.js.map +1 -1
  30. package/dist/components/ds-markdown.js +1 -1
  31. package/dist/components/ds-menu.js +1 -1
  32. package/dist/components/ds-message-composer.js +1 -1
  33. package/dist/components/ds-message-scroller.js +1 -1
  34. package/dist/components/ds-mobile-bar-nav.js +1 -1
  35. package/dist/components/ds-mobile-bar-nav.js.map +1 -1
  36. package/dist/components/ds-mobile-header.js +1 -1
  37. package/dist/components/ds-mobile-section-switcher.js +1 -1
  38. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  39. package/dist/components/ds-mobile-sheet-nav.js.map +1 -1
  40. package/dist/components/ds-modal.js +1 -1
  41. package/dist/components/ds-panel-nav.js +1 -1
  42. package/dist/components/ds-panel-nav.js.map +1 -1
  43. package/dist/components/ds-panel-sub-nav.js +1 -1
  44. package/dist/components/ds-panel-sub-nav.js.map +1 -1
  45. package/dist/components/ds-panel-tool-header.js +1 -1
  46. package/dist/components/ds-panel-tool-search.js +1 -1
  47. package/dist/components/ds-panel-tools.js +1 -1
  48. package/dist/components/ds-radio.js +1 -1
  49. package/dist/components/ds-radio.js.map +1 -1
  50. package/dist/components/ds-select-multi.js +1 -1
  51. package/dist/components/ds-select-multi.js.map +1 -1
  52. package/dist/components/ds-select.js +1 -1
  53. package/dist/components/ds-select.js.map +1 -1
  54. package/dist/components/ds-shell-gradient-picker.js +1 -1
  55. package/dist/components/ds-shell-gradient-swatch.js +1 -1
  56. package/dist/components/ds-shell-gradient-swatch.js.map +1 -1
  57. package/dist/components/ds-shell-tools.js +1 -1
  58. package/dist/components/ds-shell-tools.js.map +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-swatch-picker.js +1 -1
  62. package/dist/components/ds-switch.js +1 -1
  63. package/dist/components/ds-tab-group.js +1 -1
  64. package/dist/components/ds-tag.js +1 -1
  65. package/dist/components/ds-tag.js.map +1 -1
  66. package/dist/components/ds-toast.js +1 -1
  67. package/dist/components/ds-toast.js.map +1 -1
  68. package/dist/components/p-B9bxDfZV.js +2 -0
  69. package/dist/components/p-B9bxDfZV.js.map +1 -0
  70. package/dist/components/{p-CICGtu7H.js → p-B9xVWER6.js} +2 -2
  71. package/dist/components/{p-D4o2D1hk.js → p-BLea0TrP.js} +2 -2
  72. package/dist/components/p-BbgPGF2a.js +2 -0
  73. package/dist/components/{p-CXcjwduV.js.map → p-BbgPGF2a.js.map} +1 -1
  74. package/dist/components/p-BwsGYxZx.js +2 -0
  75. package/dist/components/{p-YtgcuJYx.js.map → p-BwsGYxZx.js.map} +1 -1
  76. package/dist/components/{p-D9iN80yX.js → p-CWTih3Gc.js} +2 -2
  77. package/dist/components/{p-CvKKmZJj.js → p-Ccj5Op6E.js} +2 -2
  78. package/dist/components/p-CiqnkENu.js +2 -0
  79. package/dist/components/{p-DTea1Du7.js.map → p-CiqnkENu.js.map} +1 -1
  80. package/dist/components/{p-B3rbhRAk.js → p-CnHrJTD5.js} +2 -2
  81. package/dist/components/{p-B3rbhRAk.js.map → p-CnHrJTD5.js.map} +1 -1
  82. package/dist/components/{p-BHYrsGBw.js → p-DIoiApa-.js} +2 -2
  83. package/dist/components/{p-BHYrsGBw.js.map → p-DIoiApa-.js.map} +1 -1
  84. package/dist/components/p-DPME1wUJ.js +2 -0
  85. package/dist/components/{p-DcFfilOX.js.map → p-DPME1wUJ.js.map} +1 -1
  86. package/dist/components/{p-DvVW0hxm.js → p-DUlqUNbE.js} +2 -2
  87. package/dist/components/{p-CLzlL3G4.js → p-DXLWm4PF.js} +2 -2
  88. package/dist/components/{p-CLzlL3G4.js.map → p-DXLWm4PF.js.map} +1 -1
  89. package/dist/components/p-DYcC5-R9.js +2 -0
  90. package/dist/components/{p-46nk9-RO.js.map → p-DYcC5-R9.js.map} +1 -1
  91. package/dist/components/p-DqubRds2.js +2 -0
  92. package/dist/components/p-DqubRds2.js.map +1 -0
  93. package/dist/components/p-DwX7UmLl.js +2 -0
  94. package/dist/components/{p-CFpWrYAH.js.map → p-DwX7UmLl.js.map} +1 -1
  95. package/dist/components/p-uQhuDEVt.js +2 -0
  96. package/dist/components/p-uQhuDEVt.js.map +1 -0
  97. package/dist/docs/components.json +2 -2
  98. package/dist/mcp-data/registry/agent-activity.json +1 -1
  99. package/dist/mcp-data/registry/agent-source-list.json +1 -1
  100. package/dist/mcp-data/registry/agent-tool-call.json +1 -1
  101. package/dist/mcp-data/registry/bar-nav.json +1 -1
  102. package/dist/mcp-data/registry/bar-title.json +1 -1
  103. package/dist/mcp-data/registry/breadcrumb.json +1 -1
  104. package/dist/mcp-data/registry/button-filled.json +1 -1
  105. package/dist/mcp-data/registry/button-unfilled.json +1 -1
  106. package/dist/mcp-data/registry/checkbox.json +1 -1
  107. package/dist/mcp-data/registry/chip.json +1 -1
  108. package/dist/mcp-data/registry/menu.json +7 -7
  109. package/dist/mcp-data/registry/mobile-bar-nav.json +6 -4
  110. package/dist/mcp-data/registry/mobile-section-switcher.json +4 -3
  111. package/dist/mcp-data/registry/mobile-sheet-nav.json +1 -1
  112. package/dist/mcp-data/registry/panel-nav.json +1 -1
  113. package/dist/mcp-data/registry/panel-sub-nav.json +1 -1
  114. package/dist/mcp-data/registry/radio.json +1 -1
  115. package/dist/mcp-data/registry/registry.json +9 -6
  116. package/dist/mcp-data/registry/select.json +1 -1
  117. package/dist/mcp-data/registry/shell-gradient-swatch.json +1 -1
  118. package/dist/mcp-data/registry/slider.json +1 -1
  119. package/dist/mcp-data/registry/swatch-picker.json +1 -1
  120. package/dist/mcp-data/registry/switch.json +1 -1
  121. package/dist/mcp-data/registry/tab-group.json +1 -1
  122. package/dist/mcp-data/registry/tag.json +1 -1
  123. package/dist/mcp-data/registry/toast.json +1 -1
  124. package/dist/types/components/Menu/Menu.d.ts +1 -1
  125. package/dist/types/components/MobileBarNav/MobileBarNav.d.ts +11 -0
  126. package/dist/types/components.d.ts +2 -2
  127. package/package.json +1 -1
  128. package/dist/components/p--bSkr_yn.js +0 -2
  129. package/dist/components/p--bSkr_yn.js.map +0 -1
  130. package/dist/components/p-46nk9-RO.js +0 -2
  131. package/dist/components/p-CFpWrYAH.js +0 -2
  132. package/dist/components/p-CXcjwduV.js +0 -2
  133. package/dist/components/p-DD1Lgs7O.js +0 -2
  134. package/dist/components/p-DD1Lgs7O.js.map +0 -1
  135. package/dist/components/p-DTea1Du7.js +0 -2
  136. package/dist/components/p-DcFfilOX.js +0 -2
  137. package/dist/components/p-YtgcuJYx.js +0 -2
  138. package/dist/components/p-loq_--ce.js +0 -2
  139. package/dist/components/p-loq_--ce.js.map +0 -1
  140. /package/dist/components/{p-CICGtu7H.js.map → p-B9xVWER6.js.map} +0 -0
  141. /package/dist/components/{p-D4o2D1hk.js.map → p-BLea0TrP.js.map} +0 -0
  142. /package/dist/components/{p-D9iN80yX.js.map → p-CWTih3Gc.js.map} +0 -0
  143. /package/dist/components/{p-CvKKmZJj.js.map → p-Ccj5Op6E.js.map} +0 -0
  144. /package/dist/components/{p-DvVW0hxm.js.map → p-DUlqUNbE.js.map} +0 -0
@@ -349,7 +349,7 @@
349
349
  "files": [
350
350
  {
351
351
  "path": "src/wc/components/Checkbox/Checkbox.css",
352
- "content": "@import '../../utils/control-inactive.css';\n@import '../../utils/control-density.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n\n:host {\n display: inline-flex;\n align-items: center;\n box-sizing: border-box;\n gap: var(--ds-control-gap);\n height: var(--ds-control-height);\n padding: 0 var(--ds-control-padding-inline);\n border-radius: var(--ds-control-radius);\n cursor: pointer;\n user-select: none;\n}\n\n:host(.checkbox--presentation) {\n width: var(--ds-control-icon);\n height: var(--ds-control-icon);\n padding: 0;\n gap: 0;\n pointer-events: none;\n}\n\n/* Inactive: `.ds-control-inactive` (50% opacity). */\n\n:host(.checkbox--md) {\n --_checkbox-box: var(--dimension-iconography-sm);\n --_checkbox-stroke: var(--dimension-stroke-width-015);\n}\n\n:host(.checkbox--sm) {\n --_checkbox-box: var(--dimension-iconography-xs);\n --_checkbox-stroke: var(--dimension-stroke-width-012);\n}\n\n:host(.checkbox--xs) {\n --_checkbox-box: var(--dimension-size-100);\n --_checkbox-stroke: calc(var(--dimension-stroke-width-base) * 3 / 32);\n}\n\n.checkbox__placement {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--ds-control-icon);\n height: var(--ds-control-icon);\n}\n\n.box {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: var(--_checkbox-box);\n height: var(--_checkbox-box);\n border: 0;\n border-radius: var(--dimension-radius-025);\n box-shadow: inset 0 0 0 var(--_checkbox-stroke) var(--color-foreground-tertiary);\n transition:\n background-color var(--effect-motion-short-2),\n box-shadow var(--effect-motion-short-2);\n}\n\n.box--marked {\n color: var(--color-foreground-on-bold-background-primary);\n background-color: var(--color-background-bold-brand);\n box-shadow: none;\n}\n\n.checkbox__mark {\n width: 100%;\n height: 100%;\n flex-shrink: 0;\n stroke: currentColor;\n stroke-width: var(--_checkbox-stroke);\n stroke-linecap: butt;\n stroke-linejoin: round;\n}\n\n.checkbox__mark--checked {\n transform: translateY(-0.5px);\n}\n\n.checkbox__label {\n flex: 1;\n min-width: 0;\n padding: 0 var(--ds-control-label-inset);\n}\n",
352
+ "content": "@import '../../utils/control-inactive.css';\n@import '../../utils/control-density.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n\n:host {\n display: inline-flex;\n align-items: center;\n box-sizing: border-box;\n gap: var(--ds-control-gap);\n height: var(--ds-control-height);\n padding: 0 var(--ds-control-padding-inline);\n border-radius: var(--ds-control-radius);\n cursor: default;\n user-select: none;\n}\n\n:host(.checkbox--presentation) {\n width: var(--ds-control-icon);\n height: var(--ds-control-icon);\n padding: 0;\n gap: 0;\n pointer-events: none;\n}\n\n/* Inactive: `.ds-control-inactive` (50% opacity). */\n\n:host(.checkbox--md) {\n --_checkbox-box: var(--dimension-iconography-sm);\n --_checkbox-stroke: var(--dimension-stroke-width-015);\n}\n\n:host(.checkbox--sm) {\n --_checkbox-box: var(--dimension-iconography-xs);\n --_checkbox-stroke: var(--dimension-stroke-width-012);\n}\n\n:host(.checkbox--xs) {\n --_checkbox-box: var(--dimension-size-100);\n --_checkbox-stroke: calc(var(--dimension-stroke-width-base) * 3 / 32);\n}\n\n.checkbox__placement {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--ds-control-icon);\n height: var(--ds-control-icon);\n}\n\n.box {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: var(--_checkbox-box);\n height: var(--_checkbox-box);\n border: 0;\n border-radius: var(--dimension-radius-025);\n box-shadow: inset 0 0 0 var(--_checkbox-stroke) var(--color-foreground-tertiary);\n transition:\n background-color var(--effect-motion-short-2),\n box-shadow var(--effect-motion-short-2);\n}\n\n.box--marked {\n color: var(--color-foreground-on-bold-background-primary);\n background-color: var(--color-background-bold-brand);\n box-shadow: none;\n}\n\n.checkbox__mark {\n width: 100%;\n height: 100%;\n flex-shrink: 0;\n stroke: currentColor;\n stroke-width: var(--_checkbox-stroke);\n stroke-linecap: butt;\n stroke-linejoin: round;\n}\n\n.checkbox__mark--checked {\n transform: translateY(-0.5px);\n}\n\n.checkbox__label {\n flex: 1;\n min-width: 0;\n padding: 0 var(--ds-control-label-inset);\n}\n",
353
353
  "type": "registry:ui"
354
354
  },
355
355
  {
@@ -264,7 +264,7 @@
264
264
  "files": [
265
265
  {
266
266
  "path": "src/wc/components/Chip/Chip.css",
267
- "content": "@import '../../utils/tag-chip.css';\n\n/*\n * Semantic state colors — Chip’s differentiator from Tag.\n * All states use the faint contrast weight; hue comes from the state.\n */\n:host(.chip--default) {\n background-color: var(--color-background-faint-neutral);\n color: var(--color-foreground-bold-neutral);\n}\n\n:host(.chip--active) {\n background-color: var(--color-background-faint-brand);\n color: var(--color-foreground-bold-brand);\n}\n\n:host(.chip--error) {\n background-color: var(--color-background-faint-negative);\n color: var(--color-foreground-bold-negative);\n}\n\n:host(.chip--caution) {\n background-color: var(--color-background-faint-caution);\n color: var(--color-foreground-bold-caution);\n}\n\n/*\n * Label↔dismiss uses host gap, mirroring Tag icon↔label spacing:\n * [inset][text][inset][gap][icon].\n */\n.tag__remove {\n padding: 0;\n margin: 0;\n border: none;\n background: none;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n color: inherit;\n flex-shrink: 0;\n position: relative;\n z-index: 2;\n border-radius: var(--dimension-radius-half);\n}\n\n.tag__remove::before {\n content: '';\n position: absolute;\n inset: var(--dimension-space-n025);\n border-radius: var(--dimension-radius-half);\n background-color: transparent;\n transition: var(--effect-transition-interaction-background-short-2);\n pointer-events: none;\n}\n\n.tag__remove:hover::before {\n background-color: var(--color-interaction-hover);\n}\n\n.tag__remove:active::before {\n background-color: var(--color-interaction-pressed);\n}\n\n:host(.tag--size-sm) .tag__remove::before {\n inset: var(--dimension-space-n012);\n}\n\n:host(.tag--size-xs) .tag__remove::before {\n inset: 0;\n}\n\n.tag__remove-x {\n display: inline-flex;\n flex-shrink: 0;\n width: var(--ds-control-icon);\n height: var(--ds-control-icon);\n}\n",
267
+ "content": "@import '../../utils/tag-chip.css';\n\n/*\n * Semantic state colors — Chip’s differentiator from Tag.\n * All states use the faint contrast weight; hue comes from the state.\n */\n:host(.chip--default) {\n background-color: var(--color-background-faint-neutral);\n color: var(--color-foreground-bold-neutral);\n}\n\n:host(.chip--active) {\n background-color: var(--color-background-faint-brand);\n color: var(--color-foreground-bold-brand);\n}\n\n:host(.chip--error) {\n background-color: var(--color-background-faint-negative);\n color: var(--color-foreground-bold-negative);\n}\n\n:host(.chip--caution) {\n background-color: var(--color-background-faint-caution);\n color: var(--color-foreground-bold-caution);\n}\n\n/*\n * Label↔dismiss uses host gap, mirroring Tag icon↔label spacing:\n * [inset][text][inset][gap][icon].\n */\n.tag__remove {\n padding: 0;\n margin: 0;\n border: none;\n background: none;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: default;\n color: inherit;\n flex-shrink: 0;\n position: relative;\n z-index: 2;\n border-radius: var(--dimension-radius-half);\n}\n\n.tag__remove::before {\n content: '';\n position: absolute;\n inset: var(--dimension-space-n025);\n border-radius: var(--dimension-radius-half);\n background-color: transparent;\n transition: var(--effect-transition-interaction-background-short-2);\n pointer-events: none;\n}\n\n.tag__remove:hover::before {\n background-color: var(--color-interaction-hover);\n}\n\n.tag__remove:active::before {\n background-color: var(--color-interaction-pressed);\n}\n\n:host(.tag--size-sm) .tag__remove::before {\n inset: var(--dimension-space-n012);\n}\n\n:host(.tag--size-xs) .tag__remove::before {\n inset: 0;\n}\n\n.tag__remove-x {\n display: inline-flex;\n flex-shrink: 0;\n width: var(--ds-control-icon);\n height: var(--ds-control-icon);\n}\n",
268
268
  "type": "registry:ui"
269
269
  },
270
270
  {
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
3
  "name": "menu",
4
4
  "title": "Menu",
5
- "description": "Anchored action menu with flat commands, grouped sections, single-selection radio choices, supplemental notification dots, switches, and destructive actions.",
5
+ "description": "Anchored action menu with flat commands, grouped sections, single-selection choices, supplemental notification dots, switches, and destructive actions.",
6
6
  "type": "registry:ui",
7
7
  "meta": {
8
8
  "library": "@ds-mo/ui",
@@ -13,7 +13,7 @@
13
13
  "intent": {
14
14
  "audience": "general",
15
15
  "status": "stable",
16
- "summary": "Anchored action menu with flat commands, grouped sections, single-selection radio choices, supplemental notification dots, switches, and destructive actions.",
16
+ "summary": "Anchored action menu with flat commands, grouped sections, single-selection choices, supplemental notification dots, switches, and destructive actions.",
17
17
  "useWhen": [
18
18
  "A trigger reveals a short contextual list of commands or state-changing actions.",
19
19
  "Actions need grouping, mutually exclusive selected state, supplemental notification dots, switch rows, or destructive treatment."
@@ -54,7 +54,7 @@
54
54
  "Open with visible initial focus only when the trigger was activated from the keyboard.",
55
55
  "Keep disabled items discoverable only when their unavailable state is meaningful.",
56
56
  "Switch rows use menuitemcheckbox semantics on the row; the nested ds-switch is an aria-hidden md presentation indicator.",
57
- "Single-selection menus use menuitemradio semantics and aria-checked on each row; the leading radio mark is presentation-only.",
57
+ "Single-selection menus use menuitemradio semantics and aria-checked on each row, but render no nested radio control; selected interaction fill and primary foreground provide the visual state.",
58
58
  "A swatch-picker section changes the popup to a non-modal dialog so its radio group and ordinary preference buttons keep valid nested semantics; Tab moves between its controls, then leaves and closes at the popup boundary.",
59
59
  "Use notification dots only as supplemental status and not as the sole indication of important information."
60
60
  ],
@@ -197,7 +197,7 @@
197
197
  "default": "'none'",
198
198
  "required": false,
199
199
  "mutable": false,
200
- "description": "Give ordinary rows mutually-exclusive radio-menu semantics using isSelected."
200
+ "description": "Give ordinary rows mutually-exclusive menu semantics using isSelected."
201
201
  },
202
202
  "side": {
203
203
  "type": "MenuSide",
@@ -364,7 +364,7 @@
364
364
  "default": "'none'",
365
365
  "required": false,
366
366
  "mutable": false,
367
- "description": "Give ordinary rows mutually-exclusive radio-menu semantics using isSelected."
367
+ "description": "Give ordinary rows mutually-exclusive menu semantics using isSelected."
368
368
  },
369
369
  "side": {
370
370
  "type": "MenuSide",
@@ -470,12 +470,12 @@
470
470
  "files": [
471
471
  {
472
472
  "path": "src/wc/components/Menu/Menu.css",
473
- "content": "@import '../../utils/choice-list.css';\n\n/* Inactive: `.ds-control-inactive` (50% opacity). */\n\n.menu-item--destructive .menu-item__label {\n /* Beat ds-text color-* host classes for destructive rows. */\n color: var(--color-foreground-bold-negative) !important;\n}\n\n/* Standard md icon zone keeps the dot aligned with other control suffixes. */\n.menu-item__dot-box {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--ds-control-icon, var(--dimension-iconography-md));\n height: var(--ds-control-icon, var(--dimension-iconography-md));\n pointer-events: none;\n}\n\n/* ── Switch suffix ───────────────────────────────────────────────────────── */\n\n.menu-item__switch {\n flex-shrink: 0;\n}\n\n/* ── Single-selection radio indicator ───────────────────────────────────── */\n\n.menu-item__radio-box {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--ds-control-icon, var(--dimension-iconography-md));\n height: var(--ds-control-icon, var(--dimension-iconography-md));\n pointer-events: none;\n}\n\n.menu-item__radio {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: var(--dimension-iconography-sm);\n height: var(--dimension-iconography-sm);\n border-radius: var(--dimension-radius-half);\n box-shadow: inset 0 0 0 var(--dimension-stroke-width-015) var(--color-foreground-tertiary);\n}\n\n.menu-item__radio--checked {\n background-color: var(--color-background-bold-brand);\n box-shadow: none;\n}\n\n.menu-item__radio-dot {\n width: var(--dimension-size-100);\n height: var(--dimension-size-100);\n border-radius: var(--dimension-radius-half);\n background-color: var(--color-foreground-on-bold-background-primary);\n}\n",
473
+ "content": "@import '../../utils/choice-list.css';\n\n/* Inactive: `.ds-control-inactive` (50% opacity). */\n\n.menu-item--destructive .menu-item__label {\n /* Beat ds-text color-* host classes for destructive rows. */\n color: var(--color-foreground-bold-negative) !important;\n}\n\n/* Standard md icon zone keeps the dot aligned with other control suffixes. */\n.menu-item__dot-box {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--ds-control-icon, var(--dimension-iconography-md));\n height: var(--ds-control-icon, var(--dimension-iconography-md));\n pointer-events: none;\n}\n\n/* ── Switch suffix ───────────────────────────────────────────────────────── */\n\n.menu-item__switch {\n flex-shrink: 0;\n}\n",
474
474
  "type": "registry:ui"
475
475
  },
476
476
  {
477
477
  "path": "src/wc/components/Menu/Menu.tsx",
478
- "content": "import {\n Component,\n Prop,\n State,\n Event,\n EventEmitter,\n Element,\n Watch,\n Listen,\n h,\n Host,\n} from '@stencil/core';\nimport {\n choicePopupMinWidth,\n resolveChoicePopupAlignOffset,\n resolveCssLengthPx,\n resolveMotionTimeMs,\n TOKEN_CSS_LENGTHS,\n TOKEN_DEFAULTS,\n type ChoicePopupAnchorAlignment,\n} from '../../utils';\nimport { computeMenuPosition, type MenuAlign, type MenuSide } from './menu-position';\nimport type { MenuItemData, MenuSection } from './menu-types';\n\nexport type MenuSelectionMode = 'none' | 'single';\nimport {\n isMenuGradientPickerSection,\n isMenuPickerSection,\n isMenuSwatchPickerSection,\n} from './menu-types';\nimport {\n shellGradientPickerSections,\n type ShellGradientPreset,\n} from '../../shell/shell-gradient-presets';\n\n/** rAF retries while the popup mounts or the anchor resolves. */\nconst POSITION_RETRY_BUDGET = 8;\nconst MENU_FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled]):not([type=\"hidden\"])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(',');\n\nfunction snapshotMenuSections(sections: MenuSection[]): MenuSection[] {\n return sections.map(section => {\n if (isMenuGradientPickerSection(section)) return { ...section };\n if (isMenuSwatchPickerSection(section)) {\n return {\n ...section,\n options: section.options?.map(option => ({ ...option })),\n sections: section.sections?.map(swatchSection => ({\n ...swatchSection,\n options: swatchSection.options.map(option => ({ ...option })),\n })),\n };\n }\n return { ...section, items: section.items.map(item => ({ ...item })) };\n });\n}\n\n@Component({\n tag: 'ds-menu',\n styleUrl: 'Menu.css',\n scoped: true,\n})\nexport class Menu {\n @Element() el!: HTMLElement;\n\n @Prop({ mutable: true }) open: boolean = false;\n @Prop() items: MenuItemData[] = [];\n /** Give ordinary rows mutually-exclusive radio-menu semantics using isSelected. */\n @Prop() selectionMode: MenuSelectionMode = 'none';\n @Prop() sections: MenuSection[] = [];\n /** Preferred side; placement flips to the opposite side when that offers a better fit. */\n @Prop() side: MenuSide = 'bottom';\n @Prop() align: MenuAlign = 'start';\n /** Align choice-row edges to the anchor by default; use `popup-frame` only for custom frame geometry. */\n @Prop() anchorAlignment: ChoicePopupAnchorAlignment = 'choice-cell';\n /** Gap between anchor and menu — number (px) or TokoMo length (`var(--dimension-space-050)`, etc.). */\n @Prop() sideOffset: number | string = TOKEN_CSS_LENGTHS.space050;\n /** Cross-axis offset — number (px) or TokoMo length. */\n @Prop() alignOffset: number | string = 0;\n @Prop() menuWidth: string | undefined;\n @Prop() minWidth: string | undefined;\n /** External trigger element to position against. Set via JS: menuEl.anchor = buttonEl */\n @Prop() anchor: HTMLElement | undefined;\n /** ID of the external trigger element for positioning */\n @Prop() anchorId: string | undefined;\n /** Show a visible ring on the initially focused menu item. Use only when the opener was keyboard-driven. */\n @Prop() initialFocusVisible: boolean = false;\n /** Accessible name for the popup menu. */\n @Prop() menuLabel: string = 'Menu';\n\n @State() private shouldRender: boolean = false;\n @State() private closing: boolean = false;\n @State() private pos: { x: number; y: number } = { x: 0, y: 0 };\n @State() private focusedIndex: number = 0;\n @State() private positionReady: boolean = false;\n @State() private focusRingVisible: boolean = false;\n\n @Event() dsClose!: EventEmitter<void>;\n /** Emitted after the popup's exit motion is complete and its rendered content is removed. */\n @Event() dsAfterClose!: EventEmitter<void>;\n @Event() dsSelect!: EventEmitter<MenuItemData>;\n /** Emitted when a `gradient-picker` section swatch is chosen. */\n @Event() dsGradientSelect!: EventEmitter<ShellGradientPreset>;\n /** Emitted when a generic `swatch-picker` section option is chosen. */\n @Event() dsSwatchSelect!: EventEmitter<string>;\n\n private clickOutsideHandler: ((e: MouseEvent) => void) | null = null;\n private scrollResizeHandler: (() => void) | null = null;\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n private positionRetryRaf: number | null = null;\n private livePositionRaf: number | null = null;\n /** Last content actually painted while open; retained unchanged through exit motion. */\n private lastRenderedSections: MenuSection[] = [];\n private closingSections: MenuSection[] | null = null;\n\n componentDidLoad() {\n if (this.open) this.onOpenChange(true);\n }\n\n componentDidRender() {\n if (!this.shouldRender) return;\n const popup = this.el.querySelector<HTMLElement>('.menu-popup');\n if (!popup || typeof popup.showPopover !== 'function') return;\n if (!popup.matches(':popover-open')) popup.showPopover();\n }\n\n disconnectedCallback() {\n this.cancelPositionRetry();\n this.cancelLivePositionUpdate();\n this.teardownListeners();\n }\n\n @Watch('open')\n onOpenChange(isOpen: boolean) {\n if (isOpen) {\n this.teardownListeners();\n this.closingSections = null;\n this.shouldRender = true;\n this.closing = false;\n this.positionReady = false;\n this.focusRingVisible = this.initialFocusVisible;\n this.setupListeners();\n this.schedulePositionUpdate(() => {\n this.focusInitialItem();\n });\n } else if (this.shouldRender) {\n this.cancelPositionRetry();\n this.captureClosingSections();\n this.closing = true;\n this.teardownListeners();\n const closeAnimationMs = this.closeAnimationMs;\n if (closeAnimationMs <= 0) {\n this.finishClose();\n return;\n }\n this.closeTimer = setTimeout(() => {\n this.closeTimer = null;\n this.finishClose();\n }, closeAnimationMs);\n }\n }\n\n @Watch('anchor')\n @Watch('anchorId')\n onAnchorChange() {\n if (this.open) this.schedulePositionUpdate();\n }\n\n @Watch('side')\n @Watch('align')\n @Watch('anchorAlignment')\n @Watch('sideOffset')\n @Watch('alignOffset')\n onPositionPropsChange() {\n if (this.open) this.schedulePositionUpdate();\n }\n\n private get viewportPadPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n }\n\n private get sideOffsetPx(): number {\n return resolveCssLengthPx(this.sideOffset, TOKEN_DEFAULTS.space050);\n }\n\n private get alignOffsetPx(): number {\n return resolveCssLengthPx(this.alignOffset, 0);\n }\n\n private get popupFallbackWidthPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuWidthXs, TOKEN_DEFAULTS.menuWidthXs);\n }\n\n private get popupFallbackHeightPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuFallbackHeight, TOKEN_DEFAULTS.menuFallbackHeight);\n }\n\n private get closeAnimationMs(): number {\n return resolveMotionTimeMs(TOKEN_DEFAULTS.motionShort2, TOKEN_DEFAULTS.animationDurationShort3);\n }\n\n private finishClose() {\n const popup = this.el.querySelector<HTMLElement>('.menu-popup');\n if (popup?.matches(':popover-open')) popup.hidePopover();\n this.shouldRender = false;\n this.closing = false;\n this.lastRenderedSections = [];\n this.closingSections = null;\n this.dsAfterClose.emit();\n }\n\n private captureClosingSections() {\n if (this.closingSections) return;\n const visibleSections = this.lastRenderedSections.length\n ? this.lastRenderedSections\n : this.activeSections;\n this.closingSections = snapshotMenuSections(visibleSections);\n }\n\n private get resolvedAnchor(): HTMLElement | null {\n if (this.anchor) return this.anchor;\n if (this.anchorId) return document.getElementById(this.anchorId);\n return null;\n }\n\n private get anchorFocusTarget(): HTMLElement | null {\n const anchor = this.resolvedAnchor;\n if (!anchor) return null;\n if (anchor.matches(MENU_FOCUSABLE_SELECTOR)) return anchor;\n return anchor.querySelector<HTMLElement>(MENU_FOCUSABLE_SELECTOR) ?? anchor;\n }\n\n private focusAnchor() {\n const anchor = this.resolvedAnchor as\n | (HTMLElement & { setFocus?: () => Promise<void> | void })\n | null;\n if (anchor?.setFocus) {\n anchor.setFocus();\n return;\n }\n this.anchorFocusTarget?.focus();\n }\n\n private moveFocusAfterTab(backwards: boolean) {\n const anchor = this.anchorFocusTarget;\n if (!anchor) return;\n if (backwards) {\n anchor.focus();\n return;\n }\n\n const candidates = Array.from(\n document.querySelectorAll<HTMLElement>(MENU_FOCUSABLE_SELECTOR)\n ).filter(\n element =>\n !element.closest('.menu-popup') &&\n !element.closest('[inert]') &&\n element.getClientRects().length > 0\n );\n const index = candidates.indexOf(anchor);\n (candidates[index + 1] ?? anchor).focus();\n }\n\n private compositeTabLeavesPopup(event: KeyboardEvent): boolean {\n const popup = this.el.querySelector<HTMLElement>('.menu-popup');\n if (!popup) return true;\n const focusables = Array.from(\n popup.querySelectorAll<HTMLElement>(MENU_FOCUSABLE_SELECTOR)\n ).filter(element => element.getClientRects().length > 0);\n const currentIndex = focusables.indexOf(event.target as HTMLElement);\n if (currentIndex < 0) return true;\n return event.shiftKey ? currentIndex === 0 : currentIndex === focusables.length - 1;\n }\n\n private get activeSections(): MenuSection[] {\n if (this.sections.length > 0) return this.sections;\n if (this.items.length > 0) return [{ items: this.items }];\n return [];\n }\n\n private get flatItems(): MenuItemData[] {\n return this.activeSections.flatMap(section =>\n isMenuPickerSection(section) ? [] : section.items\n );\n }\n\n /** Rich preference content is a non-modal dialog, not an ARIA action menu. */\n private get hasCompositeSections(): boolean {\n return this.activeSections.some(isMenuPickerSection);\n }\n\n private cancelPositionRetry() {\n if (this.positionRetryRaf !== null) {\n cancelAnimationFrame(this.positionRetryRaf);\n this.positionRetryRaf = null;\n }\n }\n\n private cancelLivePositionUpdate() {\n if (this.livePositionRaf !== null) {\n cancelAnimationFrame(this.livePositionRaf);\n this.livePositionRaf = null;\n }\n }\n\n /** Reposition after the current scroll/layout frame without hiding the open popup. */\n private scheduleLivePositionUpdate() {\n if (this.livePositionRaf !== null) return;\n this.livePositionRaf = requestAnimationFrame(() => {\n if (this.shouldRender && !this.closing) this.calculatePosition();\n // Scroll-driven owners such as ShellPage may commit their compact header\n // render in this frame. Re-read once after that render, then stop tracking.\n this.livePositionRaf = requestAnimationFrame(() => {\n this.livePositionRaf = null;\n if (this.shouldRender && !this.closing) this.calculatePosition();\n });\n });\n }\n\n /** Retry until anchor + popup exist — do not reveal at 0,0 on a failed first pass. */\n private schedulePositionUpdate(onReady?: () => void) {\n if (!this.open) return;\n\n this.cancelPositionRetry();\n this.positionReady = false;\n\n let remaining = POSITION_RETRY_BUDGET;\n\n const attempt = () => {\n this.positionRetryRaf = null;\n if (!this.open) return;\n\n if (this.calculatePosition()) {\n this.positionReady = true;\n onReady?.();\n return;\n }\n\n if (remaining > 0) {\n remaining -= 1;\n this.positionRetryRaf = requestAnimationFrame(attempt);\n }\n };\n\n this.positionRetryRaf = requestAnimationFrame(attempt);\n }\n\n /** @returns `true` when anchor and popup were found and `pos` was updated. */\n private calculatePosition(): boolean {\n const anchorEl = this.resolvedAnchor;\n if (!anchorEl) return false;\n const popup = this.el.querySelector('.menu-popup') as HTMLElement | null;\n if (!popup) return false;\n\n const anchorRect = anchorEl.getBoundingClientRect();\n const sectionInsetPx = this.viewportPadPx;\n if (!this.minWidth) {\n if (\n this.anchorAlignment === 'choice-cell' &&\n (this.side === 'top' || this.side === 'bottom')\n ) {\n popup.style.minWidth = `max(var(--dimension-menu-width-xs), ${choicePopupMinWidth(\n anchorRect.width,\n sectionInsetPx\n )}px)`;\n } else {\n popup.style.removeProperty('min-width');\n }\n }\n\n const next = computeMenuPosition({\n anchorRect,\n popupWidth: popup.offsetWidth || this.popupFallbackWidthPx,\n popupHeight: popup.offsetHeight || this.popupFallbackHeightPx,\n side: this.side,\n align: this.align,\n sideOffsetPx: this.sideOffsetPx,\n alignOffsetPx: resolveChoicePopupAlignOffset({\n align: this.align,\n alignOffsetPx: this.alignOffsetPx,\n sectionInsetPx,\n anchorAlignment: this.anchorAlignment,\n }),\n viewportPadPx: this.viewportPadPx,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n });\n if (next.x !== this.pos.x || next.y !== this.pos.y) this.pos = next;\n return true;\n }\n\n /** Focus the selected item when present, otherwise the first enabled item. */\n private focusInitialItem() {\n if (this.hasCompositeSections) {\n requestAnimationFrame(() => {\n this.el.querySelector<HTMLElement>('.menu-popup [tabindex=\"0\"]')?.focus();\n });\n return;\n }\n\n const flat = this.flatItems;\n const selectedIdx = flat.findIndex(it => it.isSelected && !it.isInactive);\n const firstEnabledIdx = flat.findIndex(it => !it.isInactive);\n this.focusedIndex = selectedIdx >= 0 ? selectedIdx : firstEnabledIdx >= 0 ? firstEnabledIdx : 0;\n\n requestAnimationFrame(() => {\n const btns = this.el.querySelectorAll<HTMLElement>('.menu-item');\n btns[this.focusedIndex]?.focus();\n });\n }\n\n private setupListeners() {\n this.clickOutsideHandler = (e: MouseEvent) => {\n const t = e.target as Node;\n const popup = this.el.querySelector('.menu-popup');\n const anchorEl = this.resolvedAnchor;\n if (popup?.contains(t) || anchorEl?.contains(t)) return;\n this.close();\n };\n\n this.scrollResizeHandler = () => this.scheduleLivePositionUpdate();\n\n document.addEventListener('mousedown', this.clickOutsideHandler, true);\n window.addEventListener('scroll', this.scrollResizeHandler, true);\n window.addEventListener('resize', this.scrollResizeHandler);\n }\n\n private teardownListeners() {\n this.cancelLivePositionUpdate();\n if (this.clickOutsideHandler) {\n document.removeEventListener('mousedown', this.clickOutsideHandler, true);\n this.clickOutsideHandler = null;\n }\n if (this.scrollResizeHandler) {\n window.removeEventListener('scroll', this.scrollResizeHandler, true);\n window.removeEventListener('resize', this.scrollResizeHandler);\n this.scrollResizeHandler = null;\n }\n if (this.closeTimer) {\n clearTimeout(this.closeTimer);\n this.closeTimer = null;\n }\n }\n\n private close(restoreFocus = true) {\n this.captureClosingSections();\n if (restoreFocus) this.focusAnchor();\n this.dsClose.emit();\n this.open = false;\n this.onOpenChange(false);\n }\n\n @Listen('keydown')\n handleKeyDown(e: KeyboardEvent) {\n if (!this.shouldRender || this.closing) return;\n\n if (e.key === 'Escape') {\n e.preventDefault();\n this.close();\n return;\n }\n\n if (e.key === 'Tab') {\n if (this.hasCompositeSections && !this.compositeTabLeavesPopup(e)) return;\n e.preventDefault();\n this.close(false);\n this.moveFocusAfterTab(e.shiftKey);\n return;\n }\n\n if (this.hasCompositeSections) return;\n\n const flat = this.flatItems;\n const enabled = flat\n .map((it, i) => ({ it, i }))\n .filter(({ it }) => !it.isInactive)\n .map(({ i }) => i);\n if (!enabled.length) return;\n\n const cur = enabled.indexOf(this.focusedIndex);\n const safe = cur < 0 ? 0 : cur;\n\n switch (e.key) {\n case 'ArrowDown':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[(safe + 1) % enabled.length];\n this.focusItem(this.focusedIndex);\n break;\n case 'ArrowUp':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[(safe - 1 + enabled.length) % enabled.length];\n this.focusItem(this.focusedIndex);\n break;\n case 'Home':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[0];\n this.focusItem(this.focusedIndex);\n break;\n case 'End':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[enabled.length - 1];\n this.focusItem(this.focusedIndex);\n break;\n }\n }\n\n private focusItem(idx: number) {\n const btns = this.el.querySelectorAll<HTMLElement>('.menu-item');\n btns[idx]?.focus();\n }\n\n private handleItemClick(item: MenuItemData) {\n if (item.isInactive) return;\n // Consumers commonly change the item source while handling selection.\n // Capture first so the exit animation never paints the next menu context.\n this.captureClosingSections();\n this.dsSelect.emit(item);\n }\n\n private handleGradientSelect(preset: ShellGradientPreset) {\n this.dsGradientSelect.emit(preset);\n }\n\n private handleSwatchSelect(value: string) {\n this.dsSwatchSelect.emit(value);\n }\n\n render() {\n if (!this.shouldRender) return <Host style={{ display: 'contents' }} />;\n\n const sections = this.closing\n ? (this.closingSections ?? this.lastRenderedSections)\n : this.activeSections;\n const hasCompositeSections = sections.some(isMenuPickerSection);\n if (!this.closing) {\n this.lastRenderedSections = snapshotMenuSections(sections);\n this.closingSections = null;\n }\n let flatIdx = 0;\n\n const popupStyle: Record<string, string> = {\n position: 'fixed',\n left: '0',\n top: '0',\n transform: `translate(${Math.round(this.pos.x)}px, ${Math.round(this.pos.y)}px)`,\n zIndex: '9998',\n visibility: this.positionReady ? 'visible' : 'hidden',\n };\n\n if (this.menuWidth) popupStyle['width'] = this.menuWidth;\n if (this.minWidth) popupStyle['min-width'] = this.minWidth;\n\n return (\n <Host style={{ display: 'contents' }}>\n <div\n popover=\"manual\"\n class={{\n 'menu-popup': true,\n 'menu-popup--closing': this.closing,\n 'ds-choice-popup': true,\n 'ds-choice-popup--closing': this.closing,\n }}\n style={popupStyle}\n role={hasCompositeSections ? 'dialog' : 'menu'}\n aria-label={this.menuLabel}\n aria-orientation={hasCompositeSections ? undefined : 'vertical'}\n >\n <div class=\"ds-choice-list\">\n {sections.map((section, si) => (\n <div\n key={si}\n class={{\n 'menu-section': true,\n 'menu-section--divided': si < sections.length - 1,\n 'menu-section--gradient-picker': isMenuPickerSection(section),\n 'ds-choice-section': true,\n 'ds-choice-section--divided': si < sections.length - 1,\n }}\n role={section.header ? 'group' : undefined}\n aria-label={section.header}\n >\n {section.header && (\n <ds-text\n class=\"section-header ds-choice-section__header ds-control--md\"\n as=\"span\"\n variant=\"text-body-small\"\n emphasis\n color=\"primary\"\n aria-hidden=\"true\"\n >\n {section.header}\n </ds-text>\n )}\n {isMenuGradientPickerSection(section) ? (\n <ds-swatch-picker\n value={section.value}\n groupLabel={section.header ?? 'Shell gradient theme'}\n sections={shellGradientPickerSections()}\n onDsChange={(e: CustomEvent<string>) => {\n e.stopPropagation();\n this.handleGradientSelect(e.detail as ShellGradientPreset);\n }}\n />\n ) : isMenuSwatchPickerSection(section) ? (\n <ds-swatch-picker\n value={section.value}\n groupLabel={section.groupLabel ?? section.header ?? 'Swatch options'}\n options={section.options ?? []}\n sections={section.sections ?? []}\n onDsChange={(e: CustomEvent<string>) => {\n e.stopPropagation();\n this.handleSwatchSelect(e.detail);\n }}\n />\n ) : (\n section.items.map(item => {\n const idx = flatIdx++;\n const isFocused = this.focusedIndex === idx;\n const isRadioItem =\n !hasCompositeSections &&\n this.selectionMode === 'single' &&\n !item.showSwitch;\n return (\n <button\n key={idx}\n type=\"button\"\n class={{\n 'menu-item': true,\n 'ds-choice-item': true,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-focus-ring--visible': isFocused && this.focusRingVisible,\n 'ds-interaction-fill': !item.isInactive,\n 'ds-interaction-fill--selected': !!item.isSelected && !item.isInactive,\n 'menu-item--selected': !!item.isSelected,\n 'ds-control-inactive': !!item.isInactive,\n 'menu-item--destructive': !!item.isDestructive,\n 'menu-item--focused': isFocused,\n }}\n role={\n hasCompositeSections\n ? undefined\n : item.showSwitch\n ? 'menuitemcheckbox'\n : isRadioItem\n ? 'menuitemradio'\n : 'menuitem'\n }\n aria-checked={\n !hasCompositeSections && item.showSwitch\n ? String(!!item.switchValue)\n : isRadioItem\n ? String(!!item.isSelected)\n : undefined\n }\n aria-pressed={\n hasCompositeSections\n ? String(item.showSwitch ? !!item.switchValue : !!item.isSelected)\n : undefined\n }\n aria-current={\n !hasCompositeSections &&\n !item.showSwitch &&\n !isRadioItem &&\n item.isSelected\n ? 'true'\n : undefined\n }\n disabled={item.isInactive}\n tabIndex={hasCompositeSections ? 0 : isFocused ? 0 : -1}\n onMouseDown={() => {\n this.focusRingVisible = false;\n }}\n onClick={() => this.handleItemClick(item)}\n onFocus={() => {\n this.focusedIndex = idx;\n }}\n >\n {isRadioItem && (\n <span\n class=\"menu-item__radio-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <span\n class={{\n 'menu-item__radio': true,\n 'menu-item__radio--checked': !!item.isSelected,\n }}\n >\n {item.isSelected && <span class=\"menu-item__radio-dot\" />}\n </span>\n </span>\n )}\n <div class=\"menu-item__content ds-choice-item__content ds-interaction-fill__content\">\n <ds-text\n class=\"menu-item__label ds-choice-item__label\"\n as=\"span\"\n variant=\"text-body-medium\"\n color={item.isSelected ? 'primary' : 'secondary'}\n >\n {item.label}\n </ds-text>\n {item.subtext && (\n <ds-text\n class=\"menu-item__subtext ds-choice-item__subtext\"\n as=\"span\"\n variant=\"text-body-small\"\n color=\"secondary\"\n >\n {item.subtext}\n </ds-text>\n )}\n </div>\n {item.dot && (\n <span\n class=\"menu-item__dot-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-badge\n class=\"menu-item__dot\"\n variant=\"dot\"\n hasRing={false}\n label=\"\"\n />\n </span>\n )}\n {item.showSwitch && (\n <ds-switch\n class=\"menu-item__switch ds-interaction-fill__content\"\n size=\"md\"\n checked={!!item.switchValue}\n presentation\n />\n )}\n </button>\n );\n })\n )}\n </div>\n ))}\n </div>\n </div>\n </Host>\n );\n }\n}\n",
478
+ "content": "import {\n Component,\n Prop,\n State,\n Event,\n EventEmitter,\n Element,\n Watch,\n Listen,\n h,\n Host,\n} from '@stencil/core';\nimport {\n choicePopupMinWidth,\n resolveChoicePopupAlignOffset,\n resolveCssLengthPx,\n resolveMotionTimeMs,\n TOKEN_CSS_LENGTHS,\n TOKEN_DEFAULTS,\n type ChoicePopupAnchorAlignment,\n} from '../../utils';\nimport { computeMenuPosition, type MenuAlign, type MenuSide } from './menu-position';\nimport type { MenuItemData, MenuSection } from './menu-types';\n\nexport type MenuSelectionMode = 'none' | 'single';\nimport {\n isMenuGradientPickerSection,\n isMenuPickerSection,\n isMenuSwatchPickerSection,\n} from './menu-types';\nimport {\n shellGradientPickerSections,\n type ShellGradientPreset,\n} from '../../shell/shell-gradient-presets';\n\n/** rAF retries while the popup mounts or the anchor resolves. */\nconst POSITION_RETRY_BUDGET = 8;\nconst MENU_FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled]):not([type=\"hidden\"])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(',');\n\nfunction snapshotMenuSections(sections: MenuSection[]): MenuSection[] {\n return sections.map(section => {\n if (isMenuGradientPickerSection(section)) return { ...section };\n if (isMenuSwatchPickerSection(section)) {\n return {\n ...section,\n options: section.options?.map(option => ({ ...option })),\n sections: section.sections?.map(swatchSection => ({\n ...swatchSection,\n options: swatchSection.options.map(option => ({ ...option })),\n })),\n };\n }\n return { ...section, items: section.items.map(item => ({ ...item })) };\n });\n}\n\n@Component({\n tag: 'ds-menu',\n styleUrl: 'Menu.css',\n scoped: true,\n})\nexport class Menu {\n @Element() el!: HTMLElement;\n\n @Prop({ mutable: true }) open: boolean = false;\n @Prop() items: MenuItemData[] = [];\n /** Give ordinary rows mutually-exclusive menu semantics using isSelected. */\n @Prop() selectionMode: MenuSelectionMode = 'none';\n @Prop() sections: MenuSection[] = [];\n /** Preferred side; placement flips to the opposite side when that offers a better fit. */\n @Prop() side: MenuSide = 'bottom';\n @Prop() align: MenuAlign = 'start';\n /** Align choice-row edges to the anchor by default; use `popup-frame` only for custom frame geometry. */\n @Prop() anchorAlignment: ChoicePopupAnchorAlignment = 'choice-cell';\n /** Gap between anchor and menu — number (px) or TokoMo length (`var(--dimension-space-050)`, etc.). */\n @Prop() sideOffset: number | string = TOKEN_CSS_LENGTHS.space050;\n /** Cross-axis offset — number (px) or TokoMo length. */\n @Prop() alignOffset: number | string = 0;\n @Prop() menuWidth: string | undefined;\n @Prop() minWidth: string | undefined;\n /** External trigger element to position against. Set via JS: menuEl.anchor = buttonEl */\n @Prop() anchor: HTMLElement | undefined;\n /** ID of the external trigger element for positioning */\n @Prop() anchorId: string | undefined;\n /** Show a visible ring on the initially focused menu item. Use only when the opener was keyboard-driven. */\n @Prop() initialFocusVisible: boolean = false;\n /** Accessible name for the popup menu. */\n @Prop() menuLabel: string = 'Menu';\n\n @State() private shouldRender: boolean = false;\n @State() private closing: boolean = false;\n @State() private pos: { x: number; y: number } = { x: 0, y: 0 };\n @State() private focusedIndex: number = 0;\n @State() private positionReady: boolean = false;\n @State() private focusRingVisible: boolean = false;\n\n @Event() dsClose!: EventEmitter<void>;\n /** Emitted after the popup's exit motion is complete and its rendered content is removed. */\n @Event() dsAfterClose!: EventEmitter<void>;\n @Event() dsSelect!: EventEmitter<MenuItemData>;\n /** Emitted when a `gradient-picker` section swatch is chosen. */\n @Event() dsGradientSelect!: EventEmitter<ShellGradientPreset>;\n /** Emitted when a generic `swatch-picker` section option is chosen. */\n @Event() dsSwatchSelect!: EventEmitter<string>;\n\n private clickOutsideHandler: ((e: MouseEvent) => void) | null = null;\n private scrollResizeHandler: (() => void) | null = null;\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n private positionRetryRaf: number | null = null;\n private livePositionRaf: number | null = null;\n /** Last content actually painted while open; retained unchanged through exit motion. */\n private lastRenderedSections: MenuSection[] = [];\n private closingSections: MenuSection[] | null = null;\n\n componentDidLoad() {\n if (this.open) this.onOpenChange(true);\n }\n\n componentDidRender() {\n if (!this.shouldRender) return;\n const popup = this.el.querySelector<HTMLElement>('.menu-popup');\n if (!popup || typeof popup.showPopover !== 'function') return;\n if (!popup.matches(':popover-open')) popup.showPopover();\n }\n\n disconnectedCallback() {\n this.cancelPositionRetry();\n this.cancelLivePositionUpdate();\n this.teardownListeners();\n }\n\n @Watch('open')\n onOpenChange(isOpen: boolean) {\n if (isOpen) {\n this.teardownListeners();\n this.closingSections = null;\n this.shouldRender = true;\n this.closing = false;\n this.positionReady = false;\n this.focusRingVisible = this.initialFocusVisible;\n this.setupListeners();\n this.schedulePositionUpdate(() => {\n this.focusInitialItem();\n });\n } else if (this.shouldRender) {\n this.cancelPositionRetry();\n this.captureClosingSections();\n this.closing = true;\n this.teardownListeners();\n const closeAnimationMs = this.closeAnimationMs;\n if (closeAnimationMs <= 0) {\n this.finishClose();\n return;\n }\n this.closeTimer = setTimeout(() => {\n this.closeTimer = null;\n this.finishClose();\n }, closeAnimationMs);\n }\n }\n\n @Watch('anchor')\n @Watch('anchorId')\n onAnchorChange() {\n if (this.open) this.schedulePositionUpdate();\n }\n\n @Watch('side')\n @Watch('align')\n @Watch('anchorAlignment')\n @Watch('sideOffset')\n @Watch('alignOffset')\n onPositionPropsChange() {\n if (this.open) this.schedulePositionUpdate();\n }\n\n private get viewportPadPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n }\n\n private get sideOffsetPx(): number {\n return resolveCssLengthPx(this.sideOffset, TOKEN_DEFAULTS.space050);\n }\n\n private get alignOffsetPx(): number {\n return resolveCssLengthPx(this.alignOffset, 0);\n }\n\n private get popupFallbackWidthPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuWidthXs, TOKEN_DEFAULTS.menuWidthXs);\n }\n\n private get popupFallbackHeightPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuFallbackHeight, TOKEN_DEFAULTS.menuFallbackHeight);\n }\n\n private get closeAnimationMs(): number {\n return resolveMotionTimeMs(TOKEN_DEFAULTS.motionShort2, TOKEN_DEFAULTS.animationDurationShort3);\n }\n\n private finishClose() {\n const popup = this.el.querySelector<HTMLElement>('.menu-popup');\n if (popup?.matches(':popover-open')) popup.hidePopover();\n this.shouldRender = false;\n this.closing = false;\n this.lastRenderedSections = [];\n this.closingSections = null;\n this.dsAfterClose.emit();\n }\n\n private captureClosingSections() {\n if (this.closingSections) return;\n const visibleSections = this.lastRenderedSections.length\n ? this.lastRenderedSections\n : this.activeSections;\n this.closingSections = snapshotMenuSections(visibleSections);\n }\n\n private get resolvedAnchor(): HTMLElement | null {\n if (this.anchor) return this.anchor;\n if (this.anchorId) return document.getElementById(this.anchorId);\n return null;\n }\n\n private get anchorFocusTarget(): HTMLElement | null {\n const anchor = this.resolvedAnchor;\n if (!anchor) return null;\n if (anchor.matches(MENU_FOCUSABLE_SELECTOR)) return anchor;\n return anchor.querySelector<HTMLElement>(MENU_FOCUSABLE_SELECTOR) ?? anchor;\n }\n\n private focusAnchor() {\n const anchor = this.resolvedAnchor as\n | (HTMLElement & { setFocus?: () => Promise<void> | void })\n | null;\n if (anchor?.setFocus) {\n anchor.setFocus();\n return;\n }\n this.anchorFocusTarget?.focus();\n }\n\n private moveFocusAfterTab(backwards: boolean) {\n const anchor = this.anchorFocusTarget;\n if (!anchor) return;\n if (backwards) {\n anchor.focus();\n return;\n }\n\n const candidates = Array.from(\n document.querySelectorAll<HTMLElement>(MENU_FOCUSABLE_SELECTOR)\n ).filter(\n element =>\n !element.closest('.menu-popup') &&\n !element.closest('[inert]') &&\n element.getClientRects().length > 0\n );\n const index = candidates.indexOf(anchor);\n (candidates[index + 1] ?? anchor).focus();\n }\n\n private compositeTabLeavesPopup(event: KeyboardEvent): boolean {\n const popup = this.el.querySelector<HTMLElement>('.menu-popup');\n if (!popup) return true;\n const focusables = Array.from(\n popup.querySelectorAll<HTMLElement>(MENU_FOCUSABLE_SELECTOR)\n ).filter(element => element.getClientRects().length > 0);\n const currentIndex = focusables.indexOf(event.target as HTMLElement);\n if (currentIndex < 0) return true;\n return event.shiftKey ? currentIndex === 0 : currentIndex === focusables.length - 1;\n }\n\n private get activeSections(): MenuSection[] {\n if (this.sections.length > 0) return this.sections;\n if (this.items.length > 0) return [{ items: this.items }];\n return [];\n }\n\n private get flatItems(): MenuItemData[] {\n return this.activeSections.flatMap(section =>\n isMenuPickerSection(section) ? [] : section.items\n );\n }\n\n /** Rich preference content is a non-modal dialog, not an ARIA action menu. */\n private get hasCompositeSections(): boolean {\n return this.activeSections.some(isMenuPickerSection);\n }\n\n private cancelPositionRetry() {\n if (this.positionRetryRaf !== null) {\n cancelAnimationFrame(this.positionRetryRaf);\n this.positionRetryRaf = null;\n }\n }\n\n private cancelLivePositionUpdate() {\n if (this.livePositionRaf !== null) {\n cancelAnimationFrame(this.livePositionRaf);\n this.livePositionRaf = null;\n }\n }\n\n /** Reposition after the current scroll/layout frame without hiding the open popup. */\n private scheduleLivePositionUpdate() {\n if (this.livePositionRaf !== null) return;\n this.livePositionRaf = requestAnimationFrame(() => {\n if (this.shouldRender && !this.closing) this.calculatePosition();\n // Scroll-driven owners such as ShellPage may commit their compact header\n // render in this frame. Re-read once after that render, then stop tracking.\n this.livePositionRaf = requestAnimationFrame(() => {\n this.livePositionRaf = null;\n if (this.shouldRender && !this.closing) this.calculatePosition();\n });\n });\n }\n\n /** Retry until anchor + popup exist — do not reveal at 0,0 on a failed first pass. */\n private schedulePositionUpdate(onReady?: () => void) {\n if (!this.open) return;\n\n this.cancelPositionRetry();\n this.positionReady = false;\n\n let remaining = POSITION_RETRY_BUDGET;\n\n const attempt = () => {\n this.positionRetryRaf = null;\n if (!this.open) return;\n\n if (this.calculatePosition()) {\n this.positionReady = true;\n onReady?.();\n return;\n }\n\n if (remaining > 0) {\n remaining -= 1;\n this.positionRetryRaf = requestAnimationFrame(attempt);\n }\n };\n\n this.positionRetryRaf = requestAnimationFrame(attempt);\n }\n\n /** @returns `true` when anchor and popup were found and `pos` was updated. */\n private calculatePosition(): boolean {\n const anchorEl = this.resolvedAnchor;\n if (!anchorEl) return false;\n const popup = this.el.querySelector('.menu-popup') as HTMLElement | null;\n if (!popup) return false;\n\n const anchorRect = anchorEl.getBoundingClientRect();\n const sectionInsetPx = this.viewportPadPx;\n if (!this.minWidth) {\n if (\n this.anchorAlignment === 'choice-cell' &&\n (this.side === 'top' || this.side === 'bottom')\n ) {\n popup.style.minWidth = `max(var(--dimension-menu-width-xs), ${choicePopupMinWidth(\n anchorRect.width,\n sectionInsetPx\n )}px)`;\n } else {\n popup.style.removeProperty('min-width');\n }\n }\n\n const next = computeMenuPosition({\n anchorRect,\n popupWidth: popup.offsetWidth || this.popupFallbackWidthPx,\n popupHeight: popup.offsetHeight || this.popupFallbackHeightPx,\n side: this.side,\n align: this.align,\n sideOffsetPx: this.sideOffsetPx,\n alignOffsetPx: resolveChoicePopupAlignOffset({\n align: this.align,\n alignOffsetPx: this.alignOffsetPx,\n sectionInsetPx,\n anchorAlignment: this.anchorAlignment,\n }),\n viewportPadPx: this.viewportPadPx,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n });\n if (next.x !== this.pos.x || next.y !== this.pos.y) this.pos = next;\n return true;\n }\n\n /** Focus the selected item when present, otherwise the first enabled item. */\n private focusInitialItem() {\n if (this.hasCompositeSections) {\n requestAnimationFrame(() => {\n this.el.querySelector<HTMLElement>('.menu-popup [tabindex=\"0\"]')?.focus();\n });\n return;\n }\n\n const flat = this.flatItems;\n const selectedIdx = flat.findIndex(it => it.isSelected && !it.isInactive);\n const firstEnabledIdx = flat.findIndex(it => !it.isInactive);\n this.focusedIndex = selectedIdx >= 0 ? selectedIdx : firstEnabledIdx >= 0 ? firstEnabledIdx : 0;\n\n requestAnimationFrame(() => {\n const btns = this.el.querySelectorAll<HTMLElement>('.menu-item');\n btns[this.focusedIndex]?.focus();\n });\n }\n\n private setupListeners() {\n this.clickOutsideHandler = (e: MouseEvent) => {\n const t = e.target as Node;\n const popup = this.el.querySelector('.menu-popup');\n const anchorEl = this.resolvedAnchor;\n if (popup?.contains(t) || anchorEl?.contains(t)) return;\n this.close();\n };\n\n this.scrollResizeHandler = () => this.scheduleLivePositionUpdate();\n\n document.addEventListener('mousedown', this.clickOutsideHandler, true);\n window.addEventListener('scroll', this.scrollResizeHandler, true);\n window.addEventListener('resize', this.scrollResizeHandler);\n }\n\n private teardownListeners() {\n this.cancelLivePositionUpdate();\n if (this.clickOutsideHandler) {\n document.removeEventListener('mousedown', this.clickOutsideHandler, true);\n this.clickOutsideHandler = null;\n }\n if (this.scrollResizeHandler) {\n window.removeEventListener('scroll', this.scrollResizeHandler, true);\n window.removeEventListener('resize', this.scrollResizeHandler);\n this.scrollResizeHandler = null;\n }\n if (this.closeTimer) {\n clearTimeout(this.closeTimer);\n this.closeTimer = null;\n }\n }\n\n private close(restoreFocus = true) {\n this.captureClosingSections();\n if (restoreFocus) this.focusAnchor();\n this.dsClose.emit();\n this.open = false;\n this.onOpenChange(false);\n }\n\n @Listen('keydown')\n handleKeyDown(e: KeyboardEvent) {\n if (!this.shouldRender || this.closing) return;\n\n if (e.key === 'Escape') {\n e.preventDefault();\n this.close();\n return;\n }\n\n if (e.key === 'Tab') {\n if (this.hasCompositeSections && !this.compositeTabLeavesPopup(e)) return;\n e.preventDefault();\n this.close(false);\n this.moveFocusAfterTab(e.shiftKey);\n return;\n }\n\n if (this.hasCompositeSections) return;\n\n const flat = this.flatItems;\n const enabled = flat\n .map((it, i) => ({ it, i }))\n .filter(({ it }) => !it.isInactive)\n .map(({ i }) => i);\n if (!enabled.length) return;\n\n const cur = enabled.indexOf(this.focusedIndex);\n const safe = cur < 0 ? 0 : cur;\n\n switch (e.key) {\n case 'ArrowDown':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[(safe + 1) % enabled.length];\n this.focusItem(this.focusedIndex);\n break;\n case 'ArrowUp':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[(safe - 1 + enabled.length) % enabled.length];\n this.focusItem(this.focusedIndex);\n break;\n case 'Home':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[0];\n this.focusItem(this.focusedIndex);\n break;\n case 'End':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[enabled.length - 1];\n this.focusItem(this.focusedIndex);\n break;\n }\n }\n\n private focusItem(idx: number) {\n const btns = this.el.querySelectorAll<HTMLElement>('.menu-item');\n btns[idx]?.focus();\n }\n\n private handleItemClick(item: MenuItemData) {\n if (item.isInactive) return;\n // Consumers commonly change the item source while handling selection.\n // Capture first so the exit animation never paints the next menu context.\n this.captureClosingSections();\n this.dsSelect.emit(item);\n }\n\n private handleGradientSelect(preset: ShellGradientPreset) {\n this.dsGradientSelect.emit(preset);\n }\n\n private handleSwatchSelect(value: string) {\n this.dsSwatchSelect.emit(value);\n }\n\n render() {\n if (!this.shouldRender) return <Host style={{ display: 'contents' }} />;\n\n const sections = this.closing\n ? (this.closingSections ?? this.lastRenderedSections)\n : this.activeSections;\n const hasCompositeSections = sections.some(isMenuPickerSection);\n if (!this.closing) {\n this.lastRenderedSections = snapshotMenuSections(sections);\n this.closingSections = null;\n }\n let flatIdx = 0;\n\n const popupStyle: Record<string, string> = {\n position: 'fixed',\n left: '0',\n top: '0',\n transform: `translate(${Math.round(this.pos.x)}px, ${Math.round(this.pos.y)}px)`,\n zIndex: '9998',\n visibility: this.positionReady ? 'visible' : 'hidden',\n };\n\n if (this.menuWidth) popupStyle['width'] = this.menuWidth;\n if (this.minWidth) popupStyle['min-width'] = this.minWidth;\n\n return (\n <Host style={{ display: 'contents' }}>\n <div\n popover=\"manual\"\n class={{\n 'menu-popup': true,\n 'menu-popup--closing': this.closing,\n 'ds-choice-popup': true,\n 'ds-choice-popup--closing': this.closing,\n }}\n style={popupStyle}\n role={hasCompositeSections ? 'dialog' : 'menu'}\n aria-label={this.menuLabel}\n aria-orientation={hasCompositeSections ? undefined : 'vertical'}\n >\n <div class=\"ds-choice-list\">\n {sections.map((section, si) => (\n <div\n key={si}\n class={{\n 'menu-section': true,\n 'menu-section--divided': si < sections.length - 1,\n 'menu-section--gradient-picker': isMenuPickerSection(section),\n 'ds-choice-section': true,\n 'ds-choice-section--divided': si < sections.length - 1,\n }}\n role={section.header ? 'group' : undefined}\n aria-label={section.header}\n >\n {section.header && (\n <ds-text\n class=\"section-header ds-choice-section__header ds-control--md\"\n as=\"span\"\n variant=\"text-body-small\"\n emphasis\n color=\"primary\"\n aria-hidden=\"true\"\n >\n {section.header}\n </ds-text>\n )}\n {isMenuGradientPickerSection(section) ? (\n <ds-swatch-picker\n value={section.value}\n groupLabel={section.header ?? 'Shell gradient theme'}\n sections={shellGradientPickerSections()}\n onDsChange={(e: CustomEvent<string>) => {\n e.stopPropagation();\n this.handleGradientSelect(e.detail as ShellGradientPreset);\n }}\n />\n ) : isMenuSwatchPickerSection(section) ? (\n <ds-swatch-picker\n value={section.value}\n groupLabel={section.groupLabel ?? section.header ?? 'Swatch options'}\n options={section.options ?? []}\n sections={section.sections ?? []}\n onDsChange={(e: CustomEvent<string>) => {\n e.stopPropagation();\n this.handleSwatchSelect(e.detail);\n }}\n />\n ) : (\n section.items.map(item => {\n const idx = flatIdx++;\n const isFocused = this.focusedIndex === idx;\n const isSingleSelectionItem =\n !hasCompositeSections &&\n this.selectionMode === 'single' &&\n !item.showSwitch;\n return (\n <button\n key={idx}\n type=\"button\"\n class={{\n 'menu-item': true,\n 'ds-choice-item': true,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-focus-ring--visible': isFocused && this.focusRingVisible,\n 'ds-interaction-fill': !item.isInactive,\n 'ds-interaction-fill--selected': !!item.isSelected && !item.isInactive,\n 'menu-item--selected': !!item.isSelected,\n 'ds-control-inactive': !!item.isInactive,\n 'menu-item--destructive': !!item.isDestructive,\n 'menu-item--focused': isFocused,\n }}\n role={\n hasCompositeSections\n ? undefined\n : item.showSwitch\n ? 'menuitemcheckbox'\n : isSingleSelectionItem\n ? 'menuitemradio'\n : 'menuitem'\n }\n aria-checked={\n !hasCompositeSections && item.showSwitch\n ? String(!!item.switchValue)\n : isSingleSelectionItem\n ? String(!!item.isSelected)\n : undefined\n }\n aria-pressed={\n hasCompositeSections\n ? String(item.showSwitch ? !!item.switchValue : !!item.isSelected)\n : undefined\n }\n aria-current={\n !hasCompositeSections &&\n !item.showSwitch &&\n !isSingleSelectionItem &&\n item.isSelected\n ? 'true'\n : undefined\n }\n disabled={item.isInactive}\n tabIndex={hasCompositeSections ? 0 : isFocused ? 0 : -1}\n onMouseDown={() => {\n this.focusRingVisible = false;\n }}\n onClick={() => this.handleItemClick(item)}\n onFocus={() => {\n this.focusedIndex = idx;\n }}\n >\n <div class=\"menu-item__content ds-choice-item__content ds-interaction-fill__content\">\n <ds-text\n class=\"menu-item__label ds-choice-item__label\"\n as=\"span\"\n variant=\"text-body-medium\"\n color={item.isSelected ? 'primary' : 'secondary'}\n >\n {item.label}\n </ds-text>\n {item.subtext && (\n <ds-text\n class=\"menu-item__subtext ds-choice-item__subtext\"\n as=\"span\"\n variant=\"text-body-small\"\n color=\"secondary\"\n >\n {item.subtext}\n </ds-text>\n )}\n </div>\n {item.dot && (\n <span\n class=\"menu-item__dot-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-badge\n class=\"menu-item__dot\"\n variant=\"dot\"\n hasRing={false}\n label=\"\"\n />\n </span>\n )}\n {item.showSwitch && (\n <ds-switch\n class=\"menu-item__switch ds-interaction-fill__content\"\n size=\"md\"\n checked={!!item.switchValue}\n presentation\n />\n )}\n </button>\n );\n })\n )}\n </div>\n ))}\n </div>\n </div>\n </Host>\n );\n }\n}\n",
479
479
  "type": "registry:ui"
480
480
  },
481
481
  {
@@ -35,11 +35,13 @@
35
35
  "The fixed groups are Menu plus current area, followed by Search, Inbox, and Agents.",
36
36
  "Selection promotes foreground to primary without painting a selected fill; unselected destinations use tertiary foreground.",
37
37
  "Opening Mobile Sheet Nav replaces the Menu glyph with Cross while preserving its accessible name.",
38
- "Reselecting an active destination does not reset its nested state."
38
+ "Reselecting an active destination does not reset its nested state.",
39
+ "Direct touch paints the pressed overlay immediately, keeps a quick tap visible for one short frame, and clears it after release or cancellation without relying on Safari's latch-prone active state."
39
40
  ],
40
41
  "responsiveBehavior": [
41
42
  "Render only below the mobile breakpoint and preserve safe-area clearance.",
42
- "The two icon-only control groups remain centered with fixed 40px targets and a divider between Menu and current area."
43
+ "The secondary-background bar keeps an 8px outer inset; its two icon-only groups anchor to opposite edges while retaining fixed 40px targets and a divider between Menu and current area.",
44
+ "Bar chrome and its divider are not text-selectable and suppress the iOS touch callout."
43
45
  ],
44
46
  "references": [
45
47
  {
@@ -316,12 +318,12 @@
316
318
  "files": [
317
319
  {
318
320
  "path": "src/wc/components/MobileBarNav/MobileBarNav.css",
319
- "content": "@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n\n:host {\n display: block;\n min-width: 0;\n background: var(--color-background-primary);\n color: var(--color-foreground-tertiary);\n}\n\n.mobile-bar-nav {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--dimension-space-200);\n min-width: 0;\n padding: var(--dimension-space-200) var(--dimension-space-200)\n max(var(--dimension-space-200), env(safe-area-inset-bottom));\n border-top: var(--dimension-stroke-width-012) solid var(--color-border-tertiary);\n background: var(--color-background-primary);\n box-sizing: border-box;\n}\n\n.mobile-bar-nav__group {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-100);\n}\n\n.mobile-bar-nav__item {\n appearance: none;\n flex: 0 0 var(--dimension-size-500);\n width: var(--dimension-size-500);\n height: var(--dimension-size-500);\n padding: var(--dimension-space-100);\n border: 0;\n border-radius: var(--dimension-radius-025);\n background: transparent;\n color: inherit;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n}\n\n.mobile-bar-nav__item--selected {\n color: var(--color-foreground-primary);\n}\n\n.mobile-bar-nav__divider {\n flex: 0 0 var(--dimension-stroke-width-012);\n width: var(--dimension-stroke-width-012);\n height: var(--dimension-iconography-lg);\n background: var(--color-border-tertiary);\n}\n\n.mobile-bar-nav__icon {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--dimension-iconography-lg);\n height: var(--dimension-iconography-lg);\n}\n\n.mobile-bar-nav__dot {\n position: absolute;\n inset-block-start: calc(var(--dimension-space-025) * -1);\n inset-inline-end: calc(var(--dimension-space-025) * -1);\n}\n",
321
+ "content": "@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n\n:host {\n display: block;\n min-width: 0;\n background: var(--color-background-secondary);\n color: var(--color-foreground-tertiary);\n -webkit-user-select: none;\n user-select: none;\n -webkit-touch-callout: none;\n}\n\n.mobile-bar-nav {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--dimension-space-200);\n min-width: 0;\n padding: var(--dimension-space-100) var(--dimension-space-100)\n max(var(--dimension-space-100), env(safe-area-inset-bottom));\n border-top: var(--dimension-stroke-width-012) solid var(--color-border-tertiary);\n background: var(--color-background-secondary);\n box-sizing: border-box;\n -webkit-user-select: none;\n user-select: none;\n -webkit-touch-callout: none;\n}\n\n.mobile-bar-nav__group {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-100);\n}\n\n.mobile-bar-nav__item {\n appearance: none;\n flex: 0 0 var(--dimension-size-500);\n width: var(--dimension-size-500);\n height: var(--dimension-size-500);\n padding: var(--dimension-space-100);\n border: 0;\n border-radius: var(--dimension-radius-025);\n background: transparent;\n color: inherit;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n cursor: default;\n}\n\n.mobile-bar-nav__item--selected {\n color: var(--color-foreground-primary);\n}\n\n/*\n * Safari can retain its native :active state after a direct touch. On coarse\n * touch-only pointers, the component-owned class provides one deterministic\n * press frame and clears it after release/cancel.\n */\n@media (hover: none) and (pointer: coarse) {\n .mobile-bar-nav__item.ds-interaction-fill:active:not(:disabled)::after {\n background: transparent;\n }\n\n .mobile-bar-nav__item--pressed.ds-interaction-fill::after,\n .mobile-bar-nav__item--pressed.ds-interaction-fill:active:not(:disabled)::after {\n background: var(--ds-interaction-pressed);\n }\n}\n\n.mobile-bar-nav__divider {\n flex: 0 0 var(--dimension-stroke-width-012);\n width: var(--dimension-stroke-width-012);\n height: var(--dimension-iconography-lg);\n background: var(--color-border-tertiary);\n pointer-events: none;\n -webkit-user-select: none;\n user-select: none;\n}\n\n.mobile-bar-nav__icon {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--dimension-iconography-lg);\n height: var(--dimension-iconography-lg);\n}\n\n.mobile-bar-nav__dot {\n position: absolute;\n inset-block-start: calc(var(--dimension-space-025) * -1);\n inset-inline-end: calc(var(--dimension-space-025) * -1);\n}\n",
320
322
  "type": "registry:ui"
321
323
  },
322
324
  {
323
325
  "path": "src/wc/components/MobileBarNav/MobileBarNav.tsx",
324
- "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n} from '@stencil/core';\nimport {\n shouldEmitMobileDestinationChange,\n type MobileDestination,\n} from '../../shell/shell-responsive';\nimport type { PanelNavItem } from '../PanelNav/panel-nav-types';\nimport type { MobileBarNavDestinationDetail } from './mobile-bar-nav-types';\n\ninterface DestinationConfig {\n id: MobileDestination;\n icon: string;\n label: string;\n dot: boolean;\n}\n\n@Component({\n tag: 'ds-mobile-bar-nav',\n styleUrl: 'MobileBarNav.css',\n scoped: true,\n})\nexport class MobileBarNav {\n @Element() el!: HTMLElement;\n\n @Prop() activeDestination: MobileDestination = 'area';\n @Prop() currentArea: PanelNavItem = {\n id: 'tracking',\n icon: 'MapPage',\n label: 'Tracking',\n };\n @Prop() sheetNavExpanded: boolean = false;\n @Prop() menuLabel: string = 'Menu';\n @Prop() searchLabel: string = 'Search';\n @Prop() agentsLabel: string = 'Agents';\n @Prop() inboxLabel: string = 'Inbox';\n @Prop() helpLabel: string = 'Help & Support';\n @Prop() searchDot: boolean = false;\n @Prop() agentsDot: boolean = false;\n @Prop() inboxDot: boolean = false;\n\n @Event() dsSheetNavToggle!: EventEmitter<boolean>;\n @Event() dsDestinationChange!: EventEmitter<MobileBarNavDestinationDetail>;\n\n private destinationConfig(): DestinationConfig[] {\n const currentArea =\n this.activeDestination === 'help'\n ? {\n id: 'help' as const,\n icon: 'CircleQuestion',\n label: this.helpLabel,\n dot: false,\n }\n : {\n id: 'area' as const,\n icon: this.currentArea.icon || 'MapPage',\n label: this.currentArea.label || 'Area',\n dot: false,\n };\n\n return [\n currentArea,\n { id: 'search', icon: 'MagnifyingGlass', label: this.searchLabel, dot: this.searchDot },\n { id: 'inbox', icon: 'Inbox', label: this.inboxLabel, dot: this.inboxDot },\n { id: 'agents', icon: 'AI', label: this.agentsLabel, dot: this.agentsDot },\n ];\n }\n\n private selectDestination(destination: MobileDestination) {\n if (\n !shouldEmitMobileDestinationChange(\n this.activeDestination,\n destination,\n this.sheetNavExpanded\n )\n ) {\n return;\n }\n this.dsDestinationChange.emit({ destination });\n }\n\n @Method()\n async focusDestination(destination: MobileDestination | 'sheet-nav') {\n const target = this.el.querySelector<HTMLElement>(\n `#${\n destination === 'sheet-nav'\n ? 'ds-mobile-sheet-nav-trigger'\n : `ds-mobile-bar-${destination}-trigger`\n }`\n );\n target?.focus({ preventScroll: true });\n }\n\n private renderDestination(item: DestinationConfig) {\n const selected = !this.sheetNavExpanded && item.id === this.activeDestination;\n return (\n <button\n id={`ds-mobile-bar-${item.id}-trigger`}\n type=\"button\"\n class={{\n 'mobile-bar-nav__item': true,\n 'mobile-bar-nav__item--selected': selected,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n }}\n aria-label={item.label}\n aria-current={selected ? 'page' : undefined}\n onClick={() => this.selectDestination(item.id)}\n >\n <span class=\"mobile-bar-nav__icon ds-interaction-fill__content\">\n <ds-icon name={item.icon} size=\"lg\" color=\"inherit\" />\n {item.dot && (\n <ds-badge class=\"mobile-bar-nav__dot\" variant=\"dot\" label=\"\" aria-hidden=\"true\" />\n )}\n </span>\n </button>\n );\n }\n\n render() {\n const [currentArea, ...tools] = this.destinationConfig();\n\n return (\n <Host>\n <nav class=\"mobile-bar-nav\" aria-label=\"Primary\">\n <div class=\"mobile-bar-nav__group mobile-bar-nav__group--context\">\n <button\n id=\"ds-mobile-sheet-nav-trigger\"\n type=\"button\"\n class={{\n 'mobile-bar-nav__item': true,\n 'mobile-bar-nav__item--selected': this.sheetNavExpanded,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n }}\n aria-label={this.menuLabel}\n aria-expanded={String(this.sheetNavExpanded)}\n aria-controls=\"ds-mobile-sheet-nav\"\n onClick={() => this.dsSheetNavToggle.emit(!this.sheetNavExpanded)}\n >\n <span class=\"mobile-bar-nav__icon ds-interaction-fill__content\">\n <ds-icon\n name={this.sheetNavExpanded ? 'Cross' : 'Hamburger'}\n size=\"lg\"\n color=\"inherit\"\n />\n </span>\n </button>\n <span class=\"mobile-bar-nav__divider\" aria-hidden=\"true\" />\n {this.renderDestination(currentArea)}\n </div>\n <div class=\"mobile-bar-nav__group mobile-bar-nav__group--tools\">\n {tools.map(item => this.renderDestination(item))}\n </div>\n </nav>\n </Host>\n );\n }\n}\n",
326
+ "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n} from '@stencil/core';\nimport {\n shouldEmitMobileDestinationChange,\n type MobileDestination,\n} from '../../shell/shell-responsive';\nimport type { PanelNavItem } from '../PanelNav/panel-nav-types';\nimport type { MobileBarNavDestinationDetail } from './mobile-bar-nav-types';\n\ninterface DestinationConfig {\n id: MobileDestination;\n icon: string;\n label: string;\n dot: boolean;\n}\n\nconst MINIMUM_DIRECT_PRESS_MS = 120;\n\n@Component({\n tag: 'ds-mobile-bar-nav',\n styleUrl: 'MobileBarNav.css',\n scoped: true,\n})\nexport class MobileBarNav {\n @Element() el!: HTMLElement;\n\n @Prop() activeDestination: MobileDestination = 'area';\n @Prop() currentArea: PanelNavItem = {\n id: 'tracking',\n icon: 'MapPage',\n label: 'Tracking',\n };\n @Prop() sheetNavExpanded: boolean = false;\n @Prop() menuLabel: string = 'Menu';\n @Prop() searchLabel: string = 'Search';\n @Prop() agentsLabel: string = 'Agents';\n @Prop() inboxLabel: string = 'Inbox';\n @Prop() helpLabel: string = 'Help & Support';\n @Prop() searchDot: boolean = false;\n @Prop() agentsDot: boolean = false;\n @Prop() inboxDot: boolean = false;\n\n @State() private pressedItemId: string | null = null;\n\n @Event() dsSheetNavToggle!: EventEmitter<boolean>;\n @Event() dsDestinationChange!: EventEmitter<MobileBarNavDestinationDetail>;\n\n private activePointerId: number | null = null;\n private pressedAt = 0;\n private pressClearTimer: ReturnType<typeof setTimeout> | null = null;\n\n disconnectedCallback() {\n this.clearPressedItem();\n }\n\n private clearPressTimer() {\n if (this.pressClearTimer === null) return;\n clearTimeout(this.pressClearTimer);\n this.pressClearTimer = null;\n }\n\n private clearPressedItem() {\n this.clearPressTimer();\n this.activePointerId = null;\n this.pressedItemId = null;\n }\n\n private beginDirectPress(itemId: string, event: PointerEvent) {\n if (!event.isPrimary || event.pointerType === 'mouse' || event.button !== 0) return;\n\n this.clearPressTimer();\n this.activePointerId = event.pointerId;\n this.pressedAt = performance.now();\n this.pressedItemId = itemId;\n\n const target = event.currentTarget as HTMLElement;\n try {\n target.setPointerCapture(event.pointerId);\n } catch {\n // Pointer capture can fail if Safari has already cancelled the stream.\n }\n }\n\n private finishDirectPress(event: PointerEvent) {\n if (event.pointerId !== this.activePointerId) return;\n\n this.activePointerId = null;\n const remaining = Math.max(\n 0,\n MINIMUM_DIRECT_PRESS_MS - (performance.now() - this.pressedAt)\n );\n\n this.clearPressTimer();\n this.pressClearTimer = setTimeout(() => {\n this.pressClearTimer = null;\n this.pressedItemId = null;\n }, remaining);\n }\n\n private cancelDirectPress(event: PointerEvent) {\n if (event.pointerId !== this.activePointerId) return;\n this.clearPressedItem();\n }\n\n private pressHandlers(itemId: string) {\n return {\n onPointerDown: (event: PointerEvent) => this.beginDirectPress(itemId, event),\n onPointerUp: (event: PointerEvent) => this.finishDirectPress(event),\n onPointerCancel: (event: PointerEvent) => this.cancelDirectPress(event),\n onLostPointerCapture: (event: PointerEvent) => this.finishDirectPress(event),\n };\n }\n\n private destinationConfig(): DestinationConfig[] {\n const currentArea =\n this.activeDestination === 'help'\n ? {\n id: 'help' as const,\n icon: 'CircleQuestion',\n label: this.helpLabel,\n dot: false,\n }\n : {\n id: 'area' as const,\n icon: this.currentArea.icon || 'MapPage',\n label: this.currentArea.label || 'Area',\n dot: false,\n };\n\n return [\n currentArea,\n { id: 'search', icon: 'MagnifyingGlass', label: this.searchLabel, dot: this.searchDot },\n { id: 'inbox', icon: 'Inbox', label: this.inboxLabel, dot: this.inboxDot },\n { id: 'agents', icon: 'AI', label: this.agentsLabel, dot: this.agentsDot },\n ];\n }\n\n private selectDestination(destination: MobileDestination) {\n if (\n !shouldEmitMobileDestinationChange(\n this.activeDestination,\n destination,\n this.sheetNavExpanded\n )\n ) {\n return;\n }\n this.dsDestinationChange.emit({ destination });\n }\n\n @Method()\n async focusDestination(destination: MobileDestination | 'sheet-nav') {\n const target = this.el.querySelector<HTMLElement>(\n `#${\n destination === 'sheet-nav'\n ? 'ds-mobile-sheet-nav-trigger'\n : `ds-mobile-bar-${destination}-trigger`\n }`\n );\n target?.focus({ preventScroll: true });\n }\n\n private renderDestination(item: DestinationConfig) {\n const selected = !this.sheetNavExpanded && item.id === this.activeDestination;\n return (\n <button\n id={`ds-mobile-bar-${item.id}-trigger`}\n type=\"button\"\n class={{\n 'mobile-bar-nav__item': true,\n 'mobile-bar-nav__item--selected': selected,\n 'mobile-bar-nav__item--pressed': this.pressedItemId === item.id,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n }}\n aria-label={item.label}\n aria-current={selected ? 'page' : undefined}\n onClick={() => this.selectDestination(item.id)}\n {...this.pressHandlers(item.id)}\n >\n <span class=\"mobile-bar-nav__icon ds-interaction-fill__content\">\n <ds-icon name={item.icon} size=\"lg\" color=\"inherit\" />\n {item.dot && (\n <ds-badge class=\"mobile-bar-nav__dot\" variant=\"dot\" label=\"\" aria-hidden=\"true\" />\n )}\n </span>\n </button>\n );\n }\n\n render() {\n const [currentArea, ...tools] = this.destinationConfig();\n\n return (\n <Host>\n <nav class=\"mobile-bar-nav\" aria-label=\"Primary\">\n <div class=\"mobile-bar-nav__group mobile-bar-nav__group--context\">\n <button\n id=\"ds-mobile-sheet-nav-trigger\"\n type=\"button\"\n class={{\n 'mobile-bar-nav__item': true,\n 'mobile-bar-nav__item--selected': this.sheetNavExpanded,\n 'mobile-bar-nav__item--pressed':\n this.pressedItemId === 'sheet-nav',\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n }}\n aria-label={this.menuLabel}\n aria-expanded={String(this.sheetNavExpanded)}\n aria-controls=\"ds-mobile-sheet-nav\"\n onClick={() => this.dsSheetNavToggle.emit(!this.sheetNavExpanded)}\n {...this.pressHandlers('sheet-nav')}\n >\n <span class=\"mobile-bar-nav__icon ds-interaction-fill__content\">\n <ds-icon\n name={this.sheetNavExpanded ? 'Cross' : 'Hamburger'}\n size=\"lg\"\n color=\"inherit\"\n />\n </span>\n </button>\n <span class=\"mobile-bar-nav__divider\" aria-hidden=\"true\" />\n {this.renderDestination(currentArea)}\n </div>\n <div class=\"mobile-bar-nav__group mobile-bar-nav__group--tools\">\n {tools.map(item => this.renderDestination(item))}\n </div>\n </nav>\n </Host>\n );\n }\n}\n",
325
327
  "type": "registry:ui"
326
328
  },
327
329
  {
@@ -30,7 +30,8 @@
30
30
  "accessibility": [
31
31
  "The trigger exposes the current section, popup relationship, and expanded state.",
32
32
  "Decorative previous and next position hints are hidden from assistive technology.",
33
- "The menu is always available so a future swipe gesture is never the only navigation mechanism."
33
+ "The menu is always available so a future swipe gesture is never the only navigation mechanism.",
34
+ "Peer destinations use menuitem semantics with aria-current on the active page; they do not present a radio-control affordance."
34
35
  ],
35
36
  "states": [
36
37
  "Selection is controlled and dsChange reports intent without mutating value.",
@@ -188,12 +189,12 @@
188
189
  "files": [
189
190
  {
190
191
  "path": "src/wc/components/MobileSectionSwitcher/MobileSectionSwitcher.css",
191
- "content": "@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n\n:host {\n display: block;\n min-width: 0;\n max-width: 100%;\n color: var(--color-foreground-primary);\n}\n\n.mobile-section-switcher {\n appearance: none;\n display: grid;\n grid-template-columns: var(--dimension-size-100) minmax(0, auto) var(--dimension-size-100);\n align-items: center;\n justify-content: center;\n gap: var(--dimension-space-050);\n max-width: 100%;\n min-height: var(--dimension-size-400);\n padding-inline: var(--dimension-space-100);\n border: 0;\n border-radius: var(--dimension-radius-100);\n background: var(--color-background-secondary);\n color: inherit;\n box-sizing: border-box;\n cursor: pointer;\n}\n\n.mobile-section-switcher--expanded {\n background: var(--color-background-tertiary);\n}\n\n.mobile-section-switcher__label {\n min-width: 0;\n max-width: 100%;\n text-align: center;\n}\n\n.mobile-section-switcher__position {\n width: var(--dimension-size-050);\n height: var(--dimension-size-050);\n border-radius: var(--dimension-radius-half);\n background: currentColor;\n opacity: 0;\n}\n\n.mobile-section-switcher__position--visible {\n opacity: 1;\n}\n",
192
+ "content": "@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n\n:host {\n display: block;\n min-width: 0;\n max-width: 100%;\n color: var(--color-foreground-primary);\n}\n\n.mobile-section-switcher {\n appearance: none;\n display: grid;\n grid-template-columns: var(--dimension-size-100) minmax(0, auto) var(--dimension-size-100);\n align-items: center;\n justify-content: center;\n gap: var(--dimension-space-050);\n max-width: 100%;\n min-height: var(--dimension-size-400);\n padding-inline: var(--dimension-space-100);\n border: 0;\n border-radius: var(--dimension-radius-100);\n background: var(--color-background-secondary);\n color: inherit;\n box-sizing: border-box;\n cursor: default;\n}\n\n.mobile-section-switcher--expanded {\n background: var(--color-background-tertiary);\n}\n\n.mobile-section-switcher__label {\n min-width: 0;\n max-width: 100%;\n text-align: center;\n}\n\n.mobile-section-switcher__position {\n width: var(--dimension-size-050);\n height: var(--dimension-size-050);\n border-radius: var(--dimension-radius-half);\n background: currentColor;\n opacity: 0;\n}\n\n.mobile-section-switcher__position--visible {\n opacity: 1;\n}\n",
192
193
  "type": "registry:ui"
193
194
  },
194
195
  {
195
196
  "path": "src/wc/components/MobileSectionSwitcher/MobileSectionSwitcher.tsx",
196
- "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport type { MenuItemData, MenuSection } from '../Menu/menu-types';\nimport {\n getSelectableTabs,\n isTabDivider,\n type TabItem,\n type TabItemTab,\n} from '../TabGroup/tab-item-utils';\nimport { resolveMobileSectionPosition } from './mobile-section-switcher-utils';\n\nlet nextMobileSectionSwitcherId = 0;\n\n@Component({\n tag: 'ds-mobile-section-switcher',\n styleUrl: 'MobileSectionSwitcher.css',\n scoped: true,\n})\nexport class MobileSectionSwitcher {\n @Element() el!: HTMLElement;\n\n /** Ordered controlled sections. Divider entries group the popup menu. */\n @Prop() sections: TabItem[] = [];\n /** JSON fallback for `sections`. */\n @Prop({ attribute: 'sections-json' }) sectionsJson: string = '';\n /** Controlled selected section id. */\n @Prop() value: string = '';\n /** Accessible name for the section chooser. */\n @Prop() navigationLabel: string = 'Change page section';\n\n /** Selection intent. The route, tool, or workflow owner updates `value`. */\n @Event() dsChange!: EventEmitter<string>;\n\n @State() private menuOpen = false;\n @State() private initialFocusVisible = false;\n\n private readonly instanceId = nextMobileSectionSwitcherId++;\n private readonly triggerId = `ds-mobile-section-switcher-trigger-${this.instanceId}`;\n private readonly menuId = `ds-mobile-section-switcher-menu-${this.instanceId}`;\n private triggerEl: HTMLButtonElement | null = null;\n\n private get resolvedSections(): TabItem[] {\n if (!this.sectionsJson.trim()) return this.sections ?? [];\n try {\n const parsed = JSON.parse(this.sectionsJson);\n return Array.isArray(parsed) ? (parsed as TabItem[]) : [];\n } catch {\n return [];\n }\n }\n\n private get selectableSections(): TabItemTab[] {\n return getSelectableTabs(this.resolvedSections);\n }\n\n private get selectedIndex(): number {\n return resolveMobileSectionPosition(this.resolvedSections, this.value).selectedIndex;\n }\n\n private get selectedSection(): TabItemTab | undefined {\n return this.selectableSections[this.selectedIndex];\n }\n\n private get hasPrevious(): boolean {\n return resolveMobileSectionPosition(this.resolvedSections, this.value).hasPrevious;\n }\n\n private get hasNext(): boolean {\n return resolveMobileSectionPosition(this.resolvedSections, this.value).hasNext;\n }\n\n private get triggerAriaLabel(): string {\n return `${this.navigationLabel}. Current section: ${this.selectedSection?.label ?? ''}`;\n }\n\n private get menuSections(): MenuSection[] {\n const groups: MenuSection[] = [];\n let items: MenuItemData[] = [];\n const commit = () => {\n if (items.length) groups.push({ items });\n items = [];\n };\n\n for (const section of this.resolvedSections) {\n if (isTabDivider(section)) {\n commit();\n continue;\n }\n items.push({\n label: section.label,\n value: section.id,\n dot: section.dot,\n isSelected: section.id === this.selectedSection?.id,\n isInactive: section.isInactive,\n });\n }\n commit();\n return groups;\n }\n\n @Watch('sections')\n @Watch('sectionsJson')\n handleSectionsChange() {\n if (this.selectableSections.length <= 1) this.closeMenu();\n }\n\n private toggleMenu = (event: MouseEvent) => {\n if (this.selectableSections.length <= 1) return;\n this.initialFocusVisible = event.detail === 0;\n this.menuOpen = !this.menuOpen;\n };\n\n private closeMenu = () => {\n this.menuOpen = false;\n };\n\n private handleSelect = (event: CustomEvent<MenuItemData>) => {\n const id = String(event.detail.value ?? '');\n const section = this.selectableSections.find(candidate => candidate.id === id);\n if (!section || section.isInactive) return;\n this.closeMenu();\n if (id !== this.selectedSection?.id) this.dsChange.emit(id);\n requestAnimationFrame(() => this.triggerEl?.focus());\n };\n\n render() {\n const selected = this.selectedSection;\n if (!selected) return <Host />;\n\n return (\n <Host>\n <button\n ref={element => {\n this.triggerEl = element ?? null;\n }}\n id={this.triggerId}\n class={{\n 'mobile-section-switcher': true,\n 'mobile-section-switcher--expanded': this.menuOpen,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n }}\n type=\"button\"\n aria-haspopup=\"menu\"\n aria-controls={this.menuId}\n aria-expanded={String(this.menuOpen)}\n aria-label={this.triggerAriaLabel}\n onClick={this.toggleMenu}\n >\n <span\n class={{\n 'mobile-section-switcher__position': true,\n 'mobile-section-switcher__position--visible': this.hasPrevious,\n 'ds-interaction-fill__content': true,\n }}\n aria-hidden=\"true\"\n />\n <ds-text\n class=\"mobile-section-switcher__label ds-interaction-fill__content\"\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis\n color=\"inherit\"\n lineTruncation={1}\n >\n {selected.label}\n </ds-text>\n <span\n class={{\n 'mobile-section-switcher__position': true,\n 'mobile-section-switcher__position--visible': this.hasNext,\n 'ds-interaction-fill__content': true,\n }}\n aria-hidden=\"true\"\n />\n </button>\n\n {this.selectableSections.length > 1 ? (\n <ds-menu\n id={this.menuId}\n anchorId={this.triggerId}\n align=\"center\"\n selectionMode=\"single\"\n menuLabel={this.navigationLabel}\n open={this.menuOpen}\n initialFocusVisible={this.initialFocusVisible}\n sections={this.menuSections}\n onDsClose={this.closeMenu}\n onDsSelect={this.handleSelect}\n />\n ) : null}\n </Host>\n );\n }\n}\n",
197
+ "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport type { MenuItemData, MenuSection } from '../Menu/menu-types';\nimport {\n getSelectableTabs,\n isTabDivider,\n type TabItem,\n type TabItemTab,\n} from '../TabGroup/tab-item-utils';\nimport { resolveMobileSectionPosition } from './mobile-section-switcher-utils';\n\nlet nextMobileSectionSwitcherId = 0;\n\n@Component({\n tag: 'ds-mobile-section-switcher',\n styleUrl: 'MobileSectionSwitcher.css',\n scoped: true,\n})\nexport class MobileSectionSwitcher {\n @Element() el!: HTMLElement;\n\n /** Ordered controlled sections. Divider entries group the popup menu. */\n @Prop() sections: TabItem[] = [];\n /** JSON fallback for `sections`. */\n @Prop({ attribute: 'sections-json' }) sectionsJson: string = '';\n /** Controlled selected section id. */\n @Prop() value: string = '';\n /** Accessible name for the section chooser. */\n @Prop() navigationLabel: string = 'Change page section';\n\n /** Selection intent. The route, tool, or workflow owner updates `value`. */\n @Event() dsChange!: EventEmitter<string>;\n\n @State() private menuOpen = false;\n @State() private initialFocusVisible = false;\n\n private readonly instanceId = nextMobileSectionSwitcherId++;\n private readonly triggerId = `ds-mobile-section-switcher-trigger-${this.instanceId}`;\n private readonly menuId = `ds-mobile-section-switcher-menu-${this.instanceId}`;\n private triggerEl: HTMLButtonElement | null = null;\n\n private get resolvedSections(): TabItem[] {\n if (!this.sectionsJson.trim()) return this.sections ?? [];\n try {\n const parsed = JSON.parse(this.sectionsJson);\n return Array.isArray(parsed) ? (parsed as TabItem[]) : [];\n } catch {\n return [];\n }\n }\n\n private get selectableSections(): TabItemTab[] {\n return getSelectableTabs(this.resolvedSections);\n }\n\n private get selectedIndex(): number {\n return resolveMobileSectionPosition(this.resolvedSections, this.value).selectedIndex;\n }\n\n private get selectedSection(): TabItemTab | undefined {\n return this.selectableSections[this.selectedIndex];\n }\n\n private get hasPrevious(): boolean {\n return resolveMobileSectionPosition(this.resolvedSections, this.value).hasPrevious;\n }\n\n private get hasNext(): boolean {\n return resolveMobileSectionPosition(this.resolvedSections, this.value).hasNext;\n }\n\n private get triggerAriaLabel(): string {\n return `${this.navigationLabel}. Current section: ${this.selectedSection?.label ?? ''}`;\n }\n\n private get menuSections(): MenuSection[] {\n const groups: MenuSection[] = [];\n let items: MenuItemData[] = [];\n const commit = () => {\n if (items.length) groups.push({ items });\n items = [];\n };\n\n for (const section of this.resolvedSections) {\n if (isTabDivider(section)) {\n commit();\n continue;\n }\n items.push({\n label: section.label,\n value: section.id,\n dot: section.dot,\n isSelected: section.id === this.selectedSection?.id,\n isInactive: section.isInactive,\n });\n }\n commit();\n return groups;\n }\n\n @Watch('sections')\n @Watch('sectionsJson')\n handleSectionsChange() {\n if (this.selectableSections.length <= 1) this.closeMenu();\n }\n\n private toggleMenu = (event: MouseEvent) => {\n if (this.selectableSections.length <= 1) return;\n this.initialFocusVisible = event.detail === 0;\n this.menuOpen = !this.menuOpen;\n };\n\n private closeMenu = () => {\n this.menuOpen = false;\n };\n\n private handleSelect = (event: CustomEvent<MenuItemData>) => {\n const id = String(event.detail.value ?? '');\n const section = this.selectableSections.find(candidate => candidate.id === id);\n if (!section || section.isInactive) return;\n this.closeMenu();\n if (id !== this.selectedSection?.id) this.dsChange.emit(id);\n requestAnimationFrame(() => this.triggerEl?.focus());\n };\n\n render() {\n const selected = this.selectedSection;\n if (!selected) return <Host />;\n\n return (\n <Host>\n <button\n ref={element => {\n this.triggerEl = element ?? null;\n }}\n id={this.triggerId}\n class={{\n 'mobile-section-switcher': true,\n 'mobile-section-switcher--expanded': this.menuOpen,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n }}\n type=\"button\"\n aria-haspopup=\"menu\"\n aria-controls={this.menuId}\n aria-expanded={String(this.menuOpen)}\n aria-label={this.triggerAriaLabel}\n onClick={this.toggleMenu}\n >\n <span\n class={{\n 'mobile-section-switcher__position': true,\n 'mobile-section-switcher__position--visible': this.hasPrevious,\n 'ds-interaction-fill__content': true,\n }}\n aria-hidden=\"true\"\n />\n <ds-text\n class=\"mobile-section-switcher__label ds-interaction-fill__content\"\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis\n color=\"inherit\"\n lineTruncation={1}\n >\n {selected.label}\n </ds-text>\n <span\n class={{\n 'mobile-section-switcher__position': true,\n 'mobile-section-switcher__position--visible': this.hasNext,\n 'ds-interaction-fill__content': true,\n }}\n aria-hidden=\"true\"\n />\n </button>\n\n {this.selectableSections.length > 1 ? (\n <ds-menu\n id={this.menuId}\n anchorId={this.triggerId}\n align=\"center\"\n menuLabel={this.navigationLabel}\n open={this.menuOpen}\n initialFocusVisible={this.initialFocusVisible}\n sections={this.menuSections}\n onDsClose={this.closeMenu}\n onDsSelect={this.handleSelect}\n />\n ) : null}\n </Host>\n );\n }\n}\n",
197
198
  "type": "registry:ui"
198
199
  },
199
200
  {
@@ -305,7 +305,7 @@
305
305
  "files": [
306
306
  {
307
307
  "path": "src/wc/components/MobileSheetNav/MobileSheetNav.css",
308
- "content": "@import '../../utils/control-density.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n\n:host {\n display: block;\n min-width: 0;\n min-height: 0;\n height: 100%;\n background: var(--color-background-primary);\n}\n\n:host(:not([open])) {\n display: none;\n}\n\n.mobile-sheet-nav {\n display: grid;\n grid-template-rows: auto minmax(0, 1fr);\n height: 100%;\n min-height: 0;\n background: var(--color-background-primary);\n}\n\n.mobile-sheet-nav__header {\n position: relative;\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);\n align-items: center;\n gap: var(--dimension-space-100);\n padding: var(--dimension-space-200);\n box-sizing: border-box;\n}\n\n.mobile-sheet-nav__header::after {\n content: '';\n position: absolute;\n inset-inline: 0;\n inset-block-end: 0;\n height: var(--dimension-stroke-width-012);\n background: var(--color-border-tertiary);\n pointer-events: none;\n}\n\n.mobile-sheet-nav__brand {\n display: flex;\n align-items: center;\n justify-content: flex-start;\n min-width: 0;\n}\n\n.mobile-sheet-nav__logo {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--dimension-size-500);\n height: var(--dimension-size-500);\n padding: var(--dimension-space-100);\n box-sizing: border-box;\n color: var(--color-foreground-primary);\n}\n\n.mobile-sheet-nav__logo-mark {\n width: var(--dimension-iconography-lg);\n height: var(--dimension-iconography-lg);\n flex: 0 0 auto;\n}\n\n.mobile-sheet-nav__context {\n justify-self: center;\n}\n\n.mobile-sheet-nav__actions {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: var(--dimension-space-050);\n min-width: 0;\n}\n\n.mobile-sheet-nav__body {\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n padding: var(--dimension-space-200);\n}\n\n.mobile-sheet-nav__items {\n display: grid;\n gap: var(--dimension-space-100);\n}\n\n.mobile-sheet-nav__item {\n appearance: none;\n width: 100%;\n height: var(--ds-control-height);\n min-height: var(--ds-control-height);\n padding-inline: var(--ds-control-padding-inline);\n border: 0;\n border-radius: var(--ds-control-radius);\n background: transparent;\n color: var(--color-foreground-secondary);\n display: flex;\n align-items: center;\n justify-content: flex-start;\n gap: var(--ds-control-gap);\n cursor: pointer;\n text-align: start;\n box-sizing: border-box;\n}\n\n.mobile-sheet-nav__item--selected {\n color: var(--color-foreground-primary);\n}\n\n.mobile-sheet-nav__item-label {\n min-width: 0;\n padding-inline: var(--ds-control-label-inset);\n}\n\n.mobile-sheet-nav__dot {\n margin-inline-start: auto;\n}\n",
308
+ "content": "@import '../../utils/control-density.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n\n:host {\n display: block;\n min-width: 0;\n min-height: 0;\n height: 100%;\n background: var(--color-background-primary);\n}\n\n:host(:not([open])) {\n display: none;\n}\n\n.mobile-sheet-nav {\n display: grid;\n grid-template-rows: auto minmax(0, 1fr);\n height: 100%;\n min-height: 0;\n background: var(--color-background-primary);\n}\n\n.mobile-sheet-nav__header {\n position: relative;\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);\n align-items: center;\n gap: var(--dimension-space-100);\n padding: var(--dimension-space-200);\n box-sizing: border-box;\n}\n\n.mobile-sheet-nav__header::after {\n content: '';\n position: absolute;\n inset-inline: 0;\n inset-block-end: 0;\n height: var(--dimension-stroke-width-012);\n background: var(--color-border-tertiary);\n pointer-events: none;\n}\n\n.mobile-sheet-nav__brand {\n display: flex;\n align-items: center;\n justify-content: flex-start;\n min-width: 0;\n}\n\n.mobile-sheet-nav__logo {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--dimension-size-500);\n height: var(--dimension-size-500);\n padding: var(--dimension-space-100);\n box-sizing: border-box;\n color: var(--color-foreground-primary);\n}\n\n.mobile-sheet-nav__logo-mark {\n width: var(--dimension-iconography-lg);\n height: var(--dimension-iconography-lg);\n flex: 0 0 auto;\n}\n\n.mobile-sheet-nav__context {\n justify-self: center;\n}\n\n.mobile-sheet-nav__actions {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: var(--dimension-space-050);\n min-width: 0;\n}\n\n.mobile-sheet-nav__body {\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n padding: var(--dimension-space-200);\n}\n\n.mobile-sheet-nav__items {\n display: grid;\n gap: var(--dimension-space-100);\n}\n\n.mobile-sheet-nav__item {\n appearance: none;\n width: 100%;\n height: var(--ds-control-height);\n min-height: var(--ds-control-height);\n padding-inline: var(--ds-control-padding-inline);\n border: 0;\n border-radius: var(--ds-control-radius);\n background: transparent;\n color: var(--color-foreground-secondary);\n display: flex;\n align-items: center;\n justify-content: flex-start;\n gap: var(--ds-control-gap);\n cursor: default;\n text-align: start;\n box-sizing: border-box;\n}\n\n.mobile-sheet-nav__item--selected {\n color: var(--color-foreground-primary);\n}\n\n.mobile-sheet-nav__item-label {\n min-width: 0;\n padding-inline: var(--ds-control-label-inset);\n}\n\n.mobile-sheet-nav__dot {\n margin-inline-start: auto;\n}\n",
309
309
  "type": "registry:ui"
310
310
  },
311
311
  {
@@ -619,7 +619,7 @@
619
619
  "files": [
620
620
  {
621
621
  "path": "src/wc/components/PanelNav/PanelNav.css",
622
- "content": "@import '../../utils/control-density.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n@import '../../utils/scrollbar-hidden.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-shell-app: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(--ds-control-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}\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(--ds-control-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 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",
622
+ "content": "@import '../../utils/control-density.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n@import '../../utils/scrollbar-hidden.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-shell-app: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(--ds-control-radius, var(--dimension-radius-025, 2px));\n cursor: default;\n color: var(--_nav-fg-primary);\n flex-shrink: 0;\n box-sizing: border-box;\n transition: color var(--effect-motion-short-3, 200ms ease);\n /* Hover/press: `.ds-interaction-fill` (tokens from --_nav-* via --ds-interaction-*). */\n}\n\n.panel-nav--breakpoint-locked .panel-nav__header-btn {\n cursor: default;\n}\n\n/* Logo — visible by default; instant swap to toggle on hover (no crossfade overlap) */\n.panel-nav__header-logo {\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n z-index: 2;\n opacity: 1;\n}\n\n.panel-nav__m-mark {\n width: var(--dimension-iconography-md, 20px);\n height: var(--dimension-iconography-md, 20px);\n display: block;\n}\n\n/* Toggle icon — stacked on logo; under hover wash (::after z-index 3) */\n.panel-nav__header-toggle {\n display: flex;\n align-items: center;\n justify-content: center;\n position: absolute;\n inset: 0;\n z-index: 2;\n opacity: 0;\n pointer-events: none;\n}\n\n.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}\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(--ds-control-radius, var(--dimension-radius-025, 2px));\n cursor: default;\n color: var(--_nav-fg-secondary);\n box-sizing: border-box;\n width: 100%;\n user-select: none;\n transition: color var(--effect-motion-short-1, 50ms ease-in-out);\n /* Hover/press: `.ds-interaction-fill` — no --selected (icon/type weight only). */\n}\n\n/* ── Active item ─────────────────────────────────────────────────────────── */\n\n.panel-nav__item--active {\n color: var(--_nav-fg-primary);\n /* Bold typography via ds-text (text-body-medium + emphasis) on the label */\n}\n\n/* ── Item icon ───────────────────────────────────────────────────────────── */\n\n.panel-nav__item-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--dimension-iconography-md, 20px);\n height: var(--dimension-iconography-md, 20px);\n position: relative;\n z-index: 2;\n}\n\n/* ── Item label ──────────────────────────────────────────────────────────── */\n\n.panel-nav__item-label {\n position: relative;\n z-index: 2;\n flex: 1;\n min-width: 0;\n height: var(--typography-lineheight-md, 20px);\n opacity: 1;\n /* Gradient wipe: mask sweeps left-to-right on expand, right-to-left on collapse */\n /* stylelint-disable-next-line color-named -- mask luminance stops, not theme colors */\n mask-image: linear-gradient(to right, black 50%, transparent 100%);\n /* stylelint-disable-next-line color-named -- mask luminance stops, not theme colors */\n -webkit-mask-image: linear-gradient(to right, black 50%, transparent 100%);\n mask-size: 200% 100%;\n -webkit-mask-size: 200% 100%;\n mask-position: 0% 0;\n -webkit-mask-position: 0% 0;\n transition:\n opacity var(--_nav-label-fade-motion) 0ms,\n mask-position var(--_nav-label-fade-motion) 0ms,\n -webkit-mask-position var(--_nav-label-fade-motion) 0ms;\n}\n\n.panel-nav__item-label-text {\n padding: 0 var(--ds-control-label-inset);\n}\n\n/* ── Item notification dot — 20px suffix zone matches the icon geometry ──── */\n\n.panel-nav__item-dot-box {\n position: absolute;\n z-index: 2;\n right: var(--dimension-space-075, 6px);\n top: var(--dimension-space-075, 6px);\n display: flex;\n align-items: center;\n justify-content: center;\n width: var(--dimension-iconography-md, 20px);\n height: var(--dimension-iconography-md, 20px);\n pointer-events: none;\n}\n\n.panel-nav__item-dot {\n transition:\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",
623
623
  "type": "registry:ui"
624
624
  },
625
625
  {
@@ -232,7 +232,7 @@
232
232
  "files": [
233
233
  {
234
234
  "path": "src/wc/components/PanelSubNav/PanelSubNav.css",
235
- "content": "@import '../../utils/control-density.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n@import '../../utils/control-inactive.css';\n\n:host {\n display: block;\n width: 100%;\n}\n\n.panel-sub-nav {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050);\n width: 100%;\n}\n\n.panel-sub-nav__item {\n display: flex;\n align-items: center;\n justify-content: flex-start;\n gap: var(--ds-control-gap);\n width: 100%;\n height: var(--ds-control-height);\n padding: 0 var(--ds-control-padding-inline);\n box-sizing: border-box;\n border: none;\n border-radius: var(--ds-control-radius);\n background: transparent;\n color: var(--color-foreground-secondary);\n cursor: pointer;\n text-align: left;\n -webkit-user-select: none;\n user-select: none;\n transition: color var(--effect-motion-short-2);\n}\n\n.panel-sub-nav__item--selected {\n color: var(--color-foreground-primary);\n}\n\n.panel-sub-nav__item--on-medium {\n color: var(--color-foreground-on-medium-background-secondary);\n}\n\n.panel-sub-nav__item--on-medium.panel-sub-nav__item--selected {\n color: var(--color-foreground-on-medium-background-primary);\n}\n\n.panel-sub-nav__item--on-bold {\n color: var(--color-foreground-on-bold-background-secondary);\n}\n\n.panel-sub-nav__item--on-bold.panel-sub-nav__item--selected {\n color: var(--color-foreground-on-bold-background-primary);\n}\n\n.panel-sub-nav__item--on-strong {\n color: var(--color-foreground-on-strong-background-secondary);\n}\n\n.panel-sub-nav__item--on-strong.panel-sub-nav__item--selected {\n color: var(--color-foreground-on-strong-background-primary);\n}\n\n.panel-sub-nav__item--on-translucent {\n color: var(--color-translucent-foreground-secondary);\n}\n\n.panel-sub-nav__item--on-translucent.panel-sub-nav__item--selected {\n color: var(--color-translucent-foreground-primary);\n}\n\n.panel-sub-nav__item--on-inverted {\n color: var(--color-inverted-foreground-secondary);\n}\n\n.panel-sub-nav__item--on-inverted.panel-sub-nav__item--selected {\n color: var(--color-inverted-foreground-primary);\n}\n\n.panel-sub-nav__item--on-media {\n color: var(--color-media-foreground-secondary);\n}\n\n.panel-sub-nav__item--on-media.panel-sub-nav__item--selected {\n color: var(--color-media-foreground-primary);\n}\n\n.panel-sub-nav__item--on-always-dark {\n color: var(--color-always-dark-foreground-secondary);\n}\n\n.panel-sub-nav__item--on-always-dark.panel-sub-nav__item--selected {\n color: var(--color-always-dark-foreground-primary);\n}\n\nds-text.panel-sub-nav__label {\n flex: 1;\n min-width: 0;\n padding: 0 var(--ds-control-label-inset);\n}\n",
235
+ "content": "@import '../../utils/control-density.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n@import '../../utils/control-inactive.css';\n\n:host {\n display: block;\n width: 100%;\n}\n\n.panel-sub-nav {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050);\n width: 100%;\n}\n\n.panel-sub-nav__item {\n display: flex;\n align-items: center;\n justify-content: flex-start;\n gap: var(--ds-control-gap);\n width: 100%;\n height: var(--ds-control-height);\n padding: 0 var(--ds-control-padding-inline);\n box-sizing: border-box;\n border: none;\n border-radius: var(--ds-control-radius);\n background: transparent;\n color: var(--color-foreground-secondary);\n cursor: default;\n text-align: left;\n -webkit-user-select: none;\n user-select: none;\n transition: color var(--effect-motion-short-2);\n}\n\n.panel-sub-nav__item--selected {\n color: var(--color-foreground-primary);\n}\n\n.panel-sub-nav__item--on-medium {\n color: var(--color-foreground-on-medium-background-secondary);\n}\n\n.panel-sub-nav__item--on-medium.panel-sub-nav__item--selected {\n color: var(--color-foreground-on-medium-background-primary);\n}\n\n.panel-sub-nav__item--on-bold {\n color: var(--color-foreground-on-bold-background-secondary);\n}\n\n.panel-sub-nav__item--on-bold.panel-sub-nav__item--selected {\n color: var(--color-foreground-on-bold-background-primary);\n}\n\n.panel-sub-nav__item--on-strong {\n color: var(--color-foreground-on-strong-background-secondary);\n}\n\n.panel-sub-nav__item--on-strong.panel-sub-nav__item--selected {\n color: var(--color-foreground-on-strong-background-primary);\n}\n\n.panel-sub-nav__item--on-translucent {\n color: var(--color-translucent-foreground-secondary);\n}\n\n.panel-sub-nav__item--on-translucent.panel-sub-nav__item--selected {\n color: var(--color-translucent-foreground-primary);\n}\n\n.panel-sub-nav__item--on-inverted {\n color: var(--color-inverted-foreground-secondary);\n}\n\n.panel-sub-nav__item--on-inverted.panel-sub-nav__item--selected {\n color: var(--color-inverted-foreground-primary);\n}\n\n.panel-sub-nav__item--on-media {\n color: var(--color-media-foreground-secondary);\n}\n\n.panel-sub-nav__item--on-media.panel-sub-nav__item--selected {\n color: var(--color-media-foreground-primary);\n}\n\n.panel-sub-nav__item--on-always-dark {\n color: var(--color-always-dark-foreground-secondary);\n}\n\n.panel-sub-nav__item--on-always-dark.panel-sub-nav__item--selected {\n color: var(--color-always-dark-foreground-primary);\n}\n\nds-text.panel-sub-nav__label {\n flex: 1;\n min-width: 0;\n padding: 0 var(--ds-control-label-inset);\n}\n",
236
236
  "type": "registry:ui"
237
237
  },
238
238
  {
@@ -362,7 +362,7 @@
362
362
  "files": [
363
363
  {
364
364
  "path": "src/wc/components/Radio/Radio.css",
365
- "content": "@import '../../utils/control-inactive.css';\n@import '../../utils/control-density.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n\n:host {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-025);\n}\n\n:host(.radio--horizontal) {\n flex-direction: row;\n gap: var(--dimension-space-100);\n}\n\n:host(.radio--md) {\n --_radio-circle: var(--dimension-iconography-sm);\n --_radio-dot: var(--dimension-size-100);\n --_radio-stroke: var(--dimension-stroke-width-015);\n}\n\n:host(.radio--sm) {\n --_radio-circle: var(--dimension-iconography-xs);\n --_radio-dot: var(--dimension-size-075);\n --_radio-stroke: var(--dimension-stroke-width-012);\n}\n\n:host(.radio--xs) {\n --_radio-circle: var(--dimension-size-100);\n --_radio-dot: var(--dimension-size-050);\n --_radio-stroke: calc(var(--dimension-stroke-width-base) * 3 / 32);\n}\n\n.radio__item {\n display: inline-flex;\n align-items: center;\n box-sizing: border-box;\n gap: var(--ds-control-gap);\n height: var(--ds-control-height);\n padding: 0 var(--ds-control-padding-inline);\n border-radius: var(--ds-control-radius);\n cursor: pointer;\n user-select: none;\n}\n\n.radio__placement {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--ds-control-icon);\n height: var(--ds-control-icon);\n}\n\n.radio__circle {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: var(--_radio-circle);\n height: var(--_radio-circle);\n border: 0;\n border-radius: var(--dimension-radius-half);\n box-shadow: inset 0 0 0 var(--_radio-stroke) var(--color-foreground-tertiary);\n transition:\n background-color var(--effect-motion-short-2),\n box-shadow var(--effect-motion-short-2);\n}\n\n.radio__circle--checked {\n background-color: var(--color-background-bold-brand);\n box-shadow: none;\n}\n\n.radio__dot {\n width: var(--_radio-dot);\n height: var(--_radio-dot);\n border-radius: var(--dimension-radius-half);\n background-color: var(--color-foreground-on-bold-background-primary);\n}\n\n.radio__label {\n flex: 1;\n min-width: 0;\n padding: 0 var(--ds-control-label-inset);\n}\n",
365
+ "content": "@import '../../utils/control-inactive.css';\n@import '../../utils/control-density.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n\n:host {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-025);\n}\n\n:host(.radio--horizontal) {\n flex-direction: row;\n gap: var(--dimension-space-100);\n}\n\n:host(.radio--md) {\n --_radio-circle: var(--dimension-iconography-sm);\n --_radio-dot: var(--dimension-size-100);\n --_radio-stroke: var(--dimension-stroke-width-015);\n}\n\n:host(.radio--sm) {\n --_radio-circle: var(--dimension-iconography-xs);\n --_radio-dot: var(--dimension-size-075);\n --_radio-stroke: var(--dimension-stroke-width-012);\n}\n\n:host(.radio--xs) {\n --_radio-circle: var(--dimension-size-100);\n --_radio-dot: var(--dimension-size-050);\n --_radio-stroke: calc(var(--dimension-stroke-width-base) * 3 / 32);\n}\n\n.radio__item {\n display: inline-flex;\n align-items: center;\n box-sizing: border-box;\n gap: var(--ds-control-gap);\n height: var(--ds-control-height);\n padding: 0 var(--ds-control-padding-inline);\n border-radius: var(--ds-control-radius);\n cursor: default;\n user-select: none;\n}\n\n.radio__placement {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--ds-control-icon);\n height: var(--ds-control-icon);\n}\n\n.radio__circle {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: var(--_radio-circle);\n height: var(--_radio-circle);\n border: 0;\n border-radius: var(--dimension-radius-half);\n box-shadow: inset 0 0 0 var(--_radio-stroke) var(--color-foreground-tertiary);\n transition:\n background-color var(--effect-motion-short-2),\n box-shadow var(--effect-motion-short-2);\n}\n\n.radio__circle--checked {\n background-color: var(--color-background-bold-brand);\n box-shadow: none;\n}\n\n.radio__dot {\n width: var(--_radio-dot);\n height: var(--_radio-dot);\n border-radius: var(--dimension-radius-half);\n background-color: var(--color-foreground-on-bold-background-primary);\n}\n\n.radio__label {\n flex: 1;\n min-width: 0;\n padding: 0 var(--ds-control-label-inset);\n}\n",
366
366
  "type": "registry:ui"
367
367
  },
368
368
  {