@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,68 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "scrollbar",
4
+ "title": "Scrollbar",
5
+ "description": "Custom scrollbar with draggable thumb, proportional sizing, and hover reveal.",
6
+ "type": "registry:ui",
7
+ "meta": {
8
+ "library": "@compomo/ui",
9
+ "distribution": "npm",
10
+ "storybook": "https://zainadeel.github.io/CompoMo/?path=/story/scrollbar",
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 { Scrollbar } from '@compomo/ui';",
15
+ "cssImport": "import '@compomo/ui/css';",
16
+ "typeImport": "import type { ScrollbarProps, ScrollbarVariant } 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
+ "children": {
28
+ "type": "React.ReactNode",
29
+ "required": true
30
+ },
31
+ "variant": {
32
+ "type": "'default' | 'thick'",
33
+ "default": "'default'"
34
+ },
35
+ "showTrackOnHover": {
36
+ "type": "boolean",
37
+ "default": "true"
38
+ },
39
+ "onScroll": {
40
+ "type": "(e: React.UIEvent<HTMLDivElement>) => void"
41
+ }
42
+ },
43
+ "exports": [
44
+ "Scrollbar"
45
+ ],
46
+ "types": [
47
+ "ScrollbarProps",
48
+ "ScrollbarVariant"
49
+ ]
50
+ },
51
+ "dependencies": [
52
+ "@compomo/ui",
53
+ "@ds-mo/tokens"
54
+ ],
55
+ "registryDependencies": [],
56
+ "files": [
57
+ {
58
+ "path": "src/components/Scrollbar/Scrollbar.module.css",
59
+ "content": ".container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n\n.content {\n width: 100%;\n height: 100%;\n min-height: 0;\n overflow-y: auto;\n overflow-x: auto;\n scrollbar-width: none;\n -ms-overflow-style: none;\n}\n\n.content::-webkit-scrollbar { display: none; }\n\n.track {\n position: absolute;\n top: 0; right: 0; bottom: 0;\n width: var(--dimension-size-050, 4px);\n pointer-events: none;\n z-index: 10;\n opacity: 0;\n transition: opacity var(--effect-motion-short-2);\n}\n\n.container[data-show-track='true'] .track {\n opacity: 1;\n pointer-events: auto;\n}\n\n.thumb {\n position: absolute;\n right: 0;\n width: var(--dimension-size-050, 4px);\n cursor: grab;\n border-radius: 0;\n background-color: var(--color-foreground-quaternary);\n transition: background-color var(--effect-motion-short-2);\n -webkit-user-select: none;\n user-select: none;\n}\n\n.thumb:active { cursor: grabbing; }\n\n.trackHorizontal {\n position: absolute;\n left: 0; right: 0; bottom: 0;\n height: var(--dimension-size-050, 4px);\n pointer-events: none;\n z-index: 10;\n opacity: 0;\n transition: opacity var(--effect-motion-short-2);\n}\n\n.container[data-show-track='true'] .trackHorizontal {\n opacity: 1;\n pointer-events: auto;\n}\n\n.thumbHorizontal {\n position: absolute;\n bottom: 0;\n height: var(--dimension-size-050, 4px);\n cursor: grab;\n border-radius: 0;\n background-color: var(--color-foreground-quaternary);\n transition: background-color var(--effect-motion-short-2);\n -webkit-user-select: none;\n user-select: none;\n}\n\n.thumbHorizontal:active { cursor: grabbing; }\n\n.thick .track { width: var(--dimension-size-100, 8px); }\n.thick .thumb { width: var(--dimension-size-100, 8px); }\n.thick .trackHorizontal { height: var(--dimension-size-100, 8px); }\n.thick .thumbHorizontal { height: var(--dimension-size-100, 8px); }\n",
60
+ "type": "registry:ui"
61
+ },
62
+ {
63
+ "path": "src/components/Scrollbar/Scrollbar.tsx",
64
+ "content": "import React, { useRef, useEffect, useState, useCallback } from 'react';\nimport { cn } from '@/utils/cn';\nimport styles from './Scrollbar.module.css';\n\nexport type ScrollbarVariant = 'default' | 'thick';\n\nexport interface ScrollbarProps {\n children: React.ReactNode;\n className?: string;\n variant?: ScrollbarVariant;\n showTrackOnHover?: boolean;\n onScroll?: (e: React.UIEvent<HTMLDivElement>) => void;\n fadeOverlay?: React.ReactNode;\n contentRef?: React.RefObject<HTMLDivElement>;\n}\n\nexport const Scrollbar: React.FC<ScrollbarProps> = ({\n children,\n className,\n variant = 'default',\n showTrackOnHover = true,\n onScroll,\n fadeOverlay,\n contentRef: externalContentRef,\n}) => {\n const containerRef = useRef<HTMLDivElement>(null);\n const internalContentRef = useRef<HTMLDivElement>(null);\n const contentRef = externalContentRef || internalContentRef;\n const thumbRef = useRef<HTMLDivElement>(null);\n const thumbHRef = useRef<HTMLDivElement>(null);\n const [thumbHeight, setThumbHeight] = useState(0);\n const [thumbTop, setThumbTop] = useState(0);\n const [thumbWidth, setThumbWidth] = useState(0);\n const [thumbLeft, setThumbLeft] = useState(0);\n const [isDragging, setIsDragging] = useState(false);\n const [isDraggingH, setIsDraggingH] = useState(false);\n const [isPointerInside, setIsPointerInside] = useState(false);\n const dragStartY = useRef(0);\n const dragStartScrollTop = useRef(0);\n const dragStartX = useRef(0);\n const dragStartScrollLeft = useRef(0);\n\n const handleMouseEnter = useCallback(() => {\n if (showTrackOnHover) setIsPointerInside(true);\n }, [showTrackOnHover]);\n\n const handleMouseLeave = useCallback(() => {\n setIsPointerInside(false);\n }, []);\n\n const updateThumb = useCallback(() => {\n const container = containerRef.current;\n const content = contentRef.current;\n if (!container || !content) return;\n\n const containerHeight = container.clientHeight;\n const containerWidth = container.clientWidth;\n const contentHeight = content.scrollHeight;\n const contentWidth = content.scrollWidth;\n const scrollTop = content.scrollTop;\n const scrollLeft = content.scrollLeft;\n\n if (contentHeight <= containerHeight) {\n setThumbHeight(0);\n } else {\n const ratio = containerHeight / contentHeight;\n const h = Math.max(20, containerHeight * ratio);\n setThumbHeight(h);\n setThumbTop((scrollTop / (contentHeight - containerHeight)) * (containerHeight - h));\n }\n\n if (contentWidth <= containerWidth) {\n setThumbWidth(0);\n } else {\n const ratio = containerWidth / contentWidth;\n const w = Math.max(20, containerWidth * ratio);\n setThumbWidth(w);\n setThumbLeft((scrollLeft / (contentWidth - containerWidth)) * (containerWidth - w));\n }\n }, [contentRef]);\n\n useEffect(() => {\n const content = contentRef.current;\n if (!content) return;\n const handleScroll = (e: Event) => {\n updateThumb();\n if (onScroll) onScroll(e as unknown as React.UIEvent<HTMLDivElement>);\n };\n content.addEventListener('scroll', handleScroll);\n return () => content.removeEventListener('scroll', handleScroll);\n }, [updateThumb, onScroll, contentRef]);\n\n useEffect(() => {\n const content = contentRef.current;\n if (!content) return;\n const handleResize = () => updateThumb();\n window.addEventListener('resize', handleResize);\n if (typeof ResizeObserver !== 'undefined') {\n const observer = new ResizeObserver(handleResize);\n observer.observe(content);\n return () => { window.removeEventListener('resize', handleResize); observer.disconnect(); };\n }\n return () => window.removeEventListener('resize', handleResize);\n }, [updateThumb, contentRef]);\n\n useEffect(() => { updateThumb(); }, [updateThumb, children]);\n\n const handleThumbMouseDown = useCallback((e: React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n const content = contentRef.current;\n if (!content) return;\n setIsDragging(true);\n dragStartY.current = e.clientY;\n dragStartScrollTop.current = content.scrollTop;\n const handleMouseMove = (e: MouseEvent) => {\n const container = containerRef.current;\n if (!container || !content) return;\n const deltaY = e.clientY - dragStartY.current;\n const scrollableHeight = content.scrollHeight - container.clientHeight;\n const scrollRatio = scrollableHeight / (container.clientHeight - thumbHeight);\n content.scrollTop = Math.max(0, Math.min(scrollableHeight, dragStartScrollTop.current + deltaY * scrollRatio));\n };\n const handleMouseUp = () => {\n setIsDragging(false);\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n };\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n }, [thumbHeight, contentRef]);\n\n const handleThumbHMouseDown = useCallback((e: React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n const content = contentRef.current;\n if (!content) return;\n setIsDraggingH(true);\n dragStartX.current = e.clientX;\n dragStartScrollLeft.current = content.scrollLeft;\n const handleMouseMove = (e: MouseEvent) => {\n const container = containerRef.current;\n if (!container || !content) return;\n const deltaX = e.clientX - dragStartX.current;\n const scrollableWidth = content.scrollWidth - container.clientWidth;\n const scrollRatio = scrollableWidth / (container.clientWidth - thumbWidth);\n content.scrollLeft = Math.max(0, Math.min(scrollableWidth, dragStartScrollLeft.current + deltaX * scrollRatio));\n };\n const handleMouseUp = () => {\n setIsDraggingH(false);\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n };\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n }, [thumbWidth, contentRef]);\n\n const showThumb = thumbHeight > 0;\n const showThumbH = thumbWidth > 0;\n const showTrack = showTrackOnHover && (isPointerInside || isDragging || isDraggingH);\n\n return (\n <div\n ref={containerRef}\n className={cn(styles.container, variant === 'thick' && styles.thick, className)}\n onMouseEnter={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n data-dragging={isDragging || isDraggingH}\n data-show-track={showTrack ? 'true' : 'false'}\n >\n <div ref={contentRef} className={styles.content}>{children}</div>\n {fadeOverlay}\n {showThumb && (\n <div className={styles.track}>\n <div ref={thumbRef} className={styles.thumb} style={{ height: `${thumbHeight}px`, top: `${thumbTop}px` }} onMouseDown={handleThumbMouseDown} />\n </div>\n )}\n {showThumbH && (\n <div className={styles.trackHorizontal}>\n <div ref={thumbHRef} className={styles.thumbHorizontal} style={{ width: `${thumbWidth}px`, left: `${thumbLeft}px` }} onMouseDown={handleThumbHMouseDown} />\n </div>\n )}\n </div>\n );\n};\n\nScrollbar.displayName = 'Scrollbar';\n",
65
+ "type": "registry:ui"
66
+ }
67
+ ]
68
+ }
@@ -0,0 +1,93 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "select",
4
+ "title": "Select",
5
+ "description": "Dropdown select using Menu internally. Supports placeholder, inactive state, and custom chevron icon.",
6
+ "type": "registry:ui",
7
+ "meta": {
8
+ "library": "@compomo/ui",
9
+ "distribution": "npm",
10
+ "storybook": "https://zainadeel.github.io/CompoMo/?path=/story/select",
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 { Select } from '@compomo/ui';",
15
+ "cssImport": "import '@compomo/ui/css';",
16
+ "typeImport": "import type { SelectProps, SelectOption } from '@compomo/ui';",
17
+ "internalDependencies": [
18
+ {
19
+ "component": "Menu",
20
+ "note": "Used internally by Select. No action needed — it's bundled in @compomo/ui."
21
+ },
22
+ {
23
+ "component": "Surface",
24
+ "note": "Used internally by Select. No action needed — it's bundled in @compomo/ui."
25
+ },
26
+ {
27
+ "component": "Text",
28
+ "note": "Used internally by Select. 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
+ "value": {
42
+ "type": "string | number",
43
+ "required": true
44
+ },
45
+ "onChange": {
46
+ "type": "(value: string | number) => void",
47
+ "required": true
48
+ },
49
+ "options": {
50
+ "type": "SelectOption[]",
51
+ "required": true
52
+ },
53
+ "placeholder": {
54
+ "type": "string",
55
+ "default": "'Select option'"
56
+ },
57
+ "inactive": {
58
+ "type": "boolean"
59
+ },
60
+ "chevronIcon": {
61
+ "type": "IconComponent"
62
+ }
63
+ },
64
+ "exports": [
65
+ "Select"
66
+ ],
67
+ "types": [
68
+ "SelectProps",
69
+ "SelectOption"
70
+ ]
71
+ },
72
+ "dependencies": [
73
+ "@compomo/ui",
74
+ "@ds-mo/tokens"
75
+ ],
76
+ "registryDependencies": [
77
+ "menu",
78
+ "surface",
79
+ "text"
80
+ ],
81
+ "files": [
82
+ {
83
+ "path": "src/components/Select/Select.module.css",
84
+ "content": ".selectWrapper {\n width: 100%;\n position: relative;\n}\n\n.selectButton {\n display: flex;\n align-items: center;\n justify-content: space-between;\n width: 100%;\n height: var(--dimension-size-400);\n padding: 0 var(--dimension-space-075);\n gap: var(--dimension-space-050);\n cursor: pointer;\n -webkit-user-select: none;\n user-select: none;\n border: none;\n outline: none;\n}\n\n.selectButton:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.chevron {\n display: flex;\n align-items: center;\n color: var(--color-foreground-secondary);\n flex-shrink: 0;\n}\n",
85
+ "type": "registry:ui"
86
+ },
87
+ {
88
+ "path": "src/components/Select/Select.tsx",
89
+ "content": "import React, { forwardRef, useState, useRef } from 'react';\nimport { Surface } from '@/components/Surface';\nimport { Text } from '@/components/Text';\nimport { Menu } from '@/components/Menu';\nimport type { MenuItemData } from '@/components/Menu';\nimport type { IconComponent } from '@/types';\nimport styles from './Select.module.css';\n\nexport interface SelectOption {\n label: string;\n value: string | number;\n}\n\nexport interface SelectProps {\n value: string | number;\n onChange: (value: string | number) => void;\n options: SelectOption[];\n placeholder?: string;\n className?: string;\n inactive?: boolean;\n id?: string;\n chevronIcon?: IconComponent;\n}\n\nexport const Select = forwardRef<HTMLDivElement, SelectProps>(\n ({ value, onChange, options, placeholder = 'Select option', className, inactive = false, id, chevronIcon: ChevronIcon }, ref) => {\n const [isMenuOpen, setIsMenuOpen] = useState(false);\n const buttonRef = useRef<HTMLButtonElement>(null);\n\n const selectedOption = options.find(opt => opt.value === value);\n const selectedLabel = selectedOption?.label || placeholder;\n\n const menuItems: MenuItemData[] = options.map(option => ({\n label: option.label,\n onClick: () => {\n onChange(option.value);\n setIsMenuOpen(false);\n },\n isSelected: option.value === value,\n }));\n\n const handleToggle = () => {\n if (!inactive) setIsMenuOpen(prev => !prev);\n };\n\n return (\n <div ref={ref} className={`${styles.selectWrapper} ${className || ''}`}>\n <Surface\n id={id}\n ref={buttonRef as React.RefObject<HTMLElement>}\n as=\"button\"\n type=\"button\"\n elevation=\"elevated\"\n radius=\"sm\"\n interactive\n selected={isMenuOpen}\n className={styles.selectButton}\n onClick={handleToggle}\n inactive={inactive}\n >\n <Text\n variant=\"text-body-medium\"\n as=\"span\"\n \n color={selectedOption ? 'primary' : 'secondary'}\n >\n {selectedLabel}\n </Text>\n {ChevronIcon && (\n <span className={styles.chevron}>\n <ChevronIcon size={16} />\n </span>\n )}\n </Surface>\n <Menu\n isOpen={isMenuOpen}\n onClose={() => setIsMenuOpen(false)}\n anchorRef={buttonRef as React.RefObject<HTMLElement | null>}\n items={menuItems}\n side=\"bottom\"\n align=\"start\"\n sideOffset={4}\n alignOffset={-4}\n matchAnchorWidth\n matchAnchorWidthOffset={8}\n />\n </div>\n );\n }\n);\n\nSelect.displayName = 'Select';\n",
90
+ "type": "registry:ui"
91
+ }
92
+ ]
93
+ }
@@ -0,0 +1,105 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "sidebar",
4
+ "title": "Sidebar",
5
+ "description": "Resizable navigation sidebar with drag handle, snap-to-collapse, mobile mode, and nested sections/items.",
6
+ "type": "registry:ui",
7
+ "meta": {
8
+ "library": "@compomo/ui",
9
+ "distribution": "npm",
10
+ "storybook": "https://zainadeel.github.io/CompoMo/?path=/story/sidebar",
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 { Sidebar, SidebarSection, SidebarItem } from '@compomo/ui';",
15
+ "cssImport": "import '@compomo/ui/css';",
16
+ "typeImport": "import type { SidebarProps, SidebarWidth, SidebarSectionProps, SidebarItemProps } from '@compomo/ui';",
17
+ "internalDependencies": [
18
+ {
19
+ "component": "Text",
20
+ "note": "Used internally by Sidebar. No action needed — it's bundled in @compomo/ui."
21
+ },
22
+ {
23
+ "component": "Surface",
24
+ "note": "Used internally by Sidebar. No action needed — it's bundled in @compomo/ui."
25
+ }
26
+ ],
27
+ "peerDependencies": {
28
+ "required": [
29
+ "react >=17",
30
+ "react-dom >=17",
31
+ "@ds-mo/tokens >=0.1.0"
32
+ ],
33
+ "optional": []
34
+ }
35
+ },
36
+ "props": {
37
+ "children": {
38
+ "type": "React.ReactNode",
39
+ "required": true
40
+ },
41
+ "isCollapsed": {
42
+ "type": "boolean",
43
+ "default": "false"
44
+ },
45
+ "onToggle": {
46
+ "type": "() => void"
47
+ },
48
+ "width": {
49
+ "type": "'mini' | 'default' | number",
50
+ "default": "'default'"
51
+ },
52
+ "resizable": {
53
+ "type": "boolean",
54
+ "default": "true"
55
+ },
56
+ "footer": {
57
+ "type": "React.ReactNode"
58
+ },
59
+ "isMobile": {
60
+ "type": "boolean"
61
+ }
62
+ },
63
+ "exports": [
64
+ "Sidebar",
65
+ "SidebarSection",
66
+ "SidebarItem"
67
+ ],
68
+ "types": [
69
+ "SidebarProps",
70
+ "SidebarWidth",
71
+ "SidebarSectionProps",
72
+ "SidebarItemProps"
73
+ ]
74
+ },
75
+ "dependencies": [
76
+ "@compomo/ui",
77
+ "@ds-mo/tokens"
78
+ ],
79
+ "registryDependencies": [
80
+ "text",
81
+ "surface"
82
+ ],
83
+ "files": [
84
+ {
85
+ "path": "src/components/Sidebar/Sidebar.tsx",
86
+ "content": "import React, { forwardRef, useRef, useCallback, useEffect, useState } from 'react';\nimport { cn } from '@/utils/cn';\nimport styles from './Sidebar.module.css';\n\nexport type SidebarWidth = 'mini' | 'default' | number;\n\nexport interface SidebarProps {\n children: React.ReactNode;\n isCollapsed?: boolean;\n onToggle?: () => void;\n width?: SidebarWidth;\n onWidthChange?: (width: SidebarWidth) => void;\n resizable?: boolean;\n footer?: React.ReactNode;\n className?: string;\n isMobile?: boolean;\n}\n\nconst MINI_WIDTH = 56;\nconst DEFAULT_WIDTH = 240;\nconst MIN_DRAG_WIDTH = 180;\nconst SNAP_THRESHOLD = 140;\n\nexport const Sidebar = forwardRef<HTMLElement, SidebarProps>(\n ({\n children,\n isCollapsed = false,\n onToggle,\n width = 'default',\n onWidthChange,\n resizable = true,\n footer,\n className,\n isMobile = false,\n }, ref) => {\n const internalRef = useRef<HTMLElement>(null);\n const sidebarRef = (ref as React.RefObject<HTMLElement>) || internalRef;\n const [isResizing, setIsResizing] = useState(false);\n\n const isMini = width === 'mini';\n const resolvedWidth = width === 'mini' ? MINI_WIDTH : width === 'default' ? DEFAULT_WIDTH : width;\n\n const handleMouseDown = useCallback((e: React.MouseEvent) => {\n if (!resizable || !onWidthChange) return;\n e.preventDefault();\n setIsResizing(true);\n const startX = e.clientX;\n\n const handleMouseMove = (e: MouseEvent) => {\n const newWidth = e.clientX;\n if (newWidth < SNAP_THRESHOLD) {\n onWidthChange('mini');\n } else if (newWidth < MIN_DRAG_WIDTH) {\n onWidthChange('default');\n } else {\n onWidthChange(newWidth);\n }\n };\n\n const handleMouseUp = () => {\n setIsResizing(false);\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n };\n\n document.body.style.cursor = 'col-resize';\n document.body.style.userSelect = 'none';\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n }, [resizable, onWidthChange]);\n\n // Close sidebar on Escape (mobile)\n useEffect(() => {\n if (!isMobile || isCollapsed) return;\n const handleEscape = (e: KeyboardEvent) => {\n if (e.key === 'Escape') onToggle?.();\n };\n document.addEventListener('keydown', handleEscape);\n return () => document.removeEventListener('keydown', handleEscape);\n }, [isMobile, isCollapsed, onToggle]);\n\n if (isCollapsed && !isMobile) return null;\n\n return (\n <>\n {/* Mobile backdrop */}\n {isMobile && !isCollapsed && (\n <div className={styles.backdrop} onClick={onToggle} aria-hidden />\n )}\n <aside\n ref={sidebarRef}\n className={cn(\n styles.sidebar,\n isMini && styles.mini,\n isMobile && styles.mobile,\n isCollapsed && styles.collapsed,\n isResizing && styles.resizing,\n className\n )}\n style={{ width: isCollapsed ? 0 : resolvedWidth }}\n role=\"navigation\"\n aria-label=\"Sidebar\"\n >\n <div className={styles.content}>\n {children}\n </div>\n {footer && (\n <div className={styles.footer}>\n {footer}\n </div>\n )}\n {resizable && !isMobile && (\n <div\n className={styles.resizeHandle}\n onMouseDown={handleMouseDown}\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label=\"Resize sidebar\"\n />\n )}\n </aside>\n </>\n );\n }\n);\n\nSidebar.displayName = 'Sidebar';\n",
87
+ "type": "registry:ui"
88
+ },
89
+ {
90
+ "path": "src/components/Sidebar/Sidebar.module.css",
91
+ "content": "/* Sidebar */\n.sidebar {\n display: flex;\n flex-direction: column;\n height: 100%;\n background-color: var(--color-background-secondary);\n border-right: var(--dimension-stroke-width-015) solid var(--color-border-primary);\n position: relative;\n flex-shrink: 0;\n overflow: hidden;\n transition: width var(--effect-motion-short-2);\n}\n\n.sidebar.resizing { transition: none; }\n\n.sidebar.collapsed { width: 0 !important; border-right: none; }\n\n.sidebar.mobile {\n position: fixed;\n top: 0;\n left: 0;\n bottom: 0;\n z-index: 9997;\n}\n\n.sidebar.mini .itemLabel { display: none; }\n.sidebar.mini .itemBadge { display: none; }\n.sidebar.mini .sectionHeader { display: none; }\n\n.backdrop {\n position: fixed;\n inset: 0;\n background-color: var(--color-background-shade);\n z-index: 9996;\n}\n\n.content {\n flex: 1;\n overflow-y: auto;\n overflow-x: hidden;\n padding: var(--dimension-space-050);\n scrollbar-width: none;\n}\n\n.content::-webkit-scrollbar { display: none; }\n\n.footer {\n flex-shrink: 0;\n padding: var(--dimension-space-050);\n border-top: var(--dimension-stroke-width-015) solid var(--color-border-primary);\n}\n\n/* Resize Handle */\n.resizeHandle {\n position: absolute;\n top: 0;\n right: -3px;\n bottom: 0;\n width: 6px;\n cursor: col-resize;\n z-index: 10;\n}\n\n.resizeHandle:hover {\n background-color: var(--color-border-brand);\n opacity: 0.5;\n}\n\n/* Sections */\n.section {\n display: flex;\n flex-direction: column;\n}\n\n.section + .section {\n margin-top: var(--dimension-space-050);\n padding-top: var(--dimension-space-050);\n border-top: var(--dimension-stroke-width-015) solid var(--color-border-primary);\n}\n\n.sectionHeader {\n padding: var(--dimension-space-050) var(--dimension-space-075);\n -webkit-user-select: none;\n user-select: none;\n}\n\n.sectionHeaderClickable { cursor: pointer; }\n\n.sectionItems {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-025);\n}\n\n/* Items */\n.itemWrapper {\n display: flex;\n flex-direction: column;\n}\n\n.item {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-075);\n padding: var(--dimension-space-075);\n background-color: transparent;\n border: none;\n border-radius: var(--dimension-radius-075);\n cursor: pointer;\n width: 100%;\n text-align: left;\n position: relative;\n min-height: var(--dimension-size-400);\n transition: background-color var(--effect-motion-short-2);\n}\n\n.item::before {\n content: '';\n position: absolute;\n inset: 0;\n background-color: transparent;\n border-radius: var(--dimension-radius-075);\n transition: var(--effect-transition-interaction-background-short-2);\n pointer-events: none;\n}\n\n.item:hover::before { background-color: var(--color-interaction-hover); }\n.item:active::before { background-color: var(--color-interaction-pressed); }\n\n.itemSelected { background-color: var(--color-background-faint-brand); }\n.itemSelected::before { display: none; }\n\n.itemDisabled { opacity: 0.5; cursor: not-allowed; pointer-events: none; }\n\n.itemIcon {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n color: var(--color-foreground-secondary);\n}\n\n.itemSelected .itemIcon { color: var(--color-foreground-primary); }\n\n.itemLabel {\n flex: 1;\n min-width: 0;\n}\n\n.itemBadge {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n}\n\n/* Sub-items */\n.subItems {\n display: flex;\n flex-direction: column;\n padding-left: var(--dimension-space-300);\n gap: var(--dimension-space-025);\n}\n",
92
+ "type": "registry:ui"
93
+ },
94
+ {
95
+ "path": "src/components/Sidebar/SidebarItem.tsx",
96
+ "content": "import React, { forwardRef } from 'react';\nimport { cn } from '@/utils/cn';\nimport { Text } from '@/components/Text';\nimport type { IconComponent } from '@/types';\nimport styles from './Sidebar.module.css';\n\nexport interface SidebarItemProps {\n label: string;\n icon?: IconComponent;\n isSelected?: boolean;\n isDisabled?: boolean;\n onClick?: () => void;\n badge?: React.ReactNode;\n children?: React.ReactNode;\n className?: string;\n}\n\nexport const SidebarItem = forwardRef<HTMLButtonElement, SidebarItemProps>(\n ({ label, icon: Icon, isSelected = false, isDisabled = false, onClick, badge, children, className }, ref) => (\n <div className={cn(styles.itemWrapper, className)}>\n <button\n ref={ref}\n className={cn(\n styles.item,\n isSelected && styles.itemSelected,\n isDisabled && styles.itemDisabled\n )}\n onClick={onClick}\n disabled={isDisabled}\n type=\"button\"\n aria-current={isSelected ? 'page' : undefined}\n >\n {Icon && (\n <span className={styles.itemIcon}>\n <Icon size={20} />\n </span>\n )}\n <Text\n variant={isSelected ? 'text-body-medium-emphasis' : 'text-body-medium'}\n as=\"span\"\n className={styles.itemLabel}\n lineTruncation={1}\n >\n {label}\n </Text>\n {badge && <span className={styles.itemBadge}>{badge}</span>}\n </button>\n {children && isSelected && (\n <div className={styles.subItems}>\n {children}\n </div>\n )}\n </div>\n )\n);\n\nSidebarItem.displayName = 'SidebarItem';\n",
97
+ "type": "registry:ui"
98
+ },
99
+ {
100
+ "path": "src/components/Sidebar/SidebarSection.tsx",
101
+ "content": "import React, { forwardRef, useState } from 'react';\nimport { cn } from '@/utils/cn';\nimport { Text } from '@/components/Text';\nimport styles from './Sidebar.module.css';\n\nexport interface SidebarSectionProps {\n label?: string;\n children: React.ReactNode;\n collapsible?: boolean;\n defaultCollapsed?: boolean;\n className?: string;\n}\n\nexport const SidebarSection = forwardRef<HTMLDivElement, SidebarSectionProps>(\n ({ label, children, collapsible = false, defaultCollapsed = false, className }, ref) => {\n const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);\n\n return (\n <div ref={ref} className={cn(styles.section, className)}>\n {label && (\n <div\n className={cn(styles.sectionHeader, collapsible && styles.sectionHeaderClickable)}\n onClick={collapsible ? () => setIsCollapsed(prev => !prev) : undefined}\n role={collapsible ? 'button' : undefined}\n aria-expanded={collapsible ? !isCollapsed : undefined}\n >\n <Text variant=\"text-caption-emphasis\" as=\"span\" color=\"tertiary\">\n {label}\n </Text>\n </div>\n )}\n {!isCollapsed && (\n <div className={styles.sectionItems}>\n {children}\n </div>\n )}\n </div>\n );\n }\n);\n\nSidebarSection.displayName = 'SidebarSection';\n",
102
+ "type": "registry:ui"
103
+ }
104
+ ]
105
+ }
@@ -0,0 +1,72 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "skeleton",
4
+ "title": "Skeleton",
5
+ "description": "Loading placeholder with text, circular, and rectangular variants. Supports multi-line text and shimmer animation.",
6
+ "type": "registry:ui",
7
+ "meta": {
8
+ "library": "@compomo/ui",
9
+ "distribution": "npm",
10
+ "storybook": "https://zainadeel.github.io/CompoMo/?path=/story/skeleton",
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 { Skeleton } from '@compomo/ui';",
15
+ "cssImport": "import '@compomo/ui/css';",
16
+ "typeImport": "import type { SkeletonProps, SkeletonVariant } 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": "'text' | 'circular' | 'rectangular'",
29
+ "default": "'text'"
30
+ },
31
+ "width": {
32
+ "type": "string | number"
33
+ },
34
+ "height": {
35
+ "type": "string | number"
36
+ },
37
+ "lines": {
38
+ "type": "number",
39
+ "default": "1",
40
+ "description": "Text variant only. Last line renders at 60% width."
41
+ },
42
+ "animate": {
43
+ "type": "boolean",
44
+ "default": "true"
45
+ }
46
+ },
47
+ "exports": [
48
+ "Skeleton"
49
+ ],
50
+ "types": [
51
+ "SkeletonProps",
52
+ "SkeletonVariant"
53
+ ]
54
+ },
55
+ "dependencies": [
56
+ "@compomo/ui",
57
+ "@ds-mo/tokens"
58
+ ],
59
+ "registryDependencies": [],
60
+ "files": [
61
+ {
62
+ "path": "src/components/Skeleton/Skeleton.module.css",
63
+ "content": ".skeleton {\n background-color: var(--color-background-secondary);\n position: relative;\n overflow: hidden;\n}\n\n/* ─── Variants ──────────────────────────────────────────────────────────────── */\n\n.text {\n height: 14px;\n border-radius: var(--dimension-radius-050);\n width: 100%;\n}\n\n.circular {\n width: 40px;\n height: 40px;\n border-radius: var(--dimension-radius-half);\n}\n\n.rectangular {\n width: 100%;\n height: 80px;\n border-radius: var(--dimension-radius-075);\n}\n\n/* ─── Text group ────────────────────────────────────────────────────────────── */\n\n.textGroup {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-075);\n}\n\n/* ─── Shimmer animation ─────────────────────────────────────────────────────── */\n\n.animate::after {\n content: '';\n position: absolute;\n inset: 0;\n background: linear-gradient(\n 90deg,\n transparent 0%,\n var(--color-interaction-hover) 50%,\n transparent 100%\n );\n animation: shimmer 1.5s ease-in-out infinite;\n}\n\n@keyframes shimmer {\n 0% { transform: translateX(-100%); }\n 100% { transform: translateX(100%); }\n}\n",
64
+ "type": "registry:ui"
65
+ },
66
+ {
67
+ "path": "src/components/Skeleton/Skeleton.tsx",
68
+ "content": "import React, { forwardRef } from 'react';\nimport { cn } from '@/utils/cn';\nimport styles from './Skeleton.module.css';\n\nexport type SkeletonVariant = 'text' | 'circular' | 'rectangular';\n\nexport interface SkeletonProps {\n /** Shape of the skeleton. */\n variant?: SkeletonVariant;\n /** Width (CSS value). */\n width?: string | number;\n /** Height (CSS value). */\n height?: string | number;\n /** Number of text lines to render. Only applies to 'text' variant. */\n lines?: number;\n /** Whether to animate the shimmer. */\n animate?: boolean;\n className?: string;\n}\n\nexport const Skeleton = forwardRef<HTMLDivElement, SkeletonProps>(\n (\n {\n variant = 'text',\n width,\n height,\n lines = 1,\n animate = true,\n className,\n },\n ref\n ) => {\n if (variant === 'text' && lines > 1) {\n return (\n <div ref={ref} className={cn(styles.textGroup, className)} style={{ width }}>\n {Array.from({ length: lines }, (_, i) => (\n <div\n key={i}\n className={cn(styles.skeleton, styles.text, animate && styles.animate)}\n style={{\n width: i === lines - 1 ? '60%' : '100%',\n height,\n }}\n />\n ))}\n </div>\n );\n }\n\n return (\n <div\n ref={ref}\n className={cn(\n styles.skeleton,\n styles[variant],\n animate && styles.animate,\n className\n )}\n style={{ width, height }}\n aria-hidden=\"true\"\n />\n );\n }\n);\n\nSkeleton.displayName = 'Skeleton';\n",
69
+ "type": "registry:ui"
70
+ }
71
+ ]
72
+ }
@@ -0,0 +1,87 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "slider",
4
+ "title": "Slider",
5
+ "description": "Range input with visual fill track, label, and current value display.",
6
+ "type": "registry:ui",
7
+ "meta": {
8
+ "library": "@compomo/ui",
9
+ "distribution": "npm",
10
+ "storybook": "https://zainadeel.github.io/CompoMo/?path=/story/slider",
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 { Slider } from '@compomo/ui';",
15
+ "cssImport": "import '@compomo/ui/css';",
16
+ "typeImport": "import type { SliderProps } from '@compomo/ui';",
17
+ "internalDependencies": [
18
+ {
19
+ "component": "Text",
20
+ "note": "Used internally by Slider. 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
+ "value": {
34
+ "type": "number",
35
+ "required": true
36
+ },
37
+ "onChange": {
38
+ "type": "(value: number) => void",
39
+ "required": true
40
+ },
41
+ "min": {
42
+ "type": "number",
43
+ "default": "0"
44
+ },
45
+ "max": {
46
+ "type": "number",
47
+ "default": "100"
48
+ },
49
+ "step": {
50
+ "type": "number",
51
+ "default": "1"
52
+ },
53
+ "label": {
54
+ "type": "string",
55
+ "required": true
56
+ },
57
+ "inactive": {
58
+ "type": "boolean"
59
+ }
60
+ },
61
+ "exports": [
62
+ "Slider"
63
+ ],
64
+ "types": [
65
+ "SliderProps"
66
+ ]
67
+ },
68
+ "dependencies": [
69
+ "@compomo/ui",
70
+ "@ds-mo/tokens"
71
+ ],
72
+ "registryDependencies": [
73
+ "text"
74
+ ],
75
+ "files": [
76
+ {
77
+ "path": "src/components/Slider/Slider.module.css",
78
+ "content": ".field { display: flex; flex-direction: column; gap: var(--dimension-space-050); width: 100%; }\n.labelRow { display: flex; align-items: baseline; justify-content: space-between; gap: var(--dimension-space-050); }\n\n.track {\n position: relative; width: 100%; height: var(--dimension-size-400);\n padding: 0 var(--dimension-space-025); border-radius: var(--dimension-radius-075); cursor: pointer; -webkit-user-select: none;\n user-select: none;\n}\n\n.trackInner {\n position: absolute; inset: 0; border-radius: inherit;\n background-color: var(--color-background-primary);\n box-shadow: var(--effect-elevation-depressed-sm); pointer-events: none; z-index: 1;\n}\n\n.fillTrack { position: absolute; top: var(--dimension-space-025); right: 0; bottom: var(--dimension-space-025); left: 0; z-index: 1; }\n\n.fill {\n position: absolute; left: var(--dimension-space-025); top: 0; bottom: 0;\n width: max(0px, calc((var(--slider-pct, 0) / 100) * (100% - var(--dimension-size-150)) - var(--dimension-space-025)));\n background-color: var(--color-background-bold-brand);\n box-shadow: var(--effect-elevation-elevated-sm);\n border-radius: var(--dimension-radius-050);\n transition: opacity var(--effect-motion-short-2);\n}\n\n.trackAtMin .fill { opacity: 0; }\n\n.thumb {\n position: absolute; top: 0; bottom: 0;\n left: calc((var(--slider-pct, 0) / 100) * (100% - var(--dimension-size-150)));\n width: var(--dimension-size-150); padding: var(--dimension-space-025);\n box-sizing: border-box; pointer-events: none; z-index: 2;\n}\n\n.thumb::before {\n content: ''; position: absolute; inset: var(--dimension-space-025);\n background-color: var(--color-background-primary);\n box-shadow: var(--effect-elevation-elevated-sm);\n border-radius: var(--dimension-radius-050);\n}\n\n.thumb::after {\n content: ''; position: absolute; inset: var(--dimension-space-025);\n background-color: transparent; border-radius: var(--dimension-radius-050);\n transition: var(--effect-transition-interaction-background-instant);\n}\n\n.track:hover:not(.trackInactive) .thumb::after { background-color: var(--color-interaction-hover); }\n.track:active:not(.trackInactive) .thumb::after { background-color: var(--color-interaction-pressed); }\n\n.trackInactive { cursor: not-allowed; opacity: 0.5; }\n\n.input {\n position: absolute; inset: 0; width: 100%; height: 100%;\n margin: 0; padding: 0; opacity: 0; cursor: pointer;\n -webkit-appearance: none; appearance: none; outline: none; z-index: 3;\n}\n\n.input:disabled { cursor: not-allowed; }\n.input:hover { cursor: grab; }\n.input:active { cursor: grabbing; }\n",
79
+ "type": "registry:ui"
80
+ },
81
+ {
82
+ "path": "src/components/Slider/Slider.tsx",
83
+ "content": "import React, { forwardRef, useCallback, useEffect, useId, useRef, useState } from 'react';\nimport { cn } from '@/utils/cn';\nimport { Text } from '@/components/Text';\nimport styles from './Slider.module.css';\n\nexport interface SliderProps {\n value: number;\n onChange: (value: number) => void;\n min?: number;\n max?: number;\n step?: number;\n label: string;\n id?: string;\n inactive?: boolean;\n className?: string;\n}\n\nexport const Slider = forwardRef<HTMLDivElement, SliderProps>(\n ({ value, onChange, min = 0, max = 100, step = 1, label, id: idProp, inactive = false, className }, ref) => {\n const generatedId = useId();\n const id = idProp ?? generatedId;\n const containerRef = useRef<HTMLDivElement>(null);\n const [pct, setPct] = useState(() => {\n const range = max - min;\n return range === 0 ? 0 : Math.round(((value - min) / range) * 100);\n });\n\n const sync = useCallback(() => {\n const range = max - min;\n setPct(range === 0 ? 0 : Math.round(((value - min) / range) * 100));\n }, [value, min, max]);\n\n useEffect(() => { sync(); }, [sync]);\n\n const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {\n onChange(Number(e.target.value));\n };\n\n return (\n <div ref={ref} className={cn(styles.field, className)}>\n <div className={styles.labelRow}>\n <Text variant=\"text-body-small-emphasis\" as=\"label\" htmlFor={id}>{label}</Text>\n <Text variant=\"text-body-small-emphasis\" as=\"span\">{value}</Text>\n </div>\n <div\n ref={containerRef}\n className={cn(styles.track, inactive && styles.trackInactive, pct === 0 && styles.trackAtMin)}\n style={{ '--slider-pct': pct } as React.CSSProperties}\n >\n <div className={styles.trackInner}>\n <div className={styles.fillTrack}><div className={styles.fill} /></div>\n <div className={styles.thumb} />\n </div>\n <input\n type=\"range\"\n id={id}\n min={min}\n max={max}\n step={step}\n value={value}\n onChange={handleInput}\n disabled={inactive}\n className={styles.input}\n aria-valuemin={min}\n aria-valuemax={max}\n aria-valuenow={value}\n />\n </div>\n </div>\n );\n }\n);\n\nSlider.displayName = 'Slider';\n",
84
+ "type": "registry:ui"
85
+ }
86
+ ]
87
+ }
@@ -0,0 +1,97 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "surface",
4
+ "title": "Surface",
5
+ "description": "Foundational container with intent-based or background-based coloring, elevation, edges, radius, and interactive states.",
6
+ "type": "registry:ui",
7
+ "meta": {
8
+ "library": "@compomo/ui",
9
+ "distribution": "npm",
10
+ "storybook": "https://zainadeel.github.io/CompoMo/?path=/story/surface",
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 { Surface } from '@compomo/ui';",
15
+ "cssImport": "import '@compomo/ui/css';",
16
+ "typeImport": "import type { SurfaceProps, SurfaceIntent, SurfaceBackground, SurfaceContrast, SurfaceElevation, SurfaceEdge, SurfaceRadius, SurfaceRadiusPreset, SurfaceElement } 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
+ "intent": {
28
+ "type": "'brand' | 'positive' | 'negative' | 'warning' | 'caution' | 'ai' | 'neutral' | 'walkthrough' | 'guide'",
29
+ "description": "Semantic intent coloring (mutually exclusive with background)."
30
+ },
31
+ "contrast": {
32
+ "type": "'faint' | 'medium' | 'bold' | 'strong'",
33
+ "default": "'faint'"
34
+ },
35
+ "background": {
36
+ "type": "'primary' | 'secondary' | 'transparent' | 'translucent'",
37
+ "description": "Background mode (mutually exclusive with intent)."
38
+ },
39
+ "elevation": {
40
+ "type": "'none' | 'depressed' | 'depressed-md' | 'flat' | 'elevated' | 'floating' | 'overlayTop' | 'overlayRight' | 'overlayBottom' | 'overlayLeft'",
41
+ "default": "'none'"
42
+ },
43
+ "edge": {
44
+ "type": "SurfaceEdge | SurfaceEdge[]",
45
+ "description": "Border edges. Only applies when elevation is none."
46
+ },
47
+ "radius": {
48
+ "type": "'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full' | string"
49
+ },
50
+ "interactive": {
51
+ "type": "boolean",
52
+ "description": "Enables hover/press overlay."
53
+ },
54
+ "selected": {
55
+ "type": "boolean"
56
+ },
57
+ "inactive": {
58
+ "type": "boolean"
59
+ },
60
+ "as": {
61
+ "type": "'div' | 'section' | 'aside' | 'article' | 'header' | 'footer' | 'main' | 'nav' | 'span' | 'button' | 'td' | 'th'",
62
+ "default": "'div'"
63
+ }
64
+ },
65
+ "exports": [
66
+ "Surface"
67
+ ],
68
+ "types": [
69
+ "SurfaceProps",
70
+ "SurfaceIntent",
71
+ "SurfaceBackground",
72
+ "SurfaceContrast",
73
+ "SurfaceElevation",
74
+ "SurfaceEdge",
75
+ "SurfaceRadius",
76
+ "SurfaceRadiusPreset",
77
+ "SurfaceElement"
78
+ ]
79
+ },
80
+ "dependencies": [
81
+ "@compomo/ui",
82
+ "@ds-mo/tokens"
83
+ ],
84
+ "registryDependencies": [],
85
+ "files": [
86
+ {
87
+ "path": "src/components/Surface/Surface.module.css",
88
+ "content": "/**\n * Surface — lean container primitive.\n * Backgrounds, elevation, edges, radius, and interactive states.\n * Hover/pressed via direct background-color + color-mix(). No pseudo-element overlays.\n */\n\n.surface {\n box-sizing: border-box;\n position: relative;\n /* Reset native button/anchor chrome when rendered as those elements */\n border: none;\n padding: 0;\n margin: 0;\n font: inherit;\n color: inherit;\n background: none;\n text-decoration: none;\n text-align: left;\n cursor: default;\n appearance: none;\n -webkit-appearance: none;\n}\n\n/* ==========================================================================\n Backgrounds\n ========================================================================== */\n\n.bgPrimary {\n background-color: var(--color-background-primary);\n}\n\n.bgSecondary {\n background-color: var(--color-background-secondary);\n}\n\n.bgTransparent {\n background-color: transparent;\n}\n\n.bgTranslucent {\n background-color: var(--color-background-translucent);\n backdrop-filter: blur(var(--effect-blur-md));\n -webkit-backdrop-filter: blur(var(--effect-blur-md));\n}\n\n/* ==========================================================================\n Semantic Intent Backgrounds — intent{Intent}{Contrast}\n ========================================================================== */\n\n/* Brand */\n.intentBrandFaint { background-color: var(--color-background-faint-brand); }\n.intentBrandMedium { background-color: var(--color-background-medium-brand); }\n.intentBrandBold { background-color: var(--color-background-bold-brand); }\n.intentBrandStrong { background-color: var(--color-background-strong-brand); }\n\n/* Positive */\n.intentPositiveFaint { background-color: var(--color-background-faint-positive); }\n.intentPositiveMedium { background-color: var(--color-background-medium-positive); }\n.intentPositiveBold { background-color: var(--color-background-bold-positive); }\n.intentPositiveStrong { background-color: var(--color-background-strong-positive); }\n\n/* Negative */\n.intentNegativeFaint { background-color: var(--color-background-faint-negative); }\n.intentNegativeMedium { background-color: var(--color-background-medium-negative); }\n.intentNegativeBold { background-color: var(--color-background-bold-negative); }\n.intentNegativeStrong { background-color: var(--color-background-strong-negative); }\n\n/* Warning */\n.intentWarningFaint { background-color: var(--color-background-faint-warning); }\n.intentWarningMedium { background-color: var(--color-background-medium-warning); }\n.intentWarningBold { background-color: var(--color-background-bold-warning); }\n.intentWarningStrong { background-color: var(--color-background-strong-warning); }\n\n/* Caution */\n.intentCautionFaint { background-color: var(--color-background-faint-caution); }\n.intentCautionMedium { background-color: var(--color-background-medium-caution); }\n.intentCautionBold { background-color: var(--color-background-bold-caution); }\n.intentCautionStrong { background-color: var(--color-background-strong-caution); }\n\n/* AI */\n.intentAiFaint { background-color: var(--color-background-faint-ai); }\n.intentAiMedium { background-color: var(--color-background-medium-ai); }\n.intentAiBold { background-color: var(--color-background-bold-ai); }\n.intentAiStrong { background-color: var(--color-background-strong-ai); }\n\n/* Neutral */\n.intentNeutralFaint { background-color: var(--color-background-faint-neutral); }\n.intentNeutralMedium { background-color: var(--color-background-medium-neutral); }\n.intentNeutralBold { background-color: var(--color-background-bold-neutral); }\n.intentNeutralStrong { background-color: var(--color-background-strong-neutral); }\n\n/* Walkthrough */\n.intentWalkthroughFaint { background-color: var(--color-background-faint-walkthrough); }\n.intentWalkthroughMedium { background-color: var(--color-background-medium-walkthrough); }\n.intentWalkthroughBold { background-color: var(--color-background-bold-walkthrough); }\n.intentWalkthroughStrong { background-color: var(--color-background-strong-walkthrough); }\n\n/* Guide */\n.intentGuideFaint { background-color: var(--color-background-faint-guide); }\n.intentGuideMedium { background-color: var(--color-background-medium-guide); }\n.intentGuideBold { background-color: var(--color-background-bold-guide); }\n.intentGuideStrong { background-color: var(--color-background-strong-guide); }\n\n/* ==========================================================================\n Elevation — progressive: none → flat → elevated → floating\n ========================================================================== */\n\n.elevationNone {}\n\n.elevationDepressed {\n box-shadow: var(--effect-elevation-depressed-sm);\n}\n\n.elevationDepressedMd {\n box-shadow: var(--effect-elevation-depressed-md);\n}\n\n.elevationFlat {\n box-shadow: var(--effect-elevation-elevated-sm);\n}\n\n.elevationElevated {\n box-shadow: var(--effect-elevation-elevated-md);\n}\n\n.elevationFloating {\n box-shadow: var(--effect-elevation-elevated-floating);\n}\n\n.elevationOverlayTop {\n box-shadow: var(--effect-elevation-elevated-panel-top);\n}\n\n.elevationOverlayRight {\n box-shadow: var(--effect-elevation-elevated-panel-right);\n}\n\n.elevationOverlayBottom {\n box-shadow: var(--effect-elevation-elevated-panel-bottom);\n}\n\n.elevationOverlayLeft {\n box-shadow: var(--effect-elevation-elevated-panel-left);\n}\n\n/* ==========================================================================\n Edge Borders (only when elevation='none')\n ========================================================================== */\n\n.edgeTop { border-top: var(--dimension-stroke-width-015) solid var(--color-border-tertiary); }\n.edgeRight { border-right: var(--dimension-stroke-width-015) solid var(--color-border-tertiary); }\n.edgeBottom { border-bottom: var(--dimension-stroke-width-015) solid var(--color-border-tertiary); }\n.edgeLeft { border-left: var(--dimension-stroke-width-015) solid var(--color-border-tertiary); }\n\n/* ==========================================================================\n Corner Radius\n ========================================================================== */\n\n.radiusNone { border-radius: var(--dimension-radius-000); }\n.radiusXs { border-radius: var(--dimension-radius-050); }\n.radiusSm { border-radius: var(--dimension-radius-075); }\n.radiusMd { border-radius: var(--dimension-radius-100); }\n.radiusLg { border-radius: var(--dimension-radius-125); }\n.radiusXl { border-radius: var(--dimension-radius-175); }\n.radius2xl { border-radius: var(--dimension-radius-200); }\n.radiusFull { border-radius: var(--dimension-radius-half); }\n\n/* ==========================================================================\n Interactive States — direct background-color, no pseudo-elements\n ========================================================================== */\n\n.interactive {\n cursor: pointer;\n transition: transform var(--effect-motion-short-2);\n\n /* Interaction — ghost defaults */\n --_hover-bg: var(--color-interaction-hover);\n --_pressed-bg: var(--color-interaction-pressed);\n}\n\n.interactive:hover {\n background-color: var(--_hover-bg);\n}\n\n.interactive:active {\n background-color: var(--_pressed-bg);\n transform: scale(var(--dimension-scale-subtle));\n}\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 Selected State — persistent overlay on any background via ::before\n ========================================================================== */\n\n.selected::before {\n content: '';\n position: absolute;\n inset: 0;\n border-radius: inherit;\n pointer-events: none;\n z-index: 0;\n background-color: var(--color-interaction-hover);\n}\n\n.selected > * {\n position: relative;\n z-index: 1;\n}\n\n/* ==========================================================================\n Inactive State\n ========================================================================== */\n\n.inactive {\n opacity: 0.5;\n cursor: not-allowed;\n pointer-events: none;\n}\n",
89
+ "type": "registry:ui"
90
+ },
91
+ {
92
+ "path": "src/components/Surface/Surface.tsx",
93
+ "content": "import React, { forwardRef } from 'react';\nimport styles from './Surface.module.css';\n\n/**\n * Semantic intent for colored surfaces.\n * Each intent has four contrast levels (faint, medium, bold, strong).\n */\nexport type SurfaceIntent =\n | 'brand'\n | 'positive'\n | 'negative'\n | 'warning'\n | 'caution'\n | 'ai'\n | 'neutral'\n | 'walkthrough'\n | 'guide';\n\n/** Background type for non-intent surfaces. */\nexport type SurfaceBackground = 'primary' | 'secondary' | 'transparent' | 'translucent';\n\n/** Contrast level for semantic intents. */\nexport type SurfaceContrast = 'faint' | 'medium' | 'bold' | 'strong';\n\n/** Elevation for depth/shadow effects. Progressive: none → flat → elevated → floating. */\nexport type SurfaceElevation =\n | 'none'\n | 'depressed'\n | 'depressed-md'\n | 'flat'\n | 'elevated'\n | 'floating'\n | 'overlayTop'\n | 'overlayRight'\n | 'overlayBottom'\n | 'overlayLeft';\n\n/** Edge divider border direction. */\nexport type SurfaceEdge = 'top' | 'right' | 'bottom' | 'left';\n\n/** Corner radius presets. */\nexport type SurfaceRadiusPreset = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';\n\n/** Corner radius — preset, CSS variable, or custom string (e.g. '12px 12px 0 0'). */\nexport type SurfaceRadius = SurfaceRadiusPreset | (string & {});\n\n/** HTML elements Surface can render as. */\nexport type SurfaceElement =\n | 'div'\n | 'section'\n | 'aside'\n | 'article'\n | 'header'\n | 'footer'\n | 'main'\n | 'nav'\n | 'span'\n | 'button'\n | 'td'\n | 'th';\n\n/** Props shared by both surface modes. */\ninterface SurfaceBaseProps extends React.HTMLAttributes<HTMLElement> {\n children?: React.ReactNode;\n /** Elevation for depth/shadow effects. Defaults to 'none'. */\n elevation?: SurfaceElevation;\n /** Edge divider border(s). Only applies when elevation is 'none'. */\n edge?: SurfaceEdge | SurfaceEdge[];\n /** Corner radius — preset or CSS variable. */\n radius?: SurfaceRadius;\n /** Enable hover/pressed interaction overlay. */\n interactive?: boolean;\n /** Apply selected background state. */\n selected?: boolean;\n /** Dim and disable pointer events. */\n inactive?: boolean;\n /** HTML element to render. Defaults to 'div'. */\n as?: SurfaceElement;\n /** Button type — only used when as='button'. */\n type?: 'button' | 'submit' | 'reset';\n}\n\n/** Non-intent surface — uses a basic background color. */\ninterface SurfaceDefaultProps extends SurfaceBaseProps {\n /** Background color. Defaults to 'transparent'. */\n background?: SurfaceBackground;\n intent?: never;\n contrast?: never;\n}\n\n/** Intent surface — uses a semantic color with contrast level. */\ninterface SurfaceIntentProps extends SurfaceBaseProps {\n /** Semantic intent color. */\n intent: SurfaceIntent;\n /** Contrast level for the intent background. Defaults to 'faint'. */\n contrast?: SurfaceContrast;\n background?: never;\n}\n\nexport type SurfaceProps = SurfaceDefaultProps | SurfaceIntentProps;\n\nconst backgroundClassMap: Record<SurfaceBackground, string> = {\n primary: styles.bgPrimary,\n secondary: styles.bgSecondary,\n transparent: styles.bgTransparent,\n translucent: styles.bgTranslucent,\n};\n\nconst elevationClassMap: Record<SurfaceElevation, string> = {\n none: styles.elevationNone,\n depressed: styles.elevationDepressed,\n 'depressed-md': styles.elevationDepressedMd,\n flat: styles.elevationFlat,\n elevated: styles.elevationElevated,\n floating: styles.elevationFloating,\n overlayTop: styles.elevationOverlayTop,\n overlayRight: styles.elevationOverlayRight,\n overlayBottom: styles.elevationOverlayBottom,\n overlayLeft: styles.elevationOverlayLeft,\n};\n\nconst edgeClassMap: Record<SurfaceEdge, string> = {\n top: styles.edgeTop,\n right: styles.edgeRight,\n bottom: styles.edgeBottom,\n left: styles.edgeLeft,\n};\n\nconst radiusClassMap: Record<SurfaceRadiusPreset, string> = {\n none: styles.radiusNone,\n xs: styles.radiusXs,\n sm: styles.radiusSm,\n md: styles.radiusMd,\n lg: styles.radiusLg,\n xl: styles.radiusXl,\n '2xl': styles.radius2xl,\n full: styles.radiusFull,\n};\n\nconst getIntentContrastClass = (intent: SurfaceIntent, contrast: SurfaceContrast): string => {\n const intentKey = intent.charAt(0).toUpperCase() + intent.slice(1);\n const contrastKey = contrast.charAt(0).toUpperCase() + contrast.slice(1);\n return styles[`intent${intentKey}${contrastKey}`] || '';\n};\n\nconst isPresetRadius = (radius: SurfaceRadius): radius is SurfaceRadiusPreset =>\n radius in radiusClassMap;\n\nexport const Surface = forwardRef<HTMLElement, SurfaceProps>(\n (\n {\n children,\n background = 'transparent',\n intent,\n contrast = 'faint',\n elevation = 'none',\n edge,\n radius,\n interactive = false,\n selected = false,\n inactive = false,\n as: Component = 'div',\n className = '',\n style,\n type,\n ...rest\n },\n ref\n ) => {\n const classes = [\n styles.surface,\n // Background or intent+contrast\n intent\n ? getIntentContrastClass(intent, contrast)\n : backgroundClassMap[background],\n // Elevation\n elevationClassMap[elevation],\n // Edge(s) — only when elevation is 'none'\n ...(edge && elevation === 'none'\n ? Array.isArray(edge)\n ? edge.map(e => edgeClassMap[e])\n : [edgeClassMap[edge]]\n : []),\n // Radius (preset only — custom handled via inline style)\n radius && isPresetRadius(radius) ? radiusClassMap[radius] : '',\n // States\n interactive ? styles.interactive : '',\n selected ? styles.selected : '',\n inactive ? styles.inactive : '',\n className,\n ]\n .filter(Boolean)\n .join(' ');\n\n const computedStyle: React.CSSProperties = {\n ...style,\n ...(radius && !isPresetRadius(radius) ? { borderRadius: radius } : {}),\n };\n\n const hasStyle =\n (radius && !isPresetRadius(radius)) || (style && Object.keys(style).length > 0);\n\n const elementProps = {\n ref,\n className: classes,\n style: hasStyle ? computedStyle : undefined,\n disabled: Component === 'button' ? inactive : undefined,\n type: Component === 'button' ? type || 'button' : undefined,\n ...rest,\n };\n\n return (\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n <Component {...(elementProps as any)}>{children}</Component>\n );\n }\n);\n\nSurface.displayName = 'Surface';\n",
94
+ "type": "registry:ui"
95
+ }
96
+ ]
97
+ }