@ds-mo/ui 0.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 (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/dist/index.css +1 -0
  4. package/dist/index.d.ts +969 -0
  5. package/dist/index.js +2623 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/r/accordion.json +79 -0
  8. package/dist/r/badge.json +69 -0
  9. package/dist/r/banner.json +90 -0
  10. package/dist/r/breadcrumb.json +70 -0
  11. package/dist/r/button-group.json +65 -0
  12. package/dist/r/button.json +137 -0
  13. package/dist/r/card.json +82 -0
  14. package/dist/r/checkbox.json +91 -0
  15. package/dist/r/divider.json +57 -0
  16. package/dist/r/empty-state.json +69 -0
  17. package/dist/r/error-boundary.json +66 -0
  18. package/dist/r/fade.json +65 -0
  19. package/dist/r/field.json +71 -0
  20. package/dist/r/header.json +67 -0
  21. package/dist/r/input.json +77 -0
  22. package/dist/r/loader.json +54 -0
  23. package/dist/r/menu.json +130 -0
  24. package/dist/r/modal.json +93 -0
  25. package/dist/r/pagination.json +80 -0
  26. package/dist/r/radio.json +85 -0
  27. package/dist/r/registry.json +1600 -0
  28. package/dist/r/scrollbar.json +68 -0
  29. package/dist/r/select.json +93 -0
  30. package/dist/r/sidebar.json +105 -0
  31. package/dist/r/skeleton.json +72 -0
  32. package/dist/r/slider.json +87 -0
  33. package/dist/r/surface.json +97 -0
  34. package/dist/r/tab-group.json +72 -0
  35. package/dist/r/tab.json +71 -0
  36. package/dist/r/table.json +110 -0
  37. package/dist/r/tag.json +110 -0
  38. package/dist/r/text.json +94 -0
  39. package/dist/r/toast.json +77 -0
  40. package/dist/r/toggle-button-group.json +65 -0
  41. package/dist/r/toggle-button.json +94 -0
  42. package/dist/r/toggle.json +65 -0
  43. package/dist/r/tooltip.json +86 -0
  44. package/package.json +80 -0
  45. package/src/components/Accordion/Accordion.module.css +66 -0
  46. package/src/components/Accordion/Accordion.stories.tsx +95 -0
  47. package/src/components/Accordion/Accordion.tsx +122 -0
  48. package/src/components/Accordion/index.ts +2 -0
  49. package/src/components/Badge/Badge.module.css +6 -0
  50. package/src/components/Badge/Badge.stories.tsx +99 -0
  51. package/src/components/Badge/Badge.tsx +23 -0
  52. package/src/components/Badge/index.ts +2 -0
  53. package/src/components/Banner/Banner.module.css +67 -0
  54. package/src/components/Banner/Banner.stories.tsx +81 -0
  55. package/src/components/Banner/Banner.tsx +109 -0
  56. package/src/components/Banner/index.ts +2 -0
  57. package/src/components/Breadcrumb/Breadcrumb.module.css +46 -0
  58. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +72 -0
  59. package/src/components/Breadcrumb/Breadcrumb.tsx +68 -0
  60. package/src/components/Breadcrumb/index.ts +2 -0
  61. package/src/components/Button/Button.module.css +343 -0
  62. package/src/components/Button/Button.stories.tsx +575 -0
  63. package/src/components/Button/Button.tsx +201 -0
  64. package/src/components/Button/index.ts +10 -0
  65. package/src/components/ButtonGroup/ButtonGroup.module.css +157 -0
  66. package/src/components/ButtonGroup/ButtonGroup.stories.tsx +384 -0
  67. package/src/components/ButtonGroup/ButtonGroup.tsx +122 -0
  68. package/src/components/ButtonGroup/index.ts +2 -0
  69. package/src/components/Card/Card.module.css +36 -0
  70. package/src/components/Card/Card.stories.tsx +78 -0
  71. package/src/components/Card/Card.tsx +53 -0
  72. package/src/components/Card/index.ts +2 -0
  73. package/src/components/Checkbox/Checkbox.module.css +47 -0
  74. package/src/components/Checkbox/Checkbox.stories.tsx +129 -0
  75. package/src/components/Checkbox/Checkbox.tsx +68 -0
  76. package/src/components/Checkbox/index.ts +2 -0
  77. package/src/components/Divider/Divider.module.css +19 -0
  78. package/src/components/Divider/Divider.stories.tsx +55 -0
  79. package/src/components/Divider/Divider.tsx +28 -0
  80. package/src/components/Divider/index.ts +2 -0
  81. package/src/components/EmptyState/EmptyState.module.css +8 -0
  82. package/src/components/EmptyState/EmptyState.stories.tsx +53 -0
  83. package/src/components/EmptyState/EmptyState.tsx +31 -0
  84. package/src/components/EmptyState/index.ts +2 -0
  85. package/src/components/ErrorBoundary/ErrorBoundary.module.css +18 -0
  86. package/src/components/ErrorBoundary/ErrorBoundary.stories.tsx +38 -0
  87. package/src/components/ErrorBoundary/ErrorBoundary.tsx +50 -0
  88. package/src/components/ErrorBoundary/index.ts +2 -0
  89. package/src/components/Fade/Fade.module.css +31 -0
  90. package/src/components/Fade/Fade.stories.tsx +35 -0
  91. package/src/components/Fade/Fade.tsx +33 -0
  92. package/src/components/Fade/index.ts +2 -0
  93. package/src/components/Field/Field.module.css +5 -0
  94. package/src/components/Field/Field.stories.tsx +111 -0
  95. package/src/components/Field/Field.tsx +22 -0
  96. package/src/components/Field/index.ts +2 -0
  97. package/src/components/Header/Header.module.css +33 -0
  98. package/src/components/Header/Header.stories.tsx +52 -0
  99. package/src/components/Header/Header.tsx +42 -0
  100. package/src/components/Header/index.ts +2 -0
  101. package/src/components/Input/Input.module.css +71 -0
  102. package/src/components/Input/Input.stories.tsx +135 -0
  103. package/src/components/Input/Input.tsx +76 -0
  104. package/src/components/Input/index.ts +2 -0
  105. package/src/components/LabelWrap/LabelWrap.module.css +34 -0
  106. package/src/components/LabelWrap/LabelWrap.tsx +32 -0
  107. package/src/components/LabelWrap/index.ts +1 -0
  108. package/src/components/Loader/Loader.module.css +9 -0
  109. package/src/components/Loader/Loader.stories.tsx +44 -0
  110. package/src/components/Loader/Loader.tsx +43 -0
  111. package/src/components/Loader/index.ts +2 -0
  112. package/src/components/Menu/DestructiveMenuItem.module.css +57 -0
  113. package/src/components/Menu/DestructiveMenuItem.tsx +64 -0
  114. package/src/components/Menu/Menu.module.css +49 -0
  115. package/src/components/Menu/Menu.stories.tsx +129 -0
  116. package/src/components/Menu/Menu.tsx +269 -0
  117. package/src/components/Menu/MenuItem.module.css +84 -0
  118. package/src/components/Menu/MenuItem.tsx +101 -0
  119. package/src/components/Menu/index.ts +6 -0
  120. package/src/components/Modal/Modal.module.css +67 -0
  121. package/src/components/Modal/Modal.stories.tsx +71 -0
  122. package/src/components/Modal/Modal.tsx +122 -0
  123. package/src/components/Modal/index.ts +2 -0
  124. package/src/components/Pagination/Pagination.module.css +64 -0
  125. package/src/components/Pagination/Pagination.stories.tsx +51 -0
  126. package/src/components/Pagination/Pagination.tsx +103 -0
  127. package/src/components/Pagination/index.ts +2 -0
  128. package/src/components/Radio/Radio.module.css +61 -0
  129. package/src/components/Radio/Radio.stories.tsx +101 -0
  130. package/src/components/Radio/Radio.tsx +108 -0
  131. package/src/components/Radio/index.ts +2 -0
  132. package/src/components/Scrollbar/Scrollbar.module.css +81 -0
  133. package/src/components/Scrollbar/Scrollbar.stories.tsx +46 -0
  134. package/src/components/Scrollbar/Scrollbar.tsx +187 -0
  135. package/src/components/Scrollbar/index.ts +2 -0
  136. package/src/components/Select/Select.module.css +31 -0
  137. package/src/components/Select/Select.stories.tsx +59 -0
  138. package/src/components/Select/Select.tsx +92 -0
  139. package/src/components/Select/index.ts +2 -0
  140. package/src/components/Sidebar/Sidebar.module.css +161 -0
  141. package/src/components/Sidebar/Sidebar.stories.tsx +125 -0
  142. package/src/components/Sidebar/Sidebar.tsx +129 -0
  143. package/src/components/Sidebar/SidebarItem.tsx +57 -0
  144. package/src/components/Sidebar/SidebarSection.tsx +42 -0
  145. package/src/components/Sidebar/index.ts +6 -0
  146. package/src/components/Skeleton/Skeleton.module.css +53 -0
  147. package/src/components/Skeleton/Skeleton.stories.tsx +89 -0
  148. package/src/components/Skeleton/Skeleton.tsx +66 -0
  149. package/src/components/Skeleton/index.ts +2 -0
  150. package/src/components/Slider/Slider.module.css +62 -0
  151. package/src/components/Slider/Slider.stories.tsx +120 -0
  152. package/src/components/Slider/Slider.tsx +74 -0
  153. package/src/components/Slider/index.ts +2 -0
  154. package/src/components/Surface/Surface.module.css +222 -0
  155. package/src/components/Surface/Surface.stories.tsx +213 -0
  156. package/src/components/Surface/Surface.tsx +219 -0
  157. package/src/components/Surface/index.ts +12 -0
  158. package/src/components/Tab/Tab.module.css +66 -0
  159. package/src/components/Tab/Tab.stories.tsx +103 -0
  160. package/src/components/Tab/Tab.tsx +41 -0
  161. package/src/components/Tab/index.ts +2 -0
  162. package/src/components/TabGroup/TabGroup.module.css +26 -0
  163. package/src/components/TabGroup/TabGroup.stories.tsx +60 -0
  164. package/src/components/TabGroup/TabGroup.tsx +83 -0
  165. package/src/components/TabGroup/index.ts +2 -0
  166. package/src/components/Table/Table.module.css +130 -0
  167. package/src/components/Table/Table.stories.tsx +111 -0
  168. package/src/components/Table/Table.tsx +226 -0
  169. package/src/components/Table/index.ts +11 -0
  170. package/src/components/Tag/Tag.module.css +270 -0
  171. package/src/components/Tag/Tag.stories.tsx +399 -0
  172. package/src/components/Tag/Tag.tsx +162 -0
  173. package/src/components/Tag/index.ts +2 -0
  174. package/src/components/Text/Text.module.css +166 -0
  175. package/src/components/Text/Text.stories.tsx +212 -0
  176. package/src/components/Text/Text.tsx +181 -0
  177. package/src/components/Text/index.ts +13 -0
  178. package/src/components/Toast/Toast.module.css +91 -0
  179. package/src/components/Toast/Toast.stories.tsx +66 -0
  180. package/src/components/Toast/Toast.tsx +182 -0
  181. package/src/components/Toast/index.ts +8 -0
  182. package/src/components/Toggle/Toggle.module.css +87 -0
  183. package/src/components/Toggle/Toggle.stories.tsx +85 -0
  184. package/src/components/Toggle/Toggle.tsx +52 -0
  185. package/src/components/Toggle/index.ts +2 -0
  186. package/src/components/ToggleButton/ToggleButton.module.css +172 -0
  187. package/src/components/ToggleButton/ToggleButton.stories.tsx +299 -0
  188. package/src/components/ToggleButton/ToggleButton.tsx +118 -0
  189. package/src/components/ToggleButton/index.ts +2 -0
  190. package/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css +163 -0
  191. package/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx +341 -0
  192. package/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx +111 -0
  193. package/src/components/ToggleButtonGroup/index.ts +2 -0
  194. package/src/components/Tooltip/Tooltip.module.css +45 -0
  195. package/src/components/Tooltip/Tooltip.stories.tsx +46 -0
  196. package/src/components/Tooltip/Tooltip.tsx +224 -0
  197. package/src/components/Tooltip/index.ts +2 -0
  198. package/src/css-modules.d.ts +4 -0
  199. package/src/docs/ColorUsage.mdx +195 -0
  200. package/src/docs/ElevationUsage.mdx +95 -0
  201. package/src/docs/Introduction.mdx +97 -0
  202. package/src/docs/OpticalSizing.mdx +178 -0
  203. package/src/docs/TypographyUsage.mdx +121 -0
  204. package/src/index.ts +154 -0
  205. package/src/stories/Colors.stories.tsx +528 -0
  206. package/src/stories/Dimensions.stories.tsx +200 -0
  207. package/src/stories/Effects.stories.tsx +231 -0
  208. package/src/stories/Icons.stories.tsx +159 -0
  209. package/src/stories/Typography.stories.tsx +157 -0
  210. package/src/types/icons.ts +15 -0
  211. package/src/types/index.ts +1 -0
  212. package/src/utils/cn.ts +6 -0
  213. package/src/utils/css-tokens.ts +22 -0
@@ -0,0 +1,72 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "tab-group",
4
+ "title": "TabGroup",
5
+ "description": "Tab orchestrator that wraps Tab components with an animated sliding indicator and active state management.",
6
+ "type": "registry:ui",
7
+ "meta": {
8
+ "library": "@compomo/ui",
9
+ "distribution": "npm",
10
+ "storybook": "https://zainadeel.github.io/CompoMo/?path=/story/tab-group",
11
+ "consumption": {
12
+ "install": "npm install @compomo/ui @ds-mo/tokens",
13
+ "cssSetup": "Import tokens CSS at your app entry point: import '@ds-mo/tokens/css';\nThis provides all var(--color-*), var(--dimension-*), var(--effect-*), and var(--typography-*) custom properties.\nFor dark mode, set data-theme=\"dark\" on <html>.",
14
+ "import": "import { TabGroup } from '@compomo/ui';",
15
+ "cssImport": "import '@compomo/ui/css';",
16
+ "typeImport": "import type { TabGroupProps, TabGroupTab } from '@compomo/ui';",
17
+ "internalDependencies": [
18
+ {
19
+ "component": "Tab",
20
+ "note": "Used internally by TabGroup. No action needed — it's bundled in @compomo/ui."
21
+ }
22
+ ],
23
+ "peerDependencies": {
24
+ "required": [
25
+ "react >=17",
26
+ "react-dom >=17",
27
+ "@ds-mo/tokens >=0.1.0"
28
+ ],
29
+ "optional": []
30
+ }
31
+ },
32
+ "props": {
33
+ "tabs": {
34
+ "type": "TabGroupTab[]",
35
+ "required": true
36
+ },
37
+ "activeIndex": {
38
+ "type": "number",
39
+ "default": "0"
40
+ },
41
+ "onTabChange": {
42
+ "type": "(index: number) => void"
43
+ }
44
+ },
45
+ "exports": [
46
+ "TabGroup"
47
+ ],
48
+ "types": [
49
+ "TabGroupProps",
50
+ "TabGroupTab"
51
+ ]
52
+ },
53
+ "dependencies": [
54
+ "@compomo/ui",
55
+ "@ds-mo/tokens"
56
+ ],
57
+ "registryDependencies": [
58
+ "tab"
59
+ ],
60
+ "files": [
61
+ {
62
+ "path": "src/components/TabGroup/TabGroup.module.css",
63
+ "content": ".tabGroup {\n display: flex;\n flex-direction: column;\n position: relative;\n}\n\n.tabsRow {\n display: flex;\n gap: var(--dimension-space-025);\n}\n\n.track {\n height: 2px;\n background-color: var(--color-border-secondary);\n position: relative;\n}\n\n.indicator {\n position: absolute;\n bottom: 0;\n height: 2px;\n background-color: var(--color-foreground-primary);\n border-radius: 1px;\n transition: transform var(--effect-motion-short-3), width var(--effect-motion-short-3);\n will-change: transform, width;\n}\n",
64
+ "type": "registry:ui"
65
+ },
66
+ {
67
+ "path": "src/components/TabGroup/TabGroup.tsx",
68
+ "content": "import React, { forwardRef, useRef, useEffect, useState, useCallback } from 'react';\nimport { cn } from '@/utils/cn';\nimport { Tab } from '@/components/Tab';\nimport styles from './TabGroup.module.css';\n\nexport interface TabGroupTab {\n label: string;\n id?: string;\n}\n\nexport interface TabGroupProps {\n /** Tab definitions. */\n tabs: TabGroupTab[];\n /** Index of the active tab. */\n activeIndex?: number;\n /** Called when a tab is clicked. */\n onTabChange?: (index: number) => void;\n className?: string;\n}\n\nexport const TabGroup = forwardRef<HTMLDivElement, TabGroupProps>(\n ({ tabs, activeIndex = 0, onTabChange, className }, ref) => {\n const tabsRef = useRef<(HTMLButtonElement | null)[]>([]);\n const [indicator, setIndicator] = useState({ left: 0, width: 0 });\n\n const updateIndicator = useCallback(() => {\n const el = tabsRef.current[activeIndex];\n if (el) {\n const parent = el.parentElement;\n if (parent) {\n setIndicator({\n left: el.offsetLeft,\n width: el.offsetWidth,\n });\n }\n }\n }, [activeIndex]);\n\n useEffect(() => {\n updateIndicator();\n }, [updateIndicator]);\n\n // Recalculate on resize\n useEffect(() => {\n const observer = new ResizeObserver(updateIndicator);\n const container = tabsRef.current[0]?.parentElement;\n if (container) observer.observe(container);\n return () => observer.disconnect();\n }, [updateIndicator]);\n\n return (\n <div\n ref={ref}\n role=\"tablist\"\n className={cn(styles.tabGroup, className)}\n >\n <div className={styles.tabsRow}>\n {tabs.map((tab, i) => (\n <Tab\n key={tab.id ?? i}\n ref={el => { tabsRef.current[i] = el; }}\n id={tab.id}\n label={tab.label}\n isSelected={i === activeIndex}\n onClick={() => onTabChange?.(i)}\n />\n ))}\n </div>\n <div className={styles.track}>\n <div\n className={styles.indicator}\n style={{\n transform: `translateX(${indicator.left}px)`,\n width: indicator.width,\n }}\n />\n </div>\n </div>\n );\n }\n);\n\nTabGroup.displayName = 'TabGroup';\n",
69
+ "type": "registry:ui"
70
+ }
71
+ ]
72
+ }
@@ -0,0 +1,71 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "tab",
4
+ "title": "Tab",
5
+ "description": "Individual tab button with selected state and hover overlay.",
6
+ "type": "registry:ui",
7
+ "meta": {
8
+ "library": "@compomo/ui",
9
+ "distribution": "npm",
10
+ "storybook": "https://zainadeel.github.io/CompoMo/?path=/story/tab",
11
+ "consumption": {
12
+ "install": "npm install @compomo/ui @ds-mo/tokens",
13
+ "cssSetup": "Import tokens CSS at your app entry point: import '@ds-mo/tokens/css';\nThis provides all var(--color-*), var(--dimension-*), var(--effect-*), and var(--typography-*) custom properties.\nFor dark mode, set data-theme=\"dark\" on <html>.",
14
+ "import": "import { Tab } from '@compomo/ui';",
15
+ "cssImport": "import '@compomo/ui/css';",
16
+ "typeImport": "import type { TabProps } from '@compomo/ui';",
17
+ "internalDependencies": [
18
+ {
19
+ "component": "Text",
20
+ "note": "Used internally by Tab. No action needed — it's bundled in @compomo/ui."
21
+ }
22
+ ],
23
+ "peerDependencies": {
24
+ "required": [
25
+ "react >=17",
26
+ "react-dom >=17",
27
+ "@ds-mo/tokens >=0.1.0"
28
+ ],
29
+ "optional": []
30
+ }
31
+ },
32
+ "props": {
33
+ "label": {
34
+ "type": "string",
35
+ "required": true
36
+ },
37
+ "isSelected": {
38
+ "type": "boolean",
39
+ "default": "false"
40
+ },
41
+ "onClick": {
42
+ "type": "() => void"
43
+ }
44
+ },
45
+ "exports": [
46
+ "Tab"
47
+ ],
48
+ "types": [
49
+ "TabProps"
50
+ ]
51
+ },
52
+ "dependencies": [
53
+ "@compomo/ui",
54
+ "@ds-mo/tokens"
55
+ ],
56
+ "registryDependencies": [
57
+ "text"
58
+ ],
59
+ "files": [
60
+ {
61
+ "path": "src/components/Tab/Tab.module.css",
62
+ "content": "/* ==========================================================================\n Tab — hover/pressed via direct background-color, no pseudo-element overlay.\n ========================================================================== */\n\n.tab {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--dimension-space-075);\n background-color: transparent;\n border: none;\n border-radius: var(--dimension-radius-075);\n cursor: pointer;\n -webkit-user-select: none;\n user-select: none;\n color: var(--color-foreground-secondary);\n position: relative;\n will-change: color;\n transition: color var(--effect-motion-short-2),\n background-color var(--effect-motion-short-2);\n flex-shrink: 0;\n height: var(--dimension-size-400);\n box-sizing: border-box;\n\n /* Interaction — ghost defaults */\n --_hover-bg: var(--color-interaction-hover);\n --_pressed-bg: var(--color-interaction-pressed);\n}\n\n.tab:hover { background-color: var(--_hover-bg); }\n.tab:active { background-color: var(--_pressed-bg); }\n\n.tabSelected { color: var(--color-foreground-primary); }\n\n/* ==========================================================================\n Background context — parent surface awareness\n ========================================================================== */\n\n.onMedium {\n --_hover-bg: var(--color-interaction-on-medium-background-hover);\n --_pressed-bg: var(--color-interaction-on-medium-background-pressed);\n}\n\n.onBold {\n --_hover-bg: var(--color-interaction-on-bold-background-hover);\n --_pressed-bg: var(--color-interaction-on-bold-background-pressed);\n color: var(--color-foreground-on-bold-background-secondary);\n}\n\n.onBold.tabSelected { color: var(--color-foreground-on-bold-background-primary); }\n\n.onStrong {\n --_hover-bg: var(--color-interaction-on-strong-background-hover);\n --_pressed-bg: var(--color-interaction-on-strong-background-pressed);\n color: var(--color-foreground-on-bold-background-secondary);\n}\n\n.onStrong.tabSelected { color: var(--color-foreground-on-bold-background-primary); }\n\n.onAlwaysDark {\n --_hover-bg: var(--color-always-dark-interaction-hover);\n --_pressed-bg: var(--color-always-dark-interaction-pressed);\n color: var(--color-always-dark-foreground-secondary);\n}\n\n.onAlwaysDark.tabSelected { color: var(--color-always-dark-foreground-primary); }\n",
63
+ "type": "registry:ui"
64
+ },
65
+ {
66
+ "path": "src/components/Tab/Tab.tsx",
67
+ "content": "import React, { forwardRef } from 'react';\nimport { cn } from '@/utils/cn';\nimport { Text } from '@/components/Text';\nimport styles from './Tab.module.css';\n\nexport type TabBackground = 'faint' | 'medium' | 'bold' | 'strong' | 'always-dark';\n\nexport interface TabProps {\n id?: string;\n label: string;\n isSelected?: boolean;\n /** Parent surface context. Adjusts hover tokens and fg for tabs on colored backgrounds. */\n background?: TabBackground;\n onClick?: () => void;\n className?: string;\n}\n\nexport const Tab = forwardRef<HTMLButtonElement, TabProps>(\n ({ id, label, isSelected = false, background, onClick, className }, ref) => {\n const bgClass = background && background !== 'faint'\n ? styles[background === 'always-dark' ? 'onAlwaysDark' : `on${background.charAt(0).toUpperCase() + background.slice(1)}`]\n : undefined;\n return (\n <button\n ref={ref}\n id={id}\n className={cn(styles.tab, isSelected && styles.tabSelected, bgClass, className)}\n onClick={onClick}\n type=\"button\"\n aria-selected={isSelected}\n role=\"tab\"\n >\n <Text variant={isSelected ? 'text-body-medium-emphasis' : 'text-body-medium'} as=\"span\">\n {label}\n </Text>\n </button>\n );\n }\n);\n\nTab.displayName = 'Tab';\n",
68
+ "type": "registry:ui"
69
+ }
70
+ ]
71
+ }
@@ -0,0 +1,110 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "table",
4
+ "title": "Table",
5
+ "description": "Data table with CSS grid layout, sorting, pagination, loading skeleton, row selection, and custom cell rendering.",
6
+ "type": "registry:ui",
7
+ "meta": {
8
+ "library": "@compomo/ui",
9
+ "distribution": "npm",
10
+ "storybook": "https://zainadeel.github.io/CompoMo/?path=/story/table",
11
+ "consumption": {
12
+ "install": "npm install @compomo/ui @ds-mo/tokens",
13
+ "cssSetup": "Import tokens CSS at your app entry point: import '@ds-mo/tokens/css';\nThis provides all var(--color-*), var(--dimension-*), var(--effect-*), and var(--typography-*) custom properties.\nFor dark mode, set data-theme=\"dark\" on <html>.",
14
+ "import": "import { Table } from '@compomo/ui';",
15
+ "cssImport": "import '@compomo/ui/css';",
16
+ "typeImport": "import type { TableProps, TableColumn, SortState, SortDirection, CellType, FilterValue, FilterState, PaginationState } from '@compomo/ui';",
17
+ "internalDependencies": [
18
+ {
19
+ "component": "Text",
20
+ "note": "Used internally by Table. No action needed — it's bundled in @compomo/ui."
21
+ },
22
+ {
23
+ "component": "Surface",
24
+ "note": "Used internally by Table. No action needed — it's bundled in @compomo/ui."
25
+ },
26
+ {
27
+ "component": "Skeleton",
28
+ "note": "Used internally by Table. No action needed — it's bundled in @compomo/ui."
29
+ }
30
+ ],
31
+ "peerDependencies": {
32
+ "required": [
33
+ "react >=17",
34
+ "react-dom >=17",
35
+ "@ds-mo/tokens >=0.1.0"
36
+ ],
37
+ "optional": []
38
+ }
39
+ },
40
+ "props": {
41
+ "columns": {
42
+ "type": "TableColumn<T>[]",
43
+ "required": true
44
+ },
45
+ "data": {
46
+ "type": "T[]",
47
+ "required": true
48
+ },
49
+ "sortState": {
50
+ "type": "SortState"
51
+ },
52
+ "onSort": {
53
+ "type": "(columnId: string) => void"
54
+ },
55
+ "onRowClick": {
56
+ "type": "(row: T, rowIndex: number) => void"
57
+ },
58
+ "selectedRows": {
59
+ "type": "Set<number>"
60
+ },
61
+ "loading": {
62
+ "type": "boolean"
63
+ },
64
+ "pagination": {
65
+ "type": "PaginationState"
66
+ },
67
+ "onPaginationChange": {
68
+ "type": "(pageIndex: number, pageSize: number) => void"
69
+ },
70
+ "emptyMessage": {
71
+ "type": "string",
72
+ "default": "'No results found.'"
73
+ }
74
+ },
75
+ "exports": [
76
+ "Table"
77
+ ],
78
+ "types": [
79
+ "TableProps",
80
+ "TableColumn",
81
+ "SortState",
82
+ "SortDirection",
83
+ "CellType",
84
+ "FilterValue",
85
+ "FilterState",
86
+ "PaginationState"
87
+ ]
88
+ },
89
+ "dependencies": [
90
+ "@compomo/ui",
91
+ "@ds-mo/tokens"
92
+ ],
93
+ "registryDependencies": [
94
+ "text",
95
+ "surface",
96
+ "skeleton"
97
+ ],
98
+ "files": [
99
+ {
100
+ "path": "src/components/Table/Table.module.css",
101
+ "content": ".tableWrapper {\n display: flex;\n flex-direction: column;\n width: 100%;\n overflow: hidden;\n border-radius: var(--dimension-radius-075);\n}\n\n.tableScroll {\n overflow-x: auto;\n width: 100%;\n}\n\n.table {\n display: grid;\n width: 100%;\n min-width: max-content;\n}\n\n.headerRow {\n display: contents;\n}\n\n.headerCell {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-050);\n padding: var(--dimension-space-075) var(--dimension-space-100);\n background-color: var(--color-background-secondary);\n border-bottom: var(--dimension-stroke-width-015) solid var(--color-border-primary);\n -webkit-user-select: none;\n user-select: none;\n position: sticky;\n top: 0;\n z-index: 2;\n}\n\n.headerCell.sortable {\n cursor: pointer;\n}\n\n.headerCell.sortable:hover {\n background-color: var(--color-background-translucent);\n}\n\n.sortIndicator {\n font-size: 12px;\n color: var(--color-foreground-secondary);\n}\n\n.row {\n display: contents;\n}\n\n.row:hover .cell {\n background-color: var(--color-interaction-hover);\n}\n\n.rowSelected .cell {\n background-color: var(--color-background-faint-brand);\n}\n\n.rowClickable {\n cursor: pointer;\n}\n\n.cell {\n display: flex;\n align-items: center;\n padding: var(--dimension-space-075) var(--dimension-space-100);\n border-bottom: var(--dimension-stroke-width-015) solid var(--color-border-primary);\n min-height: var(--dimension-size-500);\n background-color: var(--color-background-primary);\n transition: background-color var(--effect-motion-short-2);\n}\n\n.alignRight { justify-content: flex-end; text-align: right; }\n.alignCenter { justify-content: center; text-align: center; }\n\n.emptyRow {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--dimension-space-400);\n background-color: var(--color-background-primary);\n}\n\n.skeleton {\n height: 14px;\n width: 60%;\n background-color: var(--color-background-translucent);\n border-radius: var(--dimension-radius-050);\n animation: shimmer 1.5s infinite;\n}\n\n@keyframes shimmer {\n 0% { opacity: 0.5; }\n 50% { opacity: 1; }\n 100% { opacity: 0.5; }\n}\n\n.pagination {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--dimension-space-100);\n padding: var(--dimension-space-100);\n border-top: var(--dimension-stroke-width-015) solid var(--color-border-primary);\n background-color: var(--color-background-primary);\n}\n\n.pageButton {\n display: flex;\n align-items: center;\n padding: var(--dimension-space-050) var(--dimension-space-100);\n background-color: transparent;\n border: none;\n border-radius: var(--dimension-radius-075);\n cursor: pointer;\n transition: background-color var(--effect-motion-short-2);\n}\n\n.pageButton:hover:not(:disabled) {\n background-color: var(--color-interaction-hover);\n}\n\n.pageButton:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n",
102
+ "type": "registry:ui"
103
+ },
104
+ {
105
+ "path": "src/components/Table/Table.tsx",
106
+ "content": "import React, { useMemo, useState, useCallback } from 'react';\nimport { cn } from '@/utils/cn';\nimport { Text } from '@/components/Text';\nimport styles from './Table.module.css';\n\nexport type SortDirection = 'asc' | 'desc';\nexport type CellType = 'string' | 'numbers' | 'tag' | 'checkbox' | 'button' | 'spacer' | 'image' | 'icon' | 'empty';\nexport type FilterValue = string | number | Array<string | number> | undefined;\nexport type FilterState = Record<string, FilterValue>;\n\nexport interface SortState {\n columnId: string;\n direction: SortDirection;\n}\n\nexport interface PaginationState {\n pageIndex: number;\n pageSize: number;\n}\n\nexport interface TableColumn<T = unknown> {\n id: string;\n header: string | React.ReactNode;\n accessorKey?: string;\n accessor?: (row: T) => unknown;\n sortable?: boolean;\n align?: 'left' | 'right' | 'center';\n width?: string | number;\n minWidth?: string | number;\n maxWidth?: string | number;\n renderCell?: (row: T, rowIndex: number) => React.ReactNode;\n cellType?: CellType;\n hide?: boolean;\n pinned?: 'left' | 'right';\n}\n\nexport interface TableProps<T = unknown> {\n columns: TableColumn<T>[];\n data: T[];\n sortState?: SortState;\n onSort?: (columnId: string) => void;\n className?: string;\n onRowClick?: (row: T, rowIndex: number) => void;\n selectedRows?: Set<number>;\n loading?: boolean;\n pagination?: PaginationState;\n onPaginationChange?: (pageIndex: number, pageSize: number) => void;\n emptyMessage?: string;\n}\n\nfunction getColumnValue<T>(column: TableColumn<T>, row: T): unknown {\n if (column.accessorKey) return (row as Record<string, unknown>)[column.accessorKey];\n if (column.accessor) return column.accessor(row);\n return undefined;\n}\n\nfunction compareValues(a: unknown, b: unknown, direction: SortDirection): number {\n const mult = direction === 'asc' ? 1 : -1;\n if (a == null && b == null) return 0;\n if (a == null) return mult;\n if (b == null) return -mult;\n if (typeof a === 'number' && typeof b === 'number') return (a - b) * mult;\n return String(a).localeCompare(String(b)) * mult;\n}\n\nexport function Table<T>({\n columns,\n data,\n sortState,\n onSort,\n className,\n onRowClick,\n selectedRows,\n loading = false,\n pagination,\n onPaginationChange,\n emptyMessage = 'No results found.',\n}: TableProps<T>) {\n const visibleColumns = useMemo(() => columns.filter(c => !c.hide), [columns]);\n\n const gridTemplateColumns = useMemo(() =>\n visibleColumns.map(col => {\n if (col.width) return typeof col.width === 'number' ? `${col.width}px` : col.width;\n return 'minmax(120px, 1fr)';\n }).join(' '),\n [visibleColumns]\n );\n\n const sortedData = useMemo(() => {\n if (!sortState) return data;\n const col = columns.find(c => c.id === sortState.columnId);\n if (!col) return data;\n return [...data].sort((a, b) =>\n compareValues(getColumnValue(col, a), getColumnValue(col, b), sortState.direction)\n );\n }, [data, columns, sortState]);\n\n const pagedData = useMemo(() => {\n if (!pagination) return sortedData;\n const start = pagination.pageIndex * pagination.pageSize;\n return sortedData.slice(start, start + pagination.pageSize);\n }, [sortedData, pagination]);\n\n const totalPages = pagination ? Math.ceil(sortedData.length / pagination.pageSize) : 1;\n const isEmpty = !loading && pagedData.length === 0;\n\n const handleSort = (columnId: string) => {\n if (onSort) onSort(columnId);\n };\n\n return (\n <div className={cn(styles.tableWrapper, className)}>\n <div className={styles.tableScroll}>\n <div className={styles.table} style={{ gridTemplateColumns }} role=\"table\">\n {/* Header */}\n <div className={styles.headerRow} role=\"row\">\n {visibleColumns.map(col => (\n <div\n key={col.id}\n className={cn(\n styles.headerCell,\n col.sortable && styles.sortable,\n col.align === 'right' && styles.alignRight,\n col.align === 'center' && styles.alignCenter\n )}\n role=\"columnheader\"\n onClick={() => col.sortable && handleSort(col.id)}\n style={col.minWidth ? { minWidth: typeof col.minWidth === 'number' ? `${col.minWidth}px` : col.minWidth } : undefined}\n >\n {typeof col.header === 'string' ? (\n <Text variant=\"text-body-small-emphasis\" as=\"span\" color=\"secondary\">\n {col.header}\n </Text>\n ) : col.header}\n {sortState?.columnId === col.id && (\n <span className={styles.sortIndicator} aria-hidden>\n {sortState.direction === 'asc' ? '\\u2191' : '\\u2193'}\n </span>\n )}\n </div>\n ))}\n </div>\n\n {/* Loading skeleton */}\n {loading && Array.from({ length: 5 }).map((_, i) => (\n <div key={`skeleton-${i}`} className={styles.row} role=\"row\">\n {visibleColumns.map(col => (\n <div key={col.id} className={styles.cell} role=\"cell\">\n <div className={styles.skeleton} />\n </div>\n ))}\n </div>\n ))}\n\n {/* Empty state */}\n {isEmpty && (\n <div className={styles.emptyRow} style={{ gridColumn: `1 / -1` }}>\n <Text variant=\"text-body-medium\" as=\"p\" color=\"secondary\">{emptyMessage}</Text>\n </div>\n )}\n\n {/* Data rows */}\n {!loading && pagedData.map((row, rowIndex) => {\n const isSelected = selectedRows?.has(rowIndex);\n return (\n <div\n key={rowIndex}\n className={cn(\n styles.row,\n isSelected && styles.rowSelected,\n onRowClick && styles.rowClickable\n )}\n role=\"row\"\n onClick={() => onRowClick?.(row, rowIndex)}\n >\n {visibleColumns.map(col => (\n <div\n key={col.id}\n className={cn(\n styles.cell,\n col.align === 'right' && styles.alignRight,\n col.align === 'center' && styles.alignCenter\n )}\n role=\"cell\"\n >\n {col.renderCell\n ? col.renderCell(row, rowIndex)\n : <Text variant=\"text-body-medium\" as=\"span\" lineTruncation={1}>{String(getColumnValue(col, row) ?? '')}</Text>\n }\n </div>\n ))}\n </div>\n );\n })}\n </div>\n </div>\n\n {/* Pagination */}\n {pagination && onPaginationChange && totalPages > 1 && (\n <div className={styles.pagination}>\n <button\n className={styles.pageButton}\n disabled={pagination.pageIndex === 0}\n onClick={() => onPaginationChange(pagination.pageIndex - 1, pagination.pageSize)}\n type=\"button\"\n >\n <Text variant=\"text-body-small\" as=\"span\">Previous</Text>\n </button>\n <Text variant=\"text-body-small\" as=\"span\" color=\"secondary\">\n {`Page ${pagination.pageIndex + 1} of ${totalPages}`}\n </Text>\n <button\n className={styles.pageButton}\n disabled={pagination.pageIndex >= totalPages - 1}\n onClick={() => onPaginationChange(pagination.pageIndex + 1, pagination.pageSize)}\n type=\"button\"\n >\n <Text variant=\"text-body-small\" as=\"span\">Next</Text>\n </button>\n </div>\n )}\n </div>\n );\n}\n\nTable.displayName = 'Table';\n",
107
+ "type": "registry:ui"
108
+ }
109
+ ]
110
+ }
@@ -0,0 +1,110 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "tag",
4
+ "title": "Tag",
5
+ "description": "Semantic label with intent coloring, removable chip, pressable filter-chip mode, and icon support.",
6
+ "type": "registry:ui",
7
+ "meta": {
8
+ "library": "@compomo/ui",
9
+ "distribution": "npm",
10
+ "storybook": "https://zainadeel.github.io/CompoMo/?path=/story/tag",
11
+ "consumption": {
12
+ "install": "npm install @compomo/ui @ds-mo/tokens @ds-mo/icons",
13
+ "cssSetup": "Import tokens CSS at your app entry point: import '@ds-mo/tokens/css';\nThis provides all var(--color-*), var(--dimension-*), var(--effect-*), and var(--typography-*) custom properties.\nFor dark mode, set data-theme=\"dark\" on <html>.",
14
+ "import": "import { Tag } from '@compomo/ui';",
15
+ "cssImport": "import '@compomo/ui/css';",
16
+ "typeImport": "import type { TagProps, TagIntent, TagContrast, TagElevation, TagSize } from '@compomo/ui';",
17
+ "iconImport": "import { IconName } from '@ds-mo/icons'; // Tree-shakeable, import only what you need",
18
+ "internalDependencies": [
19
+ {
20
+ "component": "Text",
21
+ "note": "Used internally by Tag. No action needed — it's bundled in @compomo/ui."
22
+ }
23
+ ],
24
+ "peerDependencies": {
25
+ "required": [
26
+ "react >=17",
27
+ "react-dom >=17",
28
+ "@ds-mo/tokens >=0.1.0"
29
+ ],
30
+ "optional": [
31
+ "@ds-mo/icons >=0.1.0 (for icon props)"
32
+ ]
33
+ }
34
+ },
35
+ "props": {
36
+ "label": {
37
+ "type": "string",
38
+ "required": true
39
+ },
40
+ "intent": {
41
+ "type": "'neutral' | 'brand' | 'ai' | 'negative' | 'warning' | 'caution' | 'positive'",
42
+ "default": "'neutral'"
43
+ },
44
+ "contrast": {
45
+ "type": "'strong' | 'bold' | 'medium' | 'faint'",
46
+ "default": "'faint'"
47
+ },
48
+ "size": {
49
+ "type": "'md' | 'sm' | 'xs'",
50
+ "default": "'md'"
51
+ },
52
+ "rounded": {
53
+ "type": "boolean"
54
+ },
55
+ "icon": {
56
+ "type": "IconComponent"
57
+ },
58
+ "removable": {
59
+ "type": "boolean"
60
+ },
61
+ "onRemove": {
62
+ "type": "() => void"
63
+ },
64
+ "pressed": {
65
+ "type": "boolean",
66
+ "description": "For filter-chip behavior."
67
+ },
68
+ "onPressedChange": {
69
+ "type": "(pressed: boolean) => void"
70
+ },
71
+ "onClick": {
72
+ "type": "() => void",
73
+ "description": "Makes tag clickable."
74
+ },
75
+ "inactive": {
76
+ "type": "boolean"
77
+ }
78
+ },
79
+ "exports": [
80
+ "Tag"
81
+ ],
82
+ "types": [
83
+ "TagProps",
84
+ "TagIntent",
85
+ "TagContrast",
86
+ "TagElevation",
87
+ "TagSize"
88
+ ]
89
+ },
90
+ "dependencies": [
91
+ "@compomo/ui",
92
+ "@ds-mo/tokens",
93
+ "@ds-mo/icons"
94
+ ],
95
+ "registryDependencies": [
96
+ "text"
97
+ ],
98
+ "files": [
99
+ {
100
+ "path": "src/components/Tag/Tag.module.css",
101
+ "content": "/* ==========================================================================\n Tag — hover/pressed via direct background-color + color-mix().\n No pseudo-element overlays.\n ========================================================================== */\n\n.tag {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--dimension-radius-050);\n box-sizing: border-box;\n color: var(--color-foreground-primary);\n position: relative;\n\n /* Interaction — ghost defaults */\n --_hover-bg: var(--color-interaction-hover);\n --_pressed-bg: var(--color-interaction-pressed);\n}\n\n/* ==========================================================================\n Sizes\n md 28px — no size-350 token, expressed as 7/2 × base\n sm 20px — size-250\n xs 12px — size-150\n ========================================================================== */\n\n.sizeMd { height: calc(var(--dimension-size-base) * 7 / 2); padding: 0 var(--dimension-space-075); gap: 0; }\n.sizeSm { height: var(--dimension-size-250); padding: 0 var(--dimension-space-050); gap: 0; }\n.sizeXs { height: var(--dimension-size-150); padding: 0 var(--dimension-space-012); gap: 0; }\n\n/* ==========================================================================\n Rounded\n ========================================================================== */\n\n.rounded { border-radius: var(--dimension-radius-half); }\n\n/*\n * Pill padding correction — add space-050 to sides that have no content anchor.\n * Applied by Tag.tsx based on whether a leading icon or trailing remove is present:\n * no leading icon → roundedLeft{Size} (left side is bare label text)\n * no trailing icon → roundedRight{Size} (right side is bare label text)\n * Placed after size rules so same-specificity cascade wins correctly.\n */\n\n/* MD — base: space-075 */\n.roundedLeftMD { padding-left: calc(var(--dimension-space-075) + var(--dimension-space-050)); }\n.roundedRightMD { padding-right: calc(var(--dimension-space-075) + var(--dimension-space-050)); }\n\n/* SM — base: space-050 */\n.roundedLeftSM { padding-left: calc(var(--dimension-space-050) + var(--dimension-space-050)); }\n.roundedRightSM { padding-right: calc(var(--dimension-space-050) + var(--dimension-space-050)); }\n\n/* XS — base: space-012 (1px) */\n.roundedLeftXS { padding-left: calc(var(--dimension-space-012) + var(--dimension-space-050)); }\n.roundedRightXS { padding-right: calc(var(--dimension-space-012) + var(--dimension-space-050)); }\n\n/*\n * Icon-edge padding correction — when an icon (leading or remove) sits on a\n * side, that side's padding should match the top/bottom optical gap:\n * (height − icon_size) / 2\n *\n * md: (28 − 20) / 2 = 4px → space-050\n * sm: (20 − 16) / 2 = 2px → space-025\n * xs: (12 − 12) / 2 = 0px → 0\n *\n * Applied by Tag.tsx: iconLeft when Icon present, iconRight when removable.\n * Placed after size + rounded rules so same-specificity cascade wins.\n */\n.iconLeftMD { padding-left: var(--dimension-space-050); }\n.iconRightMD { padding-right: var(--dimension-space-050); }\n\n.iconLeftSM { padding-left: var(--dimension-space-025); }\n.iconRightSM { padding-right: var(--dimension-space-025); }\n\n.iconLeftXS { padding-left: 0; }\n.iconRightXS { padding-right: 0; }\n\n/* ==========================================================================\n Background context — parent surface awareness\n ========================================================================== */\n\n.onMedium {\n --_hover-bg: var(--color-interaction-on-medium-background-hover);\n --_pressed-bg: var(--color-interaction-on-medium-background-pressed);\n}\n\n.onBold {\n --_hover-bg: var(--color-interaction-on-bold-background-hover);\n --_pressed-bg: var(--color-interaction-on-bold-background-pressed);\n}\n\n.onStrong {\n --_hover-bg: var(--color-interaction-on-strong-background-hover);\n --_pressed-bg: var(--color-interaction-on-strong-background-pressed);\n}\n\n.onAlwaysDark {\n --_hover-bg: var(--color-always-dark-interaction-hover);\n --_pressed-bg: var(--color-always-dark-interaction-pressed);\n}\n\n/* ==========================================================================\n Elevation\n none — filled intent bg, no border, no shadow (default)\n flat — transparent bg + intent-coloured border ring\n elevated — filled intent bg + elevated shadow\n ========================================================================== */\n\n/* elevationNone — no extra chrome needed, intent+contrast classes handle fill */\n\n.elevationFlat {\n background-color: transparent !important;\n box-shadow: inset 0 0 0 var(--dimension-stroke-width-015) currentColor;\n}\n\n/* Flat: no background — use on-surface fg tokens matched to the contrast level.\n The border ring is currentColor so it follows text color automatically.\n 3-class specificity beats the 2-class intent+contrast filled rules above. */\n\n.elevationFlat.intentNeutral.contrastBold { color: var(--color-foreground-bold-neutral); }\n.elevationFlat.intentBrand.contrastBold { color: var(--color-foreground-bold-brand); }\n.elevationFlat.intentAi.contrastBold { color: var(--color-foreground-bold-ai); }\n.elevationFlat.intentNegative.contrastBold { color: var(--color-foreground-bold-negative); }\n.elevationFlat.intentWarning.contrastBold { color: var(--color-foreground-bold-warning); }\n.elevationFlat.intentCaution.contrastBold { color: var(--color-foreground-bold-caution); }\n.elevationFlat.intentPositive.contrastBold { color: var(--color-foreground-bold-positive); }\n\n.elevationFlat.intentNeutral.contrastStrong { color: var(--color-foreground-strong-neutral); }\n.elevationFlat.intentBrand.contrastStrong { color: var(--color-foreground-strong-brand); }\n.elevationFlat.intentAi.contrastStrong { color: var(--color-foreground-strong-ai); }\n.elevationFlat.intentNegative.contrastStrong { color: var(--color-foreground-strong-negative); }\n.elevationFlat.intentWarning.contrastStrong { color: var(--color-foreground-strong-warning); }\n.elevationFlat.intentCaution.contrastStrong { color: var(--color-foreground-strong-caution); }\n.elevationFlat.intentPositive.contrastStrong { color: var(--color-foreground-strong-positive); }\n\n.elevationFlat.intentNeutral.contrastMedium { color: var(--color-foreground-medium-neutral); }\n.elevationFlat.intentBrand.contrastMedium { color: var(--color-foreground-medium-brand); }\n.elevationFlat.intentAi.contrastMedium { color: var(--color-foreground-medium-ai); }\n.elevationFlat.intentNegative.contrastMedium { color: var(--color-foreground-medium-negative); }\n.elevationFlat.intentWarning.contrastMedium { color: var(--color-foreground-medium-warning); }\n.elevationFlat.intentCaution.contrastMedium { color: var(--color-foreground-medium-caution); }\n.elevationFlat.intentPositive.contrastMedium { color: var(--color-foreground-medium-positive); }\n\n.elevationFlat.intentNeutral.contrastFaint { color: var(--color-foreground-faint-neutral); }\n.elevationFlat.intentBrand.contrastFaint { color: var(--color-foreground-faint-brand); }\n.elevationFlat.intentAi.contrastFaint { color: var(--color-foreground-faint-ai); }\n.elevationFlat.intentNegative.contrastFaint { color: var(--color-foreground-faint-negative); }\n.elevationFlat.intentWarning.contrastFaint { color: var(--color-foreground-faint-warning); }\n.elevationFlat.intentCaution.contrastFaint { color: var(--color-foreground-faint-caution); }\n.elevationFlat.intentPositive.contrastFaint { color: var(--color-foreground-faint-positive); }\n\n.elevationElevated { box-shadow: var(--effect-elevation-elevated-sm); }\n\n/* ==========================================================================\n Intent + contrast — strong\n ========================================================================== */\n\n.intentNeutral.contrastStrong { background-color: var(--color-background-strong-neutral); color: var(--color-foreground-medium-neutral); }\n.intentBrand.contrastStrong { background-color: var(--color-background-strong-brand); color: var(--color-foreground-medium-brand); }\n.intentAi.contrastStrong { background-color: var(--color-background-strong-ai); color: var(--color-foreground-medium-ai); }\n.intentNegative.contrastStrong { background-color: var(--color-background-strong-negative); color: var(--color-foreground-medium-negative); }\n.intentWarning.contrastStrong { background-color: var(--color-background-strong-warning); color: var(--color-foreground-medium-warning); }\n.intentCaution.contrastStrong { background-color: var(--color-background-strong-caution); color: var(--color-foreground-medium-caution); }\n.intentPositive.contrastStrong { background-color: var(--color-background-strong-positive); color: var(--color-foreground-medium-positive); }\n\n/* ==========================================================================\n Intent + contrast — bold\n ========================================================================== */\n\n.intentNeutral.contrastBold { background-color: var(--color-background-bold-neutral); color: var(--color-foreground-on-bold-background-primary); }\n.intentBrand.contrastBold { background-color: var(--color-background-bold-brand); color: var(--color-foreground-on-bold-background-primary); }\n.intentAi.contrastBold { background-color: var(--color-background-bold-ai); color: var(--color-foreground-on-bold-background-primary); }\n.intentNegative.contrastBold { background-color: var(--color-background-bold-negative); color: var(--color-foreground-on-bold-background-primary); }\n.intentWarning.contrastBold { background-color: var(--color-background-bold-warning); color: var(--color-foreground-on-bold-background-primary); }\n.intentCaution.contrastBold { background-color: var(--color-background-bold-caution); color: var(--color-foreground-on-bold-background-primary); }\n.intentPositive.contrastBold { background-color: var(--color-background-bold-positive); color: var(--color-foreground-on-bold-background-primary); }\n\n/* ==========================================================================\n Intent + contrast — medium\n ========================================================================== */\n\n.intentNeutral.contrastMedium { background-color: var(--color-background-medium-neutral); color: var(--color-foreground-strong-neutral); }\n.intentBrand.contrastMedium { background-color: var(--color-background-medium-brand); color: var(--color-foreground-strong-brand); }\n.intentAi.contrastMedium { background-color: var(--color-background-medium-ai); color: var(--color-foreground-strong-ai); }\n.intentNegative.contrastMedium { background-color: var(--color-background-medium-negative); color: var(--color-foreground-strong-negative); }\n.intentWarning.contrastMedium { background-color: var(--color-background-medium-warning); color: var(--color-foreground-strong-warning); }\n.intentCaution.contrastMedium { background-color: var(--color-background-medium-caution); color: var(--color-foreground-strong-caution); }\n.intentPositive.contrastMedium { background-color: var(--color-background-medium-positive); color: var(--color-foreground-strong-positive); }\n\n/* ==========================================================================\n Intent + contrast — faint\n ========================================================================== */\n\n.intentNeutral.contrastFaint { background-color: var(--color-background-faint-neutral); color: var(--color-foreground-bold-neutral); }\n.intentBrand.contrastFaint { background-color: var(--color-background-faint-brand); color: var(--color-foreground-bold-brand); }\n.intentAi.contrastFaint { background-color: var(--color-background-faint-ai); color: var(--color-foreground-bold-ai); }\n.intentNegative.contrastFaint { background-color: var(--color-background-faint-negative); color: var(--color-foreground-bold-negative); }\n.intentWarning.contrastFaint { background-color: var(--color-background-faint-warning); color: var(--color-foreground-bold-warning); }\n.intentCaution.contrastFaint { background-color: var(--color-background-faint-caution); color: var(--color-foreground-bold-caution); }\n.intentPositive.contrastFaint { background-color: var(--color-background-faint-positive); color: var(--color-foreground-bold-positive); }\n\n/* ==========================================================================\n Interactive (clickable / filter-chip)\n ========================================================================== */\n\n.interactive {\n cursor: pointer;\n -webkit-user-select: none;\n user-select: none;\n outline: none;\n}\n\n.interactive:hover { background-color: var(--_hover-bg); }\n.interactive:active { background-color: var(--_pressed-bg); }\n\n.interactive:focus-visible {\n outline: var(--dimension-stroke-width-025) solid var(--color-interaction-focus);\n outline-offset: var(--dimension-space-025);\n}\n\n/* ==========================================================================\n Pressed / selected (filter-chip active state)\n Always brand — same language as ToggleButton\n ========================================================================== */\n\n.tag.pressed {\n background-color: var(--color-background-faint-brand) !important;\n color: var(--color-foreground-bold-brand) !important;\n border-color: transparent !important;\n}\n\n/* ==========================================================================\n Remove button — keeps ::before overlay for its own isolated hit area\n ========================================================================== */\n\n.removable { gap: var(--dimension-space-025); }\n.sizeXs.removable { gap: 0; }\n\n.removeButton {\n padding: 0; margin: 0; border: none; background: none;\n display: flex; align-items: center; justify-content: center;\n cursor: pointer; color: inherit; flex-shrink: 0;\n position: relative; border-radius: var(--dimension-radius-050);\n}\n\n.removeButton::before {\n content: ''; position: absolute; inset: -2px;\n border-radius: var(--dimension-radius-025);\n background-color: transparent;\n transition: var(--effect-transition-interaction-background-short-2);\n pointer-events: none;\n}\n\n.removeButton:hover::before { background-color: var(--color-interaction-hover); }\n.removeButton:active::before { background-color: var(--color-interaction-pressed); }\n\n/* SM — 1px gap, inset -1px → 18×18 hover area */\n.sizeSm .removeButton::before { inset: -1px; border-radius: 3px; }\n\n/* XS — flush, inset 0 → 12×12 hover area matching tag radius */\n.sizeXs .removeButton::before { inset: 0; border-radius: var(--dimension-radius-050); }\n\n.rounded .removeButton,\n.rounded .removeButton::before { border-radius: var(--dimension-radius-half); }\n\n/* ==========================================================================\n Disabled\n ========================================================================== */\n\n.inactive { opacity: 0.5; pointer-events: none; cursor: not-allowed; }\n",
102
+ "type": "registry:ui"
103
+ },
104
+ {
105
+ "path": "src/components/Tag/Tag.tsx",
106
+ "content": "import React, { forwardRef } from 'react';\nimport { cn } from '@/utils/cn';\nimport { Text } from '@/components/Text';\nimport { LabelWrap } from '@/components/LabelWrap';\nimport { Cross } from '@ds-mo/icons';\nimport type { IconComponent } from '@/types/icons';\nimport styles from './Tag.module.css';\n\nexport type TagIntent = 'neutral' | 'brand' | 'ai' | 'negative' | 'warning' | 'caution' | 'positive';\nexport type TagContrast = 'strong' | 'bold' | 'medium' | 'faint';\nexport type TagElevation = 'none' | 'flat' | 'elevated';\nexport type TagSize = 'md' | 'sm' | 'xs';\nexport type TagBackground = 'faint' | 'medium' | 'bold' | 'strong' | 'always-dark';\n\nexport interface TagProps {\n label: string;\n intent?: TagIntent;\n contrast?: TagContrast;\n /**\n * Visual chrome level.\n * none — filled with intent bg, no border, no shadow [default]\n * flat — transparent bg + intent-coloured border ring\n * elevated — filled with intent bg + elevated shadow\n */\n elevation?: TagElevation;\n size?: TagSize;\n rounded?: boolean;\n /** Leading icon component. */\n icon?: IconComponent;\n removable?: boolean;\n onRemove?: () => void;\n /** Override the default Cross remove icon. */\n removeIcon?: IconComponent;\n maxWidth?: string | number;\n inactive?: boolean;\n /** Parent surface context. Adjusts hover tokens for tags on colored backgrounds. */\n background?: TagBackground;\n /** Makes the tag clickable. Pair with pressed/onPressedChange for filter-chip behaviour. */\n onClick?: () => void;\n pressed?: boolean;\n onPressedChange?: (pressed: boolean) => void;\n className?: string;\n}\n\nconst ICON_SIZE: Record<TagSize, number> = { md: 20, sm: 16, xs: 12 };\n\nconst TEXT_STYLE_MAP: Record<TagSize, string> = {\n md: 'text-body-medium',\n sm: 'text-body-small',\n xs: 'text-caption',\n};\n\nexport const Tag = forwardRef<HTMLDivElement, TagProps>(\n (\n {\n label,\n intent = 'neutral',\n contrast = 'faint',\n elevation = 'none',\n size = 'md',\n rounded = false,\n icon: Icon,\n removable = false,\n onRemove,\n removeIcon: RemoveIcon,\n maxWidth,\n inactive = false,\n background,\n onClick,\n pressed,\n onPressedChange,\n className,\n },\n ref\n ) => {\n const isInteractive = !!onClick || !!onPressedChange;\n const hasRemove = removable && !!onRemove;\n const iconSize = ICON_SIZE[size];\n const textStyle = TEXT_STYLE_MAP[size];\n const elevKey = elevation.charAt(0).toUpperCase() + elevation.slice(1);\n const sizeKey = size.toUpperCase() as 'MD' | 'SM' | 'XS';\n\n const classes = cn(\n styles.tag,\n styles[`intent${intent.charAt(0).toUpperCase() + intent.slice(1)}`],\n styles[`contrast${contrast.charAt(0).toUpperCase() + contrast.slice(1)}`],\n styles[`elevation${elevKey}`],\n styles[`size${size.charAt(0).toUpperCase() + size.slice(1)}`],\n rounded && styles.rounded,\n // Pill padding correction — extra space-050 on bare sides (no content anchor)\n rounded && !Icon && (styles as Record<string, string>)[`roundedLeft${sizeKey}`],\n rounded && !hasRemove && (styles as Record<string, string>)[`roundedRight${sizeKey}`],\n // Icon-edge correction — reduce padding to (height − icon) / 2 on icon sides\n !!Icon && (styles as Record<string, string>)[`iconLeft${sizeKey}`],\n hasRemove && (styles as Record<string, string>)[`iconRight${sizeKey}`],\n removable && styles.removable,\n isInteractive && styles.interactive,\n background && background !== 'faint' && styles[\n background === 'always-dark' ? 'onAlwaysDark'\n : `on${background.charAt(0).toUpperCase() + background.slice(1)}`\n ],\n inactive && styles.inactive,\n pressed && styles.pressed,\n className,\n );\n\n const style = maxWidth != null\n ? { maxWidth: typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth }\n : undefined;\n\n const handleInteract = isInteractive && !inactive\n ? () => {\n onPressedChange?.(!pressed);\n onClick?.();\n }\n : undefined;\n\n const handleKeyDown = isInteractive && !inactive\n ? (e: React.KeyboardEvent<HTMLDivElement>) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onPressedChange?.(!pressed);\n onClick?.();\n }\n }\n : undefined;\n\n return (\n <div\n ref={ref}\n className={classes}\n style={style}\n onClick={handleInteract}\n onKeyDown={handleKeyDown}\n role={isInteractive ? 'button' : undefined}\n tabIndex={isInteractive && !inactive ? 0 : undefined}\n aria-pressed={onPressedChange != null ? pressed : undefined}\n aria-disabled={inactive || undefined}\n >\n {Icon && <Icon size={iconSize} />}\n <LabelWrap size={size}>\n <Text variant={textStyle as never} as=\"span\" color=\"inherit\">\n {label}\n </Text>\n </LabelWrap>\n {removable && onRemove && (\n <button\n type=\"button\"\n className={styles.removeButton}\n onClick={(e) => { e.stopPropagation(); onRemove(); }}\n aria-label={`Remove ${label}`}\n tabIndex={-1}\n >\n {RemoveIcon ? <RemoveIcon size={iconSize} /> : <Cross size={iconSize} />}\n </button>\n )}\n </div>\n );\n }\n);\n\nTag.displayName = 'Tag';\n",
107
+ "type": "registry:ui"
108
+ }
109
+ ]
110
+ }
@@ -0,0 +1,94 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "text",
4
+ "title": "Text",
5
+ "description": "Typography primitive supporting all design-system text variants, colors, truncation, and wrapping.",
6
+ "type": "registry:ui",
7
+ "meta": {
8
+ "library": "@compomo/ui",
9
+ "distribution": "npm",
10
+ "storybook": "https://zainadeel.github.io/CompoMo/?path=/story/text",
11
+ "consumption": {
12
+ "install": "npm install @compomo/ui @ds-mo/tokens",
13
+ "cssSetup": "Import tokens CSS at your app entry point: import '@ds-mo/tokens/css';\nThis provides all var(--color-*), var(--dimension-*), var(--effect-*), and var(--typography-*) custom properties.\nFor dark mode, set data-theme=\"dark\" on <html>.",
14
+ "import": "import { Text } from '@compomo/ui';",
15
+ "cssImport": "import '@compomo/ui/css';",
16
+ "typeImport": "import type { TextProps, TextVariant, TextStyle, TextColor, TextColorToken, TextDecoration, TextAlign, LineTruncation, TextWrap, TextElement } from '@compomo/ui';",
17
+ "peerDependencies": {
18
+ "required": [
19
+ "react >=17",
20
+ "react-dom >=17",
21
+ "@ds-mo/tokens >=0.1.0"
22
+ ],
23
+ "optional": []
24
+ }
25
+ },
26
+ "props": {
27
+ "variant": {
28
+ "type": "TextVariant",
29
+ "default": "'text-body-medium'",
30
+ "description": "Typography variant from design tokens."
31
+ },
32
+ "children": {
33
+ "type": "React.ReactNode",
34
+ "required": true
35
+ },
36
+ "color": {
37
+ "type": "TextColor",
38
+ "description": "Token-based text color."
39
+ },
40
+ "as": {
41
+ "type": "'p' | 'span' | 'div' | 'label' | 'h1'-'h6'",
42
+ "default": "'p'"
43
+ },
44
+ "lineTruncation": {
45
+ "type": "1 | 2 | 3 | 4 | 5 | \"none\"",
46
+ "default": "'none'"
47
+ },
48
+ "decoration": {
49
+ "type": "'none' | 'underline' | 'dotted-underline'"
50
+ },
51
+ "italic": {
52
+ "type": "boolean"
53
+ },
54
+ "align": {
55
+ "type": "'left' | 'center' | 'right'"
56
+ },
57
+ "wrap": {
58
+ "type": "'wrap' | 'nowrap' | 'balance' | 'pretty'"
59
+ }
60
+ },
61
+ "exports": [
62
+ "Text"
63
+ ],
64
+ "types": [
65
+ "TextProps",
66
+ "TextVariant",
67
+ "TextStyle",
68
+ "TextColor",
69
+ "TextColorToken",
70
+ "TextDecoration",
71
+ "TextAlign",
72
+ "LineTruncation",
73
+ "TextWrap",
74
+ "TextElement"
75
+ ]
76
+ },
77
+ "dependencies": [
78
+ "@compomo/ui",
79
+ "@ds-mo/tokens"
80
+ ],
81
+ "registryDependencies": [],
82
+ "files": [
83
+ {
84
+ "path": "src/components/Text/Text.module.css",
85
+ "content": ".text {\n margin: 0;\n}\n\n/* ============================================\n LINE TRUNCATION\n ============================================ */\n\n.truncate1Line {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.truncate2Lines {\n overflow: hidden;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n text-overflow: ellipsis;\n word-break: break-word;\n}\n\n.truncate3Lines {\n overflow: hidden;\n display: -webkit-box;\n -webkit-line-clamp: 3;\n -webkit-box-orient: vertical;\n text-overflow: ellipsis;\n word-break: break-word;\n}\n\n.truncate4Lines {\n overflow: hidden;\n display: -webkit-box;\n -webkit-line-clamp: 4;\n -webkit-box-orient: vertical;\n text-overflow: ellipsis;\n word-break: break-word;\n}\n\n.truncate5Lines {\n overflow: hidden;\n display: -webkit-box;\n -webkit-line-clamp: 5;\n -webkit-box-orient: vertical;\n text-overflow: ellipsis;\n word-break: break-word;\n}\n\n.noTruncation {\n word-break: break-word;\n white-space: normal;\n}\n\n.wrapNowrap {\n white-space: nowrap;\n word-break: normal;\n overflow: hidden;\n text-overflow: ellipsis;\n min-width: 0;\n}\n\n.wrapBalance {\n text-wrap: balance;\n}\n\n.wrapPretty {\n text-wrap: pretty;\n}\n\n/* ============================================\n TEXT COLOR - Semantic Tokens\n ============================================ */\n\n.colorPrimary {\n color: var(--color-foreground-primary);\n}\n\n.colorSecondary {\n color: var(--color-foreground-secondary);\n}\n\n.colorTertiary {\n color: var(--color-foreground-tertiary);\n}\n\n.colorBrand {\n color: var(--color-foreground-bold-brand);\n}\n\n.colorNegative {\n color: var(--color-foreground-bold-negative);\n}\n\n.colorPositive {\n color: var(--color-foreground-bold-positive);\n}\n\n.colorWarning {\n color: var(--color-foreground-bold-warning);\n}\n\n.colorCaution {\n color: var(--color-foreground-bold-caution);\n}\n\n.colorAi {\n color: var(--color-foreground-bold-ai);\n}\n\n.colorOnStrong {\n color: var(--color-foreground-on-strong-background-primary);\n}\n\n.colorOnBold {\n color: var(--color-foreground-on-bold-background-primary);\n}\n\n.colorInherit {\n color: inherit;\n}\n\n/* ============================================\n TEXT DECORATION\n ============================================ */\n\n.decorationNone {\n text-decoration: none;\n}\n\n.decorationUnderline {\n text-decoration: underline;\n text-underline-offset: 2px;\n text-decoration-thickness: 1px;\n}\n\n.decorationDottedUnderline {\n text-decoration: underline dotted;\n text-underline-offset: 2px;\n text-decoration-thickness: 1px;\n}\n\n/* ============================================\n FONT STYLE\n ============================================ */\n\n.italic {\n font-style: italic;\n}\n\n/* ============================================\n TEXT ALIGNMENT\n ============================================ */\n\n.alignLeft {\n text-align: left;\n}\n\n.alignCenter {\n text-align: center;\n}\n\n.alignRight {\n text-align: right;\n}\n",
86
+ "type": "registry:ui"
87
+ },
88
+ {
89
+ "path": "src/components/Text/Text.tsx",
90
+ "content": "import React, { forwardRef } from 'react';\nimport styles from './Text.module.css';\n\n/**\n * Text variant options.\n * Each variant maps to a TokoMo typography class with predefined font-size, line-height,\n * font-weight, and letter-spacing.\n */\nexport type TextVariant =\n | 'text-display-medium'\n | 'text-display-small'\n | 'text-title-large'\n | 'text-title-medium'\n | 'text-title-small'\n | 'text-body-large'\n | 'text-body-large-emphasis'\n | 'text-body-medium'\n | 'text-body-medium-emphasis'\n | 'text-body-small'\n | 'text-body-small-emphasis'\n | 'text-caption'\n | 'text-caption-emphasis';\n\n/** @deprecated Use `TextVariant` instead. */\nexport type TextStyle = TextVariant;\n\n/**\n * Semantic color tokens for text.\n * Maps to TokoMo --color-foreground-* CSS custom properties.\n */\nexport type TextColorToken =\n | 'primary'\n | 'secondary'\n | 'tertiary'\n | 'brand'\n | 'negative'\n | 'positive'\n | 'warning'\n | 'caution'\n | 'ai'\n | 'on-strong'\n | 'on-bold'\n | 'inherit';\n\n/** Text color — semantic token or CSS variable escape hatch. */\nexport type TextColor = TextColorToken | `var(--${string})`;\n\nexport type TextDecoration = 'none' | 'underline' | 'dotted-underline';\nexport type TextAlign = 'left' | 'center' | 'right';\nexport type LineTruncation = 1 | 2 | 3 | 4 | 5 | 'none';\nexport type TextWrap = 'wrap' | 'nowrap' | 'balance' | 'pretty';\nexport type TextElement = 'p' | 'span' | 'div' | 'label' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';\n\nexport interface TextProps {\n /** Typography variant. Maps to a TokoMo typography class. Defaults to 'text-body-medium'. */\n variant?: TextVariant;\n children: React.ReactNode;\n\n // Appearance\n color?: TextColor;\n decoration?: TextDecoration;\n italic?: boolean;\n align?: TextAlign;\n\n // Truncation / wrap\n /** Max lines before ellipsis. Ignored when wrap is 'nowrap'. Defaults to 'none'. */\n lineTruncation?: LineTruncation;\n /** 'nowrap' = single line, truncates with ellipsis when constrained. */\n wrap?: TextWrap;\n\n // Semantic HTML\n as?: TextElement;\n id?: string;\n htmlFor?: string;\n\n // Escape hatches\n className?: string;\n style?: React.CSSProperties;\n}\n\nconst colorClassMap: Record<TextColorToken, string> = {\n primary: styles.colorPrimary,\n secondary: styles.colorSecondary,\n tertiary: styles.colorTertiary,\n brand: styles.colorBrand,\n negative: styles.colorNegative,\n positive: styles.colorPositive,\n warning: styles.colorWarning,\n caution: styles.colorCaution,\n ai: styles.colorAi,\n 'on-strong': styles.colorOnStrong,\n 'on-bold': styles.colorOnBold,\n inherit: styles.colorInherit,\n};\n\nconst decorationClassMap: Record<TextDecoration, string> = {\n none: styles.decorationNone,\n underline: styles.decorationUnderline,\n 'dotted-underline': styles.decorationDottedUnderline,\n};\n\nconst alignClassMap: Record<TextAlign, string> = {\n left: styles.alignLeft,\n center: styles.alignCenter,\n right: styles.alignRight,\n};\n\nconst truncationClassMap: Record<LineTruncation, string> = {\n 1: styles.truncate1Line,\n 2: styles.truncate2Lines,\n 3: styles.truncate3Lines,\n 4: styles.truncate4Lines,\n 5: styles.truncate5Lines,\n none: styles.noTruncation,\n};\n\nconst isCustomColor = (color: TextColor): color is `var(--${string})` =>\n color.startsWith('var(--');\n\nexport const Text = forwardRef<HTMLElement, TextProps>(\n (\n {\n variant = 'text-body-medium',\n children,\n color,\n decoration,\n italic,\n align,\n lineTruncation = 'none',\n wrap,\n as: Component = 'p',\n id,\n htmlFor,\n className = '',\n style,\n },\n ref\n ) => {\n const wrapClass =\n wrap === 'nowrap' ? styles.wrapNowrap\n : wrap === 'balance' ? styles.wrapBalance\n : wrap === 'pretty' ? styles.wrapPretty\n : '';\n const truncationClass =\n wrap === 'nowrap' ? '' : truncationClassMap[lineTruncation];\n\n const classes = [\n variant,\n styles.text,\n wrapClass,\n truncationClass,\n color && !isCustomColor(color) ? colorClassMap[color] : '',\n decoration ? decorationClassMap[decoration] : '',\n italic ? styles.italic : '',\n align ? alignClassMap[align] : '',\n className,\n ]\n .filter(Boolean)\n .join(' ');\n\n const computedStyle: React.CSSProperties = {\n ...style,\n ...(color && isCustomColor(color) ? { color } : {}),\n };\n\n const props: React.HTMLAttributes<HTMLElement> & { htmlFor?: string } = {\n className: classes,\n style: Object.keys(computedStyle).length > 0 ? computedStyle : undefined,\n ...(id ? { id } : {}),\n ...(Component === 'label' && htmlFor ? { htmlFor } : {}),\n };\n\n return (\n <Component ref={ref as React.Ref<never>} {...props}>\n {children}\n </Component>\n );\n }\n);\n\nText.displayName = 'Text';\n",
91
+ "type": "registry:ui"
92
+ }
93
+ ]
94
+ }