@opencosmos/ui 1.3.1

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 (260) hide show
  1. package/.claude/CLAUDE.md +239 -0
  2. package/README.md +161 -0
  3. package/dist/cli.mjs +151 -0
  4. package/dist/dates.d.mts +20 -0
  5. package/dist/dates.d.ts +20 -0
  6. package/dist/dates.js +240 -0
  7. package/dist/dates.js.map +1 -0
  8. package/dist/dates.mjs +203 -0
  9. package/dist/dates.mjs.map +1 -0
  10. package/dist/dnd.d.mts +126 -0
  11. package/dist/dnd.d.ts +126 -0
  12. package/dist/dnd.js +274 -0
  13. package/dist/dnd.js.map +1 -0
  14. package/dist/dnd.mjs +250 -0
  15. package/dist/dnd.mjs.map +1 -0
  16. package/dist/fontThemes-Dh8mtXES.d.mts +868 -0
  17. package/dist/fontThemes-Dh8mtXES.d.ts +868 -0
  18. package/dist/forms.d.mts +38 -0
  19. package/dist/forms.d.ts +38 -0
  20. package/dist/forms.js +198 -0
  21. package/dist/forms.js.map +1 -0
  22. package/dist/forms.mjs +159 -0
  23. package/dist/forms.mjs.map +1 -0
  24. package/dist/hooks-1b8WaQf1.d.mts +225 -0
  25. package/dist/hooks-CKW8vE9H.d.ts +225 -0
  26. package/dist/hooks.d.mts +3 -0
  27. package/dist/hooks.d.ts +3 -0
  28. package/dist/hooks.js +971 -0
  29. package/dist/hooks.js.map +1 -0
  30. package/dist/hooks.mjs +943 -0
  31. package/dist/hooks.mjs.map +1 -0
  32. package/dist/index-DscTIrZ2.d.mts +29 -0
  33. package/dist/index-DscTIrZ2.d.ts +29 -0
  34. package/dist/index.d.mts +3382 -0
  35. package/dist/index.d.ts +3382 -0
  36. package/dist/index.js +15146 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/index.mjs +14802 -0
  39. package/dist/index.mjs.map +1 -0
  40. package/dist/providers-CXPDMsl7.d.mts +30 -0
  41. package/dist/providers-Dn_Msjvz.d.ts +30 -0
  42. package/dist/providers.d.mts +3 -0
  43. package/dist/providers.d.ts +3 -0
  44. package/dist/providers.js +1885 -0
  45. package/dist/providers.js.map +1 -0
  46. package/dist/providers.mjs +1859 -0
  47. package/dist/providers.mjs.map +1 -0
  48. package/dist/tables.d.mts +10 -0
  49. package/dist/tables.d.ts +10 -0
  50. package/dist/tables.js +248 -0
  51. package/dist/tables.js.map +1 -0
  52. package/dist/tables.mjs +218 -0
  53. package/dist/tables.mjs.map +1 -0
  54. package/dist/tokens.d.mts +1065 -0
  55. package/dist/tokens.d.ts +1065 -0
  56. package/dist/tokens.js +2637 -0
  57. package/dist/tokens.js.map +1 -0
  58. package/dist/tokens.mjs +2555 -0
  59. package/dist/tokens.mjs.map +1 -0
  60. package/dist/utils-CIIM7dAC.d.ts +986 -0
  61. package/dist/utils-Cs04sxth.d.mts +986 -0
  62. package/dist/utils.d.mts +4 -0
  63. package/dist/utils.d.ts +4 -0
  64. package/dist/utils.js +874 -0
  65. package/dist/utils.js.map +1 -0
  66. package/dist/utils.mjs +806 -0
  67. package/dist/utils.mjs.map +1 -0
  68. package/dist/validation-Bj1ye-v_.d.mts +114 -0
  69. package/dist/validation-Bj1ye-v_.d.ts +114 -0
  70. package/dist/webgl.d.mts +104 -0
  71. package/dist/webgl.d.ts +104 -0
  72. package/dist/webgl.js +226 -0
  73. package/dist/webgl.js.map +1 -0
  74. package/dist/webgl.mjs +195 -0
  75. package/dist/webgl.mjs.map +1 -0
  76. package/package.json +267 -0
  77. package/src/cli.ts +206 -0
  78. package/src/component-registry.ts +183 -0
  79. package/src/components/actions/Button.test.tsx +61 -0
  80. package/src/components/actions/Button.tsx +70 -0
  81. package/src/components/actions/Link.tsx +78 -0
  82. package/src/components/actions/Magnetic.tsx +68 -0
  83. package/src/components/actions/Toggle.test.tsx +40 -0
  84. package/src/components/actions/Toggle.tsx +47 -0
  85. package/src/components/actions/ToggleGroup.tsx +70 -0
  86. package/src/components/actions/index.ts +5 -0
  87. package/src/components/backgrounds/FaultyTerminal.tsx +426 -0
  88. package/src/components/backgrounds/OrbBackground.tsx +424 -0
  89. package/src/components/backgrounds/WarpBackground.tsx +358 -0
  90. package/src/components/backgrounds/index.ts +3 -0
  91. package/src/components/blocks/Hero.tsx +142 -0
  92. package/src/components/blocks/social/OpenGraphCard.tsx +243 -0
  93. package/src/components/cursor/SplashCursor.tsx +1315 -0
  94. package/src/components/cursor/TargetCursor.tsx +187 -0
  95. package/src/components/cursor/index.ts +2 -0
  96. package/src/components/data-display/AspectImage.tsx +73 -0
  97. package/src/components/data-display/Avatar.test.tsx +35 -0
  98. package/src/components/data-display/Avatar.tsx +55 -0
  99. package/src/components/data-display/Badge.test.tsx +43 -0
  100. package/src/components/data-display/Badge.tsx +84 -0
  101. package/src/components/data-display/Brand.tsx +123 -0
  102. package/src/components/data-display/Calendar.tsx +70 -0
  103. package/src/components/data-display/Card.test.tsx +92 -0
  104. package/src/components/data-display/Card.tsx +115 -0
  105. package/src/components/data-display/Code.tsx +210 -0
  106. package/src/components/data-display/CollapsibleCodeBlock.tsx +238 -0
  107. package/src/components/data-display/DataTable.tsx +119 -0
  108. package/src/components/data-display/DescriptionList.tsx +41 -0
  109. package/src/components/data-display/GitHubIcon.tsx +44 -0
  110. package/src/components/data-display/Heading.test.tsx +36 -0
  111. package/src/components/data-display/Heading.tsx +83 -0
  112. package/src/components/data-display/StatCard.tsx +195 -0
  113. package/src/components/data-display/Table.tsx +133 -0
  114. package/src/components/data-display/Text.test.tsx +48 -0
  115. package/src/components/data-display/Text.tsx +144 -0
  116. package/src/components/data-display/Timeline.tsx +194 -0
  117. package/src/components/data-display/TreeView.tsx +226 -0
  118. package/src/components/data-display/Typewriter.tsx +119 -0
  119. package/src/components/data-display/VariableWeightText.tsx +130 -0
  120. package/src/components/data-display/index.ts +19 -0
  121. package/src/components/feedback/Alert.test.tsx +44 -0
  122. package/src/components/feedback/Alert.tsx +65 -0
  123. package/src/components/feedback/EmptyState.tsx +113 -0
  124. package/src/components/feedback/Progress.test.tsx +60 -0
  125. package/src/components/feedback/Progress.tsx +30 -0
  126. package/src/components/feedback/ProgressBar.tsx +158 -0
  127. package/src/components/feedback/Skeleton.test.tsx +39 -0
  128. package/src/components/feedback/Skeleton.tsx +45 -0
  129. package/src/components/feedback/Sonner.tsx +28 -0
  130. package/src/components/feedback/Spinner.test.tsx +33 -0
  131. package/src/components/feedback/Spinner.tsx +99 -0
  132. package/src/components/feedback/Stepper.tsx +307 -0
  133. package/src/components/feedback/Toast/Toast.tsx +243 -0
  134. package/src/components/feedback/Toast/index.ts +2 -0
  135. package/src/components/feedback/index.ts +9 -0
  136. package/src/components/forms/Checkbox.test.tsx +40 -0
  137. package/src/components/forms/Checkbox.tsx +31 -0
  138. package/src/components/forms/ColorPicker.tsx +118 -0
  139. package/src/components/forms/Combobox.tsx +96 -0
  140. package/src/components/forms/DragDrop.tsx +440 -0
  141. package/src/components/forms/FileUpload.tsx +252 -0
  142. package/src/components/forms/FilterButton.tsx +65 -0
  143. package/src/components/forms/Form.tsx +197 -0
  144. package/src/components/forms/Input.test.tsx +46 -0
  145. package/src/components/forms/Input.tsx +43 -0
  146. package/src/components/forms/InputOTP.tsx +81 -0
  147. package/src/components/forms/Label.test.tsx +20 -0
  148. package/src/components/forms/Label.tsx +25 -0
  149. package/src/components/forms/RadioGroup.tsx +51 -0
  150. package/src/components/forms/SearchBar.tsx +215 -0
  151. package/src/components/forms/Select.test.tsx +118 -0
  152. package/src/components/forms/Select.tsx +274 -0
  153. package/src/components/forms/Slider.tsx +29 -0
  154. package/src/components/forms/Switch.test.tsx +76 -0
  155. package/src/components/forms/Switch.tsx +30 -0
  156. package/src/components/forms/TextField.tsx +152 -0
  157. package/src/components/forms/Textarea.test.tsx +41 -0
  158. package/src/components/forms/Textarea.tsx +29 -0
  159. package/src/components/forms/ThemeSwitcher.tsx +290 -0
  160. package/src/components/forms/ThemeToggle.tsx +151 -0
  161. package/src/components/forms/index.ts +19 -0
  162. package/src/components/layout/Accordion.test.tsx +66 -0
  163. package/src/components/layout/Accordion.tsx +64 -0
  164. package/src/components/layout/AspectRatio.tsx +7 -0
  165. package/src/components/layout/Carousel.tsx +277 -0
  166. package/src/components/layout/Collapsible.test.tsx +40 -0
  167. package/src/components/layout/Collapsible.tsx +31 -0
  168. package/src/components/layout/Container.test.tsx +45 -0
  169. package/src/components/layout/Container.tsx +99 -0
  170. package/src/components/layout/CustomizerPanel.tsx +400 -0
  171. package/src/components/layout/DatePicker.tsx +57 -0
  172. package/src/components/layout/Footer/Footer.tsx +175 -0
  173. package/src/components/layout/Footer/index.ts +2 -0
  174. package/src/components/layout/GlassSurface.tsx +82 -0
  175. package/src/components/layout/Grid.test.tsx +31 -0
  176. package/src/components/layout/Grid.tsx +130 -0
  177. package/src/components/layout/Header/Header.tsx +450 -0
  178. package/src/components/layout/Header/index.ts +2 -0
  179. package/src/components/layout/PageLayout.tsx +180 -0
  180. package/src/components/layout/PageTemplate.tsx +158 -0
  181. package/src/components/layout/Resizable.tsx +48 -0
  182. package/src/components/layout/ScrollArea.tsx +53 -0
  183. package/src/components/layout/Separator.test.tsx +28 -0
  184. package/src/components/layout/Separator.tsx +29 -0
  185. package/src/components/layout/Sidebar.tsx +171 -0
  186. package/src/components/layout/Stack.test.tsx +41 -0
  187. package/src/components/layout/Stack.tsx +89 -0
  188. package/src/components/layout/glass-surface.css +60 -0
  189. package/src/components/layout/index.ts +18 -0
  190. package/src/components/motion/AnimatedBeam.tsx +159 -0
  191. package/src/components/navigation/Breadcrumb.test.tsx +57 -0
  192. package/src/components/navigation/Breadcrumb.tsx +119 -0
  193. package/src/components/navigation/Breadcrumbs.tsx +221 -0
  194. package/src/components/navigation/Command.tsx +159 -0
  195. package/src/components/navigation/Menubar.tsx +115 -0
  196. package/src/components/navigation/NavLink.tsx +55 -0
  197. package/src/components/navigation/NavigationMenu.tsx +125 -0
  198. package/src/components/navigation/Pagination.tsx +121 -0
  199. package/src/components/navigation/SecondaryNav.tsx +100 -0
  200. package/src/components/navigation/Tabs.test.tsx +47 -0
  201. package/src/components/navigation/Tabs.tsx +60 -0
  202. package/src/components/navigation/TertiaryNav.tsx +90 -0
  203. package/src/components/navigation/index.ts +10 -0
  204. package/src/components/overlays/AlertDialog.test.tsx +69 -0
  205. package/src/components/overlays/AlertDialog.tsx +166 -0
  206. package/src/components/overlays/ContextMenu.tsx +243 -0
  207. package/src/components/overlays/Dialog.test.tsx +79 -0
  208. package/src/components/overlays/Dialog.tsx +158 -0
  209. package/src/components/overlays/Drawer.tsx +128 -0
  210. package/src/components/overlays/Dropdown.tsx +253 -0
  211. package/src/components/overlays/DropdownMenu.tsx +242 -0
  212. package/src/components/overlays/HoverCard.tsx +32 -0
  213. package/src/components/overlays/Modal.tsx +250 -0
  214. package/src/components/overlays/NotificationCenter.tsx +364 -0
  215. package/src/components/overlays/Popover.test.tsx +40 -0
  216. package/src/components/overlays/Popover.tsx +46 -0
  217. package/src/components/overlays/Sheet.tsx +163 -0
  218. package/src/components/overlays/Tooltip.test.tsx +33 -0
  219. package/src/components/overlays/Tooltip.tsx +32 -0
  220. package/src/components/overlays/index.ts +12 -0
  221. package/src/dates.ts +2 -0
  222. package/src/dnd.ts +1 -0
  223. package/src/forms.ts +1 -0
  224. package/src/globals.css +187 -0
  225. package/src/hooks/index.ts +6 -0
  226. package/src/hooks/useForm.ts +247 -0
  227. package/src/hooks/useMotionPreference.test.ts +102 -0
  228. package/src/hooks/useMotionPreference.ts +78 -0
  229. package/src/hooks/useTheme.ts +58 -0
  230. package/src/hooks.ts +9 -0
  231. package/src/index.ts +168 -0
  232. package/src/lib/animations.ts +356 -0
  233. package/src/lib/breadcrumbs.ts +94 -0
  234. package/src/lib/colors.ts +493 -0
  235. package/src/lib/store/customizer.ts +482 -0
  236. package/src/lib/store/index.ts +3 -0
  237. package/src/lib/store/theme.ts +55 -0
  238. package/src/lib/syntax-parser/index.ts +50 -0
  239. package/src/lib/syntax-parser/patterns.ts +64 -0
  240. package/src/lib/syntax-parser/tokenizer.ts +117 -0
  241. package/src/lib/syntax-parser/types.ts +27 -0
  242. package/src/lib/utils.ts +6 -0
  243. package/src/lib/validation.ts +204 -0
  244. package/src/lib/webgl/Color.ts +11 -0
  245. package/src/lib/webgl/Mesh.ts +41 -0
  246. package/src/lib/webgl/Program.ts +118 -0
  247. package/src/lib/webgl/Renderer.ts +51 -0
  248. package/src/lib/webgl/Triangle.ts +27 -0
  249. package/src/lib/webgl/Vec3.ts +18 -0
  250. package/src/lib/webgl/index.ts +13 -0
  251. package/src/nativewind-env.d.ts +1 -0
  252. package/src/providers/ThemeProvider.tsx +461 -0
  253. package/src/providers/index.ts +1 -0
  254. package/src/providers.ts +7 -0
  255. package/src/tables.ts +1 -0
  256. package/src/test/setup.ts +39 -0
  257. package/src/theme.css +158 -0
  258. package/src/tokens.ts +7 -0
  259. package/src/utils.ts +12 -0
  260. package/src/webgl.ts +1 -0
