@ds-mo/ui 8.3.1 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +28 -3
  2. package/dist/.build-stamp +1 -1
  3. package/dist/agent-patterns.json +233 -0
  4. package/dist/agent.json +155 -174
  5. package/dist/angular/ds-menu.d.ts +6 -1
  6. package/dist/angular/ds-menu.js +7 -4
  7. package/dist/angular/ds-swatch-picker.d.ts +15 -0
  8. package/dist/angular/{ds-pagination.js → ds-swatch-picker.js} +15 -15
  9. package/dist/angular/index.d.ts +1 -1
  10. package/dist/angular/index.js +1 -2
  11. package/dist/angular/proxies.d.ts +1 -2
  12. package/dist/angular/proxies.js +1 -2
  13. package/dist/components/ds-app-shell.js +1 -1
  14. package/dist/components/ds-badge.js +1 -1
  15. package/dist/components/ds-banner.js +1 -1
  16. package/dist/components/ds-bar-nav.js +1 -1
  17. package/dist/components/ds-bar-nav.js.map +1 -1
  18. package/dist/components/ds-button-filled.js +1 -1
  19. package/dist/components/ds-button-unfilled.js +1 -1
  20. package/dist/components/ds-card-data-viz-donut.js +1 -1
  21. package/dist/components/ds-card-setting.js +1 -1
  22. package/dist/components/ds-card-shell-data-viz.js +1 -1
  23. package/dist/components/ds-card.js +1 -1
  24. package/dist/components/ds-chart-bar.js +1 -1
  25. package/dist/components/ds-chart-bar.js.map +1 -1
  26. package/dist/components/ds-chart-donut.js +1 -1
  27. package/dist/components/ds-chart-legend.js +1 -1
  28. package/dist/components/ds-chart-line.js +1 -1
  29. package/dist/components/ds-checkbox.js +1 -1
  30. package/dist/components/ds-chip.js +1 -1
  31. package/dist/components/ds-empty-state.js +1 -1
  32. package/dist/components/ds-field.js +1 -1
  33. package/dist/components/ds-input.js +1 -1
  34. package/dist/components/ds-menu.js +1 -1
  35. package/dist/components/ds-modal.js +1 -1
  36. package/dist/components/ds-panel-nav.js +1 -1
  37. package/dist/components/ds-panel-nav.js.map +1 -1
  38. package/dist/components/ds-panel-sub-nav.js +1 -1
  39. package/dist/components/ds-panel-tools.js +1 -1
  40. package/dist/components/ds-radio.js +1 -1
  41. package/dist/components/ds-select-multi.js +1 -1
  42. package/dist/components/ds-select-multi.js.map +1 -1
  43. package/dist/components/ds-select.js +1 -1
  44. package/dist/components/ds-select.js.map +1 -1
  45. package/dist/components/ds-shell-gradient-picker.js +1 -1
  46. package/dist/components/ds-shell-gradient-picker.js.map +1 -1
  47. package/dist/components/ds-shell-gradient-swatch.js +1 -1
  48. package/dist/components/ds-shell-gradient-swatch.js.map +1 -1
  49. package/dist/components/ds-skeleton.js +1 -1
  50. package/dist/components/ds-skeleton.js.map +1 -1
  51. package/dist/components/ds-slider.js +1 -1
  52. package/dist/components/ds-slider.js.map +1 -1
  53. package/dist/components/{ds-pagination.d.ts → ds-swatch-picker.d.ts} +4 -4
  54. package/dist/components/ds-swatch-picker.js +2 -0
  55. package/dist/components/ds-swatch-picker.js.map +1 -0
  56. package/dist/components/ds-tab-group.js +1 -1
  57. package/dist/components/ds-tag.js +1 -1
  58. package/dist/components/ds-text.js +1 -1
  59. package/dist/components/ds-tooltip-data-viz.js +1 -1
  60. package/dist/components/ds-tooltip.js +1 -1
  61. package/dist/components/{p-CysJosvS.js → p-B-zhgAvL.js} +2 -2
  62. package/dist/components/{p-C21sIElQ.js → p-BLzivrE3.js} +2 -2
  63. package/dist/components/{p-DAuadHYu.js → p-BbV68F23.js} +2 -2
  64. package/dist/components/{p-_y99wWNU.js → p-C7RObioO.js} +2 -2
  65. package/dist/components/{p-DxQ4gzbA.js → p-CZ9wemOA.js} +2 -2
  66. package/dist/components/{p-BKA6houZ.js → p-CbS_WWs2.js} +2 -2
  67. package/dist/components/p-D1HFMTdC.js +2 -0
  68. package/dist/components/p-D1HFMTdC.js.map +1 -0
  69. package/dist/components/{p-BGcSrHKl.js → p-DT2MK8Kt.js} +2 -2
  70. package/dist/components/{p-DV23KMU-.js → p-DyZj3k9S.js} +2 -2
  71. package/dist/components/p-HBIxvS5C.js +2 -0
  72. package/dist/components/p-HBIxvS5C.js.map +1 -0
  73. package/dist/components/{p-D6hvtWDk.js → p-KRGBGTu5.js} +2 -2
  74. package/dist/components/p-l6PmnW_P.js +2 -0
  75. package/dist/components/p-l6PmnW_P.js.map +1 -0
  76. package/dist/components/p-tQ1QHeOE.js +2 -0
  77. package/dist/components/p-tQ1QHeOE.js.map +1 -0
  78. package/dist/docs/components.json +546 -1024
  79. package/dist/lib/components/Menu/menu-placement.d.ts +6 -3
  80. package/dist/lib/shell/index.d.ts +2 -2
  81. package/dist/lib/shell/index.js +40 -39
  82. package/dist/lib/shell/index.js.map +3 -3
  83. package/dist/lib/shell/shell-gradient-presets.d.ts +15 -0
  84. package/dist/lib/utils/choice-popup-alignment.d.ts +20 -0
  85. package/dist/lib/utils/index.d.ts +2 -0
  86. package/dist/lib/utils/index.js +16 -0
  87. package/dist/lib/utils/index.js.map +3 -3
  88. package/dist/mcp/cli.js +31409 -0
  89. package/dist/mcp-data/registry/app-shell.json +210 -0
  90. package/dist/mcp-data/registry/badge.json +289 -0
  91. package/dist/mcp-data/registry/banner.json +206 -0
  92. package/dist/mcp-data/registry/bar-nav.json +341 -0
  93. package/dist/mcp-data/registry/button-filled.json +386 -0
  94. package/dist/mcp-data/registry/button-unfilled.json +537 -0
  95. package/dist/mcp-data/registry/card-data-viz-donut.json +143 -0
  96. package/dist/mcp-data/registry/card-setting.json +194 -0
  97. package/dist/mcp-data/registry/card-shell-data-viz.json +173 -0
  98. package/dist/mcp-data/registry/card.json +126 -0
  99. package/dist/mcp-data/registry/chart-bar.json +121 -0
  100. package/dist/mcp-data/registry/chart-donut.json +257 -0
  101. package/dist/mcp-data/registry/chart-legend.json +209 -0
  102. package/dist/mcp-data/registry/chart-line.json +153 -0
  103. package/dist/mcp-data/registry/checkbox.json +361 -0
  104. package/dist/mcp-data/registry/chip.json +276 -0
  105. package/dist/mcp-data/registry/divider.json +225 -0
  106. package/dist/mcp-data/registry/empty-state.json +197 -0
  107. package/dist/mcp-data/registry/field.json +222 -0
  108. package/dist/mcp-data/registry/icon.json +242 -0
  109. package/dist/mcp-data/registry/input.json +597 -0
  110. package/dist/mcp-data/registry/loader.json +192 -0
  111. package/dist/mcp-data/registry/menu.json +459 -0
  112. package/dist/mcp-data/registry/modal.json +150 -0
  113. package/dist/mcp-data/registry/panel-nav.json +622 -0
  114. package/dist/mcp-data/registry/panel-sub-nav.json +249 -0
  115. package/dist/mcp-data/registry/panel-tools.json +374 -0
  116. package/dist/mcp-data/registry/radio.json +374 -0
  117. package/dist/mcp-data/registry/registry.json +5389 -0
  118. package/dist/mcp-data/registry/select-multi.json +755 -0
  119. package/dist/mcp-data/registry/select.json +740 -0
  120. package/dist/mcp-data/registry/shell-gradient-picker.json +195 -0
  121. package/dist/mcp-data/registry/shell-gradient-swatch.json +215 -0
  122. package/dist/mcp-data/registry/skeleton.json +282 -0
  123. package/dist/mcp-data/registry/slider.json +657 -0
  124. package/dist/mcp-data/registry/swatch-picker.json +231 -0
  125. package/dist/mcp-data/registry/switch.json +361 -0
  126. package/dist/mcp-data/registry/tab-group.json +255 -0
  127. package/dist/mcp-data/registry/tag.json +352 -0
  128. package/dist/mcp-data/registry/text.json +371 -0
  129. package/dist/mcp-data/registry/tooltip-data-viz.json +158 -0
  130. package/dist/mcp-data/registry/tooltip.json +311 -0
  131. package/dist/react/components.d.ts +1 -2
  132. package/dist/react/components.js +1 -2
  133. package/dist/react/ds-menu.d.ts +1 -0
  134. package/dist/react/ds-menu.js +2 -1
  135. package/dist/react/ds-swatch-picker.d.ts +12 -0
  136. package/dist/react/ds-swatch-picker.js +12 -0
  137. package/dist/types/components/Menu/Menu.d.ts +7 -1
  138. package/dist/types/components/Menu/menu-placement.d.ts +6 -3
  139. package/dist/types/components/Menu/menu-types.d.ts +14 -1
  140. package/dist/types/components/ShellGradientPicker/ShellGradientPicker.d.ts +2 -5
  141. package/dist/types/components/ShellGradientSwatch/ShellGradientSwatch.d.ts +1 -0
  142. package/dist/types/components/SwatchPicker/SwatchPicker.d.ts +21 -0
  143. package/dist/types/components/SwatchPicker/swatch-picker-types.d.ts +26 -0
  144. package/dist/types/components.d.ts +117 -237
  145. package/dist/types/shell/index.d.ts +2 -2
  146. package/dist/types/shell/shell-gradient-presets.d.ts +15 -0
  147. package/dist/types/utils/choice-popup-alignment.d.ts +21 -0
  148. package/dist/types/utils/index.d.ts +2 -0
  149. package/package.json +6 -2
  150. package/dist/angular/ds-pagination.d.ts +0 -15
  151. package/dist/angular/ds-table.d.ts +0 -33
  152. package/dist/angular/ds-table.js +0 -48
  153. package/dist/components/ds-pagination.js +0 -2
  154. package/dist/components/ds-pagination.js.map +0 -1
  155. package/dist/components/ds-table.d.ts +0 -11
  156. package/dist/components/ds-table.js +0 -2
  157. package/dist/components/ds-table.js.map +0 -1
  158. package/dist/components/p-CKV6rOGa.js +0 -2
  159. package/dist/components/p-CKV6rOGa.js.map +0 -1
  160. package/dist/components/p-CSTr2132.js +0 -2
  161. package/dist/components/p-CSTr2132.js.map +0 -1
  162. package/dist/components/p-C_cu-HIT.js +0 -2
  163. package/dist/components/p-C_cu-HIT.js.map +0 -1
  164. package/dist/components/p-D0B9uDTP.js +0 -2
  165. package/dist/components/p-D0B9uDTP.js.map +0 -1
  166. package/dist/components/p-DhxWfdX0.js +0 -2
  167. package/dist/components/p-DhxWfdX0.js.map +0 -1
  168. package/dist/components/p-Pip-LNhl.js +0 -2
  169. package/dist/components/p-Pip-LNhl.js.map +0 -1
  170. package/dist/react/ds-pagination.d.ts +0 -12
  171. package/dist/react/ds-pagination.js +0 -12
  172. package/dist/react/ds-table.d.ts +0 -21
  173. package/dist/react/ds-table.js +0 -16
  174. package/dist/types/components/Pagination/Pagination.d.ts +0 -14
  175. package/dist/types/components/Table/Table.d.ts +0 -56
  176. /package/dist/components/{p-CysJosvS.js.map → p-B-zhgAvL.js.map} +0 -0
  177. /package/dist/components/{p-C21sIElQ.js.map → p-BLzivrE3.js.map} +0 -0
  178. /package/dist/components/{p-DAuadHYu.js.map → p-BbV68F23.js.map} +0 -0
  179. /package/dist/components/{p-_y99wWNU.js.map → p-C7RObioO.js.map} +0 -0
  180. /package/dist/components/{p-DxQ4gzbA.js.map → p-CZ9wemOA.js.map} +0 -0
  181. /package/dist/components/{p-BKA6houZ.js.map → p-CbS_WWs2.js.map} +0 -0
  182. /package/dist/components/{p-BGcSrHKl.js.map → p-DT2MK8Kt.js.map} +0 -0
  183. /package/dist/components/{p-DV23KMU-.js.map → p-DyZj3k9S.js.map} +0 -0
  184. /package/dist/components/{p-D6hvtWDk.js.map → p-KRGBGTu5.js.map} +0 -0
