@ds-mo/ui 14.25.0 → 14.27.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 (220) hide show
  1. package/dist/.package-ready.json +1 -1
  2. package/dist/agent-patterns.json +1 -1
  3. package/dist/agent.json +343 -16
  4. package/dist/angular/ds-score.d.ts +12 -0
  5. package/dist/angular/ds-score.js +38 -0
  6. package/dist/angular/ds-textarea.d.ts +15 -0
  7. package/dist/angular/ds-textarea.js +43 -0
  8. package/dist/angular/index.d.ts +1 -1
  9. package/dist/angular/index.js +2 -0
  10. package/dist/angular/proxies.d.ts +2 -0
  11. package/dist/angular/proxies.js +2 -0
  12. package/dist/angular/text-value-accessor.d.ts +1 -1
  13. package/dist/angular/text-value-accessor.js +2 -2
  14. package/dist/components/ds-agent-activity.js +1 -1
  15. package/dist/components/ds-agent-questionnaire.js +1 -1
  16. package/dist/components/ds-agent-response.js +1 -1
  17. package/dist/components/ds-agent-source-list.js +1 -1
  18. package/dist/components/ds-agent-tool-call.js +1 -1
  19. package/dist/components/ds-attachment-list.js +1 -1
  20. package/dist/components/ds-badge.js +1 -1
  21. package/dist/components/ds-banner.js +1 -1
  22. package/dist/components/ds-bar-action.js +1 -1
  23. package/dist/components/ds-bar-nav.js +1 -1
  24. package/dist/components/ds-bar-title.js +1 -1
  25. package/dist/components/ds-bar-workflow.js +1 -1
  26. package/dist/components/ds-breadcrumb.js +1 -1
  27. package/dist/components/ds-button-filled.js +1 -1
  28. package/dist/components/ds-button-unfilled.js +1 -1
  29. package/dist/components/ds-card-chart.js +1 -1
  30. package/dist/components/ds-card-overview.js +1 -1
  31. package/dist/components/ds-card-overview.js.map +1 -1
  32. package/dist/components/ds-card-setting.js +1 -1
  33. package/dist/components/ds-chart-legend.js +1 -1
  34. package/dist/components/ds-chart.js +1 -1
  35. package/dist/components/ds-checkbox.js +1 -1
  36. package/dist/components/ds-chip.js +1 -1
  37. package/dist/components/ds-code-block.js +1 -1
  38. package/dist/components/ds-conversation-list-item.js +1 -1
  39. package/dist/components/ds-conversation-list-section.js +1 -1
  40. package/dist/components/ds-empty-state.js +1 -1
  41. package/dist/components/ds-field.js +1 -1
  42. package/dist/components/ds-filter-menu.js +1 -1
  43. package/dist/components/ds-input.js +1 -1
  44. package/dist/components/ds-markdown.js +1 -1
  45. package/dist/components/ds-menu.js +1 -1
  46. package/dist/components/ds-message-actions.js +1 -1
  47. package/dist/components/ds-message-composer.js +1 -1
  48. package/dist/components/ds-message-scroller.js +1 -1
  49. package/dist/components/ds-message.js +1 -1
  50. package/dist/components/ds-mobile-bar-nav.js +1 -1
  51. package/dist/components/ds-mobile-header.js +1 -1
  52. package/dist/components/ds-mobile-section-switcher.js +1 -1
  53. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  54. package/dist/components/ds-modal.js +1 -1
  55. package/dist/components/ds-pagination.js +1 -1
  56. package/dist/components/ds-panel-nav.js +1 -1
  57. package/dist/components/ds-panel-sub-nav.js +1 -1
  58. package/dist/components/ds-panel-tool-header.js +1 -1
  59. package/dist/components/ds-panel-tool-search.js +1 -1
  60. package/dist/components/ds-panel-tools.js +1 -1
  61. package/dist/components/ds-paper-texture.js +1 -1
  62. package/dist/components/ds-radio.js +1 -1
  63. package/dist/components/ds-score.d.ts +11 -0
  64. package/dist/components/ds-score.js +2 -0
  65. package/dist/components/ds-score.js.map +1 -0
  66. package/dist/components/ds-select.js +1 -1
  67. package/dist/components/ds-shell-app.js +1 -1
  68. package/dist/components/ds-shell-page.js +1 -1
  69. package/dist/components/ds-shell-tools.js +1 -1
  70. package/dist/components/ds-slider.js +1 -1
  71. package/dist/components/ds-swatch-picker.js +1 -1
  72. package/dist/components/ds-tab-group.js +1 -1
  73. package/dist/components/ds-table-filter.js +1 -1
  74. package/dist/components/ds-table-group.js +1 -1
  75. package/dist/components/ds-table-group.js.map +1 -1
  76. package/dist/components/ds-table-saved-views.js +1 -1
  77. package/dist/components/ds-table-search.js +1 -1
  78. package/dist/components/ds-table-sort.js +1 -1
  79. package/dist/components/ds-table-toolbar.js +1 -1
  80. package/dist/components/ds-table.js +1 -1
  81. package/dist/components/ds-table.js.map +1 -1
  82. package/dist/components/ds-tag.js +1 -1
  83. package/dist/components/ds-text.js +1 -1
  84. package/dist/components/ds-textarea.d.ts +11 -0
  85. package/dist/components/ds-textarea.js +2 -0
  86. package/dist/components/ds-textarea.js.map +1 -0
  87. package/dist/components/ds-toast.js +1 -1
  88. package/dist/components/ds-tooltip-chart.js +1 -1
  89. package/dist/components/ds-tooltip.js +1 -1
  90. package/dist/components/ds-typing-indicator.js +1 -1
  91. package/dist/components/{p-DLaN9BPq.js → p-1604KY3B.js} +2 -2
  92. package/dist/components/{p-CYMKmJ2s.js → p-2wIPUcSL.js} +2 -2
  93. package/dist/components/{p-CEfxLsbX.js → p-537t6oRD.js} +2 -2
  94. package/dist/components/{p-kslioYhf.js → p-6_CNVbqa.js} +2 -2
  95. package/dist/components/{p-PXdvBMXB.js → p-B3Z5sQin.js} +2 -2
  96. package/dist/components/{p-CZjjDhUl.js → p-B3sRZBZk.js} +2 -2
  97. package/dist/components/{p-BZlhBJZL.js → p-B9esvbYk.js} +2 -2
  98. package/dist/components/{p-5Z4oYuiK.js → p-BEXkJ9lg.js} +2 -2
  99. package/dist/components/{p-DbLLH3hr.js → p-BIGpc__9.js} +2 -2
  100. package/dist/components/{p-DzPtYdXy.js → p-BMzn2Cmw.js} +2 -2
  101. package/dist/components/{p-BSRIF4m8.js → p-BRLDf-NK.js} +2 -2
  102. package/dist/components/{p-0X-4KGQf.js → p-BUh2GkPJ.js} +2 -2
  103. package/dist/components/{p-pUOA3iDG.js → p-BVpn_e-r.js} +2 -2
  104. package/dist/components/{p-CpeJMKJ5.js → p-BVz-r3g0.js} +2 -2
  105. package/dist/components/{p-CBymzuOO.js → p-BXDLR072.js} +2 -2
  106. package/dist/components/{p-DiS5BgLA.js → p-BZiGtBBk.js} +2 -2
  107. package/dist/components/{p-BcxidW9k.js → p-Bb9dA7sl.js} +2 -2
  108. package/dist/components/{p-BR1ayqMU.js → p-Bjt_8sLe.js} +2 -2
  109. package/dist/components/{p-BqbdABo3.js → p-BssZ4SbT.js} +2 -2
  110. package/dist/components/{p-bHnk8t5I.js → p-C68oSTb1.js} +2 -2
  111. package/dist/components/{p-C_FFU6W3.js → p-CGQnHNuW.js} +2 -2
  112. package/dist/components/{p-BqVXMEJm.js → p-CQjUOHiN.js} +2 -2
  113. package/dist/components/{p-w9fkkwL2.js → p-CaF5WCci.js} +2 -2
  114. package/dist/components/{p-Bjq1KP_Y.js → p-Cf5Ai4xx.js} +2 -2
  115. package/dist/components/{p-B0kSlcaw.js → p-Cr6pDY-e.js} +2 -2
  116. package/dist/components/{p-FxtgCvhA.js → p-Cy1oWIaV.js} +2 -2
  117. package/dist/components/{p-CTH3dSYg.js → p-DDqRCDXh.js} +2 -2
  118. package/dist/components/{p-x5u5Ueli.js → p-DIqAdmui.js} +2 -2
  119. package/dist/components/{p-Cd1JnnVK.js → p-DQQsS9Rx.js} +2 -2
  120. package/dist/components/{p-CZHIkVgj.js → p-DQsFmleZ.js} +2 -2
  121. package/dist/components/{p-CE2b5GPu.js → p-DVLghmNY.js} +2 -2
  122. package/dist/components/p-DW8ssbfI.js +2 -0
  123. package/dist/components/p-DW8ssbfI.js.map +1 -0
  124. package/dist/components/{p-BXv6RpoF.js → p-DZcIt_JJ.js} +2 -2
  125. package/dist/components/{p-CglHiyY2.js → p-DkxOEFHN.js} +2 -2
  126. package/dist/components/{p-DeK_6Uym.js → p-DlbhmXjH.js} +2 -2
  127. package/dist/components/{p-Bq8svcPs.js → p-GEi3hDJQ.js} +2 -2
  128. package/dist/components/p-LMmrjmI_.js +2 -0
  129. package/dist/components/p-LMmrjmI_.js.map +1 -0
  130. package/dist/components/{p-DI8DUozc.js → p-Q0dkeoJ6.js} +2 -2
  131. package/dist/components/{p-XaCYqck3.js → p-Q7AOgrg5.js} +2 -2
  132. package/dist/components/p-VT-5XDkB.js +2 -0
  133. package/dist/components/p-VT-5XDkB.js.map +1 -0
  134. package/dist/components/{p-B0OZx3JB.js → p-WFo1x6dN.js} +2 -2
  135. package/dist/components/{p-CEXchU5N.js → p-to8BxxgT.js} +2 -2
  136. package/dist/docs/components.json +1106 -5
  137. package/dist/mcp-data/registry/card-overview.json +8 -6
  138. package/dist/mcp-data/registry/field.json +5 -4
  139. package/dist/mcp-data/registry/panel-nav.json +2 -1
  140. package/dist/mcp-data/registry/registry.json +348 -15
  141. package/dist/mcp-data/registry/score.json +241 -0
  142. package/dist/mcp-data/registry/table-group.json +5 -5
  143. package/dist/mcp-data/registry/table.json +16 -13
  144. package/dist/mcp-data/registry/textarea.json +600 -0
  145. package/dist/mcp-data/registry/tooltip.json +5 -5
  146. package/dist/react/components.d.ts +2 -0
  147. package/dist/react/components.js +2 -0
  148. package/dist/react/ds-score.d.ts +9 -0
  149. package/dist/react/ds-score.js +12 -0
  150. package/dist/react/ds-textarea.d.ts +12 -0
  151. package/dist/react/ds-textarea.js +12 -0
  152. package/dist/styles/table.css +188 -69
  153. package/dist/types/components/CardOverview/card-overview-controller.d.ts +2 -2
  154. package/dist/types/components/CardOverview/card-overview-types.d.ts +2 -1
  155. package/dist/types/components/Score/Score.d.ts +21 -0
  156. package/dist/types/components/Score/index.d.ts +3 -0
  157. package/dist/types/components/Score/score-model.d.ts +14 -0
  158. package/dist/types/components/Score/score-types.d.ts +5 -0
  159. package/dist/types/components/Table/Table.d.ts +3 -0
  160. package/dist/types/components/Table/index.d.ts +1 -1
  161. package/dist/types/components/Table/table-cell-model.d.ts +12 -0
  162. package/dist/types/components/Table/table-model.d.ts +10 -1
  163. package/dist/types/components/Table/table-render-model.d.ts +3 -1
  164. package/dist/types/components/Table/table-types.d.ts +62 -1
  165. package/dist/types/components/Table/table-virtual-model.d.ts +6 -5
  166. package/dist/types/components/TableGroup/TableGroup.d.ts +13 -3
  167. package/dist/types/components/Textarea/Textarea.d.ts +66 -0
  168. package/dist/types/components/Textarea/index.d.ts +2 -0
  169. package/dist/types/components/Tooltip/Tooltip.d.ts +1 -1
  170. package/dist/types/components.d.ts +324 -2
  171. package/dist/vue/components.d.ts +2 -0
  172. package/dist/vue/components.js +2 -0
  173. package/dist/vue/ds-score.d.ts +3 -0
  174. package/dist/vue/ds-score.js +12 -0
  175. package/dist/vue/ds-textarea.d.ts +3 -0
  176. package/dist/vue/ds-textarea.js +36 -0
  177. package/package.json +1 -1
  178. package/dist/components/p-9X_p0bHr.js +0 -2
  179. package/dist/components/p-9X_p0bHr.js.map +0 -1
  180. package/dist/components/p-Bo6uW_8T.js +0 -2
  181. package/dist/components/p-Bo6uW_8T.js.map +0 -1
  182. /package/dist/components/{p-DLaN9BPq.js.map → p-1604KY3B.js.map} +0 -0
  183. /package/dist/components/{p-CYMKmJ2s.js.map → p-2wIPUcSL.js.map} +0 -0
  184. /package/dist/components/{p-CEfxLsbX.js.map → p-537t6oRD.js.map} +0 -0
  185. /package/dist/components/{p-kslioYhf.js.map → p-6_CNVbqa.js.map} +0 -0
  186. /package/dist/components/{p-PXdvBMXB.js.map → p-B3Z5sQin.js.map} +0 -0
  187. /package/dist/components/{p-CZjjDhUl.js.map → p-B3sRZBZk.js.map} +0 -0
  188. /package/dist/components/{p-BZlhBJZL.js.map → p-B9esvbYk.js.map} +0 -0
  189. /package/dist/components/{p-5Z4oYuiK.js.map → p-BEXkJ9lg.js.map} +0 -0
  190. /package/dist/components/{p-DbLLH3hr.js.map → p-BIGpc__9.js.map} +0 -0
  191. /package/dist/components/{p-DzPtYdXy.js.map → p-BMzn2Cmw.js.map} +0 -0
  192. /package/dist/components/{p-BSRIF4m8.js.map → p-BRLDf-NK.js.map} +0 -0
  193. /package/dist/components/{p-0X-4KGQf.js.map → p-BUh2GkPJ.js.map} +0 -0
  194. /package/dist/components/{p-pUOA3iDG.js.map → p-BVpn_e-r.js.map} +0 -0
  195. /package/dist/components/{p-CpeJMKJ5.js.map → p-BVz-r3g0.js.map} +0 -0
  196. /package/dist/components/{p-CBymzuOO.js.map → p-BXDLR072.js.map} +0 -0
  197. /package/dist/components/{p-DiS5BgLA.js.map → p-BZiGtBBk.js.map} +0 -0
  198. /package/dist/components/{p-BcxidW9k.js.map → p-Bb9dA7sl.js.map} +0 -0
  199. /package/dist/components/{p-BR1ayqMU.js.map → p-Bjt_8sLe.js.map} +0 -0
  200. /package/dist/components/{p-BqbdABo3.js.map → p-BssZ4SbT.js.map} +0 -0
  201. /package/dist/components/{p-bHnk8t5I.js.map → p-C68oSTb1.js.map} +0 -0
  202. /package/dist/components/{p-C_FFU6W3.js.map → p-CGQnHNuW.js.map} +0 -0
  203. /package/dist/components/{p-BqVXMEJm.js.map → p-CQjUOHiN.js.map} +0 -0
  204. /package/dist/components/{p-w9fkkwL2.js.map → p-CaF5WCci.js.map} +0 -0
  205. /package/dist/components/{p-Bjq1KP_Y.js.map → p-Cf5Ai4xx.js.map} +0 -0
  206. /package/dist/components/{p-B0kSlcaw.js.map → p-Cr6pDY-e.js.map} +0 -0
  207. /package/dist/components/{p-FxtgCvhA.js.map → p-Cy1oWIaV.js.map} +0 -0
  208. /package/dist/components/{p-CTH3dSYg.js.map → p-DDqRCDXh.js.map} +0 -0
  209. /package/dist/components/{p-x5u5Ueli.js.map → p-DIqAdmui.js.map} +0 -0
  210. /package/dist/components/{p-Cd1JnnVK.js.map → p-DQQsS9Rx.js.map} +0 -0
  211. /package/dist/components/{p-CZHIkVgj.js.map → p-DQsFmleZ.js.map} +0 -0
  212. /package/dist/components/{p-CE2b5GPu.js.map → p-DVLghmNY.js.map} +0 -0
  213. /package/dist/components/{p-BXv6RpoF.js.map → p-DZcIt_JJ.js.map} +0 -0
  214. /package/dist/components/{p-CglHiyY2.js.map → p-DkxOEFHN.js.map} +0 -0
  215. /package/dist/components/{p-DeK_6Uym.js.map → p-DlbhmXjH.js.map} +0 -0
  216. /package/dist/components/{p-Bq8svcPs.js.map → p-GEi3hDJQ.js.map} +0 -0
  217. /package/dist/components/{p-DI8DUozc.js.map → p-Q0dkeoJ6.js.map} +0 -0
  218. /package/dist/components/{p-XaCYqck3.js.map → p-Q7AOgrg5.js.map} +0 -0
  219. /package/dist/components/{p-B0OZx3JB.js.map → p-WFo1x6dN.js.map} +0 -0
  220. /package/dist/components/{p-CEXchU5N.js.map → p-to8BxxgT.js.map} +0 -0
