@ds-mo/ui 9.0.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +28 -0
  2. package/dist/.build-stamp +1 -1
  3. package/dist/agent-patterns.json +233 -0
  4. package/dist/agent.json +155 -19
  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-swatch-picker.js +42 -0
  9. package/dist/angular/index.d.ts +1 -1
  10. package/dist/angular/index.js +1 -0
  11. package/dist/angular/proxies.d.ts +1 -0
  12. package/dist/angular/proxies.js +1 -0
  13. package/dist/components/ds-app-shell.js +1 -1
  14. package/dist/components/ds-bar-nav.js +1 -1
  15. package/dist/components/ds-bar-nav.js.map +1 -1
  16. package/dist/components/ds-menu.js +1 -1
  17. package/dist/components/ds-panel-nav.js +1 -1
  18. package/dist/components/ds-panel-nav.js.map +1 -1
  19. package/dist/components/ds-select-multi.js +1 -1
  20. package/dist/components/ds-select-multi.js.map +1 -1
  21. package/dist/components/ds-select.js +1 -1
  22. package/dist/components/ds-select.js.map +1 -1
  23. package/dist/components/ds-shell-gradient-picker.js +1 -1
  24. package/dist/components/ds-shell-gradient-picker.js.map +1 -1
  25. package/dist/components/ds-shell-gradient-swatch.js +1 -1
  26. package/dist/components/ds-shell-gradient-swatch.js.map +1 -1
  27. package/dist/components/ds-slider.js +1 -1
  28. package/dist/components/ds-slider.js.map +1 -1
  29. package/dist/components/ds-swatch-picker.d.ts +11 -0
  30. package/dist/components/ds-swatch-picker.js +2 -0
  31. package/dist/components/ds-swatch-picker.js.map +1 -0
  32. package/dist/components/ds-tooltip-data-viz.js +1 -1
  33. package/dist/components/p-D1HFMTdC.js +2 -0
  34. package/dist/components/p-D1HFMTdC.js.map +1 -0
  35. package/dist/components/p-HBIxvS5C.js +2 -0
  36. package/dist/components/p-HBIxvS5C.js.map +1 -0
  37. package/dist/components/p-l6PmnW_P.js +2 -0
  38. package/dist/components/p-l6PmnW_P.js.map +1 -0
  39. package/dist/components/p-tQ1QHeOE.js +2 -0
  40. package/dist/components/p-tQ1QHeOE.js.map +1 -0
  41. package/dist/docs/components.json +268 -30
  42. package/dist/lib/components/Menu/menu-placement.d.ts +6 -3
  43. package/dist/lib/shell/index.d.ts +2 -2
  44. package/dist/lib/shell/index.js +40 -39
  45. package/dist/lib/shell/index.js.map +3 -3
  46. package/dist/lib/shell/shell-gradient-presets.d.ts +15 -0
  47. package/dist/lib/utils/choice-popup-alignment.d.ts +20 -0
  48. package/dist/lib/utils/index.d.ts +2 -0
  49. package/dist/lib/utils/index.js +16 -0
  50. package/dist/lib/utils/index.js.map +3 -3
  51. package/dist/mcp/cli.js +31409 -0
  52. package/dist/mcp-data/registry/app-shell.json +210 -0
  53. package/dist/mcp-data/registry/badge.json +289 -0
  54. package/dist/mcp-data/registry/banner.json +206 -0
  55. package/dist/mcp-data/registry/bar-nav.json +341 -0
  56. package/dist/mcp-data/registry/button-filled.json +386 -0
  57. package/dist/mcp-data/registry/button-unfilled.json +537 -0
  58. package/dist/mcp-data/registry/card-data-viz-donut.json +143 -0
  59. package/dist/mcp-data/registry/card-setting.json +194 -0
  60. package/dist/mcp-data/registry/card-shell-data-viz.json +173 -0
  61. package/dist/mcp-data/registry/card.json +126 -0
  62. package/dist/mcp-data/registry/chart-bar.json +121 -0
  63. package/dist/mcp-data/registry/chart-donut.json +257 -0
  64. package/dist/mcp-data/registry/chart-legend.json +209 -0
  65. package/dist/mcp-data/registry/chart-line.json +153 -0
  66. package/dist/mcp-data/registry/checkbox.json +361 -0
  67. package/dist/mcp-data/registry/chip.json +276 -0
  68. package/dist/mcp-data/registry/divider.json +225 -0
  69. package/dist/mcp-data/registry/empty-state.json +197 -0
  70. package/dist/mcp-data/registry/field.json +222 -0
  71. package/dist/mcp-data/registry/icon.json +242 -0
  72. package/dist/mcp-data/registry/input.json +597 -0
  73. package/dist/mcp-data/registry/loader.json +192 -0
  74. package/dist/mcp-data/registry/menu.json +459 -0
  75. package/dist/mcp-data/registry/modal.json +150 -0
  76. package/dist/mcp-data/registry/panel-nav.json +622 -0
  77. package/dist/mcp-data/registry/panel-sub-nav.json +249 -0
  78. package/dist/mcp-data/registry/panel-tools.json +374 -0
  79. package/dist/mcp-data/registry/radio.json +374 -0
  80. package/dist/mcp-data/registry/registry.json +5389 -0
  81. package/dist/mcp-data/registry/select-multi.json +755 -0
  82. package/dist/mcp-data/registry/select.json +740 -0
  83. package/dist/mcp-data/registry/shell-gradient-picker.json +195 -0
  84. package/dist/mcp-data/registry/shell-gradient-swatch.json +215 -0
  85. package/dist/mcp-data/registry/skeleton.json +282 -0
  86. package/dist/mcp-data/registry/slider.json +657 -0
  87. package/dist/mcp-data/registry/swatch-picker.json +231 -0
  88. package/dist/mcp-data/registry/switch.json +361 -0
  89. package/dist/mcp-data/registry/tab-group.json +255 -0
  90. package/dist/mcp-data/registry/tag.json +352 -0
  91. package/dist/mcp-data/registry/text.json +371 -0
  92. package/dist/mcp-data/registry/tooltip-data-viz.json +158 -0
  93. package/dist/mcp-data/registry/tooltip.json +311 -0
  94. package/dist/react/components.d.ts +1 -0
  95. package/dist/react/components.js +1 -0
  96. package/dist/react/ds-menu.d.ts +1 -0
  97. package/dist/react/ds-menu.js +2 -1
  98. package/dist/react/ds-swatch-picker.d.ts +12 -0
  99. package/dist/react/ds-swatch-picker.js +12 -0
  100. package/dist/types/components/Menu/Menu.d.ts +7 -1
  101. package/dist/types/components/Menu/menu-placement.d.ts +6 -3
  102. package/dist/types/components/Menu/menu-types.d.ts +14 -1
  103. package/dist/types/components/ShellGradientPicker/ShellGradientPicker.d.ts +2 -5
  104. package/dist/types/components/ShellGradientSwatch/ShellGradientSwatch.d.ts +1 -0
  105. package/dist/types/components/SwatchPicker/SwatchPicker.d.ts +21 -0
  106. package/dist/types/components/SwatchPicker/swatch-picker-types.d.ts +26 -0
  107. package/dist/types/components.d.ts +117 -0
  108. package/dist/types/shell/index.d.ts +2 -2
  109. package/dist/types/shell/shell-gradient-presets.d.ts +15 -0
  110. package/dist/types/utils/choice-popup-alignment.d.ts +21 -0
  111. package/dist/types/utils/index.d.ts +2 -0
  112. package/package.json +6 -2
  113. package/dist/components/p-Bafp65bw.js +0 -2
  114. package/dist/components/p-Bafp65bw.js.map +0 -1
  115. package/dist/components/p-BdnR3jxi.js +0 -2
  116. package/dist/components/p-BdnR3jxi.js.map +0 -1
  117. package/dist/components/p-C_cu-HIT.js +0 -2
  118. package/dist/components/p-C_cu-HIT.js.map +0 -1
  119. package/dist/components/p-D0B9uDTP.js +0 -2
  120. package/dist/components/p-D0B9uDTP.js.map +0 -1
  121. package/dist/components/p-DF0yLZG_.js +0 -2
  122. package/dist/components/p-DF0yLZG_.js.map +0 -1