@@ -0,0 +1,206 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "banner",
4
+ "title": "Banner",
5
+ "description": "Intent-colored message bar. Supports floating toast mode with auto-dismiss and fade-out animation.",
6
+ "type": "registry:ui",
7
+ "meta": {
8
+ "library": "@ds-mo/ui",
9
+ "distribution": "npm",
10
+ "storybook": "https://zainadeel.github.io/compomo/?path=/story/banner",
11
+ "source": "src/wc/components/Banner/Banner.tsx",
12
+ "intentStatus": "migration-pending",
13
+ "intent": null,
14
+ "api": {
15
+ "props": {
16
+ "contrast": {
17
+ "type": "BannerContrast",
18
+ "resolvedType": "\"bold\" | \"faint\" | \"medium\" | \"strong\"",
19
+ "attribute": "contrast",
20
+ "default": "'medium'",
21
+ "required": false,
22
+ "mutable": false
23
+ },
24
+ "dismissLabel": {
25
+ "type": "string",
26
+ "resolvedType": "string",
27
+ "attribute": "dismiss-label",
28
+ "default": "'Dismiss'",
29
+ "required": false,
30
+ "mutable": false
31
+ },
32
+ "floating": {
33
+ "type": "boolean",
34
+ "resolvedType": "boolean",
35
+ "attribute": "floating",
36
+ "default": "false",
37
+ "required": false,
38
+ "mutable": false,
39
+ "description": "Renders at a fixed position (centered top) and auto-dismisses after 4 s."
40
+ },
41
+ "header": {
42
+ "type": "boolean",
43
+ "resolvedType": "boolean",
44
+ "attribute": "header",
45
+ "default": "false",
46
+ "required": false,
47
+ "mutable": false,
48
+ "description": "Renders the diagonal-stripe accent bar above the body."
49
+ },
50
+ "intent": {
51
+ "type": "BannerIntent",
52
+ "resolvedType": "\"ai\" | \"brand\" | \"caution\" | \"guide\" | \"negative\" | \"neutral\" | \"positive\" | \"walkthrough\" | \"warning\"",
53
+ "attribute": "intent",
54
+ "default": "'neutral'",
55
+ "required": false,
56
+ "mutable": false
57
+ },
58
+ "message": {
59
+ "type": "string",
60
+ "resolvedType": "string",
61
+ "attribute": "message",
62
+ "default": "''",
63
+ "required": false,
64
+ "mutable": false
65
+ },
66
+ "showDismiss": {
67
+ "type": "boolean",
68
+ "resolvedType": "boolean",
69
+ "attribute": "show-dismiss",
70
+ "default": "false",
71
+ "required": false,
72
+ "mutable": false
73
+ }
74
+ },
75
+ "events": [
76
+ {
77
+ "name": "dsDismiss",
78
+ "detail": "void",
79
+ "bubbles": true,
80
+ "cancelable": true,
81
+ "composed": true
82
+ }
83
+ ],
84
+ "methods": [],
85
+ "slots": []
86
+ },
87
+ "props": {
88
+ "contrast": {
89
+ "type": "BannerContrast",
90
+ "resolvedType": "\"bold\" | \"faint\" | \"medium\" | \"strong\"",
91
+ "attribute": "contrast",
92
+ "default": "'medium'",
93
+ "required": false,
94
+ "mutable": false
95
+ },
96
+ "dismissLabel": {
97
+ "type": "string",
98
+ "resolvedType": "string",
99
+ "attribute": "dismiss-label",
100
+ "default": "'Dismiss'",
101
+ "required": false,
102
+ "mutable": false
103
+ },
104
+ "floating": {
105
+ "type": "boolean",
106
+ "resolvedType": "boolean",
107
+ "attribute": "floating",
108
+ "default": "false",
109
+ "required": false,
110
+ "mutable": false,
111
+ "description": "Renders at a fixed position (centered top) and auto-dismisses after 4 s."
112
+ },
113
+ "header": {
114
+ "type": "boolean",
115
+ "resolvedType": "boolean",
116
+ "attribute": "header",
117
+ "default": "false",
118
+ "required": false,
119
+ "mutable": false,
120
+ "description": "Renders the diagonal-stripe accent bar above the body."
121
+ },
122
+ "intent": {
123
+ "type": "BannerIntent",
124
+ "resolvedType": "\"ai\" | \"brand\" | \"caution\" | \"guide\" | \"negative\" | \"neutral\" | \"positive\" | \"walkthrough\" | \"warning\"",
125
+ "attribute": "intent",
126
+ "default": "'neutral'",
127
+ "required": false,
128
+ "mutable": false
129
+ },
130
+ "message": {
131
+ "type": "string",
132
+ "resolvedType": "string",
133
+ "attribute": "message",
134
+ "default": "''",
135
+ "required": false,
136
+ "mutable": false
137
+ },
138
+ "showDismiss": {
139
+ "type": "boolean",
140
+ "resolvedType": "boolean",
141
+ "attribute": "show-dismiss",
142
+ "default": "false",
143
+ "required": false,
144
+ "mutable": false
145
+ }
146
+ },
147
+ "events": [
148
+ {
149
+ "name": "dsDismiss",
150
+ "detail": "void",
151
+ "bubbles": true,
152
+ "cancelable": true,
153
+ "composed": true
154
+ }
155
+ ],
156
+ "methods": [],
157
+ "slots": [],
158
+ "exports": {
159
+ "customElement": "ds-banner",
160
+ "react": "DsBanner",
161
+ "angular": "DsBanner"
162
+ },
163
+ "consumption": {
164
+ "install": "npm install @ds-mo/ui @ds-mo/tokens @ds-mo/icons",
165
+ "cssSetup": "import '@ds-mo/tokens';\nimport '@ds-mo/tokens/reset';\nimport '@ds-mo/tokens/globals';",
166
+ "customElements": {
167
+ "import": "import '@ds-mo/ui/dist/components/ds-banner.js';",
168
+ "example": "<ds-banner></ds-banner>"
169
+ },
170
+ "react": {
171
+ "import": "import { DsBanner } from '@ds-mo/ui/react';",
172
+ "example": "<DsBanner />"
173
+ },
174
+ "angular": {
175
+ "import": "import { DsBanner } from '@ds-mo/ui/angular/ds-banner';",
176
+ "example": "<ds-banner></ds-banner>"
177
+ },
178
+ "peerDependencies": {
179
+ "required": [
180
+ "@ds-mo/tokens ^3.1.1",
181
+ "@ds-mo/icons ^6.2.0"
182
+ ],
183
+ "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
184
+ }
185
+ }
186
+ },
187
+ "dependencies": [
188
+ "@ds-mo/ui",
189
+ "@ds-mo/tokens"
190
+ ],
191
+ "registryDependencies": [
192
+ "text"
193
+ ],
194
+ "files": [
195
+ {
196
+ "path": "src/wc/components/Banner/Banner.css",
197
+ "content": ".banner-surface {\n position: relative;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n border-radius: var(--dimension-radius-075);\n}\n\n.banner-header-surface {\n width: 100%;\n height: var(--dimension-space-100);\n flex-shrink: 0;\n overflow: hidden;\n border-bottom: var(--dimension-stroke-width-015) solid var(--color-border-primary);\n}\n\n.banner-header {\n width: 100%;\n height: 100%;\n background: repeating-linear-gradient(\n -45deg,\n var(--banner-content),\n var(--banner-content) var(--dimension-size-100),\n transparent var(--dimension-size-100),\n transparent var(--dimension-size-200)\n );\n}\n\n.banner-body {\n padding: var(--dimension-space-075);\n min-height: var(--dimension-size-400);\n display: flex;\n align-items: flex-start;\n gap: var(--dimension-space-075);\n}\n\n.banner-message {\n color: var(--banner-content);\n flex: 1;\n min-width: 0;\n}\n\n.banner-dismiss {\n background: none;\n border: none;\n margin: 0;\n padding: var(--dimension-space-025);\n color: var(--banner-content);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n border-radius: var(--dimension-radius-050);\n position: relative;\n font-size: var(--typography-fontsize-md);\n line-height: var(--typography-lineheight-md);\n}\n\n.banner-dismiss::before {\n content: '';\n position: absolute;\n inset: 0;\n border-radius: inherit;\n background-color: transparent;\n transition: var(--effect-transition-interaction-background-short-2, background-color var(--effect-motion-short-2));\n pointer-events: none;\n}\n\n.banner-dismiss:hover::before { background-color: var(--color-interaction-hover); }\n.banner-dismiss:active::before { background-color: var(--color-interaction-pressed); }\n\n.banner-dismiss:focus-visible {\n outline: var(--dimension-stroke-width-025) solid var(--color-interaction-focus);\n outline-offset: var(--dimension-stroke-width-015);\n}\n\n/* ─── Floating ──────────────────────────────────────────────────────────────── */\n\n.floating-position {\n position: fixed;\n top: var(--dimension-space-200);\n left: 50%;\n transform: translateX(-50%);\n pointer-events: none;\n}\n\n.floating-toast {\n pointer-events: auto;\n animation: bannerFadeIn var(--effect-motion-short-2, 150ms) forwards;\n}\n\n.floating-toast--closing {\n animation: bannerFadeOut var(--effect-motion-short-3, 200ms) forwards;\n}\n\n@keyframes bannerFadeIn {\n from { opacity: 0; transform: translateY(-8px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n@keyframes bannerFadeOut {\n from { opacity: 1; transform: translateY(0); }\n to { opacity: 0; transform: translateY(-4px); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .floating-toast {\n animation: none;\n opacity: 1;\n transform: none;\n }\n\n .floating-toast--closing {\n opacity: 0;\n }\n}\n",
198
+ "type": "registry:ui"
199
+ },
200
+ {
201
+ "path": "src/wc/components/Banner/Banner.tsx",
202
+ "content": "import { Component, Prop, State, Event, EventEmitter, Watch, h, Host } from '@stencil/core';\nimport { resolveCssTimeMs, resolveMotionTimeMs, TOKEN_DEFAULTS } from '../../utils';\n\nexport type BannerIntent =\n | 'brand' | 'positive' | 'negative' | 'warning' | 'caution'\n | 'ai' | 'neutral' | 'walkthrough' | 'guide';\n\nexport type BannerContrast = 'faint' | 'medium' | 'bold' | 'strong';\n\n@Component({\n tag: 'ds-banner',\n styleUrl: 'Banner.css',\n scoped: true,\n})\nexport class Banner {\n @Prop() intent: BannerIntent = 'neutral';\n @Prop() contrast: BannerContrast = 'medium';\n @Prop() message: string = '';\n /** Renders the diagonal-stripe accent bar above the body. */\n @Prop() header: boolean = false;\n /** Renders at a fixed position (centered top) and auto-dismisses after 4 s. */\n @Prop() floating: boolean = false;\n @Prop() showDismiss: boolean = false;\n @Prop() dismissLabel: string = 'Dismiss';\n\n @State() private closing: boolean = false;\n\n @Event() dsDismiss!: EventEmitter<void>;\n\n private dismissTimer: ReturnType<typeof setTimeout> | null = null;\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n\n private get fadeOutMs(): number {\n return resolveMotionTimeMs(TOKEN_DEFAULTS.motionShort3, TOKEN_DEFAULTS.animationDurationShort3);\n }\n\n private get autoDismissMs(): number {\n return resolveCssTimeMs(TOKEN_DEFAULTS.animationDelayLong2, TOKEN_DEFAULTS.animationDelayLong2);\n }\n\n componentDidLoad() {\n if (this.floating && this.message) this.scheduleDismiss();\n }\n\n @Watch('message')\n onMessageChange(newVal: string) {\n if (this.floating && newVal) {\n this.closing = false;\n this.scheduleDismiss();\n }\n }\n\n disconnectedCallback() {\n this.clearTimers();\n }\n\n private clearTimers() {\n if (this.dismissTimer) { clearTimeout(this.dismissTimer); this.dismissTimer = null; }\n if (this.closeTimer) { clearTimeout(this.closeTimer); this.closeTimer = null; }\n }\n\n private scheduleDismiss() {\n this.clearTimers();\n this.dismissTimer = setTimeout(() => {\n this.dismissTimer = null;\n this.beginClose();\n }, this.autoDismissMs);\n }\n\n private beginClose() {\n this.clearTimers();\n if (this.floating) {\n const fadeOutMs = this.fadeOutMs;\n if (fadeOutMs <= 0) {\n this.closing = false;\n this.dsDismiss.emit();\n return;\n }\n this.closing = true;\n this.closeTimer = setTimeout(() => {\n this.closing = false;\n this.closeTimer = null;\n this.dsDismiss.emit();\n }, fadeOutMs);\n } else {\n this.dsDismiss.emit();\n }\n }\n\n private getContentColor(): string {\n if (this.contrast === 'bold') return 'var(--color-foreground-on-bold-background-primary)';\n const c = this.contrast === 'faint' ? 'bold' : this.contrast === 'medium' ? 'strong' : 'medium';\n return `var(--color-foreground-${c}-${this.intent})`;\n }\n\n private getBg(): string {\n return `var(--color-background-${this.contrast}-${this.intent})`;\n }\n\n private getElevation(): string {\n return this.floating\n ? 'var(--effect-elevation-elevated-floating)'\n : 'var(--effect-elevation-elevated-sm)';\n }\n\n render() {\n const contentColor = this.getContentColor();\n const isAssertive = this.intent === 'negative';\n\n const bannerEl = (\n <div\n class={{ 'banner-wrapper': true }}\n style={{ '--banner-content': contentColor } as { [key: string]: string }}\n role={isAssertive ? 'alert' : 'status'}\n aria-live={isAssertive ? undefined : 'polite'}\n >\n <div\n class=\"banner-surface\"\n style={{\n backgroundColor: this.getBg(),\n boxShadow: this.getElevation(),\n }}\n >\n {this.header && (\n <div class=\"banner-header-surface\">\n <div class=\"banner-header\" />\n </div>\n )}\n <div class=\"banner-body\">\n <ds-text class=\"banner-message\" as=\"div\" variant=\"text-body-medium\" color=\"inherit\">\n {this.message}\n </ds-text>\n {this.showDismiss && (\n <button\n type=\"button\"\n class=\"banner-dismiss\"\n onClick={() => this.beginClose()}\n aria-label={this.dismissLabel}\n >\n &times;\n </button>\n )}\n </div>\n </div>\n </div>\n );\n\n if (this.floating) {\n return (\n <Host style={{ display: 'contents' }}>\n <div class=\"floating-position\" style={{ zIndex: '9998' }}>\n <div class={{ 'floating-toast': true, 'floating-toast--closing': this.closing }}>\n {bannerEl}\n </div>\n </div>\n </Host>\n );\n }\n\n return <Host style={{ display: 'contents' }}>{bannerEl}</Host>;\n }\n}\n",
203
+ "type": "registry:ui"
204
+ }
205
+ ]
206
+ }
@@ -0,0 +1,341 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "bar-nav",
4
+ "title": "BarNav",
5
+ "description": "Required secondary route navigation within AppShell, showing peer sections of the active PanelNav destination or its destination label when no section tabs apply.",
6
+ "type": "registry:ui",
7
+ "meta": {
8
+ "library": "@ds-mo/ui",
9
+ "distribution": "npm",
10
+ "storybook": "https://zainadeel.github.io/compomo/?path=/story/bar-nav",
11
+ "source": "src/wc/components/BarNav/BarNav.tsx",
12
+ "intentStatus": "complete",
13
+ "intent": {
14
+ "audience": "specialized",
15
+ "status": "stable",
16
+ "summary": "Required secondary route navigation within AppShell, showing peer sections of the active PanelNav destination or its destination label when no section tabs apply.",
17
+ "useWhen": [
18
+ "Rendering the required secondary navigation region inside AppShell.",
19
+ "The active primary destination has peer route sections that users can navigate between.",
20
+ "The active primary destination has no route sections or is showing a detail route and its destination label must preserve the BarNav region."
21
+ ],
22
+ "avoidWhen": [
23
+ "Primary application destinations are being rendered; use PanelNav instead.",
24
+ "A third navigation level is needed; BarNav is never tertiary navigation.",
25
+ "The choices switch local content without navigating routes; use TabGroup instead.",
26
+ "Document-style tabs need reordering, pinning, closing, or user customization.",
27
+ "BarNav would be mounted outside the application's single AppShell."
28
+ ],
29
+ "alternatives": [
30
+ {
31
+ "when": "The choices are the application's top-level destinations.",
32
+ "component": "component:ds-panel-nav",
33
+ "reason": "PanelNav exclusively owns primary application navigation."
34
+ },
35
+ {
36
+ "when": "The choices switch local content without changing routes.",
37
+ "component": "component:ds-tab-group",
38
+ "reason": "TabGroup owns local view selection and tab semantics."
39
+ },
40
+ {
41
+ "when": "The choices are temporary commands or contextual options.",
42
+ "component": "component:ds-menu",
43
+ "reason": "Menu owns transient action and option collections."
44
+ }
45
+ ],
46
+ "commonlyComposedWith": [
47
+ "component:ds-app-shell",
48
+ "component:ds-panel-nav",
49
+ "component:ds-panel-tools"
50
+ ],
51
+ "patterns": [
52
+ "pattern:application-shell"
53
+ ],
54
+ "accessibility": [
55
+ "Use concise localized labels for every route section.",
56
+ "Keep section navigation keyboard accessible through BarNav's built-in horizontal roving focus and overflow menu.",
57
+ "Preserve the selected route state when sections move between the visible row and overflow menu.",
58
+ "Use notification dots only as supplemental status and not as the sole indication of important information.",
59
+ "Pass the active PanelNav destination label through heading whenever section tabs are absent or hidden."
60
+ ],
61
+ "states": [
62
+ "BarNav is the second and final application navigation level; it never replaces PanelNav and never becomes tertiary navigation.",
63
+ "The host router owns the current route; prefer currentUrl with basePath so BarNav derives the selected section.",
64
+ "Use value only when URL-derived selection is unavailable.",
65
+ "The consuming application supplies heading from the active PanelNav destination; BarNav does not inspect PanelNav to derive it.",
66
+ "When no sections apply, retain the BarNav region and show heading rather than shifting the main content upward.",
67
+ "On non-tab detail routes, show the parent PanelNav destination label in BarNav while the detail-page title remains in page content.",
68
+ "Dividers and groups visually organize peer route sections only and do not create hierarchy.",
69
+ "The application filters unauthorized sections before passing tabs to BarNav.",
70
+ "Dashboard and Settings route state updates PanelNav groups, BarNav tabs, heading, basePath, currentUrl, and navStyle as one coordinated change."
71
+ ],
72
+ "responsiveBehavior": [
73
+ "Keep the BarNav region present so shell height and main-content origin remain stable across routes.",
74
+ "Allow the built-in overflow behavior to move trailing sections into Menu when the available width is insufficient.",
75
+ "The active section does not need to remain visible in the tab row when it moves into overflow because page content supplies the page title.",
76
+ "Do not infer mobile navigation behavior from desktop BarNav; future AppShell mobile composition may use a different navigation design."
77
+ ],
78
+ "frameworkCaveats": {
79
+ "customElements": [
80
+ "Assign tabs as a JavaScript property for structured data; tabs-json is the string fallback.",
81
+ "Update tabs, heading, basePath, currentUrl, and navStyle from the same resolved route state."
82
+ ],
83
+ "react": [
84
+ "Pass tabs as an array through the generated React wrapper and synchronize currentUrl and basePath with the host router."
85
+ ],
86
+ "angular": [
87
+ "Import DsBarNav from @ds-mo/ui/angular/ds-bar-nav and bind tabs as a property.",
88
+ "Bind heading to the active PanelNav destination label and update route inputs atomically."
89
+ ]
90
+ },
91
+ "references": [
92
+ {
93
+ "label": "Executable Storybook examples",
94
+ "path": "src/wc/components/BarNav/BarNav.stories.ts"
95
+ },
96
+ {
97
+ "label": "Framework and routing integration",
98
+ "path": "docs/framework-integration.md"
99
+ },
100
+ {
101
+ "label": "Application shell composition",
102
+ "path": "agent/patterns/application-shell/pattern.agent.json"
103
+ }
104
+ ]
105
+ },
106
+ "api": {
107
+ "props": {
108
+ "basePath": {
109
+ "type": "string",
110
+ "resolvedType": "string",
111
+ "attribute": "base-path",
112
+ "default": "''",
113
+ "required": false,
114
+ "mutable": false,
115
+ "description": "Section base path (e.g. `/dashboard/safety`). Used with `currentUrl` to derive `value`."
116
+ },
117
+ "currentUrl": {
118
+ "type": "string",
119
+ "resolvedType": "string",
120
+ "attribute": "current-url",
121
+ "default": "''",
122
+ "required": false,
123
+ "mutable": false,
124
+ "description": "Current route URL. When set with `basePath`, the active tab is derived automatically."
125
+ },
126
+ "heading": {
127
+ "type": "string | undefined",
128
+ "resolvedType": "string | undefined",
129
+ "attribute": "heading",
130
+ "required": false,
131
+ "mutable": false,
132
+ "description": "Fallback heading shown when no tabs are provided.\nWhen tabs are present the heading is hidden."
133
+ },
134
+ "moreTabsLabel": {
135
+ "type": "string",
136
+ "resolvedType": "string",
137
+ "attribute": "more-tabs-label",
138
+ "default": "'More tabs'",
139
+ "required": false,
140
+ "mutable": false
141
+ },
142
+ "navStyle": {
143
+ "type": "NavChromeStyle",
144
+ "resolvedType": "\"dashboard\" | \"settings\"",
145
+ "attribute": "nav-style",
146
+ "default": "'dashboard'",
147
+ "required": false,
148
+ "mutable": false,
149
+ "description": "Style slot: `dashboard` or `settings`. Colors match for now."
150
+ },
151
+ "tabs": {
152
+ "type": "BarNavTab[]",
153
+ "resolvedType": "TabItem[]",
154
+ "default": "[]",
155
+ "required": false,
156
+ "mutable": false,
157
+ "description": "Tab items for the left section.\nSet via JS property: `el.tabs = [...]`. Replace the array reference to update."
158
+ },
159
+ "tabsJson": {
160
+ "type": "string",
161
+ "resolvedType": "string",
162
+ "attribute": "tabs-json",
163
+ "default": "''",
164
+ "required": false,
165
+ "mutable": false,
166
+ "description": "JSON fallback for `tabs` — useful when framework bindings don't propagate arrays."
167
+ },
168
+ "value": {
169
+ "type": "string",
170
+ "resolvedType": "string",
171
+ "attribute": "value",
172
+ "default": "''",
173
+ "required": false,
174
+ "mutable": true,
175
+ "description": "ID of the currently active tab. Overridden when `currentUrl` + `basePath` are set."
176
+ }
177
+ },
178
+ "events": [
179
+ {
180
+ "name": "dsTabChange",
181
+ "detail": "string",
182
+ "bubbles": true,
183
+ "cancelable": true,
184
+ "composed": true,
185
+ "description": "Emitted when the active tab changes. Detail = tab id."
186
+ }
187
+ ],
188
+ "methods": [],
189
+ "slots": []
190
+ },
191
+ "props": {
192
+ "basePath": {
193
+ "type": "string",
194
+ "resolvedType": "string",
195
+ "attribute": "base-path",
196
+ "default": "''",
197
+ "required": false,
198
+ "mutable": false,
199
+ "description": "Section base path (e.g. `/dashboard/safety`). Used with `currentUrl` to derive `value`."
200
+ },
201
+ "currentUrl": {
202
+ "type": "string",
203
+ "resolvedType": "string",
204
+ "attribute": "current-url",
205
+ "default": "''",
206
+ "required": false,
207
+ "mutable": false,
208
+ "description": "Current route URL. When set with `basePath`, the active tab is derived automatically."
209
+ },
210
+ "heading": {
211
+ "type": "string | undefined",
212
+ "resolvedType": "string | undefined",
213
+ "attribute": "heading",
214
+ "required": false,
215
+ "mutable": false,
216
+ "description": "Fallback heading shown when no tabs are provided.\nWhen tabs are present the heading is hidden."
217
+ },
218
+ "moreTabsLabel": {
219
+ "type": "string",
220
+ "resolvedType": "string",
221
+ "attribute": "more-tabs-label",
222
+ "default": "'More tabs'",
223
+ "required": false,
224
+ "mutable": false
225
+ },
226
+ "navStyle": {
227
+ "type": "NavChromeStyle",
228
+ "resolvedType": "\"dashboard\" | \"settings\"",
229
+ "attribute": "nav-style",
230
+ "default": "'dashboard'",
231
+ "required": false,
232
+ "mutable": false,
233
+ "description": "Style slot: `dashboard` or `settings`. Colors match for now."
234
+ },
235
+ "tabs": {
236
+ "type": "BarNavTab[]",
237
+ "resolvedType": "TabItem[]",
238
+ "default": "[]",
239
+ "required": false,
240
+ "mutable": false,
241
+ "description": "Tab items for the left section.\nSet via JS property: `el.tabs = [...]`. Replace the array reference to update."
242
+ },
243
+ "tabsJson": {
244
+ "type": "string",
245
+ "resolvedType": "string",
246
+ "attribute": "tabs-json",
247
+ "default": "''",
248
+ "required": false,
249
+ "mutable": false,
250
+ "description": "JSON fallback for `tabs` — useful when framework bindings don't propagate arrays."
251
+ },
252
+ "value": {
253
+ "type": "string",
254
+ "resolvedType": "string",
255
+ "attribute": "value",
256
+ "default": "''",
257
+ "required": false,
258
+ "mutable": true,
259
+ "description": "ID of the currently active tab. Overridden when `currentUrl` + `basePath` are set."
260
+ }
261
+ },
262
+ "events": [
263
+ {
264
+ "name": "dsTabChange",
265
+ "detail": "string",
266
+ "bubbles": true,
267
+ "cancelable": true,
268
+ "composed": true,
269
+ "description": "Emitted when the active tab changes. Detail = tab id."
270
+ }
271
+ ],
272
+ "methods": [],
273
+ "slots": [],
274
+ "exports": {
275
+ "customElement": "ds-bar-nav",
276
+ "react": "DsBarNav",
277
+ "angular": "DsBarNav"
278
+ },
279
+ "consumption": {
280
+ "install": "npm install @ds-mo/ui @ds-mo/tokens @ds-mo/icons",
281
+ "cssSetup": "import '@ds-mo/tokens';\nimport '@ds-mo/tokens/reset';\nimport '@ds-mo/tokens/globals';",
282
+ "customElements": {
283
+ "import": "import '@ds-mo/ui/dist/components/ds-bar-nav.js';",
284
+ "example": "<ds-bar-nav></ds-bar-nav>"
285
+ },
286
+ "react": {
287
+ "import": "import { DsBarNav } from '@ds-mo/ui/react';",
288
+ "example": "<DsBarNav />"
289
+ },
290
+ "angular": {
291
+ "import": "import { DsBarNav } from '@ds-mo/ui/angular/ds-bar-nav';",
292
+ "example": "<ds-bar-nav></ds-bar-nav>"
293
+ },
294
+ "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: tabs.",
295
+ "peerDependencies": {
296
+ "required": [
297
+ "@ds-mo/tokens ^3.1.1",
298
+ "@ds-mo/icons ^6.2.0"
299
+ ],
300
+ "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
301
+ }
302
+ }
303
+ },
304
+ "dependencies": [
305
+ "@ds-mo/ui",
306
+ "@ds-mo/tokens"
307
+ ],
308
+ "registryDependencies": [
309
+ "badge",
310
+ "button-unfilled",
311
+ "menu",
312
+ "text"
313
+ ],
314
+ "files": [
315
+ {
316
+ "path": "src/wc/components/BarNav/BarNav.css",
317
+ "content": "@import '../../utils/focus-ring.css';\n@import '../../utils/control-inactive.css';\n\n:host {\n display: block;\n}\n\n.bar-nav {\n display: flex;\n align-items: center;\n position: relative;\n height: var(--dimension-size-600);\n padding: var(--dimension-space-100);\n gap: var(--dimension-space-100);\n box-sizing: border-box;\n border-bottom: var(--dimension-stroke-width-012) solid var(--_bar-nav-border);\n background-color: var(--_bar-nav-bg);\n color: var(--_bar-nav-fg-primary);\n -webkit-user-select: none;\n user-select: none;\n overflow: hidden;\n transition:\n background-color var(--effect-motion-short-3),\n border-color var(--effect-motion-short-3),\n color var(--effect-motion-short-3);\n isolation: isolate;\n /* Default chrome — app surfaces */\n --_bar-nav-bg: var(--color-background-secondary);\n --_bar-nav-border: var(--color-border-tertiary);\n --_bar-nav-fg-primary: var(--color-foreground-primary);\n --_bar-nav-fg-secondary: var(--color-foreground-secondary);\n --_bar-nav-hover: var(--color-interaction-hover);\n --_bar-nav-pressed: var(--color-interaction-pressed);\n --_bar-nav-focus: var(--color-interaction-focus);\n --_bar-nav-dot: var(--color-foreground-bold-brand);\n --ds-focus-ring-color: var(--_bar-nav-focus);\n}\n\n/* Style slots — same app tokens for now; hook texture/glyph layers later. */\n.bar-nav--dashboard,\n.bar-nav--settings {\n --_bar-nav-bg: var(--color-background-secondary);\n --_bar-nav-border: var(--color-border-tertiary);\n --_bar-nav-fg-primary: var(--color-foreground-primary);\n --_bar-nav-fg-secondary: var(--color-foreground-secondary);\n --_bar-nav-hover: var(--color-interaction-hover);\n --_bar-nav-pressed: var(--color-interaction-pressed);\n --_bar-nav-focus: var(--color-interaction-focus);\n --_bar-nav-dot: var(--color-foreground-bold-brand);\n}\n\n/* Shell-owned chrome (bg + wash) — bar is a transparent frame. */\n:host-context(ds-app-shell:not([gradient-preset='none'])) .bar-nav {\n background-color: transparent;\n}\n\n.bar-nav > :not(.bar-nav__tabs-probe) {\n position: relative;\n z-index: 3;\n}\n\n/* Overflow — keep fitting tabs visible and pin the overflow trigger to the far edge. */\n.bar-nav--tabs-collapsed {\n display: flex;\n gap: var(--dimension-space-100);\n align-items: center;\n}\n\n/* Expanded tabs — consume full header width */\n.bar-nav__left {\n position: relative;\n flex: 1 1 0%;\n min-width: 0;\n width: 0;\n display: flex;\n align-items: center;\n}\n\n.bar-nav__tabs-visible {\n flex: 1;\n min-width: 0;\n width: 100%;\n}\n\n/* Unconstrained width so intrinsic tab row width is measurable while collapsed */\n.bar-nav__tabs-probe {\n position: absolute;\n left: 0;\n top: 0;\n visibility: hidden;\n pointer-events: none;\n overflow: visible;\n width: max-content;\n max-width: none;\n z-index: -1;\n}\n\n.bar-nav__tabs-probe > div {\n width: max-content;\n}\n\n.bar-nav__tab-list {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-050);\n position: relative;\n width: max-content;\n}\n\n.bar-nav__tab {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--dimension-space-050);\n height: var(--dimension-size-400);\n min-width: var(--dimension-size-400);\n padding: var(--dimension-space-075);\n box-sizing: border-box;\n border: none;\n border-radius: var(--dimension-radius-025);\n background-color: transparent;\n color: var(--_bar-nav-fg-secondary);\n cursor: pointer;\n position: relative;\n flex-shrink: 0;\n -webkit-user-select: none;\n user-select: none;\n transition: color var(--effect-motion-short-2);\n}\n\n.bar-nav__tab::after {\n content: '';\n position: absolute;\n inset: 0;\n border-radius: var(--dimension-radius-025);\n background: transparent;\n pointer-events: none;\n}\n\n.bar-nav__tab:hover::after {\n background: var(--_bar-nav-hover);\n}\n\n.bar-nav__tab:active::after {\n background: var(--_bar-nav-pressed);\n}\n\n.bar-nav__tab--selected {\n color: var(--_bar-nav-fg-primary);\n}\n\n.bar-nav__tab-label {\n position: relative;\n display: inline-flex;\n align-items: center;\n padding: 0 var(--dimension-space-025);\n white-space: nowrap;\n}\n\n.bar-nav__tab-label--dot {\n padding-right: calc(var(--dimension-space-025) + var(--dimension-space-050));\n}\n\n.bar-nav__tab-dot {\n position: absolute;\n top: 0;\n right: 0;\n pointer-events: none;\n}\n\n.bar-nav__tab-divider {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: var(--dimension-space-100);\n height: var(--dimension-size-400);\n padding-block: var(--dimension-space-075);\n flex-shrink: 0;\n pointer-events: none;\n}\n\n.bar-nav__tab-divider-line {\n width: var(--dimension-stroke-width-012);\n align-self: stretch;\n background-color: var(--_bar-nav-border);\n}\n\n/* In-flow placeholder while measuring overflow — avoids layout jump; hidden from view/accessibility. */\n.bar-nav__tabs-pending {\n flex: 1;\n min-width: 0;\n height: var(--dimension-size-400);\n visibility: hidden;\n pointer-events: none;\n}\n\n/* Overflow trigger — compact icon button aligned to the right edge. */\n.bar-nav__overflow-trigger {\n margin-left: auto;\n --ds-button-unfilled-fg: var(--_bar-nav-fg-secondary);\n --ds-button-unfilled-fg-active: var(--_bar-nav-fg-primary);\n --ds-interaction-hover: var(--_bar-nav-hover);\n --ds-interaction-pressed: var(--_bar-nav-pressed);\n --ds-focus-ring-color: var(--_bar-nav-focus);\n}\n\n/* Fallback heading when no tabs — matches a single (selected) tab so the area\n name shares the tab label's type token, color, height and horizontal inset.\n Inset = tab button padding (space-075, 6px) + label inset (space-025, 2px).\n Color comes from ds-text color=\"primary\"; beat Text's inline-box display. */\nds-text.bar-nav__heading {\n display: inline-flex;\n align-items: center;\n height: var(--dimension-size-400);\n padding-inline: var(--dimension-space-100);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n -webkit-user-select: none;\n user-select: none;\n}\n",
318
+ "type": "registry:ui"
319
+ },
320
+ {
321
+ "path": "src/wc/components/BarNav/BarNav.tsx",
322
+ "content": "import {\n Component,\n Prop,\n Event,\n EventEmitter,\n Element,\n Watch,\n State,\n h,\n Host,\n} from '@stencil/core';\nimport type { NavChromeStyle } from '../../shell/nav-chrome';\nimport type { MenuItemData } from '../Menu/menu-types';\nimport { getSelectableTabs, isTabDivider, type TabItemTab } from '../TabGroup/tab-item-utils';\nimport type { BarNavTab } from './bar-nav-types';\nimport {\n deriveBarNavValueFromUrl,\n parseJsonArrayProp,\n shouldResyncBarNavProps,\n} from './bar-nav-utils';\nimport {\n tabsOverflowContainer,\n tabsToOverflowMenuSections,\n trimTrailingDividers,\n visibleTabCountForWidth,\n} from './bar-nav-tabs-menu-utils';\nimport {\n CHROME_TRANSITION_END,\n CHROME_TRANSITION_START,\n ChromeTransitionGate,\n createRafCoalescer,\n readChromeTransitionSource,\n readChromeTransitionPhase,\n} from '../../shell/chrome-transition';\n\n@Component({\n tag: 'ds-bar-nav',\n styleUrl: 'BarNav.css',\n scoped: true,\n})\nexport class BarNav {\n /** Style slot: `dashboard` or `settings`. Colors match for now. */\n @Prop({ attribute: 'nav-style', reflect: true }) navStyle: NavChromeStyle = 'dashboard';\n\n @Element() el!: HTMLElement;\n\n /**\n * Tab items for the left section.\n * Set via JS property: `el.tabs = [...]`. Replace the array reference to update.\n */\n @Prop() tabs: BarNavTab[] = [];\n\n /** JSON fallback for `tabs` — useful when framework bindings don't propagate arrays. */\n @Prop({ attribute: 'tabs-json' }) tabsJson: string = '';\n\n /** ID of the currently active tab. Overridden when `currentUrl` + `basePath` are set. */\n @Prop({ mutable: true }) value: string = '';\n\n /**\n * Fallback heading shown when no tabs are provided.\n * When tabs are present the heading is hidden.\n */\n @Prop() heading: string | undefined;\n\n /** Section base path (e.g. `/dashboard/safety`). Used with `currentUrl` to derive `value`. */\n @Prop() basePath: string = '';\n\n /** Current route URL. When set with `basePath`, the active tab is derived automatically. */\n @Prop() currentUrl: string = '';\n @Prop() moreTabsLabel: string = 'More tabs';\n\n /** Emitted when the active tab changes. Detail = tab id. */\n @Event() dsTabChange!: EventEmitter<string>;\n\n @State() private resolvedTabs: BarNavTab[] = [];\n @State() private urlDerivedValue: string = '';\n @State() private hideTabsForDetailRoute = false;\n @State() private tabsCollapsed = false;\n @State() private menuOpen = false;\n /** When false, defer showing expanded tabs or the collapsed trigger until intrinsic width is measured (avoids a one-frame flash of the full tab row on narrow viewports). */\n @State() private tabLayoutCommitted = false;\n @State() private visibleTabs: BarNavTab[] = [];\n @State() private overflowTabs: BarNavTab[] = [];\n @State() private menuInitialFocusVisible = false;\n @State() private overflowRovingFocused = false;\n @State() private focusedTabId = '';\n\n private static readonly HOST_PROP_SYNC_BUDGET = 8;\n private static readonly INTRINSIC_WIDTH_RETRY_MAX = 3;\n private static readonly TAB_LAYOUT_COMMIT_MAX_FRAMES = 16;\n private static readonly OVERFLOW_HYSTERESIS_PX = 8;\n\n private headerEl: HTMLElement | null = null;\n private triggerEl: (HTMLElement & { setFocus?: () => Promise<void> }) | null = null;\n private menuEl: HTMLDsMenuElement | null = null;\n private visibleTabListEl: HTMLElement | null = null;\n private probeTabListEl: HTMLElement | null = null;\n private resizeObserver: ResizeObserver | null = null;\n private intrinsicWidthRetryCount = 0;\n private tabLayoutPendingFrames = 0;\n private readonly panelNavTransition = new ChromeTransitionGate();\n private readonly panelToolsTransition = new ChromeTransitionGate();\n private readonly overflowCoalescer = createRafCoalescer(() => {\n this.updateTabsCollapsed();\n });\n private chromeTransitionShell: HTMLElement | null = null;\n /** Matches `basePath` once tabs + URL are reconciled for the active section. */\n @State() private committedSection = '';\n /** Section waiting for URL + tabs to catch up after a cross-area navigation. */\n @State() private pendingSection = '';\n\n private get effectiveValue(): string {\n if (this.currentUrl && this.basePath) {\n return this.urlDerivedValue;\n }\n return this.value;\n }\n\n private get renderedSelectableTabs(): TabItemTab[] {\n const tabs = this.hasOverflowTabs ? this.visibleTabs : this.resolvedTabs;\n return getSelectableTabs(tabs);\n }\n\n private get hasOverflowTabs(): boolean {\n return this.tabLayoutCommitted && this.tabsCollapsed && this.overflowTabs.length > 0;\n }\n\n private syncFocusedTabId(preferred: string, tabs: TabItemTab[]) {\n if (tabs.some(tab => tab.id === preferred && !tab.isInactive)) {\n this.focusedTabId = preferred;\n return;\n }\n const first = tabs.find(tab => !tab.isInactive);\n this.focusedTabId = first?.id ?? '';\n }\n\n private digestTabsConfig(tabs: BarNavTab[]): string {\n return tabs.map(t => (isTabDivider(t) ? '|' : t.id)).join(',');\n }\n\n private resetTabOverflowLayout() {\n this.tabLayoutCommitted = false;\n this.tabsCollapsed = false;\n this.menuOpen = false;\n this.menuInitialFocusVisible = false;\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n this.visibleTabs = [];\n this.overflowTabs = [];\n }\n\n @Watch('tabs')\n @Watch('tabsJson')\n @Watch('currentUrl')\n @Watch('basePath')\n onHostPropsChange() {\n this.applyHostProps();\n }\n\n @Watch('value')\n @Watch('urlDerivedValue')\n onSelectionChange() {\n this.syncFocusedTabId(this.effectiveValue, this.renderedSelectableTabs);\n this.syncMenuSections();\n this.scheduleOverflowCheck();\n }\n\n @Watch('menuOpen')\n onMenuOpenChange(open: boolean) {\n if (this.menuEl) {\n this.menuEl.open = open;\n }\n }\n\n @Watch('tabsCollapsed')\n onTabsCollapsedChange(collapsed: boolean, prevCollapsed: boolean | undefined) {\n if (!collapsed) {\n this.menuOpen = false;\n this.menuInitialFocusVisible = false;\n this.menuEl = null;\n }\n this.scheduleOverflowCheck();\n if (collapsed) {\n requestAnimationFrame(() => {\n this.scheduleOverflowCheck();\n });\n }\n if (collapsed || prevCollapsed === undefined || prevCollapsed === collapsed) return;\n requestAnimationFrame(() => this.scheduleOverflowCheck());\n queueMicrotask(() => this.focusVisibleSelectedTab());\n }\n\n componentWillLoad() {\n this.applyHostProps();\n this.syncFocusedTabId(this.effectiveValue, getSelectableTabs(this.resolvedTabs));\n }\n\n componentDidLoad() {\n this.syncHostPropsIfNeeded();\n this.scheduleDeferredHostPropSync();\n this.setupOverflowObserver();\n this.bindChromeTransitionListeners();\n this.scheduleOverflowCheck();\n }\n\n componentDidRender() {\n if (this.chromeOverflowPaused()) {\n this.syncMenuAnchor();\n return;\n }\n this.scheduleOverflowCheck();\n this.syncMenuAnchor();\n }\n\n disconnectedCallback() {\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.overflowCoalescer.cancel();\n this.unbindChromeTransitionListeners();\n }\n\n private bindChromeTransitionListeners() {\n const shell = this.el.closest('ds-app-shell');\n if (!shell) return;\n this.chromeTransitionShell = shell;\n shell.addEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);\n shell.addEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);\n }\n\n private unbindChromeTransitionListeners() {\n if (!this.chromeTransitionShell) return;\n this.chromeTransitionShell.removeEventListener(\n CHROME_TRANSITION_START,\n this.onChromeTransitionStart,\n );\n this.chromeTransitionShell.removeEventListener(\n CHROME_TRANSITION_END,\n this.onChromeTransitionEnd,\n );\n this.chromeTransitionShell = null;\n }\n\n private onChromeTransitionStart = (event: Event) => {\n const source = readChromeTransitionSource(event);\n if (source === 'panel-nav') {\n this.panelNavTransition.enter();\n return;\n }\n if (source === 'panel-tools') {\n const phase = readChromeTransitionPhase(event) ?? 'opening';\n if (phase === 'closing') {\n this.panelToolsTransition.enter();\n }\n }\n };\n\n private chromeOverflowPaused(): boolean {\n return this.panelNavTransition.isActive || this.panelToolsTransition.isActive;\n }\n\n private onChromeTransitionEnd = (event: Event) => {\n const source = readChromeTransitionSource(event);\n if (source === 'panel-nav') {\n this.panelNavTransition.exit();\n if (!this.panelNavTransition.isActive) {\n this.scheduleOverflowCheck();\n }\n return;\n }\n if (source === 'panel-tools') {\n this.panelToolsTransition.exit();\n this.scheduleOverflowCheck();\n }\n };\n\n /** Re-resolve props assigned by the host after componentWillLoad (Angular ngAfterViewInit). */\n private syncHostPropsIfNeeded() {\n if (shouldResyncBarNavProps(this.resolvedTabs, this.tabs, this.tabsJson)) {\n this.applyHostProps();\n } else if (this.currentUrl && this.basePath) {\n this.syncValueFromUrl();\n }\n }\n\n /** Poll across animation frames — host props may land without triggering @Watch. */\n private scheduleDeferredHostPropSync() {\n let remaining = BarNav.HOST_PROP_SYNC_BUDGET;\n const tick = () => {\n this.syncHostPropsIfNeeded();\n if (--remaining > 0) {\n requestAnimationFrame(tick);\n }\n };\n queueMicrotask(tick);\n }\n\n private setupOverflowObserver() {\n if (typeof ResizeObserver === 'undefined' || !this.headerEl) return;\n this.resizeObserver?.disconnect();\n this.resizeObserver = new ResizeObserver(() => {\n if (this.chromeOverflowPaused()) return;\n this.scheduleOverflowCheck();\n });\n this.resizeObserver.observe(this.headerEl);\n }\n\n private scheduleOverflowCheck() {\n if (this.chromeOverflowPaused()) return;\n this.overflowCoalescer.schedule();\n }\n\n /** Width available for the tab row or collapsed trigger. */\n private getLeftZoneAvailableWidth(): number {\n if (!this.headerEl) return 0;\n\n const headerStyle = getComputedStyle(this.headerEl);\n const horizontalPadding =\n (parseFloat(headerStyle.paddingLeft) || 0) + (parseFloat(headerStyle.paddingRight) || 0);\n\n return Math.max(0, this.headerEl.clientWidth - horizontalPadding);\n }\n\n private getTabsIntrinsicWidth(): number {\n return this.probeTabListEl?.scrollWidth ?? 0;\n }\n\n private getHeaderGap(): number {\n if (!this.headerEl) return 0;\n const style = getComputedStyle(this.headerEl);\n return parseFloat(style.columnGap || style.gap) || 0;\n }\n\n private getOverflowTriggerReserveWidth(): number {\n if (!this.triggerEl) {\n return 40;\n }\n return this.triggerEl.getBoundingClientRect().width + this.getHeaderGap();\n }\n\n private getProbeTabMetrics():\n | {\n itemWidths: number[];\n itemGap: number;\n listExtraWidth: number;\n }\n | null {\n const tabList = this.probeTabListEl;\n if (!tabList) return null;\n\n const children = Array.from(tabList.children) as HTMLElement[];\n const itemWidths = children.map(child => child.getBoundingClientRect().width);\n if (itemWidths.length === 0 || itemWidths.some(width => width === 0)) return null;\n\n const style = getComputedStyle(tabList);\n const itemGap = parseFloat(style.columnGap || style.gap) || 0;\n const childrenWidth = itemWidths.reduce((sum, width) => sum + width, 0);\n const gapWidth = itemGap * Math.max(0, itemWidths.length - 1);\n const listExtraWidth = Math.max(0, tabList.scrollWidth - childrenWidth - gapWidth);\n\n return { itemWidths, itemGap, listExtraWidth };\n }\n\n private setOverflowTabLayout(visibleTabs: BarNavTab[], overflowTabs: BarNavTab[]) {\n const nextCollapsed = overflowTabs.length > 0;\n const visibleChanged =\n this.digestTabsConfig(visibleTabs) !== this.digestTabsConfig(this.visibleTabs);\n const overflowChanged =\n this.digestTabsConfig(overflowTabs) !== this.digestTabsConfig(this.overflowTabs);\n\n if (visibleChanged) {\n this.visibleTabs = visibleTabs;\n }\n if (overflowChanged) {\n this.overflowTabs = overflowTabs;\n }\n if (nextCollapsed !== this.tabsCollapsed) {\n this.tabsCollapsed = nextCollapsed;\n if (!nextCollapsed) {\n this.menuOpen = false;\n }\n }\n this.syncFocusedTabId(\n this.effectiveValue,\n getSelectableTabs(nextCollapsed ? visibleTabs : this.resolvedTabs),\n );\n }\n\n private scheduleIntrinsicWidthRetry() {\n if (this.intrinsicWidthRetryCount >= BarNav.INTRINSIC_WIDTH_RETRY_MAX) return;\n this.intrinsicWidthRetryCount++;\n requestAnimationFrame(() => this.scheduleOverflowCheck());\n }\n\n private forceTabLayoutCommit(fallbackCollapsed: boolean) {\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n this.setOverflowTabLayout(\n fallbackCollapsed ? [] : this.resolvedTabs,\n fallbackCollapsed ? this.resolvedTabs : [],\n );\n this.tabLayoutCommitted = true;\n }\n\n private updateTabsCollapsed() {\n if (!this.headerEl || this.resolvedTabs.length === 0 || this.hideTabsForDetailRoute) {\n if (this.tabsCollapsed) {\n this.setOverflowTabLayout([], []);\n }\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n this.tabLayoutCommitted = true;\n return;\n }\n\n const metrics = this.getProbeTabMetrics();\n const intrinsicWidth = this.getTabsIntrinsicWidth();\n if (!metrics || intrinsicWidth === 0) {\n this.tabLayoutPendingFrames += 1;\n if (this.intrinsicWidthRetryCount >= BarNav.INTRINSIC_WIDTH_RETRY_MAX) {\n this.forceTabLayoutCommit(false);\n } else if (this.tabLayoutPendingFrames >= BarNav.TAB_LAYOUT_COMMIT_MAX_FRAMES) {\n this.forceTabLayoutCommit(false);\n } else {\n this.scheduleIntrinsicWidthRetry();\n }\n return;\n }\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n\n const shouldCollapse = tabsOverflowContainer(\n intrinsicWidth,\n this.getLeftZoneAvailableWidth(),\n this.tabsCollapsed,\n BarNav.OVERFLOW_HYSTERESIS_PX,\n );\n\n if (!shouldCollapse) {\n this.setOverflowTabLayout(this.resolvedTabs, []);\n this.tabLayoutCommitted = true;\n return;\n }\n\n const visibleCount = visibleTabCountForWidth(\n metrics.itemWidths,\n this.getLeftZoneAvailableWidth(),\n this.getOverflowTriggerReserveWidth(),\n metrics.itemGap,\n metrics.listExtraWidth,\n );\n const visibleTabs = trimTrailingDividers(this.resolvedTabs.slice(0, visibleCount));\n const overflowTabs = this.resolvedTabs.slice(visibleTabs.length);\n this.setOverflowTabLayout(visibleTabs, overflowTabs);\n this.tabLayoutCommitted = true;\n }\n\n private syncMenuAnchor() {\n if (!this.menuEl || !this.triggerEl || !this.hasOverflowTabs) return;\n this.menuEl.anchor = this.triggerEl;\n this.syncMenuSections();\n }\n\n private handleMenuSelect(e: Event) {\n const detail = (e as CustomEvent<MenuItemData>).detail;\n if (detail?.value !== undefined) {\n this.selectTab(String(detail.value));\n }\n this.menuOpen = false;\n this.menuInitialFocusVisible = false;\n }\n\n private handleMenuClose() {\n this.menuOpen = false;\n this.menuInitialFocusVisible = false;\n }\n\n /** Batch tabs/basePath/currentUrl updates so URL derivation never runs with a mixed section. */\n private incomingTabs(): BarNavTab[] {\n return this.tabsJson\n ? parseJsonArrayProp(this.tabsJson, [])\n : (this.tabs ?? []);\n }\n\n private applyHostProps() {\n const nextBasePath = this.basePath;\n const sectionChanged =\n nextBasePath !== '' &&\n this.committedSection !== '' &&\n nextBasePath !== this.committedSection;\n\n if (sectionChanged) {\n this.urlDerivedValue = '';\n this.committedSection = '';\n this.pendingSection = nextBasePath;\n this.resolvedTabs = [];\n this.resetTabOverflowLayout();\n return;\n }\n\n if (this.pendingSection) {\n if (!nextBasePath || nextBasePath !== this.pendingSection) {\n return;\n }\n if (!this.currentUrl.startsWith(this.pendingSection)) {\n return;\n }\n const tabs = this.incomingTabs();\n if (tabs.length === 0) {\n return;\n }\n this.pendingSection = '';\n }\n\n const tabs = this.incomingTabs();\n const tabsConfigChanged =\n this.digestTabsConfig(tabs) !== this.digestTabsConfig(this.resolvedTabs);\n\n this.resolvedTabs = tabs;\n this.syncValueFromUrl();\n this.committedSection = nextBasePath;\n if (tabsConfigChanged) {\n this.resetTabOverflowLayout();\n }\n this.scheduleOverflowCheck();\n }\n\n private syncMenuSections() {\n if (!this.menuEl) return;\n this.menuEl.sections = tabsToOverflowMenuSections(this.overflowTabs, this.effectiveValue);\n }\n\n private syncValueFromUrl() {\n const tabs = this.incomingTabs();\n\n if (!this.currentUrl || !this.basePath) {\n this.urlDerivedValue = '';\n this.hideTabsForDetailRoute = false;\n return;\n }\n\n if (!this.currentUrl.startsWith(this.basePath)) {\n this.urlDerivedValue = '';\n this.hideTabsForDetailRoute = false;\n return;\n }\n\n const { value, hideTabs } = deriveBarNavValueFromUrl(\n this.currentUrl,\n this.basePath,\n tabs,\n );\n this.urlDerivedValue = value;\n this.hideTabsForDetailRoute = hideTabs;\n }\n\n private selectTab(id: string) {\n this.value = id;\n this.dsTabChange.emit(id);\n }\n\n /** Move focus to the selected tab in the visible tab group (after expand). */\n private focusVisibleSelectedTab() {\n const tab = this.visibleTabListEl?.querySelector(\n `[data-tab-id=\"${this.effectiveValue}\"]`,\n ) as HTMLElement | null;\n tab?.focus({ preventScroll: true });\n }\n\n private focusVisibleTab(id: string) {\n this.focusedTabId = id;\n const tab = this.visibleTabListEl?.querySelector(\n `[data-tab-id=\"${id}\"]`,\n ) as HTMLElement | null;\n tab?.focus({ preventScroll: true });\n }\n\n private focusLastVisibleTab() {\n const last = [...this.renderedSelectableTabs].reverse().find(tab => !tab.isInactive);\n if (last) this.focusVisibleTab(last.id);\n }\n\n private findEnabledVisibleTab(from: number, step: 1 | -1): number | null {\n const tabs = this.renderedSelectableTabs;\n for (let index = from + step; index >= 0 && index < tabs.length; index += step) {\n if (!tabs[index]?.isInactive) return index;\n }\n return null;\n }\n\n private handleTabKeyDown(e: KeyboardEvent) {\n const tabs = this.renderedSelectableTabs;\n if (!tabs.length) return;\n const targetId = (e.target as HTMLElement | null)?.getAttribute('data-tab-id');\n const focusedId = targetId ?? this.focusedTabId;\n\n if (e.key === 'Enter' || e.key === ' ') {\n const focused = tabs.find(tab => tab.id === focusedId);\n if (!focused || focused.isInactive) return;\n e.preventDefault();\n this.selectTab(focused.id);\n return;\n }\n\n const currentIndex = tabs.findIndex(tab => tab.id === focusedId);\n if (currentIndex < 0) return;\n\n let nextIndex: number | null;\n if (e.key === 'ArrowRight') {\n nextIndex = this.findEnabledVisibleTab(currentIndex, 1);\n if (nextIndex === null && this.hasOverflowTabs) {\n e.preventDefault();\n this.overflowRovingFocused = true;\n void this.triggerEl?.setFocus?.();\n return;\n }\n } else if (e.key === 'ArrowLeft') {\n nextIndex = this.findEnabledVisibleTab(currentIndex, -1);\n } else if (e.key === 'Home') {\n nextIndex = tabs.findIndex(tab => !tab.isInactive);\n } else if (e.key === 'End') {\n const reversedIndex = [...tabs].reverse().findIndex(tab => !tab.isInactive);\n nextIndex = reversedIndex < 0 ? null : tabs.length - 1 - reversedIndex;\n } else {\n return;\n }\n\n if (nextIndex === null || nextIndex < 0 || nextIndex === currentIndex) return;\n e.preventDefault();\n this.focusVisibleTab(tabs[nextIndex].id);\n }\n\n private toggleTabMenu(options?: { focusVisible?: boolean }) {\n if (this.menuOpen) {\n this.menuOpen = false;\n this.menuInitialFocusVisible = false;\n return;\n }\n\n if (this.menuEl && this.triggerEl) {\n this.menuEl.anchor = this.triggerEl;\n this.menuEl.minWidth = '';\n this.syncMenuSections();\n }\n\n this.menuInitialFocusVisible = options?.focusVisible ?? false;\n this.menuOpen = true;\n }\n\n private handleOverflowFocus = () => {\n this.overflowRovingFocused = true;\n };\n\n private handleOverflowBlur = () => {\n this.overflowRovingFocused = false;\n };\n\n private handleTriggerKeyDown(e: KeyboardEvent) {\n if (!this.hasOverflowTabs) return;\n\n if (!this.menuOpen) {\n if (e.key === 'ArrowLeft') {\n e.preventDefault();\n this.overflowRovingFocused = false;\n this.focusLastVisibleTab();\n return;\n }\n\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n this.toggleTabMenu({ focusVisible: true });\n }\n return;\n }\n\n if (e.key === 'Escape') {\n e.preventDefault();\n this.menuOpen = false;\n this.menuInitialFocusVisible = false;\n void this.triggerEl?.setFocus?.();\n }\n }\n\n private renderTabList(tabs: BarNavTab[], probe: boolean) {\n return (\n <div\n role=\"tablist\"\n class=\"bar-nav__tab-list\"\n aria-label={this.heading}\n ref={el => {\n if (probe) {\n const next = (el as HTMLElement) ?? null;\n if (next === this.probeTabListEl) return;\n this.probeTabListEl = next;\n if (next) {\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n this.scheduleOverflowCheck();\n }\n } else {\n this.visibleTabListEl = (el as HTMLElement) ?? null;\n }\n }}\n onKeyDown={probe ? undefined : (e: KeyboardEvent) => this.handleTabKeyDown(e)}\n onFocusin={probe ? undefined : () => {\n this.overflowRovingFocused = false;\n }}\n >\n {tabs.map((tab, index) => {\n if (isTabDivider(tab)) {\n return (\n <div class=\"bar-nav__tab-divider\" key={`divider-${index}`} aria-hidden=\"true\">\n <div class=\"bar-nav__tab-divider-line\" />\n </div>\n );\n }\n\n const isSelected = tab.id === this.effectiveValue;\n return (\n <button\n key={tab.id}\n type=\"button\"\n role=\"tab\"\n data-tab-id={tab.id}\n class={{\n 'bar-nav__tab': true,\n 'bar-nav__tab--selected': isSelected,\n 'ds-focus-ring-inset': true,\n 'ds-control-inactive': !!tab.isInactive,\n }}\n aria-selected={isSelected ? 'true' : 'false'}\n aria-disabled={tab.isInactive ? 'true' : undefined}\n aria-controls={tab.panelId ?? undefined}\n disabled={tab.isInactive}\n tabIndex={\n !probe && !this.overflowRovingFocused && tab.id === this.focusedTabId\n ? 0\n : -1\n }\n onClick={() => !probe && !tab.isInactive && this.selectTab(tab.id)}\n onFocus={() => {\n if (!probe) this.focusedTabId = tab.id;\n }}\n >\n <span\n class={{\n 'bar-nav__tab-label': true,\n 'bar-nav__tab-label--dot': !!tab.dot,\n }}\n >\n <ds-text as=\"span\" variant=\"text-body-medium\" emphasis={isSelected} color=\"inherit\">\n {tab.label}\n </ds-text>\n {tab.dot && (\n <ds-badge\n class=\"bar-nav__tab-dot\"\n variant=\"dot\"\n hasRing={false}\n label=\"\"\n aria-hidden=\"true\"\n />\n )}\n </span>\n </button>\n );\n })}\n </div>\n );\n }\n\n render() {\n // Section gating only applies when `basePath` is set (SPA section sync).\n // Tabs without a base path must still render — otherwise a single-tab bar\n // (or any non-routed tabs) silently falls through to an empty header.\n const sectionReady =\n this.basePath === '' || this.committedSection === this.basePath;\n const hasTabs =\n sectionReady && this.resolvedTabs.length > 0 && !this.hideTabsForDetailRoute;\n const tabGroupKey = this.basePath || 'no-section';\n const renderedTabs = this.hasOverflowTabs ? this.visibleTabs : this.resolvedTabs;\n\n return (\n <Host>\n <header\n class={{\n 'bar-nav': true,\n 'bar-nav--dashboard': this.navStyle === 'dashboard',\n 'bar-nav--settings': this.navStyle === 'settings',\n 'bar-nav--tabs-collapsed':\n hasTabs && this.hasOverflowTabs,\n }}\n ref={el => {\n this.headerEl = el as HTMLElement;\n if (el && !this.resizeObserver) {\n this.setupOverflowObserver();\n }\n }}\n >\n {hasTabs && (\n <div class=\"bar-nav__tabs-probe\" aria-hidden=\"true\" inert>\n <div key={`probe-${tabGroupKey}`}>\n {this.renderTabList(this.resolvedTabs, true)}\n </div>\n </div>\n )}\n\n {hasTabs && this.tabLayoutCommitted && renderedTabs.length > 0 && (\n <div class=\"bar-nav__left\">\n <div key={`visible-${tabGroupKey}`} class=\"bar-nav__tabs-visible\">\n {this.renderTabList(renderedTabs, false)}\n </div>\n </div>\n )}\n\n {hasTabs && !this.tabLayoutCommitted && (\n <div class=\"bar-nav__tabs-pending\" aria-hidden=\"true\" />\n )}\n\n {!hasTabs && this.heading && (\n <ds-text\n class=\"bar-nav__heading\"\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis\n color=\"primary\"\n wrap=\"nowrap\"\n >\n {this.heading}\n </ds-text>\n )}\n\n {hasTabs && this.hasOverflowTabs && (\n <ds-button-unfilled variant=\"icon\"\n class={{\n 'bar-nav__overflow-trigger': true,\n }}\n icon=\"Ellipses\"\n isActive={this.menuOpen}\n activeFill={false}\n hasBorder={false}\n focusTabIndex={this.overflowRovingFocused ? 0 : -1}\n ref={(el?: HTMLDsButtonUnfilledElement) => {\n this.triggerEl = (el as (HTMLElement & { setFocus?: () => Promise<void> })) ?? null;\n }}\n haspopup=\"menu\"\n expanded={this.menuOpen}\n aria-label={this.moreTabsLabel}\n onDsClick={() => this.toggleTabMenu({ focusVisible: false })}\n onFocusin={this.handleOverflowFocus}\n onFocusout={this.handleOverflowBlur}\n onKeyDown={(e: KeyboardEvent) => this.handleTriggerKeyDown(e)}\n />\n )}\n\n </header>\n\n {hasTabs && this.hasOverflowTabs && (\n <ds-menu\n ref={el => {\n this.menuEl = (el as HTMLDsMenuElement) ?? null;\n }}\n open={this.menuOpen}\n sections={tabsToOverflowMenuSections(this.overflowTabs, this.effectiveValue)}\n side=\"bottom\"\n align=\"end\"\n sideOffset=\"calc(var(--dimension-space-100) + var(--dimension-space-050))\"\n initialFocusVisible={this.menuInitialFocusVisible}\n onDsSelect={(e: Event) => this.handleMenuSelect(e)}\n onDsClose={() => this.handleMenuClose()}\n />\n )}\n </Host>\n );\n }\n}\n",
323
+ "type": "registry:ui"
324
+ },
325
+ {
326
+ "path": "src/wc/components/BarNav/bar-nav-tabs-menu-utils.ts",
327
+ "content": "import type { MenuItemData, MenuSection } from '../Menu/menu-types';\nimport {\n getSelectableTabs,\n isTabDivider,\n type TabItem,\n type TabItemTab,\n} from '../TabGroup/tab-item-utils';\n\n/** Map BarNav tabs to menu sections, preserving `{ type: 'divider' }` as section breaks. */\nexport function tabsToMenuSections(tabs: TabItem[], selectedId: string): MenuSection[] {\n const sections: MenuSection[] = [];\n let batch: MenuItemData[] = [];\n\n for (const item of tabs) {\n if (isTabDivider(item)) {\n if (batch.length > 0) {\n sections.push({ items: batch });\n batch = [];\n }\n continue;\n }\n\n batch.push({\n label: item.label,\n value: item.id,\n dot: item.dot,\n isSelected: item.id === selectedId,\n isInactive: item.isInactive,\n });\n }\n\n if (batch.length > 0) {\n sections.push({ items: batch });\n }\n\n return sections;\n}\n\nexport function tabsToOverflowMenuSections(tabs: TabItem[], selectedId: string): MenuSection[] {\n return tabsToMenuSections(trimLeadingDividers(tabs), selectedId);\n}\n\nexport function getActiveTab(tabs: TabItem[], selectedId: string): TabItemTab | undefined {\n const selectable = getSelectableTabs(tabs);\n return selectable.find(tab => tab.id === selectedId) ?? selectable[0];\n}\n\nexport function getActiveTabLabel(tabs: TabItem[], selectedId: string): string {\n return getActiveTab(tabs, selectedId)?.label ?? 'Tabs';\n}\n\n/** True when tablist content exceeds the available width (with optional hysteresis). */\nexport function tabsOverflowContainer(\n tabListScrollWidth: number,\n containerClientWidth: number,\n collapsed: boolean,\n hysteresis = 2,\n): boolean {\n const diff = tabListScrollWidth - containerClientWidth;\n if (collapsed) {\n return diff > -hysteresis;\n }\n return diff > hysteresis;\n}\n\nexport function visibleTabCountForWidth(\n itemWidths: number[],\n availableWidth: number,\n overflowTriggerWidth: number,\n itemGap: number,\n listExtraWidth: number,\n): number {\n if (itemWidths.length === 0 || availableWidth <= 0) return 0;\n\n const fullWidth = widthForItemCount(itemWidths, itemWidths.length, itemGap, listExtraWidth);\n if (fullWidth <= availableWidth) return itemWidths.length;\n\n const availableForTabs = Math.max(0, availableWidth - overflowTriggerWidth);\n let count = 0;\n\n for (let i = 1; i <= itemWidths.length; i++) {\n const width = widthForItemCount(itemWidths, i, itemGap, listExtraWidth);\n if (width > availableForTabs) break;\n count = i;\n }\n\n return count;\n}\n\nexport function trimTrailingDividers(tabs: TabItem[]): TabItem[] {\n let end = tabs.length;\n while (end > 0 && isTabDivider(tabs[end - 1])) {\n end -= 1;\n }\n return tabs.slice(0, end);\n}\n\nfunction trimLeadingDividers(tabs: TabItem[]): TabItem[] {\n let start = 0;\n while (start < tabs.length && isTabDivider(tabs[start])) {\n start += 1;\n }\n return tabs.slice(start);\n}\n\nfunction widthForItemCount(\n itemWidths: number[],\n count: number,\n itemGap: number,\n listExtraWidth: number,\n): number {\n if (count <= 0) return 0;\n const itemsWidth = itemWidths.slice(0, count).reduce((sum, width) => sum + width, 0);\n return listExtraWidth + itemsWidth + itemGap * Math.max(0, count - 1);\n}\n",
328
+ "type": "registry:ui"
329
+ },
330
+ {
331
+ "path": "src/wc/components/BarNav/bar-nav-types.ts",
332
+ "content": "import type { TabItem } from '../TabGroup/tab-item-utils';\n\nexport type BarNavTab = TabItem;\n",
333
+ "type": "registry:ui"
334
+ },
335
+ {
336
+ "path": "src/wc/components/BarNav/bar-nav-utils.ts",
337
+ "content": "import { getSelectableTabs } from '../TabGroup/tab-item-utils';\nimport type { BarNavTab } from './bar-nav-types';\n\nexport interface BarNavUrlState {\n /** Tab id derived from the URL, or empty when none applies. */\n value: string;\n /** When true, tabs should be hidden (e.g. non-tab child routes like detail pages). */\n hideTabs: boolean;\n}\n\n/** Derive BarNav tab selection from `currentUrl` relative to `basePath`.\n * Mirrors motive-webapp-lab `resolveBarNavState` tab-selection semantics. */\nexport function deriveBarNavValueFromUrl(\n path: string,\n basePath: string,\n tabs: BarNavTab[],\n): BarNavUrlState {\n if (!path || !basePath) {\n return { value: '', hideTabs: false };\n }\n\n if (path !== basePath && !path.startsWith(`${basePath}/`)) {\n return { value: '', hideTabs: false };\n }\n\n const remainder = path.slice(basePath.length);\n const seg = remainder.startsWith('/') ? remainder.slice(1).split('/')[0] : '';\n const selectableTabs = getSelectableTabs(tabs);\n\n if (!seg) {\n return { value: selectableTabs[0]?.id ?? '', hideTabs: false };\n }\n\n const tab = selectableTabs.find(t => t.id === seg);\n if (tab) {\n return { value: tab.id, hideTabs: false };\n }\n\n return { value: '', hideTabs: true };\n}\n\n/** True when host array/json props have data but resolved internal state is still empty. */\nexport function shouldResyncBarNavProps(\n resolvedTabs: BarNavTab[],\n tabs: BarNavTab[],\n tabsJson: string,\n): boolean {\n const tabsIncoming = tabsJson\n ? parseJsonArrayProp(tabsJson, [])\n : (tabs ?? []);\n\n return resolvedTabs.length === 0 && tabsIncoming.length > 0;\n}\n\n/** Parse tabs from either a JSON attribute string or a JS property array. */\nexport function parseJsonArrayProp<T>(value: string | T[] | undefined, fallback: T[]): T[] {\n if (Array.isArray(value)) return value;\n if (typeof value === 'string' && value !== '') {\n try {\n const parsed = JSON.parse(value);\n return Array.isArray(parsed) ? parsed : fallback;\n } catch {\n return fallback;\n }\n }\n return fallback;\n}\n",
338
+ "type": "registry:ui"
339
+ }
340
+ ]
341
+ }