@@ -0,0 +1,241 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "score",
4
+ "title": "Score",
5
+ "description": "Safety-score figure in a semantic fill at the three control densities.",
6
+ "type": "registry:ui",
7
+ "meta": {
8
+ "library": "@ds-mo/ui",
9
+ "distribution": "npm",
10
+ "storybook": "https://zainadeel.github.io/compomo/?path=/story/score",
11
+ "source": "src/wc/components/Score/Score.tsx",
12
+ "intentStatus": "complete",
13
+ "intent": {
14
+ "audience": "general",
15
+ "status": "experimental",
16
+ "summary": "Safety-score figure in a semantic fill at the three control densities.",
17
+ "useWhen": [
18
+ "A driver or fleet safety score needs its fair, good, or excellent fill and the numeric figure in one place.",
19
+ "The same score treatment appears in Card Overview, an in-card summary, or a compact row such as a table group header."
20
+ ],
21
+ "avoidWhen": [
22
+ "The figure is an ordinary metric without a safety-score color band; use Text.",
23
+ "The score is one cell in a comparable measures grid that also needs period chrome; use CardOverview, which composes Score.",
24
+ "The mark is a notification count or status label; use Badge or Tag.",
25
+ "A comparison trend should sit beside the fill; keep trend in the owner. Card Overview already renders score trend in its copy column."
26
+ ],
27
+ "alternatives": [
28
+ {
29
+ "when": "The figure is one of several comparable measures under a shared period bar.",
30
+ "component": "component:ds-card-overview",
31
+ "reason": "CardOverview owns the summary grid, period chrome, score-cell alignment, and score trend; it renders Score at lg for the fill."
32
+ },
33
+ {
34
+ "when": "The quantity is ordinary readable content without a safety-score fill.",
35
+ "component": "component:ds-text",
36
+ "reason": "Text preserves the figure as body content."
37
+ },
38
+ {
39
+ "when": "The compact mark is unread count or notification presence.",
40
+ "component": "component:ds-badge",
41
+ "reason": "Badge owns supplemental notification chrome, not a scored figure."
42
+ }
43
+ ],
44
+ "commonlyComposedWith": [
45
+ "component:ds-card-overview",
46
+ "component:ds-table",
47
+ "component:ds-text",
48
+ "component:ds-skeleton"
49
+ ],
50
+ "accessibility": [
51
+ "Set label when the atomic score graphic needs a spoken prefix such as Safety score; the visible number remains in the fill.",
52
+ "Colour never carries score meaning alone; the numeric figure stays visible inside the fill.",
53
+ "Score is not interactive and is not a tab stop.",
54
+ "While loading, the host exposes a busy state and a skeleton whose type metrics match the resolved size."
55
+ ],
56
+ "states": [
57
+ "lg is the Card Overview fill: 56 by 40, display-small emphasis. md is the 32px control treatment: 40 by 32, title-large emphasis. sm is the 24px compact fill: 28 by 24, title-small emphasis. The fill box crops the type canvas so line-height does not change the declared size.",
58
+ "A numeric value from 0–100 infers fair from 0–50, good from 51–80, and excellent from 81–100. Empty or whitespace-only values stay on the faint neutral surface. Supply level explicitly when another displayed value needs a semantic fill.",
59
+ "The fill uses the safety-score background and keeps its foreground at AA text contrast in light and dark themes.",
60
+ "isLoading replaces the figure with a skeleton at the matching type metrics. The skeleton uses the faint context when no score level is known and the bold context when a known or explicitly supplied level keeps its semantic fill."
61
+ ],
62
+ "responsiveBehavior": [
63
+ "The owner selects sm, md, or lg to match the local layout. Score does not change size at breakpoints.",
64
+ "sm is the density for table group headers. md is the in-card summary fill. lg is the Card Overview fill."
65
+ ],
66
+ "references": [
67
+ {
68
+ "label": "Storybook examples",
69
+ "path": "src/wc/components/Score/Score.stories.ts"
70
+ },
71
+ {
72
+ "label": "Safety-score level unit coverage",
73
+ "path": "tests/score-model.test.ts"
74
+ }
75
+ ]
76
+ },
77
+ "api": {
78
+ "props": {
79
+ "isLoading": {
80
+ "type": "boolean",
81
+ "resolvedType": "boolean",
82
+ "attribute": "is-loading",
83
+ "default": "false",
84
+ "required": false,
85
+ "mutable": false,
86
+ "description": "Replace the figure with a skeleton while data resolves."
87
+ },
88
+ "label": {
89
+ "type": "string",
90
+ "resolvedType": "string",
91
+ "attribute": "label",
92
+ "default": "''",
93
+ "required": false,
94
+ "mutable": false,
95
+ "description": "Accessible name prefix, for example “Safety score”."
96
+ },
97
+ "level": {
98
+ "type": "SafetyScoreLevel | undefined",
99
+ "resolvedType": "\"excellent\" | \"fair\" | \"good\" | undefined",
100
+ "attribute": "level",
101
+ "required": false,
102
+ "mutable": false,
103
+ "description": "Safety-score color level. Numeric values from 0–100 infer fair (0–50),\ngood (51–80), or excellent (81–100) when this is omitted."
104
+ },
105
+ "size": {
106
+ "type": "ScoreSize",
107
+ "resolvedType": "\"lg\" | \"md\" | \"sm\"",
108
+ "attribute": "size",
109
+ "default": "'md'",
110
+ "required": false,
111
+ "mutable": false,
112
+ "description": "Visual density matching control height: `sm` 24px, `md` 32px, `lg` 40px.\n`lg` is the Card Overview recipe."
113
+ },
114
+ "value": {
115
+ "type": "string | number",
116
+ "resolvedType": "number | string",
117
+ "attribute": "value",
118
+ "default": "''",
119
+ "required": false,
120
+ "mutable": false,
121
+ "description": "Display-ready headline figure."
122
+ }
123
+ },
124
+ "events": [],
125
+ "methods": [],
126
+ "slots": []
127
+ },
128
+ "props": {
129
+ "isLoading": {
130
+ "type": "boolean",
131
+ "resolvedType": "boolean",
132
+ "attribute": "is-loading",
133
+ "default": "false",
134
+ "required": false,
135
+ "mutable": false,
136
+ "description": "Replace the figure with a skeleton while data resolves."
137
+ },
138
+ "label": {
139
+ "type": "string",
140
+ "resolvedType": "string",
141
+ "attribute": "label",
142
+ "default": "''",
143
+ "required": false,
144
+ "mutable": false,
145
+ "description": "Accessible name prefix, for example “Safety score”."
146
+ },
147
+ "level": {
148
+ "type": "SafetyScoreLevel | undefined",
149
+ "resolvedType": "\"excellent\" | \"fair\" | \"good\" | undefined",
150
+ "attribute": "level",
151
+ "required": false,
152
+ "mutable": false,
153
+ "description": "Safety-score color level. Numeric values from 0–100 infer fair (0–50),\ngood (51–80), or excellent (81–100) when this is omitted."
154
+ },
155
+ "size": {
156
+ "type": "ScoreSize",
157
+ "resolvedType": "\"lg\" | \"md\" | \"sm\"",
158
+ "attribute": "size",
159
+ "default": "'md'",
160
+ "required": false,
161
+ "mutable": false,
162
+ "description": "Visual density matching control height: `sm` 24px, `md` 32px, `lg` 40px.\n`lg` is the Card Overview recipe."
163
+ },
164
+ "value": {
165
+ "type": "string | number",
166
+ "resolvedType": "number | string",
167
+ "attribute": "value",
168
+ "default": "''",
169
+ "required": false,
170
+ "mutable": false,
171
+ "description": "Display-ready headline figure."
172
+ }
173
+ },
174
+ "events": [],
175
+ "methods": [],
176
+ "slots": [],
177
+ "exports": {
178
+ "customElement": "ds-score",
179
+ "react": "DsScore",
180
+ "vue": "DsScore",
181
+ "angular": "DsScore"
182
+ },
183
+ "consumption": {
184
+ "install": "npm install @ds-mo/ui @ds-mo/tokens @ds-mo/icons",
185
+ "cssSetup": "import '@ds-mo/tokens';\nimport '@ds-mo/tokens/reset';\nimport '@ds-mo/tokens/globals';",
186
+ "customElements": {
187
+ "import": "import '@ds-mo/ui/dist/components/ds-score.js';",
188
+ "example": "<ds-score></ds-score>"
189
+ },
190
+ "react": {
191
+ "import": "import { DsScore } from '@ds-mo/ui/react';",
192
+ "example": "<DsScore />"
193
+ },
194
+ "vue": {
195
+ "import": "import { DsScore } from '@ds-mo/ui/vue';",
196
+ "example": "<DsScore />"
197
+ },
198
+ "angular": {
199
+ "import": "import { DsScore } from '@ds-mo/ui/angular/ds-score';",
200
+ "example": "<ds-score></ds-score>"
201
+ },
202
+ "peerDependencies": {
203
+ "required": [
204
+ "@ds-mo/tokens ^6.5.1",
205
+ "@ds-mo/icons ^7.0.0"
206
+ ],
207
+ "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
208
+ }
209
+ }
210
+ },
211
+ "dependencies": [
212
+ "@ds-mo/ui",
213
+ "@ds-mo/tokens"
214
+ ],
215
+ "registryDependencies": [
216
+ "skeleton",
217
+ "text"
218
+ ],
219
+ "files": [
220
+ {
221
+ "path": "src/wc/components/Score/Score.css",
222
+ "content": "@import '../../utils/control-parts.css';\n@import '../../utils/forced-colors.css';\n\n:host {\n display: inline-flex;\n align-items: center;\n box-sizing: border-box;\n min-width: 0;\n --_score-badge-inline-size: var(--dimension-size-500);\n --_score-badge-block-size: var(--dimension-size-400);\n /* Matches the text skeleton’s 75% bar so the fill inset is even on all sides. */\n --_score-skeleton-inset: calc(var(--_score-badge-block-size) / 8);\n}\n\n:host(.score--sm) {\n --_score-badge-inline-size: calc(var(--dimension-size-400) - var(--dimension-space-050));\n --_score-badge-block-size: var(--dimension-size-300);\n}\n\n:host(.score--md) {\n --_score-badge-inline-size: var(--dimension-size-500);\n --_score-badge-block-size: var(--dimension-size-400);\n}\n\n:host(.score--lg) {\n --_score-badge-inline-size: calc(var(--dimension-size-600) + var(--dimension-space-100));\n --_score-badge-block-size: var(--dimension-size-500);\n}\n\n.score__badge {\n display: flex;\n flex: 0 0 auto;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n inline-size: var(--_score-badge-inline-size);\n block-size: var(--_score-badge-block-size);\n overflow: hidden;\n border-radius: var(--dimension-radius-025);\n background: var(--color-background-faint-neutral);\n color: var(--color-foreground-bold-neutral);\n -webkit-user-select: none;\n user-select: none;\n}\n\n.score__badge--fair {\n background: var(--color-safety-score-background-fair);\n color: var(--color-safety-score-foreground-on-fair);\n}\n\n.score__badge--good {\n background: var(--color-safety-score-background-good);\n color: var(--color-safety-score-foreground-on-good);\n}\n\n.score__badge--excellent {\n background: var(--color-safety-score-background-excellent);\n color: var(--color-safety-score-foreground-on-excellent);\n}\n\n.score__value,\n.score__badge ds-skeleton {\n flex: 0 0 auto;\n}\n\n.score__badge--loading {\n padding-inline: var(--_score-skeleton-inset);\n}\n\n.score__badge--loading .score__value {\n flex: 1 1 auto;\n inline-size: 100%;\n block-size: 100%;\n max-inline-size: 100%;\n max-block-size: 100%;\n min-inline-size: 0;\n}\n\n@media (forced-colors: active) {\n .score__badge {\n border: var(--dimension-stroke-width-012) solid var(--ds-forced-color-content);\n background: var(--ds-forced-color-surface);\n color: var(--ds-forced-color-content);\n }\n}\n",
223
+ "type": "registry:ui"
224
+ },
225
+ {
226
+ "path": "src/wc/components/Score/Score.tsx",
227
+ "content": "import { Component, Prop, h, Host } from '@stencil/core';\nimport { SCORE_VALUE_VARIANT, resolveScoreLevel } from './score-model';\nimport type { SafetyScoreLevel, ScoreSize } from './score-types';\n\n@Component({\n tag: 'ds-score',\n styleUrl: 'Score.css',\n scoped: true,\n})\nexport class Score {\n /** Display-ready headline figure. */\n @Prop() value: string | number = '';\n /**\n * Visual density matching control height: `sm` 24px, `md` 32px, `lg` 40px.\n * `lg` is the Card Overview recipe.\n */\n @Prop() size: ScoreSize = 'md';\n /**\n * Safety-score color level. Numeric values from 0–100 infer fair (0–50),\n * good (51–80), or excellent (81–100) when this is omitted.\n */\n @Prop() level: SafetyScoreLevel | undefined;\n /** Replace the figure with a skeleton while data resolves. */\n @Prop() isLoading: boolean = false;\n /** Accessible name prefix, for example “Safety score”. */\n @Prop() label: string = '';\n\n render() {\n const size = this.size;\n const level = resolveScoreLevel(this.value, this.level);\n const valueVariant = SCORE_VALUE_VARIANT[size];\n const accessibleName = [this.label.trim(), this.isLoading ? '' : this.value]\n .filter(Boolean)\n .join(' ');\n\n return (\n <Host\n class={{\n score: true,\n [`score--${size}`]: true,\n 'score--loading': this.isLoading,\n }}\n role={accessibleName ? 'img' : undefined}\n aria-busy={this.isLoading ? 'true' : undefined}\n aria-label={accessibleName || undefined}\n >\n <span\n class={{\n score__badge: true,\n [`score__badge--${level}`]: Boolean(level),\n 'score__badge--loading': this.isLoading,\n }}\n >\n {this.isLoading ? (\n <ds-skeleton\n class=\"score__value\"\n variant=\"text\"\n textVariant={valueVariant}\n background={level ? 'bold' : 'faint'}\n />\n ) : (\n <ds-text\n as=\"span\"\n class=\"score__value ds-control-label-box\"\n variant={valueVariant}\n emphasis={true}\n color=\"inherit\"\n fontFeature=\"tabular-nums\"\n >\n {this.value}\n </ds-text>\n )}\n </span>\n </Host>\n );\n }\n}\n",
228
+ "type": "registry:ui"
229
+ },
230
+ {
231
+ "path": "src/wc/components/Score/score-model.ts",
232
+ "content": "import { SAFETY_SCORE_LEVELS, type SafetyScoreLevel, type ScoreSize } from './score-types';\n\nexport function isSafetyScoreLevel(value: unknown): value is SafetyScoreLevel {\n return typeof value === 'string' && (SAFETY_SCORE_LEVELS as readonly string[]).includes(value);\n}\n\n/**\n * Infer a safety-score color level from a 0–100 figure. Non-numeric or out-of-range\n * values return undefined so the owner can supply `level` explicitly.\n */\nexport function resolveSafetyScoreLevel(value: string | number): SafetyScoreLevel | undefined {\n if (typeof value === 'string' && value.trim() === '') return undefined;\n const numericValue = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(numericValue) || numericValue < 0 || numericValue > 100) return undefined;\n if (numericValue <= 50) return 'fair';\n if (numericValue <= 80) return 'good';\n return 'excellent';\n}\n\nexport function resolveScoreLevel(\n value: string | number,\n level?: SafetyScoreLevel\n): SafetyScoreLevel | undefined {\n return isSafetyScoreLevel(level) ? level : resolveSafetyScoreLevel(value);\n}\n\nexport const SCORE_VALUE_VARIANT = {\n sm: 'text-title-small',\n md: 'text-title-large',\n lg: 'text-display-small',\n} as const satisfies Record<\n ScoreSize,\n 'text-title-small' | 'text-title-large' | 'text-display-small'\n>;\n",
233
+ "type": "registry:ui"
234
+ },
235
+ {
236
+ "path": "src/wc/components/Score/score-types.ts",
237
+ "content": "export type SafetyScoreLevel = 'fair' | 'good' | 'excellent';\nexport type ScoreSize = 'sm' | 'md' | 'lg';\n\nexport const SCORE_SIZES = ['sm', 'md', 'lg'] as const;\nexport const SAFETY_SCORE_LEVELS = ['fair', 'good', 'excellent'] as const;\n",
238
+ "type": "registry:ui"
239
+ }
240
+ ]
241
+ }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
3
  "name": "table-group",