@@ -0,0 +1,255 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "tab-group",
4
+ "title": "TabGroup",
5
+ "description": "Horizontal single-selection tabs for switching adjacent local views or view modes, with uniform label, icon, or icon-label presentation and surface-aware selection.",
6
+ "type": "registry:ui",
7
+ "meta": {
8
+ "library": "@ds-mo/ui",
9
+ "distribution": "npm",
10
+ "storybook": "https://zainadeel.github.io/compomo/?path=/story/tab-group",
11
+ "source": "src/wc/components/TabGroup/TabGroup.tsx",
12
+ "intentStatus": "complete",
13
+ "intent": {
14
+ "audience": "general",
15
+ "status": "stable",
16
+ "summary": "Horizontal single-selection tabs for switching adjacent local views or view modes, with uniform label, icon, or icon-label presentation and surface-aware selection.",
17
+ "useWhen": [
18
+ "Adjacent local content views or panels fit in a horizontal tab row.",
19
+ "A compact horizontal selector switches between equivalent local view modes such as list and grid.",
20
+ "A tab set needs concise labels, icons, or icon-label content with optional supplemental notification dots."
21
+ ],
22
+ "avoidWhen": [
23
+ "The choices navigate application routes; use PanelNav or BarNav instead.",
24
+ "The choices belong in a vertical panel or card side rail; use PanelSubNav instead.",
25
+ "The choices are temporary commands or contextual options; use Menu instead.",
26
+ "The row requires mixed content variants, subtext, nested levels, or multi-row wrapping.",
27
+ "No enabled tab can remain selected while enabled choices exist."
28
+ ],
29
+ "alternatives": [
30
+ {
31
+ "when": "Local views need persistent vertical side-rail navigation.",
32
+ "component": "component:ds-panel-sub-nav",
33
+ "reason": "PanelSubNav owns the vertical local-view keyboard and selection model."
34
+ },
35
+ {
36
+ "when": "The choices are primary application destinations.",
37
+ "component": "component:ds-panel-nav",
38
+ "reason": "PanelNav owns application route selection in shell navigation."
39
+ },
40
+ {
41
+ "when": "The choices are secondary application route sections.",
42
+ "component": "component:ds-bar-nav",
43
+ "reason": "BarNav owns route sections, overflow, and shell coordination."
44
+ },
45
+ {
46
+ "when": "The choices are transient actions or options.",
47
+ "component": "component:ds-menu",
48
+ "reason": "Menu owns temporary popup collections rather than persistent local views."
49
+ }
50
+ ],
51
+ "commonlyComposedWith": [
52
+ "component:ds-text",
53
+ "component:ds-icon"
54
+ ],
55
+ "accessibility": [
56
+ "Provide an accessible tab-list name with aria-label or aria-labelledby.",
57
+ "Keep exactly one valid enabled tab selected whenever enabled tabs exist, and keep its controlled view synchronized.",
58
+ "When tabs control tabpanels, give each tab the id of its panel and label each panel from its tab.",
59
+ "ArrowLeft and ArrowRight move and activate selection; Home and End select the first and last available tabs.",
60
+ "Icon-only tabs retain their required text label as the accessible name even though the label is not visible.",
61
+ "Notification dots are supplemental status only and must never replace a label, state, or accessible name.",
62
+ "Inactive tabs remain visible but cannot be focused or selected."
63
+ ],
64
+ "states": [
65
+ "The parent owns the canonical selected value and adjacent view visibility, and should update both from dsChange.",
66
+ "A missing, empty, inactive, or unknown selected value is invalid when enabled tabs exist; the component does not choose a fallback selection.",
67
+ "Selection is singular and follows keyboard focus.",
68
+ "Use one consistent content variant across every selectable tab in a group.",
69
+ "Omitted background is for primary and secondary surfaces; pass the faint context explicitly on faint surfaces.",
70
+ "Only the omitted default context emphasizes visible selected label text and keeps the filled track; explicit faint follows the other surface contexts with a transparent track, stable text weight, surface border, and neutral active fill.",
71
+ "Dividers visually separate related subsets inside one tab list but do not create nested navigation or independent selection groups."
72
+ ],
73
+ "responsiveBehavior": [
74
+ "The component remains a single horizontal row and does not become vertical or wrap.",
75
+ "The consuming layout owns narrow-width placement and overflow, or replaces the row with a more suitable navigation pattern."
76
+ ],
77
+ "frameworkCaveats": {
78
+ "customElements": [
79
+ "Assign tabs as a JavaScript property because structured arrays are not represented by HTML attributes.",
80
+ "Use one item variant consistently, keep value synchronized to an enabled item, and update controlled content from dsChange."
81
+ ],
82
+ "react": [
83
+ "Pass tabs as an array through the generated wrapper, keep value controlled, and use onDsChange to synchronize the visible view."
84
+ ],
85
+ "angular": [
86
+ "Bind tabs and a valid value as properties, and handle dsChange to synchronize the visible view."
87
+ ]
88
+ },
89
+ "references": [
90
+ {
91
+ "label": "Storybook examples",
92
+ "path": "src/wc/components/TabGroup/TabGroup.stories.ts"
93
+ }
94
+ ]
95
+ },
96
+ "api": {
97
+ "props": {
98
+ "ariaLabel": {
99
+ "type": "string | null",
100
+ "resolvedType": "null | string",
101
+ "attribute": "aria-label",
102
+ "default": "null",
103
+ "required": false,
104
+ "mutable": false
105
+ },
106
+ "ariaLabelledby": {
107
+ "type": "string | undefined",
108
+ "resolvedType": "string | undefined",
109
+ "attribute": "aria-labelledby",
110
+ "required": false,
111
+ "mutable": false
112
+ },
113
+ "background": {
114
+ "type": "TabBackground | undefined",
115
+ "resolvedType": "\"always-dark\" | \"bold\" | \"faint\" | \"inverted\" | \"media\" | \"medium\" | \"strong\" | \"translucent\" | undefined",
116
+ "attribute": "background",
117
+ "required": false,
118
+ "mutable": false
119
+ },
120
+ "tabs": {
121
+ "type": "TabGroupItem[]",
122
+ "resolvedType": "TabGroupItem[]",
123
+ "default": "[]",
124
+ "required": false,
125
+ "mutable": false
126
+ },
127
+ "value": {
128
+ "type": "string",
129
+ "resolvedType": "string",
130
+ "attribute": "value",
131
+ "default": "''",
132
+ "required": false,
133
+ "mutable": true
134
+ }
135
+ },
136
+ "events": [
137
+ {
138
+ "name": "dsChange",
139
+ "detail": "string",
140
+ "bubbles": true,
141
+ "cancelable": true,
142
+ "composed": true
143
+ }
144
+ ],
145
+ "methods": [],
146
+ "slots": []
147
+ },
148
+ "props": {
149
+ "ariaLabel": {
150
+ "type": "string | null",
151
+ "resolvedType": "null | string",
152
+ "attribute": "aria-label",
153
+ "default": "null",
154
+ "required": false,
155
+ "mutable": false
156
+ },
157
+ "ariaLabelledby": {
158
+ "type": "string | undefined",
159
+ "resolvedType": "string | undefined",
160
+ "attribute": "aria-labelledby",
161
+ "required": false,
162
+ "mutable": false
163
+ },
164
+ "background": {
165
+ "type": "TabBackground | undefined",
166
+ "resolvedType": "\"always-dark\" | \"bold\" | \"faint\" | \"inverted\" | \"media\" | \"medium\" | \"strong\" | \"translucent\" | undefined",
167
+ "attribute": "background",
168
+ "required": false,
169
+ "mutable": false
170
+ },
171
+ "tabs": {
172
+ "type": "TabGroupItem[]",
173
+ "resolvedType": "TabGroupItem[]",
174
+ "default": "[]",
175
+ "required": false,
176
+ "mutable": false
177
+ },
178
+ "value": {
179
+ "type": "string",
180
+ "resolvedType": "string",
181
+ "attribute": "value",
182
+ "default": "''",
183
+ "required": false,
184
+ "mutable": true
185
+ }
186
+ },
187
+ "events": [
188
+ {
189
+ "name": "dsChange",
190
+ "detail": "string",
191
+ "bubbles": true,
192
+ "cancelable": true,
193
+ "composed": true
194
+ }
195
+ ],
196
+ "methods": [],
197
+ "slots": [],
198
+ "exports": {
199
+ "customElement": "ds-tab-group",
200
+ "react": "DsTabGroup",
201
+ "angular": "DsTabGroup"
202
+ },
203
+ "consumption": {
204
+ "install": "npm install @ds-mo/ui @ds-mo/tokens @ds-mo/icons",
205
+ "cssSetup": "import '@ds-mo/tokens';\nimport '@ds-mo/tokens/reset';\nimport '@ds-mo/tokens/globals';",
206
+ "customElements": {
207
+ "import": "import '@ds-mo/ui/dist/components/ds-tab-group.js';",
208
+ "example": "<ds-tab-group></ds-tab-group>"
209
+ },
210
+ "react": {
211
+ "import": "import { DsTabGroup } from '@ds-mo/ui/react';",
212
+ "example": "<DsTabGroup />"
213
+ },
214
+ "angular": {
215
+ "import": "import { DsTabGroup } from '@ds-mo/ui/angular/ds-tab-group';",
216
+ "example": "<ds-tab-group></ds-tab-group>"
217
+ },
218
+ "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: tabs.",
219
+ "peerDependencies": {
220
+ "required": [
221
+ "@ds-mo/tokens ^3.1.1",
222
+ "@ds-mo/icons ^6.2.0"
223
+ ],
224
+ "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
225
+ }
226
+ }
227
+ },
228
+ "dependencies": [
229
+ "@ds-mo/ui",
230
+ "@ds-mo/tokens",
231
+ "@ds-mo/icons"
232
+ ],
233
+ "registryDependencies": [
234
+ "badge",
235
+ "icon",
236
+ "text"
237
+ ],
238
+ "files": [
239
+ {
240
+ "path": "src/wc/components/TabGroup/TabGroup.css",
241
+ "content": "@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n@import '../../utils/control-inactive.css';\n\n:host {\n display: block;\n --_tab-group-surface-border: var(--color-border-tertiary);\n --_tab-group-divider: var(--color-border-secondary);\n}\n\n:host(.tab-group-host--on-medium) {\n --_tab-group-surface-border: var(--color-border-on-medium-background-tertiary);\n --_tab-group-divider: var(--color-border-on-medium-background-secondary);\n}\n\n:host(.tab-group-host--on-bold) {\n --_tab-group-surface-border: var(--color-border-on-bold-background-tertiary);\n --_tab-group-divider: var(--color-border-on-bold-background-secondary);\n}\n\n:host(.tab-group-host--on-strong) {\n --_tab-group-surface-border: var(--color-border-on-strong-background-tertiary);\n --_tab-group-divider: var(--color-border-on-strong-background-secondary);\n}\n\n:host(.tab-group-host--on-translucent) {\n --_tab-group-surface-border: var(--color-translucent-border-tertiary);\n --_tab-group-divider: var(--color-translucent-border-secondary);\n}\n\n:host(.tab-group-host--on-inverted) {\n --_tab-group-surface-border: var(--color-inverted-border-tertiary);\n --_tab-group-divider: var(--color-inverted-border-secondary);\n}\n\n:host(.tab-group-host--on-media) {\n --_tab-group-surface-border: var(--color-media-border-tertiary);\n --_tab-group-divider: var(--color-media-border-secondary);\n}\n\n:host(.tab-group-host--on-always-dark) {\n --_tab-group-surface-border: var(--color-always-dark-border-tertiary);\n --_tab-group-divider: var(--color-always-dark-border-secondary);\n}\n\n.tab-list {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-050);\n position: relative;\n width: max-content;\n max-width: 100%;\n height: var(--dimension-size-400);\n padding: calc(var(--dimension-space-050) - var(--dimension-stroke-width-012));\n background-color: var(--color-background-secondary);\n border: var(--dimension-stroke-width-012) solid var(--_tab-group-surface-border);\n border-radius: var(--dimension-radius-half);\n box-sizing: border-box;\n}\n\n:host(.tab-group-host--surface) .tab-list {\n background-color: transparent;\n}\n\n.tab {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--dimension-space-050);\n padding: var(--dimension-space-050);\n background-color: transparent;\n border: none;\n border-radius: var(--dimension-radius-half);\n cursor: pointer;\n -webkit-user-select: none;\n user-select: none;\n color: var(--color-foreground-secondary);\n transition: color var(--effect-motion-short-2);\n flex-shrink: 0;\n height: var(--dimension-size-300);\n min-width: var(--dimension-size-400);\n box-sizing: border-box;\n --_dot: var(--color-foreground-bold-brand);\n /* Hover/press: `.ds-interaction-fill` — fills cover full 24px box (no layout border). */\n}\n\n.tab--selected {\n color: var(--color-foreground-primary);\n background-color: var(--color-background-primary);\n /* Inset stroke on interaction-fill ::after — stays above hover wash. */\n --ds-interaction-border-width: var(--dimension-stroke-width-012);\n --ds-interaction-border-color: var(--_tab-group-surface-border);\n}\n\n:host(.tab-group-host--surface) .tab--selected {\n background-color: var(--ds-interaction-active);\n}\n\n.tab--selected:not(.ds-interaction-fill) {\n box-shadow: inset 0 0 0 var(--ds-interaction-border-width) var(--ds-interaction-border-color);\n}\n\n/* Inactive: `.ds-control-inactive` (25% opacity) — applied via class in TSX. */\n\n.tab__content {\n position: relative;\n z-index: 2;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--dimension-space-025);\n min-width: 0;\n}\n\n.tab__content--label,\n.tab__content--icon-label {\n padding: 0 var(--dimension-space-025);\n}\n\n.tab__content--dot {\n padding-right: var(--dimension-space-050);\n}\n\n.tab__content--label.tab__content--dot,\n.tab__content--icon-label.tab__content--dot {\n padding-right: calc(var(--dimension-space-025) + var(--dimension-space-050));\n}\n\n.tab__label {\n white-space: nowrap;\n}\n\n.tab__icon {\n flex-shrink: 0;\n}\n\n.tab__dot {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n pointer-events: none;\n}\n\n.on-medium {\n color: var(--color-foreground-on-medium-background-secondary);\n}\n\n.on-medium.tab--selected { color: var(--color-foreground-on-medium-background-primary); }\n\n.on-bold {\n color: var(--color-foreground-on-bold-background-secondary);\n}\n\n.on-bold.tab--selected { color: var(--color-foreground-on-bold-background-primary); }\n\n.on-strong {\n color: var(--color-foreground-on-strong-background-secondary);\n}\n\n.on-strong.tab--selected { color: var(--color-foreground-on-strong-background-primary); }\n\n.on-translucent { color: var(--color-translucent-foreground-secondary); }\n\n.on-translucent.tab--selected { color: var(--color-translucent-foreground-primary); }\n\n.on-inverted { color: var(--color-inverted-foreground-secondary); }\n\n.on-inverted.tab--selected { color: var(--color-inverted-foreground-primary); }\n\n.on-media { color: var(--color-media-foreground-secondary); }\n\n.on-media.tab--selected { color: var(--color-media-foreground-primary); }\n\n.on-always-dark {\n color: var(--color-always-dark-foreground-secondary);\n}\n\n.on-always-dark.tab--selected { color: var(--color-always-dark-foreground-primary); }\n\n.tab-divider {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: var(--dimension-stroke-width-012);\n align-self: center;\n padding: 0;\n flex-shrink: 0;\n pointer-events: none;\n}\n\n.tab-divider__line {\n width: var(--dimension-stroke-width-012);\n height: var(--typography-lineheight-sm);\n flex-shrink: 0;\n background-color: var(--_tab-group-divider);\n}\n",
242
+ "type": "registry:ui"
243
+ },
244
+ {
245
+ "path": "src/wc/components/TabGroup/TabGroup.tsx",
246
+ "content": "import { Component, Prop, Event, EventEmitter, Element, Listen, Watch, h, Host } from '@stencil/core';\nimport {\n getSelectableTabs,\n isTabDivider,\n type TabGroupItem,\n type TabItemTab,\n} from './tab-item-utils';\n\nexport type TabBackground =\n | 'faint'\n | 'medium'\n | 'bold'\n | 'strong'\n | 'translucent'\n | 'inverted'\n | 'media'\n | 'always-dark';\n\n@Component({\n tag: 'ds-tab-group',\n styleUrl: 'TabGroup.css',\n scoped: true,\n})\nexport class TabGroup {\n @Element() el!: HTMLElement;\n\n @Prop({ mutable: true }) value: string = '';\n @Prop() tabs: TabGroupItem[] = [];\n @Prop() background: TabBackground | undefined;\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n\n @Event() dsChange!: EventEmitter<string>;\n\n private get selectableTabs(): TabItemTab[] {\n return getSelectableTabs(this.tabs);\n }\n\n private get focusableTabId(): string {\n return this.selectableTabs.some(tab => tab.id === this.value && !tab.isInactive)\n ? this.value\n : (this.selectableTabs.find(tab => !tab.isInactive)?.id ?? '');\n }\n\n private selectTab(id: string, options?: { focus?: boolean }) {\n this.value = id;\n this.dsChange.emit(id);\n if (options?.focus !== false) {\n this.focusTab(id);\n }\n }\n\n private focusTab(id: string) {\n const btn = this.el.querySelector(`[data-tab-id=\"${id}\"]`) as HTMLElement | null;\n btn?.focus({ preventScroll: true });\n }\n\n /** Keep focus on the selected tab when value changes externally (e.g. BarNav menu). */\n @Watch('value')\n onValueChange(next: string, prev: string | undefined) {\n if (prev === undefined || next === prev) return;\n\n requestAnimationFrame(() => {\n const active = document.activeElement as HTMLElement | null;\n if (!active || !this.el.contains(active)) return;\n\n const activeId = active.getAttribute('data-tab-id');\n if (activeId === next) return;\n\n const focusId = this.selectableTabs.some(tab => tab.id === next && !tab.isInactive)\n ? next\n : this.focusableTabId;\n const nextTab = this.el.querySelector(`[data-tab-id=\"${focusId}\"]`) as HTMLElement | null;\n if (nextTab) {\n nextTab.focus({ preventScroll: true });\n } else {\n active.blur();\n }\n });\n }\n\n /**\n * Find the next non-inactive tab index relative to `from`, stepping by `step`.\n * Returns `from` unchanged if no other enabled tab exists.\n */\n private findEnabled(from: number, step: 1 | -1): number {\n const tabs = this.selectableTabs;\n const len = tabs.length;\n for (let n = 0; n < len; n++) {\n const i = (from + step * (n + 1) + len * (n + 1)) % len;\n if (!tabs[i]?.isInactive) return i;\n }\n return from;\n }\n\n @Listen('keydown')\n handleKeyDown(e: KeyboardEvent) {\n const tabs = this.selectableTabs;\n if (!tabs.length) return;\n\n const focusedId = (e.target as Element | null)?.closest<HTMLElement>('[data-tab-id]')?.dataset['tabId'];\n const currentIdx = tabs.findIndex(t => t.id === (focusedId ?? this.focusableTabId));\n\n let nextIdx: number | null = null;\n\n if (e.key === 'ArrowRight') {\n nextIdx = this.findEnabled(currentIdx, 1);\n } else if (e.key === 'ArrowLeft') {\n nextIdx = this.findEnabled(currentIdx, -1);\n }\n\n if (e.key === 'Home') {\n const first = tabs.findIndex(t => !t.isInactive);\n nextIdx = first === -1 ? null : first;\n } else if (e.key === 'End') {\n const lastEnabled = [...tabs].reverse().findIndex(t => !t.isInactive);\n nextIdx = lastEnabled === -1 ? null : tabs.length - 1 - lastEnabled;\n }\n\n if (nextIdx !== null) {\n e.preventDefault();\n const next = tabs[nextIdx];\n this.selectTab(next.id);\n }\n }\n\n private getBgClass(): string {\n if (!this.background) return '';\n if (this.background === 'always-dark') return 'on-always-dark';\n return `on-${this.background}`;\n }\n\n render() {\n const bgClass = this.getBgClass();\n const focusableTabId = this.focusableTabId;\n\n return (\n <Host\n class={{\n 'tab-group-host': true,\n 'tab-group-host--surface': !!bgClass,\n [`tab-group-host--${bgClass}`]: !!bgClass,\n }}\n >\n <div\n role=\"tablist\"\n class=\"tab-list\"\n aria-label={this.ariaLabel}\n aria-labelledby={this.ariaLabelledby}\n >\n {this.tabs.map((tab, index) => {\n if (isTabDivider(tab)) {\n return (\n <div\n key={`divider-${index}`}\n class=\"tab-divider\"\n aria-hidden=\"true\"\n >\n <div class=\"tab-divider__line\" />\n </div>\n );\n }\n\n const isSelected = tab.id === this.value;\n const variant = tab.variant ?? 'label';\n const showIcon = variant === 'icon' || variant === 'icon-label';\n const showLabel = variant === 'label' || variant === 'icon-label';\n const emphasizeLabel = isSelected && !bgClass;\n return (\n <button\n key={tab.id}\n type=\"button\"\n role=\"tab\"\n data-tab-id={tab.id}\n class={{\n tab: true,\n 'tab--selected': isSelected,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': !tab.isInactive,\n 'ds-interaction-fill--on-faint': bgClass === 'on-faint',\n 'ds-interaction-fill--on-medium': bgClass === 'on-medium',\n 'ds-interaction-fill--on-bold': bgClass === 'on-bold',\n 'ds-interaction-fill--on-strong': bgClass === 'on-strong',\n 'ds-interaction-fill--on-translucent': bgClass === 'on-translucent',\n 'ds-interaction-fill--on-inverted': bgClass === 'on-inverted',\n 'ds-interaction-fill--on-media': bgClass === 'on-media',\n 'ds-interaction-fill--on-always-dark': bgClass === 'on-always-dark',\n 'ds-control-inactive': !!tab.isInactive,\n [bgClass]: !!bgClass,\n }}\n aria-label={variant === 'icon' ? tab.label : undefined}\n aria-selected={isSelected}\n aria-disabled={tab.isInactive ? 'true' : undefined}\n aria-controls={tab.panelId ?? undefined}\n disabled={tab.isInactive}\n tabIndex={tab.id === focusableTabId ? 0 : -1}\n onClick={() => !tab.isInactive && this.selectTab(tab.id)}\n >\n <span class={{\n tab__content: true,\n [`tab__content--${variant}`]: true,\n 'tab__content--dot': !!tab.dot,\n }}>\n {showIcon && (\n <ds-icon\n class=\"tab__icon\"\n name={tab.icon}\n size=\"sm\"\n color=\"inherit\"\n />\n )}\n {showLabel && (\n <ds-text\n class=\"tab__label\"\n as=\"span\"\n variant=\"text-body-small\"\n emphasis={emphasizeLabel}\n color=\"inherit\"\n >\n {tab.label}\n </ds-text>\n )}\n {tab.dot && (\n <ds-badge\n class=\"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 </Host>\n );\n }\n}\n",
247
+ "type": "registry:ui"
248
+ },
249
+ {
250
+ "path": "src/wc/components/TabGroup/tab-item-utils.ts",
251
+ "content": "export interface TabItemDivider {\n type: 'divider';\n}\n\nexport interface TabItemTab {\n id: string;\n label: string;\n /** Disables the tab (same meaning as control `isInactive`). */\n isInactive?: boolean;\n /** id of the tabpanel this tab controls */\n panelId?: string;\n /** Show a notification dot (brand) on the tab — no count, matching panel-nav. */\n dot?: boolean;\n}\n\nexport type TabItem = TabItemTab | TabItemDivider;\n\nexport type TabGroupItemVariant = 'label' | 'icon' | 'icon-label';\n\nexport interface TabGroupItemLabel extends TabItemTab {\n variant?: 'label';\n icon?: never;\n}\n\nexport interface TabGroupItemIcon extends TabItemTab {\n variant: 'icon';\n icon: string;\n}\n\nexport interface TabGroupItemIconLabel extends TabItemTab {\n variant: 'icon-label';\n icon: string;\n}\n\nexport type TabGroupItem =\n | TabGroupItemLabel\n | TabGroupItemIcon\n | TabGroupItemIconLabel\n | TabItemDivider;\n\nexport function isTabDivider(item: TabItem): item is TabItemDivider {\n return 'type' in item && item.type === 'divider';\n}\n\n/** Tab entries that participate in selection, keyboard nav, and URL matching. */\nexport function getSelectableTabs(tabs: TabItem[]): TabItemTab[] {\n return tabs.filter((t): t is TabItemTab => !isTabDivider(t));\n}\n",
252
+ "type": "registry:ui"
253
+ }
254
+ ]
255
+ }
@@ -0,0 +1,352 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "tag",
4
+ "title": "Tag",
5
+ "description": "Compact metadata or semantic-status label on primary surfaces, optionally rendered as a controlled menu trigger with leading and fixed suffix icons.",
6
+ "type": "registry:ui",
7
+ "meta": {
8
+ "library": "@ds-mo/ui",
9
+ "distribution": "npm",
10
+ "storybook": "https://zainadeel.github.io/compomo/?path=/story/tag",
11
+ "source": "src/wc/components/Tag/Tag.tsx",
12
+ "intentStatus": "complete",
13
+ "intent": {
14
+ "audience": "general",
15
+ "status": "stable",
16
+ "summary": "Compact metadata or semantic-status label on primary surfaces, optionally rendered as a controlled menu trigger with leading and fixed suffix icons.",
17
+ "useWhen": [
18
+ "A value needs a compact static metadata or category label.",
19
+ "A status needs persistent semantic color and visible text without interaction.",
20
+ "Product affiliation or AI-generated content needs an explicit labelled marker.",
21
+ "A compact metadata value opens a related menu and needs a native button with controlled popup state."
22
+ ],
23
+ "avoidWhen": [
24
+ "The value must be individually removable; use Chip.",
25
+ "The element triggers an action other than opening a related menu; use the appropriate button or link.",
26
+ "The element represents persistent selection or a toggle; use a control whose active state owns that meaning.",
27
+ "A count or notification dot supplements another control; use Badge.",
28
+ "The tag would be placed on a non-primary parent surface."
29
+ ],
30
+ "alternatives": [
31
+ {
32
+ "when": "A metadata value is individually removable.",
33
+ "component": "component:ds-chip",
34
+ "reason": "Chip owns dismiss interaction and parent-managed removal."
35
+ },
36
+ {
37
+ "when": "A compact mark communicates unread count or notification presence.",
38
+ "component": "component:ds-badge",
39
+ "reason": "Badge owns supplemental notification indicators."
40
+ },
41
+ {
42
+ "when": "The compact element triggers a non-menu action or toggle.",
43
+ "component": "component:ds-button-unfilled",
44
+ "reason": "ButtonUnfilled owns general actions, focus, and active state."
45
+ }
46
+ ],
47
+ "commonlyComposedWith": [
48
+ "component:ds-card",
49
+ "component:ds-text",
50
+ "component:ds-icon",
51
+ "component:ds-menu"
52
+ ],
53
+ "accessibility": [
54
+ "The visible label provides Tag's accessible name; a separate aria-label is normally unnecessary.",
55
+ "Leading icons reinforce the visible label and remain decorative. The fixed ChevronUpDown suffix is always decorative.",
56
+ "Static Tag stays outside the tab order. Interactive Tag renders a native button with menu popup semantics.",
57
+ "The owner controls expanded state, associates the menu with aria-controls when available, and moves focus according to the Menu contract.",
58
+ "Do not communicate semantic intent by color alone; the label must remain understandable without color."
59
+ ],
60
+ "states": [
61
+ "Intent describes the represented value's semantic meaning, never selection or interaction state.",
62
+ "Neutral is ordinary metadata; brand marks Motive or product affiliation; AI marks AI-generated or AI-assisted content; positive, caution, warning, and negative communicate increasingly specific status meaning.",
63
+ "Warning signals higher urgency or risk than caution, which asks for attention before a possible issue.",
64
+ "Contrast changes visual prominence without changing semantic intent or parent surface context.",
65
+ "Rounded is an optional visual style with no semantic difference.",
66
+ "Labels stay on one line and truncate only when the owner supplies a maximum width.",
67
+ "Omitted interactive renders static metadata with no focus, event, or suffix. Interactive adds the fixed ChevronUpDown suffix and emits dsClick without toggling expanded internally.",
68
+ "isInactive disables only the interactive button and applies the shared inactive treatment."
69
+ ],
70
+ "responsiveBehavior": [
71
+ "The owner selects md, sm, or xs density to match its local layout and owns collection wrapping, overflow, and condensation.",
72
+ "Tag does not change size or behavior automatically at breakpoints."
73
+ ],
74
+ "frameworkCaveats": {
75
+ "customElements": [
76
+ "Listen for dsClick to open the associated menu, then update expanded as controlled state. Use the Tag host ID as the menu anchor when required."
77
+ ],
78
+ "react": [
79
+ "Use a stable key and parent-owned layout for Tag collections. For interactive Tag, synchronize expanded and the associated menu from the same state."
80
+ ],
81
+ "angular": [
82
+ "Use parent-owned layout for Tag collections. For interactive Tag, synchronize expanded and the associated menu from the same state."
83
+ ]
84
+ },
85
+ "references": [
86
+ {
87
+ "label": "Storybook examples",
88
+ "path": "src/wc/components/Tag/Tag.stories.ts"
89
+ }
90
+ ]
91
+ },
92
+ "api": {
93
+ "props": {
94
+ "ariaControls": {
95
+ "type": "string | undefined",
96
+ "resolvedType": "string | undefined",
97
+ "attribute": "aria-controls",
98
+ "required": false,
99
+ "mutable": false,
100
+ "description": "ID of the menu controlled by an interactive Tag."
101
+ },
102
+ "contrast": {
103
+ "type": "TagContrast",
104
+ "resolvedType": "\"bold\" | \"faint\" | \"medium\" | \"strong\"",
105
+ "attribute": "contrast",
106
+ "default": "'faint'",
107
+ "required": false,
108
+ "mutable": false
109
+ },
110
+ "expanded": {
111
+ "type": "boolean",
112
+ "resolvedType": "boolean",
113
+ "attribute": "expanded",
114
+ "default": "false",
115
+ "required": false,
116
+ "mutable": false,
117
+ "description": "Controlled open state for the menu triggered by an interactive Tag."
118
+ },
119
+ "icon": {
120
+ "type": "string",
121
+ "resolvedType": "string",
122
+ "attribute": "icon",
123
+ "default": "''",
124
+ "required": false,
125
+ "mutable": false,
126
+ "description": "Canonical icon name rendered before the visible label."
127
+ },
128
+ "intent": {
129
+ "type": "TagIntent",
130
+ "resolvedType": "\"ai\" | \"brand\" | \"caution\" | \"negative\" | \"neutral\" | \"positive\" | \"warning\"",
131
+ "attribute": "intent",
132
+ "default": "'neutral'",
133
+ "required": false,
134
+ "mutable": false
135
+ },
136
+ "interactive": {
137
+ "type": "boolean",
138
+ "resolvedType": "boolean",
139
+ "attribute": "interactive",
140
+ "default": "false",
141
+ "required": false,
142
+ "mutable": false,
143
+ "description": "Render a menu-trigger button with a fixed ChevronUpDown suffix."
144
+ },
145
+ "isInactive": {
146
+ "type": "boolean",
147
+ "resolvedType": "boolean",
148
+ "attribute": "is-inactive",
149
+ "default": "false",
150
+ "required": false,
151
+ "mutable": false,
152
+ "description": "Disable an interactive Tag and apply the shared inactive treatment."
153
+ },
154
+ "label": {
155
+ "type": "string",
156
+ "resolvedType": "string",
157
+ "attribute": "label",
158
+ "required": true,
159
+ "mutable": false
160
+ },
161
+ "maxWidth": {
162
+ "type": "string | number | undefined",
163
+ "resolvedType": "number | string | undefined",
164
+ "attribute": "max-width",
165
+ "required": false,
166
+ "mutable": false
167
+ },
168
+ "rounded": {
169
+ "type": "boolean",
170
+ "resolvedType": "boolean",
171
+ "attribute": "rounded",
172
+ "default": "false",
173
+ "required": false,
174
+ "mutable": false
175
+ },
176
+ "size": {
177
+ "type": "TagSize",
178
+ "resolvedType": "\"md\" | \"sm\" | \"xs\"",
179
+ "attribute": "size",
180
+ "default": "'md'",
181
+ "required": false,
182
+ "mutable": false
183
+ }
184
+ },
185
+ "events": [
186
+ {
187
+ "name": "dsClick",
188
+ "detail": "MouseEvent",
189
+ "bubbles": true,
190
+ "cancelable": true,
191
+ "composed": true,
192
+ "description": "Fired when the interactive Tag button is activated."
193
+ }
194
+ ],
195
+ "methods": [],
196
+ "slots": []
197
+ },
198
+ "props": {
199
+ "ariaControls": {
200
+ "type": "string | undefined",
201
+ "resolvedType": "string | undefined",
202
+ "attribute": "aria-controls",
203
+ "required": false,
204
+ "mutable": false,
205
+ "description": "ID of the menu controlled by an interactive Tag."
206
+ },
207
+ "contrast": {
208
+ "type": "TagContrast",
209
+ "resolvedType": "\"bold\" | \"faint\" | \"medium\" | \"strong\"",
210
+ "attribute": "contrast",
211
+ "default": "'faint'",
212
+ "required": false,
213
+ "mutable": false
214
+ },
215
+ "expanded": {
216
+ "type": "boolean",
217
+ "resolvedType": "boolean",
218
+ "attribute": "expanded",
219
+ "default": "false",
220
+ "required": false,
221
+ "mutable": false,
222
+ "description": "Controlled open state for the menu triggered by an interactive Tag."
223
+ },
224
+ "icon": {
225
+ "type": "string",
226
+ "resolvedType": "string",
227
+ "attribute": "icon",
228
+ "default": "''",
229
+ "required": false,
230
+ "mutable": false,
231
+ "description": "Canonical icon name rendered before the visible label."
232
+ },
233
+ "intent": {
234
+ "type": "TagIntent",
235
+ "resolvedType": "\"ai\" | \"brand\" | \"caution\" | \"negative\" | \"neutral\" | \"positive\" | \"warning\"",
236
+ "attribute": "intent",
237
+ "default": "'neutral'",
238
+ "required": false,
239
+ "mutable": false
240
+ },
241
+ "interactive": {
242
+ "type": "boolean",
243
+ "resolvedType": "boolean",
244
+ "attribute": "interactive",
245
+ "default": "false",
246
+ "required": false,
247
+ "mutable": false,
248
+ "description": "Render a menu-trigger button with a fixed ChevronUpDown suffix."
249
+ },
250
+ "isInactive": {
251
+ "type": "boolean",
252
+ "resolvedType": "boolean",
253
+ "attribute": "is-inactive",
254
+ "default": "false",
255
+ "required": false,
256
+ "mutable": false,
257
+ "description": "Disable an interactive Tag and apply the shared inactive treatment."
258
+ },
259
+ "label": {
260
+ "type": "string",
261
+ "resolvedType": "string",
262
+ "attribute": "label",
263
+ "required": true,
264
+ "mutable": false
265
+ },
266
+ "maxWidth": {
267
+ "type": "string | number | undefined",
268
+ "resolvedType": "number | string | undefined",
269
+ "attribute": "max-width",
270
+ "required": false,
271
+ "mutable": false
272
+ },
273
+ "rounded": {
274
+ "type": "boolean",
275
+ "resolvedType": "boolean",
276
+ "attribute": "rounded",
277
+ "default": "false",
278
+ "required": false,
279
+ "mutable": false
280
+ },
281
+ "size": {
282
+ "type": "TagSize",
283
+ "resolvedType": "\"md\" | \"sm\" | \"xs\"",
284
+ "attribute": "size",
285
+ "default": "'md'",
286
+ "required": false,
287
+ "mutable": false
288
+ }
289
+ },
290
+ "events": [
291
+ {
292
+ "name": "dsClick",
293
+ "detail": "MouseEvent",
294
+ "bubbles": true,
295
+ "cancelable": true,
296
+ "composed": true,
297
+ "description": "Fired when the interactive Tag button is activated."
298
+ }
299
+ ],
300
+ "methods": [],
301
+ "slots": [],
302
+ "exports": {
303
+ "customElement": "ds-tag",
304
+ "react": "DsTag",
305
+ "angular": "DsTag"
306
+ },
307
+ "consumption": {
308
+ "install": "npm install @ds-mo/ui @ds-mo/tokens @ds-mo/icons",
309
+ "cssSetup": "import '@ds-mo/tokens';\nimport '@ds-mo/tokens/reset';\nimport '@ds-mo/tokens/globals';",
310
+ "customElements": {
311
+ "import": "import '@ds-mo/ui/dist/components/ds-tag.js';",
312
+ "example": "<ds-tag></ds-tag>"
313
+ },
314
+ "react": {
315
+ "import": "import { DsTag } from '@ds-mo/ui/react';",
316
+ "example": "<DsTag />"
317
+ },
318
+ "angular": {
319
+ "import": "import { DsTag } from '@ds-mo/ui/angular/ds-tag';",
320
+ "example": "<ds-tag></ds-tag>"
321
+ },
322
+ "peerDependencies": {
323
+ "required": [
324
+ "@ds-mo/tokens ^3.1.1",
325
+ "@ds-mo/icons ^6.2.0"
326
+ ],
327
+ "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
328
+ }
329
+ }
330
+ },
331
+ "dependencies": [
332
+ "@ds-mo/ui",
333
+ "@ds-mo/tokens",
334
+ "@ds-mo/icons"
335
+ ],
336
+ "registryDependencies": [
337
+ "icon",
338
+ "text"
339
+ ],
340
+ "files": [
341
+ {
342
+ "path": "src/wc/components/Tag/Tag.css",
343
+ "content": "@import '../../utils/tag-chip.css';\n\n/* ── Intent × Contrast — strong ─────────────────────────────────────────── */\n\n:host(.tag--intent-neutral.tag--contrast-strong) { background-color: var(--color-background-strong-neutral); color: var(--color-foreground-medium-neutral); }\n:host(.tag--intent-brand.tag--contrast-strong) { background-color: var(--color-background-strong-brand); color: var(--color-foreground-on-strong-background-primary); }\n:host(.tag--intent-ai.tag--contrast-strong) { background-color: var(--color-background-strong-ai); color: var(--color-foreground-medium-ai); }\n:host(.tag--intent-negative.tag--contrast-strong) { background-color: var(--color-background-strong-negative); color: var(--color-foreground-medium-negative); }\n:host(.tag--intent-warning.tag--contrast-strong) { background-color: var(--color-background-strong-warning); color: var(--color-foreground-medium-warning); }\n:host(.tag--intent-caution.tag--contrast-strong) { background-color: var(--color-background-strong-caution); color: var(--color-foreground-medium-caution); }\n:host(.tag--intent-positive.tag--contrast-strong) { background-color: var(--color-background-strong-positive); color: var(--color-foreground-medium-positive); }\n\n/* ── Intent × Contrast — bold ───────────────────────────────────────────── */\n\n:host(.tag--intent-neutral.tag--contrast-bold) { background-color: var(--color-background-bold-neutral); color: var(--color-foreground-on-bold-background-primary); }\n:host(.tag--intent-brand.tag--contrast-bold) { background-color: var(--color-background-bold-brand); color: var(--color-foreground-on-bold-background-primary); }\n:host(.tag--intent-ai.tag--contrast-bold) { background-color: var(--color-background-bold-ai); color: var(--color-foreground-on-bold-background-primary); }\n:host(.tag--intent-negative.tag--contrast-bold) { background-color: var(--color-background-bold-negative); color: var(--color-foreground-on-bold-background-primary); }\n:host(.tag--intent-warning.tag--contrast-bold) { background-color: var(--color-background-bold-warning); color: var(--color-foreground-on-bold-background-primary); }\n:host(.tag--intent-caution.tag--contrast-bold) { background-color: var(--color-background-bold-caution); color: var(--color-foreground-on-bold-background-primary); }\n:host(.tag--intent-positive.tag--contrast-bold) { background-color: var(--color-background-bold-positive); color: var(--color-foreground-on-bold-background-primary); }\n\n/* ── Intent × Contrast — medium ─────────────────────────────────────────── */\n\n:host(.tag--intent-neutral.tag--contrast-medium) { background-color: var(--color-background-medium-neutral); color: var(--color-foreground-strong-neutral); }\n:host(.tag--intent-brand.tag--contrast-medium) { background-color: var(--color-background-medium-brand); color: var(--color-foreground-on-medium-background-primary); }\n:host(.tag--intent-ai.tag--contrast-medium) { background-color: var(--color-background-medium-ai); color: var(--color-foreground-strong-ai); }\n:host(.tag--intent-negative.tag--contrast-medium) { background-color: var(--color-background-medium-negative); color: var(--color-foreground-strong-negative); }\n:host(.tag--intent-warning.tag--contrast-medium) { background-color: var(--color-background-medium-warning); color: var(--color-foreground-strong-warning); }\n:host(.tag--intent-caution.tag--contrast-medium) { background-color: var(--color-background-medium-caution); color: var(--color-foreground-strong-caution); }\n:host(.tag--intent-positive.tag--contrast-medium) { background-color: var(--color-background-medium-positive); color: var(--color-foreground-strong-positive); }\n\n/* ── Intent × Contrast — faint ──────────────────────────────────────────── */\n\n:host(.tag--intent-neutral.tag--contrast-faint) { background-color: var(--color-background-faint-neutral); color: var(--color-foreground-bold-neutral); }\n:host(.tag--intent-brand.tag--contrast-faint) { background-color: var(--color-background-faint-brand); color: var(--color-foreground-bold-brand); }\n:host(.tag--intent-ai.tag--contrast-faint) { background-color: var(--color-background-faint-ai); color: var(--color-foreground-bold-ai); }\n:host(.tag--intent-negative.tag--contrast-faint) { background-color: var(--color-background-faint-negative); color: var(--color-foreground-bold-negative); }\n:host(.tag--intent-warning.tag--contrast-faint) { background-color: var(--color-background-faint-warning); color: var(--color-foreground-bold-warning); }\n:host(.tag--intent-caution.tag--contrast-faint) { background-color: var(--color-background-faint-caution); color: var(--color-foreground-bold-caution); }\n:host(.tag--intent-positive.tag--contrast-faint) { background-color: var(--color-background-faint-positive); color: var(--color-foreground-bold-positive); }\n\n/* ── Interactive menu trigger ───────────────────────────────────────────── */\n\n:host(.tag--interactive) {\n padding: 0;\n gap: 0;\n}\n\n.tag__button {\n all: unset;\n position: relative;\n z-index: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n min-width: 0;\n max-width: 100%;\n height: 100%;\n padding: 0 var(--ds-control-padding-inline);\n gap: var(--ds-control-gap);\n border-radius: inherit;\n color: inherit;\n cursor: pointer;\n}\n\n.tag__button:disabled {\n cursor: not-allowed;\n}\n\n/* Hover/press wash stays above the Tag's semantic background. */\n\n/* ── Icons ──────────────────────────────────────────────────────────────── */\n\n.tag__prefix-icon,\n.tag__suffix-icon {\n position: relative;\n z-index: 2;\n display: inline-flex;\n flex-shrink: 0;\n width: var(--ds-control-icon);\n height: var(--ds-control-icon);\n}\n",
344
+ "type": "registry:ui"
345
+ },
346
+ {
347
+ "path": "src/wc/components/Tag/Tag.tsx",
348
+ "content": "import { Component, Event, EventEmitter, Prop, h, Host } from '@stencil/core';\nimport { CONTROL_TEXT_VARIANT } from '../../utils';\n\nexport type TagIntent = 'neutral' | 'brand' | 'ai' | 'negative' | 'warning' | 'caution' | 'positive';\nexport type TagContrast = 'strong' | 'bold' | 'medium' | 'faint';\nexport type TagSize = 'md' | 'sm' | 'xs';\n\n/**\n * `ds-icon` size prop matching control-density icon metrics\n * (md→20 / sm→16 / xs→12 via `--dimension-iconography-*`).\n */\nconst ICON_SIZE: Record<TagSize, 'md' | 'sm' | 'xs'> = {\n md: 'md',\n sm: 'sm',\n xs: 'xs',\n};\n\n@Component({\n tag: 'ds-tag',\n styleUrl: 'Tag.css',\n scoped: true,\n})\nexport class Tag {\n @Prop() label!: string;\n /** Canonical icon name rendered before the visible label. */\n @Prop() icon: string = '';\n @Prop() intent: TagIntent = 'neutral';\n @Prop() contrast: TagContrast = 'faint';\n @Prop() size: TagSize = 'md';\n @Prop() rounded: boolean = false;\n @Prop() maxWidth: string | number | undefined;\n /** Render a menu-trigger button with a fixed ChevronUpDown suffix. */\n @Prop() interactive: boolean = false;\n /** Controlled open state for the menu triggered by an interactive Tag. */\n @Prop() expanded: boolean = false;\n /** ID of the menu controlled by an interactive Tag. */\n @Prop() ariaControls: string | undefined;\n /** Disable an interactive Tag and apply the shared inactive treatment. */\n @Prop() isInactive: boolean = false;\n\n /** Fired when the interactive Tag button is activated. */\n @Event() dsClick!: EventEmitter<MouseEvent>;\n\n private handleClick = (event: MouseEvent) => {\n if (!this.interactive || this.isInactive) return;\n this.dsClick.emit(event);\n };\n\n render() {\n const textVariant = CONTROL_TEXT_VARIANT[this.size];\n const iconSize = ICON_SIZE[this.size];\n\n const maxWidthStyle = this.maxWidth != null\n ? { maxWidth: typeof this.maxWidth === 'number' ? `${this.maxWidth}px` : this.maxWidth }\n : undefined;\n const hostClass = {\n 'tag': true,\n [`tag--intent-${this.intent}`]: true,\n [`tag--contrast-${this.contrast}`]: true,\n [`tag--size-${this.size}`]: true,\n 'ds-control--md': this.size === 'md',\n 'ds-control--sm': this.size === 'sm',\n 'ds-control--xs': this.size === 'xs',\n 'tag--rounded': this.rounded,\n 'tag--interactive': this.interactive,\n 'ds-control-inactive': this.interactive && this.isInactive,\n };\n const content = [\n this.icon && (\n <ds-icon\n class=\"tag__prefix-icon\"\n name={this.icon}\n size={iconSize}\n color=\"inherit\"\n ></ds-icon>\n ),\n <ds-text class=\"tag__label\" as=\"span\" variant={textVariant} color=\"inherit\">\n {this.label}\n </ds-text>,\n ];\n\n if (!this.interactive) {\n return <Host class={hostClass} style={maxWidthStyle}>{content}</Host>;\n }\n\n return (\n <Host class={hostClass} style={maxWidthStyle}>\n <button\n type=\"button\"\n class=\"tag__button ds-interaction-fill ds-focus-ring-inset\"\n aria-haspopup=\"menu\"\n aria-expanded={this.expanded ? 'true' : 'false'}\n aria-controls={this.ariaControls}\n disabled={this.isInactive || undefined}\n onClick={this.handleClick}\n >\n {content}\n <ds-icon\n class=\"tag__suffix-icon\"\n name=\"ChevronUpDown\"\n size={iconSize}\n color=\"inherit\"\n ></ds-icon>\n </button>\n </Host>\n );\n }\n}\n",
349
+ "type": "registry:ui"
350
+ }
351
+ ]
352
+ }