@@ -0,0 +1,187 @@
1
+ 'use client';
2
+ import React, { useEffect, useRef, useState } from 'react';
3
+
4
+ interface TargetCursorProps {
5
+ color?: string;
6
+ outerSize?: number;
7
+ innerSize?: number;
8
+ outerAlpha?: number;
9
+ innerScale?: number;
10
+ outerScale?: number;
11
+ clickScale?: number;
12
+ blendMode?: boolean;
13
+ }
14
+
15
+ export function TargetCursor({
16
+ color = '255, 255, 255',
17
+ outerSize = 40,
18
+ innerSize = 8,
19
+ outerAlpha = 0.4,
20
+ innerScale = 0.7,
21
+ outerScale = 4,
22
+ clickScale = 0.7,
23
+ blendMode = true
24
+ }: TargetCursorProps) {
25
+ const cursorOuterRef = useRef<HTMLDivElement>(null);
26
+ const cursorInnerRef = useRef<HTMLDivElement>(null);
27
+ const requestRef = useRef<number | null>(null);
28
+ const previousTimeRef = useRef<number | null>(null);
29
+
30
+ // Mouse position
31
+ const endX = useRef(0);
32
+ const endY = useRef(0);
33
+ // Current interpolated position for outer circle
34
+ const _x = useRef(0);
35
+ const _y = useRef(0);
36
+
37
+ const [isActive, setIsActive] = useState(false);
38
+ const [isActiveClickable, setIsActiveClickable] = useState(false);
39
+ const [isVisible, setIsVisible] = useState(false);
40
+
41
+ // Initial positioning
42
+ useEffect(() => {
43
+ const onMouseMove = (e: MouseEvent) => {
44
+ endX.current = e.clientX;
45
+ endY.current = e.clientY;
46
+
47
+ // Direct DOM update for performance
48
+ if (cursorInnerRef.current) {
49
+ cursorInnerRef.current.style.top = `${e.clientY}px`;
50
+ cursorInnerRef.current.style.left = `${e.clientX}px`;
51
+ }
52
+
53
+ if (!isVisible) {
54
+ _x.current = e.clientX;
55
+ _y.current = e.clientY;
56
+ setIsVisible(true);
57
+ }
58
+
59
+ // Check for clickable targets using DOM traversal (lighter than getComputedStyle)
60
+ // Note: getComputedStyle causes reflow, so avoid if possible.
61
+ // Using tagName and common patterns is preferred for performance.
62
+ const target = e.target as HTMLElement;
63
+ // Simple heuristics
64
+ if (
65
+ target.tagName.toLowerCase() === 'a' ||
66
+ target.tagName.toLowerCase() === 'button' ||
67
+ target.closest('a') ||
68
+ target.closest('button') ||
69
+ target.onclick ||
70
+ target.getAttribute('role') === 'button'
71
+ ) {
72
+ setIsActiveClickable(true);
73
+ } else {
74
+ setIsActiveClickable(false);
75
+ }
76
+ };
77
+
78
+ const onMouseDown = () => setIsActive(true);
79
+ const onMouseUp = () => setIsActive(false);
80
+ const onMouseEnter = () => setIsVisible(true);
81
+ const onMouseLeave = () => setIsVisible(false);
82
+
83
+ window.addEventListener('mousemove', onMouseMove);
84
+ window.addEventListener('mousedown', onMouseDown);
85
+ window.addEventListener('mouseup', onMouseUp);
86
+ window.addEventListener('mouseenter', onMouseEnter);
87
+ window.addEventListener('mouseleave', onMouseLeave);
88
+
89
+ return () => {
90
+ window.removeEventListener('mousemove', onMouseMove);
91
+ window.removeEventListener('mousedown', onMouseDown);
92
+ window.removeEventListener('mouseup', onMouseUp);
93
+ window.removeEventListener('mouseenter', onMouseEnter);
94
+ window.removeEventListener('mouseleave', onMouseLeave);
95
+ };
96
+ }, [isVisible]);
97
+
98
+ // Animation Loop
99
+ const animateCursor = (time: number) => {
100
+ if (previousTimeRef.current !== undefined) {
101
+ _x.current += (endX.current - _x.current) / 8;
102
+ _y.current += (endY.current - _y.current) / 8;
103
+
104
+ if (cursorOuterRef.current) {
105
+ // translate3d for position, translate(-50%, -50%) for centering
106
+ cursorOuterRef.current.style.transform = `translate3d(${_x.current}px, ${_y.current}px, 0) translate(-50%, -50%)`;
107
+ }
108
+ }
109
+ previousTimeRef.current = time;
110
+ requestRef.current = requestAnimationFrame(animateCursor);
111
+ };
112
+
113
+ useEffect(() => {
114
+ requestRef.current = requestAnimationFrame(animateCursor);
115
+ return () => cancelAnimationFrame(requestRef.current!);
116
+ }, []);
117
+
118
+ useEffect(() => {
119
+ if (isVisible) {
120
+ const style = document.createElement('style');
121
+ style.innerHTML = `body, a, button, input, [role="button"] { cursor: none !important; }`;
122
+ document.head.appendChild(style);
123
+ return () => {
124
+ if (document.head.contains(style)) {
125
+ document.head.removeChild(style);
126
+ }
127
+ };
128
+ }
129
+ }, [isVisible]);
130
+
131
+ // Inline styles for dynamic props
132
+ const styles = {
133
+ cursorInner: {
134
+ width: innerSize,
135
+ height: innerSize,
136
+ backgroundColor: `rgba(${color}, 1)`,
137
+ // transform handles centering and scale
138
+ transform: `translate(-50%, -50%) scale(${isActive || isActiveClickable ? innerScale : 1})`,
139
+ transition: 'transform 0.15s ease-out, opacity 0.15s ease-out',
140
+ },
141
+ cursorOuter: {
142
+ width: outerSize,
143
+ height: outerSize,
144
+ backgroundColor: `rgba(${color}, ${outerAlpha})`,
145
+ transition: 'width 0.3s, height 0.3s, background-color 0.3s',
146
+ ...(isActiveClickable && {
147
+ width: outerSize * 1.5,
148
+ height: outerSize * 1.5,
149
+ backgroundColor: `rgba(${color}, ${outerAlpha - 0.1})`,
150
+ })
151
+ }
152
+ };
153
+
154
+ if (!isVisible) return null;
155
+
156
+ return (
157
+ <>
158
+ <div
159
+ ref={cursorOuterRef}
160
+ style={{
161
+ position: 'fixed',
162
+ borderRadius: '50%',
163
+ pointerEvents: 'none',
164
+ zIndex: 9999,
165
+ left: 0,
166
+ top: 0,
167
+ willChange: 'transform',
168
+ mixBlendMode: blendMode ? 'difference' : 'normal',
169
+ ...styles.cursorOuter
170
+ }}
171
+ />
172
+ <div
173
+ ref={cursorInnerRef}
174
+ style={{
175
+ position: 'fixed',
176
+ borderRadius: '50%',
177
+ pointerEvents: 'none',
178
+ zIndex: 9999,
179
+ willChange: 'left, top, transform',
180
+ mixBlendMode: blendMode ? 'difference' : 'normal',
181
+ ...styles.cursorInner
182
+ }}
183
+ />
184
+ {/* Default cursor hidden via useEffect */}
185
+ </>
186
+ );
187
+ }
@@ -0,0 +1,2 @@
1
+ export * from './SplashCursor';
2
+ export * from './TargetCursor';
@@ -0,0 +1,73 @@
1
+ import React from 'react';
2
+
3
+ export interface AspectImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
4
+ /**
5
+ * Aspect ratio (width / height)
6
+ * @example 16/9, 4/3, 1
7
+ * @default 16/9
8
+ */
9
+ ratio?: number;
10
+ /**
11
+ * Image source
12
+ */
13
+ src: string;
14
+ /**
15
+ * Image alt text
16
+ */
17
+ alt: string;
18
+ /**
19
+ * Apply border radius
20
+ * @default true
21
+ */
22
+ rounded?: boolean;
23
+ /**
24
+ * Apply shadow
25
+ * @default false
26
+ */
27
+ shadow?: boolean;
28
+ /**
29
+ * Optional caption
30
+ */
31
+ caption?: React.ReactNode;
32
+ }
33
+
34
+ export const AspectImage = (
35
+ {
36
+ ref,
37
+ ratio = 16 / 9,
38
+ src,
39
+ alt,
40
+ rounded = true,
41
+ shadow = false,
42
+ caption,
43
+ className = '',
44
+ style,
45
+ ...props
46
+ }: AspectImageProps & {
47
+ ref?: React.Ref<HTMLImageElement>;
48
+ }
49
+ ) => {
50
+ return (
51
+ <figure className={`w-full ${className}`}>
52
+ <div
53
+ className={`relative overflow-hidden w-full bg-[var(--color-surface)] border border-[var(--color-border)]
54
+ ${rounded ? 'rounded-xl' : ''}
55
+ ${shadow ? 'shadow-md' : ''}`}
56
+ style={{ aspectRatio: ratio }}
57
+ >
58
+ <img
59
+ ref={ref}
60
+ src={src}
61
+ alt={alt}
62
+ className="w-full h-full object-cover transition-transform duration-500 hover:scale-105"
63
+ {...props}
64
+ />
65
+ </div>
66
+ {caption && (
67
+ <figcaption className="mt-2 text-sm text-[var(--color-text-muted)] text-center">
68
+ {caption}
69
+ </figcaption>
70
+ )}
71
+ </figure>
72
+ );
73
+ };
@@ -0,0 +1,35 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import { describe, it, expect } from 'vitest'
3
+ import { Avatar, AvatarImage, AvatarFallback } from './Avatar'
4
+
5
+ describe('Avatar', () => {
6
+ it('renders fallback text', () => {
7
+ render(
8
+ <Avatar>
9
+ <AvatarFallback>JD</AvatarFallback>
10
+ </Avatar>
11
+ )
12
+ expect(screen.getByText('JD')).toBeInTheDocument()
13
+ })
14
+
15
+ it('shows fallback when image has not loaded', () => {
16
+ // Radix Avatar only renders <img> after the load event fires,
17
+ // which never happens in jsdom — so fallback is shown instead
18
+ render(
19
+ <Avatar>
20
+ <AvatarImage src="/avatar.jpg" alt="John Doe" />
21
+ <AvatarFallback>JD</AvatarFallback>
22
+ </Avatar>
23
+ )
24
+ expect(screen.getByText('JD')).toBeInTheDocument()
25
+ })
26
+
27
+ it('applies custom className to avatar root', () => {
28
+ const { container } = render(
29
+ <Avatar className="custom-avatar">
30
+ <AvatarFallback>AB</AvatarFallback>
31
+ </Avatar>
32
+ )
33
+ expect(container.firstChild).toHaveClass('custom-avatar')
34
+ })
35
+ })
@@ -0,0 +1,55 @@
1
+ "use client";
2
+ import * as React from "react"
3
+ import * as AvatarPrimitive from "@radix-ui/react-avatar"
4
+
5
+ import { cn } from "../../lib/utils"
6
+
7
+ const Avatar = (
8
+ {
9
+ ref,
10
+ className,
11
+ ...props
12
+ }: React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> & {
13
+ ref?: React.Ref<React.ElementRef<typeof AvatarPrimitive.Root>>;
14
+ }
15
+ ) => (<AvatarPrimitive.Root
16
+ ref={ref}
17
+ className={cn(
18
+ "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
19
+ className
20
+ )}
21
+ {...props}
22
+ />)
23
+
24
+ const AvatarImage = (
25
+ {
26
+ ref,
27
+ className,
28
+ ...props
29
+ }: React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> & {
30
+ ref?: React.Ref<React.ElementRef<typeof AvatarPrimitive.Image>>;
31
+ }
32
+ ) => (<AvatarPrimitive.Image
33
+ ref={ref}
34
+ className={cn("aspect-square h-full w-full", className)}
35
+ {...props}
36
+ />)
37
+
38
+ const AvatarFallback = (
39
+ {
40
+ ref,
41
+ className,
42
+ ...props
43
+ }: React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> & {
44
+ ref?: React.Ref<React.ElementRef<typeof AvatarPrimitive.Fallback>>;
45
+ }
46
+ ) => (<AvatarPrimitive.Fallback
47
+ ref={ref}
48
+ className={cn(
49
+ "flex h-full w-full items-center justify-center rounded-full bg-muted text-muted-foreground font-medium",
50
+ className
51
+ )}
52
+ {...props}
53
+ />)
54
+
55
+ export { Avatar, AvatarImage, AvatarFallback }
@@ -0,0 +1,43 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import { describe, it, expect } from 'vitest'
3
+ import { Badge } from './Badge'
4
+
5
+ describe('Badge', () => {
6
+ it('renders with default variant', () => {
7
+ render(<Badge>Active</Badge>)
8
+ expect(screen.getByText('Active')).toBeInTheDocument()
9
+ expect(screen.getByText('Active')).toHaveClass('bg-primary')
10
+ })
11
+
12
+ it('renders different variants', () => {
13
+ const { rerender } = render(<Badge variant="secondary">Secondary</Badge>)
14
+ expect(screen.getByText('Secondary')).toHaveClass('bg-secondary')
15
+
16
+ rerender(<Badge variant="destructive">Destructive</Badge>)
17
+ expect(screen.getByText('Destructive')).toHaveClass('bg-destructive')
18
+
19
+ rerender(<Badge variant="outline">Outline</Badge>)
20
+ expect(screen.getByText('Outline')).toHaveClass('text-foreground')
21
+
22
+ rerender(<Badge variant="success">Success</Badge>)
23
+ expect(screen.getByText('Success')).toHaveClass('bg-success')
24
+ })
25
+
26
+ it('renders dot indicator when dot prop is true', () => {
27
+ render(<Badge dot>Status</Badge>)
28
+ const dot = screen.getByText('Status').querySelector('[aria-hidden="true"]')
29
+ expect(dot).toBeInTheDocument()
30
+ expect(dot).toHaveClass('animate-pulse')
31
+ })
32
+
33
+ it('does not render dot when dot prop is false', () => {
34
+ render(<Badge>No Dot</Badge>)
35
+ const dot = screen.getByText('No Dot').querySelector('[aria-hidden="true"]')
36
+ expect(dot).not.toBeInTheDocument()
37
+ })
38
+
39
+ it('applies custom className', () => {
40
+ render(<Badge className="custom-badge">Styled</Badge>)
41
+ expect(screen.getByText('Styled')).toHaveClass('custom-badge')
42
+ })
43
+ })
@@ -0,0 +1,84 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { cn } from "../../lib/utils"
4
+
5
+ const badgeVariants = cva(
6
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default:
11
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
12
+ secondary:
13
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
14
+ destructive:
15
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
16
+ outline: "text-foreground",
17
+ // Semantic variants from original design system
18
+ success: "border-transparent bg-success text-success-foreground hover:bg-success/80",
19
+ warning: "border-transparent bg-warning text-warning-foreground hover:bg-warning/80",
20
+ error: "border-transparent bg-error text-error-foreground hover:bg-error/80",
21
+ info: "border-transparent bg-info text-info-foreground hover:bg-info/80",
22
+ },
23
+ size: {
24
+ sm: "px-2 py-0.5 text-xs",
25
+ md: "px-2.5 py-1 text-sm",
26
+ lg: "px-3 py-1.5 text-base",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "default",
31
+ size: "md",
32
+ },
33
+ }
34
+ )
35
+
36
+ export interface BadgeProps
37
+ extends React.HTMLAttributes<HTMLDivElement>,
38
+ VariantProps<typeof badgeVariants> {
39
+ dot?: boolean;
40
+ }
41
+
42
+ function Badge({ className, variant, size, dot, children, style, ...props }: BadgeProps & { style?: React.CSSProperties }) {
43
+ const sizeStyles: Record<string, React.CSSProperties> = {
44
+ sm: { padding: '0.125rem 0.5rem', fontSize: 'var(--text-xs, 0.75rem)' },
45
+ md: { padding: '0.25rem 0.625rem', fontSize: 'var(--text-sm, 0.875rem)' },
46
+ lg: { padding: '0.375rem 0.75rem', fontSize: '1rem' },
47
+ };
48
+ const variantStyles: Record<string, React.CSSProperties> = {
49
+ default: { backgroundColor: 'var(--color-primary, #346BEA)', color: 'var(--color-primary-foreground, #ffffff)', borderColor: 'transparent' },
50
+ secondary: { backgroundColor: 'var(--color-secondary, #EBF0FD)', color: 'var(--color-secondary-foreground, #1E49AA)', borderColor: 'transparent' },
51
+ destructive: { backgroundColor: 'var(--color-destructive, #C62828)', color: 'var(--color-destructive-foreground, #ffffff)', borderColor: 'transparent' },
52
+ outline: { color: 'var(--color-foreground, #212121)' },
53
+ success: { backgroundColor: 'var(--color-success, #2E7D32)', color: 'var(--color-success-foreground, #ffffff)', borderColor: 'transparent' },
54
+ warning: { backgroundColor: 'var(--color-warning, #E65100)', color: 'var(--color-warning-foreground, #ffffff)', borderColor: 'transparent' },
55
+ error: { backgroundColor: 'var(--color-error, #C62828)', color: 'var(--color-error-foreground, #ffffff)', borderColor: 'transparent' },
56
+ info: { backgroundColor: 'var(--color-info, #346BEA)', color: 'var(--color-info-foreground, #ffffff)', borderColor: 'transparent' },
57
+ };
58
+ return (
59
+ <div
60
+ className={cn(badgeVariants({ variant, size }), className)}
61
+ style={{
62
+ display: 'inline-flex',
63
+ alignItems: 'center',
64
+ borderRadius: '9999px',
65
+ fontWeight: 600,
66
+ ...sizeStyles[size || 'md'],
67
+ ...variantStyles[variant || 'default'],
68
+ ...style,
69
+ }}
70
+ {...props}
71
+ >
72
+ {dot && (
73
+ <span className={cn(
74
+ "mr-1.5 rounded-full bg-current animate-pulse",
75
+ size === 'sm' ? "w-1.5 h-1.5" :
76
+ size === 'lg' ? "w-2.5 h-2.5" : "w-2 h-2"
77
+ )} aria-hidden="true" />
78
+ )}
79
+ {children}
80
+ </div>
81
+ )
82
+ }
83
+
84
+ export { Badge, badgeVariants }
@@ -0,0 +1,123 @@
1
+ import React from 'react';
2
+
3
+ export interface BrandProps {
4
+ /**
5
+ * Brand name or logo element
6
+ */
7
+ children: React.ReactNode;
8
+
9
+ /**
10
+ * Size variant
11
+ * @default 'md'
12
+ */
13
+ size?: 'sm' | 'md' | 'lg';
14
+
15
+ /**
16
+ * Optional href for link behavior
17
+ * When provided, renders as an anchor tag
18
+ */
19
+ href?: string;
20
+
21
+ /**
22
+ * Additional className for customization
23
+ */
24
+ className?: string;
25
+
26
+ /**
27
+ * Click handler (used when href is provided)
28
+ */
29
+ onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
30
+ }
31
+
32
+ /**
33
+ * Brand Component
34
+ *
35
+ * A theme-aware brand/logo component that automatically handles:
36
+ * - Dark mode support via design system tokens
37
+ * - Consistent typography and sizing
38
+ * - Optional link behavior
39
+ * - Focus states and accessibility
40
+ *
41
+ * This component encapsulates the proper styling so you never need to
42
+ * manually apply `text-[var(--color-text-primary)]` to logos.
43
+ *
44
+ * Features:
45
+ * - Automatic theme-aware text color
46
+ * - Three size variants (sm, md, lg)
47
+ * - Works standalone or as a link
48
+ * - Compatible with Next.js Link wrapper
49
+ * - Proper focus states
50
+ *
51
+ * Usage:
52
+ * ```tsx
53
+ * // Standalone brand text
54
+ * <Brand>Company Name</Brand>
55
+ *
56
+ * // As a link (use with Next.js Link)
57
+ * <Brand href="/">Company Name</Brand>
58
+ *
59
+ * // With Next.js Link wrapper
60
+ * <Brand>
61
+ * <NextLink href="/">Company Name</NextLink>
62
+ * </Brand>
63
+ *
64
+ * // Different sizes
65
+ * <Brand size="sm">Brand</Brand>
66
+ * <Brand size="lg">Brand</Brand>
67
+ * ```
68
+ */
69
+ export const Brand = (
70
+ {
71
+ ref,
72
+ children,
73
+ size = 'md',
74
+ href,
75
+ className = '',
76
+ onClick
77
+ }: BrandProps & {
78
+ ref?: React.Ref<HTMLElement>;
79
+ }
80
+ ) => {
81
+ // Size mappings following Swiss Grid typography scale
82
+ const sizeStyles = {
83
+ sm: 'text-base', // 16px
84
+ md: 'text-lg', // 18px
85
+ lg: 'text-2xl', // 24px
86
+ };
87
+
88
+ // Base styles with design system tokens
89
+ const baseStyles = `
90
+ font-bold
91
+ text-[var(--color-text-primary)]
92
+ transition-colors duration-200
93
+ focus-visible:outline-none
94
+ focus-visible:ring-2
95
+ focus-visible:ring-[var(--color-focus)]
96
+ focus-visible:ring-offset-2
97
+ ${sizeStyles[size]}
98
+ `;
99
+
100
+ // If href is provided, render as link
101
+ if (href) {
102
+ return (
103
+ <a
104
+ ref={ref as React.Ref<HTMLAnchorElement>}
105
+ href={href}
106
+ onClick={onClick}
107
+ className={`${baseStyles} hover:opacity-80 ${className}`}
108
+ >
109
+ {children}
110
+ </a>
111
+ );
112
+ }
113
+
114
+ // Otherwise render as span
115
+ return (
116
+ <span
117
+ ref={ref as React.Ref<HTMLSpanElement>}
118
+ className={`${baseStyles} ${className}`}
119
+ >
120
+ {children}
121
+ </span>
122
+ );
123
+ };
@@ -0,0 +1,70 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ChevronLeft, ChevronRight } from "lucide-react"
5
+ import { DayPicker } from "react-day-picker"
6
+
7
+ import { cn } from "../../lib/utils"
8
+ import { buttonVariants } from "../actions/Button"
9
+
10
+ export type CalendarProps = React.ComponentProps<typeof DayPicker>
11
+
12
+ function Calendar({
13
+ className,
14
+ classNames,
15
+ showOutsideDays = true,
16
+ ...props
17
+ }: CalendarProps) {
18
+ return (
19
+ <DayPicker
20
+ showOutsideDays={showOutsideDays}
21
+ className={cn("p-3", className)}
22
+ classNames={{
23
+ months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
24
+ month: "space-y-4",
25
+ caption: "flex justify-center pt-1 relative items-center",
26
+ caption_label: "text-sm font-medium",
27
+ nav: "space-x-1 flex items-center",
28
+ nav_button: cn(
29
+ buttonVariants({ variant: "outline" }),
30
+ "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
31
+ ),
32
+ nav_button_previous: "absolute left-1",
33
+ nav_button_next: "absolute right-1",
34
+ table: "w-full border-collapse space-y-1",
35
+ head_row: "flex",
36
+ head_cell:
37
+ "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
38
+ row: "flex w-full mt-2",
39
+ cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
40
+ day: cn(
41
+ buttonVariants({ variant: "ghost" }),
42
+ "h-9 w-9 p-0 font-normal aria-selected:opacity-100"
43
+ ),
44
+ day_range_end: "day-range-end",
45
+ day_selected:
46
+ "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
47
+ day_today: "bg-accent text-accent-foreground",
48
+ day_outside:
49
+ "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
50
+ day_disabled: "text-muted-foreground opacity-50",
51
+ day_range_middle:
52
+ "aria-selected:bg-accent aria-selected:text-accent-foreground",
53
+ day_hidden: "invisible",
54
+ ...classNames,
55
+ }}
56
+ components={{
57
+ Chevron: ({ ...props }) => {
58
+ if (props.orientation === "left") {
59
+ return <ChevronLeft className="h-4 w-4" />
60
+ }
61
+ return <ChevronRight className="h-4 w-4" />
62
+ },
63
+ }}
64
+ {...props}
65
+ />
66
+ )
67
+ }
68
+ Calendar.displayName = "Calendar"
69
+
70
+ export { Calendar }