4
4
  "title": "TableGroup",
5
- "description": "Two-panel toolbar control for choosing one table grouping data point and the ascending or descending order of its group sections.",
5
+ "description": "Two-panel toolbar control for choosing one table grouping data point and the order of its group sections.",
6
6
  "type": "registry:ui",
7
7
  "meta": {
8
8
  "library": "@ds-mo/ui",
@@ -13,7 +13,7 @@
13
13
  "intent": {
14
14
  "audience": "general",
15
15
  "status": "stable",
16
- "summary": "Two-panel toolbar control for choosing one table grouping data point and the ascending or descending order of its group sections.",
16
+ "summary": "Two-panel toolbar control for choosing one table grouping data point and the order of its group sections.",
17
17
  "useWhen": [
18
18
  "A table toolbar needs explicit control of one grouping level and its section order.",
19
19
  "Grouping data-point labels differ from compact table-header labels."
@@ -44,8 +44,8 @@
44
44
  "The right pane remains present before a group is active as one complete empty state without the Order heading, and explains that a group must be selected."
45
45
  ],
46
46
  "states": [
47
- "options and grouping are controlled. dsGroupChange reports a complete TableGroupingState and dsClear requests null grouping without mutating either input.",
48
- "Selecting a new data point starts its order at ascending. Re-selecting the active data point preserves its direction.",
47
+ "options and grouping are controlled. A data option may supply product-owned orderOptions; otherwise Order defaults to Ascending and Descending. dsGroupChange reports a complete TableGroupingState and dsClear requests null grouping without mutating either input.",
48
+ "Selecting a new data point starts at its first custom order option, or ascending when it has no custom order options. Re-selecting the active data point preserves its complete order state.",
49
49
  "Selecting a data point or direction applies immediately and leaves the dialog open so the other pane can be changed."
50
50
  ],
51
51
  "responsiveBehavior": [
@@ -294,7 +294,7 @@
294
294
  },
295
295
  {
296
296
  "path": "src/wc/components/TableGroup/TableGroup.tsx",
297
- "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Listen,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport {\n resolveChoicePopupAlignOffset,\n resolveCssLengthPx,\n resolveMotionTimeMs,\n TOKEN_CSS_LENGTHS,\n TOKEN_DEFAULTS,\n} from '../../utils';\nimport { AnchoredPositionController } from '../../utils/anchored-position-controller';\nimport { AnchoredOverlayInteractionController } from '../../utils/anchored-overlay-interaction-controller';\nimport { resolveAnchoredOverlayBoundaryRect } from '../../utils/anchored-overlay-boundary';\nimport { ChoiceListSection, ChoiceOptionRow } from '../../utils/choice-list-parts';\nimport type { TableGroupingState, TableSortDirection } from '../Table/table-types';\n\nexport interface TableGroupOption {\n label: string;\n value: string;\n description?: string;\n isInactive?: boolean;\n}\n\nlet tableGroupSeq = 0;\n\n@Component({\n tag: 'ds-table-group',\n styleUrl: 'TableGroup.css',\n scoped: true,\n})\nexport class TableGroup {\n @Element() private el!: HTMLElement;\n\n /** Product-owned data points that may group the table. */\n @Prop() options: TableGroupOption[] = [];\n /** Controlled grouping field and the order of its group sections. */\n @Prop() grouping: TableGroupingState | null = null;\n /** Accessible name for the trigger and non-modal dialog. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n /** Controlled popup visibility. */\n @Prop({ mutable: true }) open = false;\n /** Empty message kept in the order pane until a grouping data point is selected. */\n @Prop() emptyMessage = 'Select a group to choose its order.';\n /** Clear action label. */\n @Prop() clearLabel = 'Clear';\n\n /** Requests replacement of the complete controlled grouping state. */\n @Event() dsGroupChange!: EventEmitter<TableGroupingState>;\n /** Requests removal of the controlled grouping state. */\n @Event() dsClear!: EventEmitter<void>;\n /** Reports controlled popup visibility changes. */\n @Event() dsOpenChange!: EventEmitter<boolean>;\n\n @State() private shouldRender = false;\n @State() private closing = false;\n @State() private positionReady = false;\n @State() private pos = { x: 0, y: 0 };\n @State() private focusRingVisible = false;\n\n private readonly componentId = `ds-table-group-${++tableGroupSeq}`;\n private readonly triggerId = `${this.componentId}-trigger`;\n private readonly popupId = `${this.componentId}-popup`;\n private triggerElement: HTMLElement | null = null;\n private pendingInitialFocusVisible = false;\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n\n private readonly position = new AnchoredPositionController({\n getAnchor: () => this.triggerElement,\n getPopup: () => this.el.querySelector<HTMLElement>('.table-group-popup'),\n getOwnerDocument: () => this.el.ownerDocument,\n measure: (anchor, popup) => {\n if (!this.open) return null;\n const sectionInsetPx = resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n return {\n anchorRect: anchor.getBoundingClientRect(),\n popupWidth: popup.offsetWidth || this.popupFallbackWidth,\n popupHeight: popup.offsetHeight || this.popupFallbackHeight,\n side: 'bottom',\n align: 'end',\n sideOffsetPx: resolveCssLengthPx(TOKEN_CSS_LENGTHS.space050, TOKEN_DEFAULTS.space050),\n alignOffsetPx: resolveChoicePopupAlignOffset({\n align: 'end',\n alignOffsetPx: 0,\n sectionInsetPx,\n }),\n viewportPadPx: resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050),\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n collisionRect: resolveAnchoredOverlayBoundaryRect(anchor),\n };\n },\n apply: ({ x, y }) => {\n this.pos = { x, y };\n },\n onReady: () => {\n this.positionReady = true;\n },\n liveUpdate: 'double-frame',\n observeResize: true,\n topLayer: true,\n });\n\n private readonly interaction = new AnchoredOverlayInteractionController({\n getAnchor: () => this.triggerElement,\n getPopup: () => this.el.querySelector<HTMLElement>('.table-group-popup'),\n getOwnerDocument: () => this.el.ownerDocument,\n onOutsideActivation: () => this.close(),\n });\n\n componentDidLoad() {\n if (this.open) this.onOpenChange(true);\n }\n\n disconnectedCallback() {\n this.position.unobserve();\n this.interaction.disconnect();\n if (this.closeTimer) clearTimeout(this.closeTimer);\n }\n\n @Watch('open')\n onOpenChange(isOpen: boolean) {\n if (isOpen) {\n this.teardown();\n this.shouldRender = true;\n this.closing = false;\n this.positionReady = false;\n this.focusRingVisible = this.pendingInitialFocusVisible;\n this.interaction.connect();\n this.position.observe();\n this.position.schedule(() => this.focusInitialDataPoint());\n return;\n }\n if (!this.shouldRender) return;\n this.position.cancel();\n this.closing = true;\n this.interaction.disconnect();\n this.position.unobserve();\n const duration = resolveMotionTimeMs(\n TOKEN_DEFAULTS.motionShort2,\n TOKEN_DEFAULTS.animationDurationShort3\n );\n if (duration <= 0) this.finishClose();\n else this.closeTimer = setTimeout(() => this.finishClose(), duration);\n }\n\n @Listen('keydown')\n handleKeyDown(event: KeyboardEvent) {\n if (!this.shouldRender || this.closing) return;\n if (event.key === 'Escape') {\n event.preventDefault();\n this.close();\n return;\n }\n if (event.key !== 'Tab' || !this.interaction.tabLeavesPopup(event)) return;\n event.preventDefault();\n this.close(false);\n this.interaction.moveFocusAfterTab(event.shiftKey);\n }\n\n private get popupFallbackWidth(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuWidthLg, TOKEN_DEFAULTS.menuWidthLg);\n }\n\n private get popupFallbackHeight(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuFallbackHeight, TOKEN_DEFAULTS.menuFallbackHeight);\n }\n\n private teardown() {\n this.position.unobserve();\n this.interaction.disconnect();\n if (this.closeTimer) {\n clearTimeout(this.closeTimer);\n this.closeTimer = null;\n }\n }\n\n private toggle(fromKeyboard = false) {\n if (this.open) {\n this.close();\n return;\n }\n this.pendingInitialFocusVisible = fromKeyboard;\n this.open = true;\n this.dsOpenChange.emit(true);\n this.onOpenChange(true);\n }\n\n private close(restoreFocus = true) {\n if (restoreFocus) this.interaction.focusAnchor();\n this.open = false;\n this.dsOpenChange.emit(false);\n this.onOpenChange(false);\n }\n\n private finishClose() {\n const popup = this.el.querySelector<HTMLElement>('.table-group-popup');\n if (popup?.matches(':popover-open')) popup.hidePopover();\n this.shouldRender = false;\n this.closing = false;\n this.closeTimer = null;\n }\n\n private focusInitialDataPoint() {\n requestAnimationFrame(() => {\n const selected = this.grouping\n ? this.el.querySelector<HTMLElement>(\n `[data-group-value=\"${CSS.escape(this.grouping.columnId)}\"] [role=\"option\"]`\n )\n : null;\n const initial =\n selected ??\n this.el.querySelector<HTMLElement>(\n '[data-group-value] [role=\"option\"]:not([aria-disabled=\"true\"])'\n );\n initial?.focus();\n this.pendingInitialFocusVisible = false;\n });\n }\n\n private selectData(option: TableGroupOption) {\n if (option.isInactive) return;\n this.dsGroupChange.emit({\n columnId: option.value,\n direction: this.grouping?.columnId === option.value ? this.grouping.direction : 'asc',\n });\n }\n\n private selectDirection(direction: TableSortDirection) {\n if (!this.grouping || this.grouping.direction === direction) return;\n this.dsGroupChange.emit({ ...this.grouping, direction });\n }\n\n private moveFocus(\n event: KeyboardEvent,\n selector: string,\n currentIndex: number,\n oppositeSelector?: string\n ) {\n if (oppositeSelector && (event.key === 'ArrowLeft' || event.key === 'ArrowRight')) {\n event.preventDefault();\n this.focusRingVisible = true;\n this.el.querySelector<HTMLElement>(oppositeSelector)?.focus();\n return;\n }\n if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;\n event.preventDefault();\n const items = Array.from(this.el.querySelectorAll<HTMLElement>(selector)).filter(\n item => item.getAttribute('aria-disabled') !== 'true'\n );\n if (!items.length) return;\n let next: number;\n if (event.key === 'Home') next = 0;\n else if (event.key === 'End') next = items.length - 1;\n else next = (currentIndex + (event.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;\n this.focusRingVisible = true;\n items[next]?.focus();\n }\n\n private renderDataOption(option: TableGroupOption, index: number) {\n const selected = option.value === this.grouping?.columnId;\n return (\n <ChoiceOptionRow\n id={`${this.componentId}-data-${index}`}\n option={{ ...option }}\n selected={selected}\n active={selected}\n focusRingVisible={this.focusRingVisible}\n usesSubtext={Boolean(option.description)}\n tabIndex={selected || (!this.grouping && index === 0) ? 0 : -1}\n onHover={() => {\n this.focusRingVisible = false;\n }}\n onSelect={() => this.selectData(option)}\n onKeyDown={(event: KeyboardEvent) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n this.selectData(option);\n return;\n }\n this.moveFocus(\n event,\n '[data-group-value] [role=\"option\"]',\n this.options\n .filter(candidate => !candidate.isInactive)\n .findIndex(candidate => candidate.value === option.value),\n this.grouping\n ? `[data-group-direction=\"${this.grouping.direction}\"] [role=\"option\"]`\n : undefined\n );\n }}\n />\n );\n }\n\n private renderDirection(direction: TableSortDirection, label: string, index: number) {\n const selected = direction === this.grouping?.direction;\n return (\n <ChoiceOptionRow\n id={`${this.componentId}-direction-${direction}`}\n option={{ label, value: direction }}\n selected={selected}\n active={selected}\n focusRingVisible={this.focusRingVisible}\n usesSubtext={false}\n tabIndex={selected ? 0 : -1}\n onHover={() => {\n this.focusRingVisible = false;\n }}\n onSelect={() => this.selectDirection(direction)}\n onKeyDown={(event: KeyboardEvent) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n this.selectDirection(direction);\n return;\n }\n this.moveFocus(\n event,\n '[data-group-direction] [role=\"option\"]',\n index,\n `[data-group-value=\"${CSS.escape(this.grouping?.columnId ?? '')}\"] [role=\"option\"]`\n );\n }}\n />\n );\n }\n\n render() {\n const name = this.ariaLabel?.trim() || 'Group table';\n const popupStyle: Record<string, string> = {\n position: 'fixed',\n left: '0',\n top: '0',\n transform: `translate(${Math.round(this.pos.x)}px, ${Math.round(this.pos.y)}px)`,\n zIndex: '9998',\n visibility: this.positionReady ? 'visible' : 'hidden',\n };\n\n return (\n <Host hidden={this.options.length === 0 ? true : undefined}>\n {this.options.length ? (\n <ds-button-unfilled\n ref={element => {\n this.triggerElement = (element as HTMLElement) ?? null;\n }}\n id={this.triggerId}\n variant=\"icon-label\"\n size=\"md\"\n icon=\"SectionList\"\n label=\"Group\"\n labelEmphasis={false}\n pressScale={false}\n aria-label={name}\n hasMenu={true}\n collapseLabel={true}\n expanded={this.open}\n controls={this.popupId}\n onDsClick={(event: CustomEvent<MouseEvent>) => this.toggle(event.detail.detail === 0)}\n />\n ) : null}\n\n {this.shouldRender ? (\n <div\n id={this.popupId}\n popover=\"manual\"\n class={{\n 'table-group-popup': true,\n 'ds-choice-popup': true,\n 'ds-choice-popup--closing': this.closing,\n }}\n style={popupStyle}\n role=\"dialog\"\n aria-label={name}\n >\n <div class=\"table-group__body\">\n <div class=\"table-group__data-pane\">\n <ChoiceListSection\n heading=\"Data\"\n ariaLabel=\"Group data\"\n className=\"table-group__list\"\n >\n {this.options.map((option, index) => (\n <div data-group-value={option.value}>\n {this.renderDataOption(option, index)}\n </div>\n ))}\n </ChoiceListSection>\n <div\n class=\"table-group__footer ds-choice-footer\"\n aria-hidden={!this.grouping ? 'true' : undefined}\n >\n <div class=\"ds-choice-footer__content ds-control--md\">\n {this.grouping ? (\n <button\n class=\"ds-choice-footer__clear ds-text-action ds-focus-ring\"\n type=\"button\"\n onClick={() => this.dsClear.emit()}\n >\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"inherit\">\n {this.clearLabel}\n </ds-text>\n </button>\n ) : null}\n </div>\n </div>\n </div>\n\n <div class=\"table-group__order-pane\">\n {this.grouping ? (\n <ChoiceListSection\n heading=\"Order\"\n ariaLabel=\"Group order\"\n className=\"table-group__list\"\n >\n <div data-group-direction=\"asc\">\n {this.renderDirection('asc', 'Ascending', 0)}\n </div>\n <div data-group-direction=\"desc\">\n {this.renderDirection('desc', 'Descending', 1)}\n </div>\n </ChoiceListSection>\n ) : (\n <div class=\"table-group__empty ds-choice-empty ds-empty-region\">\n <ds-empty-state body={this.emptyMessage} />\n </div>\n )}\n </div>\n </div>\n </div>\n ) : null}\n </Host>\n );\n }\n}\n",
297
+ "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Listen,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport {\n resolveChoicePopupAlignOffset,\n resolveCssLengthPx,\n resolveMotionTimeMs,\n TOKEN_CSS_LENGTHS,\n TOKEN_DEFAULTS,\n} from '../../utils';\nimport { AnchoredPositionController } from '../../utils/anchored-position-controller';\nimport { AnchoredOverlayInteractionController } from '../../utils/anchored-overlay-interaction-controller';\nimport { resolveAnchoredOverlayBoundaryRect } from '../../utils/anchored-overlay-boundary';\nimport { ChoiceListSection, ChoiceOptionRow } from '../../utils/choice-list-parts';\nimport type { TableGroupingState, TableSortDirection } from '../Table/table-types';\n\nexport interface TableGroupOrderOption {\n label: string;\n direction: TableSortDirection;\n /** Optional application-owned data point used to order the group sections. */\n orderBy?: string;\n}\n\nexport interface TableGroupOption {\n label: string;\n value: string;\n description?: string;\n isInactive?: boolean;\n /** Product-owned order choices for this grouping data point. */\n orderOptions?: TableGroupOrderOption[];\n}\n\nconst DEFAULT_ORDER_OPTIONS: TableGroupOrderOption[] = [\n { label: 'Ascending', direction: 'asc' },\n { label: 'Descending', direction: 'desc' },\n];\n\nlet tableGroupSeq = 0;\n\n@Component({\n tag: 'ds-table-group',\n styleUrl: 'TableGroup.css',\n scoped: true,\n})\nexport class TableGroup {\n @Element() private el!: HTMLElement;\n\n /** Product-owned data points that may group the table. */\n @Prop() options: TableGroupOption[] = [];\n /** Controlled grouping field and the order of its group sections. */\n @Prop() grouping: TableGroupingState | null = null;\n /** Accessible name for the trigger and non-modal dialog. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n /** Controlled popup visibility. */\n @Prop({ mutable: true }) open = false;\n /** Empty message kept in the order pane until a grouping data point is selected. */\n @Prop() emptyMessage = 'Select a group to choose its order.';\n /** Clear action label. */\n @Prop() clearLabel = 'Clear';\n\n /** Requests replacement of the complete controlled grouping state. */\n @Event() dsGroupChange!: EventEmitter<TableGroupingState>;\n /** Requests removal of the controlled grouping state. */\n @Event() dsClear!: EventEmitter<void>;\n /** Reports controlled popup visibility changes. */\n @Event() dsOpenChange!: EventEmitter<boolean>;\n\n @State() private shouldRender = false;\n @State() private closing = false;\n @State() private positionReady = false;\n @State() private pos = { x: 0, y: 0 };\n @State() private focusRingVisible = false;\n\n private readonly componentId = `ds-table-group-${++tableGroupSeq}`;\n private readonly triggerId = `${this.componentId}-trigger`;\n private readonly popupId = `${this.componentId}-popup`;\n private triggerElement: HTMLElement | null = null;\n private pendingInitialFocusVisible = false;\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n\n private readonly position = new AnchoredPositionController({\n getAnchor: () => this.triggerElement,\n getPopup: () => this.el.querySelector<HTMLElement>('.table-group-popup'),\n getOwnerDocument: () => this.el.ownerDocument,\n measure: (anchor, popup) => {\n if (!this.open) return null;\n const sectionInsetPx = resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n return {\n anchorRect: anchor.getBoundingClientRect(),\n popupWidth: popup.offsetWidth || this.popupFallbackWidth,\n popupHeight: popup.offsetHeight || this.popupFallbackHeight,\n side: 'bottom',\n align: 'end',\n sideOffsetPx: resolveCssLengthPx(TOKEN_CSS_LENGTHS.space050, TOKEN_DEFAULTS.space050),\n alignOffsetPx: resolveChoicePopupAlignOffset({\n align: 'end',\n alignOffsetPx: 0,\n sectionInsetPx,\n }),\n viewportPadPx: resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050),\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n collisionRect: resolveAnchoredOverlayBoundaryRect(anchor),\n };\n },\n apply: ({ x, y }) => {\n this.pos = { x, y };\n },\n onReady: () => {\n this.positionReady = true;\n },\n liveUpdate: 'double-frame',\n observeResize: true,\n topLayer: true,\n });\n\n private readonly interaction = new AnchoredOverlayInteractionController({\n getAnchor: () => this.triggerElement,\n getPopup: () => this.el.querySelector<HTMLElement>('.table-group-popup'),\n getOwnerDocument: () => this.el.ownerDocument,\n onOutsideActivation: () => this.close(),\n });\n\n componentDidLoad() {\n if (this.open) this.onOpenChange(true);\n }\n\n disconnectedCallback() {\n this.position.unobserve();\n this.interaction.disconnect();\n if (this.closeTimer) clearTimeout(this.closeTimer);\n }\n\n @Watch('open')\n onOpenChange(isOpen: boolean) {\n if (isOpen) {\n this.teardown();\n this.shouldRender = true;\n this.closing = false;\n this.positionReady = false;\n this.focusRingVisible = this.pendingInitialFocusVisible;\n this.interaction.connect();\n this.position.observe();\n this.position.schedule(() => this.focusInitialDataPoint());\n return;\n }\n if (!this.shouldRender) return;\n this.position.cancel();\n this.closing = true;\n this.interaction.disconnect();\n this.position.unobserve();\n const duration = resolveMotionTimeMs(\n TOKEN_DEFAULTS.motionShort2,\n TOKEN_DEFAULTS.animationDurationShort3\n );\n if (duration <= 0) this.finishClose();\n else this.closeTimer = setTimeout(() => this.finishClose(), duration);\n }\n\n @Listen('keydown')\n handleKeyDown(event: KeyboardEvent) {\n if (!this.shouldRender || this.closing) return;\n if (event.key === 'Escape') {\n event.preventDefault();\n this.close();\n return;\n }\n if (event.key !== 'Tab' || !this.interaction.tabLeavesPopup(event)) return;\n event.preventDefault();\n this.close(false);\n this.interaction.moveFocusAfterTab(event.shiftKey);\n }\n\n private get popupFallbackWidth(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuWidthLg, TOKEN_DEFAULTS.menuWidthLg);\n }\n\n private get popupFallbackHeight(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuFallbackHeight, TOKEN_DEFAULTS.menuFallbackHeight);\n }\n\n private teardown() {\n this.position.unobserve();\n this.interaction.disconnect();\n if (this.closeTimer) {\n clearTimeout(this.closeTimer);\n this.closeTimer = null;\n }\n }\n\n private toggle(fromKeyboard = false) {\n if (this.open) {\n this.close();\n return;\n }\n this.pendingInitialFocusVisible = fromKeyboard;\n this.open = true;\n this.dsOpenChange.emit(true);\n this.onOpenChange(true);\n }\n\n private close(restoreFocus = true) {\n if (restoreFocus) this.interaction.focusAnchor();\n this.open = false;\n this.dsOpenChange.emit(false);\n this.onOpenChange(false);\n }\n\n private finishClose() {\n const popup = this.el.querySelector<HTMLElement>('.table-group-popup');\n if (popup?.matches(':popover-open')) popup.hidePopover();\n this.shouldRender = false;\n this.closing = false;\n this.closeTimer = null;\n }\n\n private focusInitialDataPoint() {\n requestAnimationFrame(() => {\n const selected = this.grouping\n ? this.el.querySelector<HTMLElement>(\n `[data-group-value=\"${CSS.escape(this.grouping.columnId)}\"] [role=\"option\"]`\n )\n : null;\n const initial =\n selected ??\n this.el.querySelector<HTMLElement>(\n '[data-group-value] [role=\"option\"]:not([aria-disabled=\"true\"])'\n );\n initial?.focus();\n this.pendingInitialFocusVisible = false;\n });\n }\n\n private selectData(option: TableGroupOption) {\n if (option.isInactive) return;\n if (this.grouping?.columnId === option.value) {\n this.dsGroupChange.emit({ ...this.grouping });\n return;\n }\n const initialOrder = option.orderOptions?.[0];\n this.dsGroupChange.emit({\n columnId: option.value,\n direction: initialOrder?.direction ?? 'asc',\n ...(initialOrder?.orderBy ? { orderBy: initialOrder.orderBy } : {}),\n });\n }\n\n private get activeOrderOptions(): TableGroupOrderOption[] {\n const configured = this.options.find(\n option => option.value === this.grouping?.columnId\n )?.orderOptions;\n return configured?.length ? configured : DEFAULT_ORDER_OPTIONS;\n }\n\n private get selectedOrderIndex(): number {\n if (!this.grouping) return -1;\n const exact = this.activeOrderOptions.findIndex(\n option =>\n option.direction === this.grouping?.direction && option.orderBy === this.grouping?.orderBy\n );\n if (exact >= 0) return exact;\n if (this.grouping.orderBy === undefined) {\n return this.activeOrderOptions.findIndex(\n option => option.direction === this.grouping?.direction\n );\n }\n return -1;\n }\n\n private selectOrder(option: TableGroupOrderOption, index: number) {\n if (!this.grouping || this.selectedOrderIndex === index) return;\n const grouping: TableGroupingState = {\n columnId: this.grouping.columnId,\n direction: option.direction,\n };\n if (option.orderBy) grouping.orderBy = option.orderBy;\n this.dsGroupChange.emit(grouping);\n }\n\n private moveFocus(\n event: KeyboardEvent,\n selector: string,\n currentIndex: number,\n oppositeSelector?: string\n ) {\n if (oppositeSelector && (event.key === 'ArrowLeft' || event.key === 'ArrowRight')) {\n event.preventDefault();\n this.focusRingVisible = true;\n this.el.querySelector<HTMLElement>(oppositeSelector)?.focus();\n return;\n }\n if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;\n event.preventDefault();\n const items = Array.from(this.el.querySelectorAll<HTMLElement>(selector)).filter(\n item => item.getAttribute('aria-disabled') !== 'true'\n );\n if (!items.length) return;\n let next: number;\n if (event.key === 'Home') next = 0;\n else if (event.key === 'End') next = items.length - 1;\n else next = (currentIndex + (event.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;\n this.focusRingVisible = true;\n items[next]?.focus();\n }\n\n private renderDataOption(option: TableGroupOption, index: number) {\n const selected = option.value === this.grouping?.columnId;\n return (\n <ChoiceOptionRow\n id={`${this.componentId}-data-${index}`}\n option={{ ...option }}\n selected={selected}\n active={selected}\n focusRingVisible={this.focusRingVisible}\n usesSubtext={Boolean(option.description)}\n tabIndex={selected || (!this.grouping && index === 0) ? 0 : -1}\n onHover={() => {\n this.focusRingVisible = false;\n }}\n onSelect={() => this.selectData(option)}\n onKeyDown={(event: KeyboardEvent) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n this.selectData(option);\n return;\n }\n this.moveFocus(\n event,\n '[data-group-value] [role=\"option\"]',\n this.options\n .filter(candidate => !candidate.isInactive)\n .findIndex(candidate => candidate.value === option.value),\n this.grouping\n ? `[data-group-order-index=\"${this.selectedOrderIndex}\"] [role=\"option\"]`\n : undefined\n );\n }}\n />\n );\n }\n\n private renderOrder(option: TableGroupOrderOption, index: number) {\n const selected = index === this.selectedOrderIndex;\n return (\n <ChoiceOptionRow\n id={`${this.componentId}-order-${index}`}\n option={{ label: option.label, value: String(index) }}\n selected={selected}\n active={selected}\n focusRingVisible={this.focusRingVisible}\n usesSubtext={false}\n tabIndex={selected ? 0 : -1}\n onHover={() => {\n this.focusRingVisible = false;\n }}\n onSelect={() => this.selectOrder(option, index)}\n onKeyDown={(event: KeyboardEvent) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n this.selectOrder(option, index);\n return;\n }\n this.moveFocus(\n event,\n '[data-group-order-index] [role=\"option\"]',\n index,\n `[data-group-value=\"${CSS.escape(this.grouping?.columnId ?? '')}\"] [role=\"option\"]`\n );\n }}\n />\n );\n }\n\n render() {\n const name = this.ariaLabel?.trim() || 'Group table';\n const popupStyle: Record<string, string> = {\n position: 'fixed',\n left: '0',\n top: '0',\n transform: `translate(${Math.round(this.pos.x)}px, ${Math.round(this.pos.y)}px)`,\n zIndex: '9998',\n visibility: this.positionReady ? 'visible' : 'hidden',\n };\n\n return (\n <Host hidden={this.options.length === 0 ? true : undefined}>\n {this.options.length ? (\n <ds-button-unfilled\n ref={element => {\n this.triggerElement = (element as HTMLElement) ?? null;\n }}\n id={this.triggerId}\n variant=\"icon-label\"\n size=\"md\"\n icon=\"SectionList\"\n label=\"Group\"\n labelEmphasis={false}\n pressScale={false}\n aria-label={name}\n hasMenu={true}\n collapseLabel={true}\n expanded={this.open}\n controls={this.popupId}\n onDsClick={(event: CustomEvent<MouseEvent>) => this.toggle(event.detail.detail === 0)}\n />\n ) : null}\n\n {this.shouldRender ? (\n <div\n id={this.popupId}\n popover=\"manual\"\n class={{\n 'table-group-popup': true,\n 'ds-choice-popup': true,\n 'ds-choice-popup--closing': this.closing,\n }}\n style={popupStyle}\n role=\"dialog\"\n aria-label={name}\n >\n <div class=\"table-group__body\">\n <div class=\"table-group__data-pane\">\n <ChoiceListSection\n heading=\"Data\"\n ariaLabel=\"Group data\"\n className=\"table-group__list\"\n >\n {this.options.map((option, index) => (\n <div data-group-value={option.value}>\n {this.renderDataOption(option, index)}\n </div>\n ))}\n </ChoiceListSection>\n <div\n class=\"table-group__footer ds-choice-footer\"\n aria-hidden={!this.grouping ? 'true' : undefined}\n >\n <div class=\"ds-choice-footer__content ds-control--md\">\n {this.grouping ? (\n <button\n class=\"ds-choice-footer__clear ds-text-action ds-focus-ring\"\n type=\"button\"\n onClick={() => this.dsClear.emit()}\n >\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"inherit\">\n {this.clearLabel}\n </ds-text>\n </button>\n ) : null}\n </div>\n </div>\n </div>\n\n <div class=\"table-group__order-pane\">\n {this.grouping ? (\n <ChoiceListSection\n heading=\"Order\"\n ariaLabel=\"Group order\"\n className=\"table-group__list\"\n >\n {this.activeOrderOptions.map((option, index) => (\n <div data-group-order-index={index}>{this.renderOrder(option, index)}</div>\n ))}\n </ChoiceListSection>\n ) : (\n <div class=\"table-group__empty ds-choice-empty ds-empty-region\">\n <ds-empty-state body={this.emptyMessage} />\n </div>\n )}\n </div>\n </div>\n </div>\n ) : null}\n </Host>\n );\n }\n}\n",
298
298
  "type": "registry:ui"
299
299
  }
300
300
  ]