@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,76 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import userEvent from '@testing-library/user-event'
3
+ import { describe, it, expect, vi } from 'vitest'
4
+ import { createRef } from 'react'
5
+ import { Switch } from './Switch'
6
+
7
+ describe('Switch', () => {
8
+ it('renders the switch', () => {
9
+ render(<Switch />)
10
+ const switchEl = screen.getByRole('switch')
11
+ expect(switchEl).toBeInTheDocument()
12
+ })
13
+
14
+ it('is unchecked by default', () => {
15
+ render(<Switch />)
16
+ const switchEl = screen.getByRole('switch')
17
+ expect(switchEl).toHaveAttribute('data-state', 'unchecked')
18
+ })
19
+
20
+ it('toggles on click', async () => {
21
+ const user = userEvent.setup()
22
+ render(<Switch />)
23
+
24
+ const switchEl = screen.getByRole('switch')
25
+ expect(switchEl).toHaveAttribute('data-state', 'unchecked')
26
+
27
+ await user.click(switchEl)
28
+ expect(switchEl).toHaveAttribute('data-state', 'checked')
29
+
30
+ await user.click(switchEl)
31
+ expect(switchEl).toHaveAttribute('data-state', 'unchecked')
32
+ })
33
+
34
+ it('calls onCheckedChange when toggled', async () => {
35
+ const user = userEvent.setup()
36
+ const handleChange = vi.fn()
37
+ render(<Switch onCheckedChange={handleChange} />)
38
+
39
+ const switchEl = screen.getByRole('switch')
40
+ await user.click(switchEl)
41
+
42
+ expect(handleChange).toHaveBeenCalledTimes(1)
43
+ expect(handleChange).toHaveBeenCalledWith(true)
44
+ })
45
+
46
+ it('can be disabled', async () => {
47
+ const user = userEvent.setup()
48
+ const handleChange = vi.fn()
49
+ render(<Switch disabled onCheckedChange={handleChange} />)
50
+
51
+ const switchEl = screen.getByRole('switch')
52
+ expect(switchEl).toBeDisabled()
53
+
54
+ await user.click(switchEl)
55
+ expect(handleChange).not.toHaveBeenCalled()
56
+ expect(switchEl).toHaveAttribute('data-state', 'unchecked')
57
+ })
58
+
59
+ it('can be initially checked', () => {
60
+ render(<Switch defaultChecked />)
61
+ const switchEl = screen.getByRole('switch')
62
+ expect(switchEl).toHaveAttribute('data-state', 'checked')
63
+ })
64
+
65
+ it('forwards ref', () => {
66
+ const ref = createRef<HTMLButtonElement>()
67
+ render(<Switch ref={ref} />)
68
+ expect(ref.current).toBeInstanceOf(HTMLButtonElement)
69
+ })
70
+
71
+ it('applies custom className', () => {
72
+ render(<Switch className="custom-switch" />)
73
+ const switchEl = screen.getByRole('switch')
74
+ expect(switchEl).toHaveClass('custom-switch')
75
+ })
76
+ })
@@ -0,0 +1,30 @@
1
+ "use client";
2
+ import * as React from "react"
3
+ import * as SwitchPrimitives from "@radix-ui/react-switch"
4
+
5
+ import { cn } from "../../lib/utils"
6
+
7
+ const Switch = (
8
+ {
9
+ ref,
10
+ className,
11
+ ...props
12
+ }: React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> & {
13
+ ref?: React.Ref<React.ElementRef<typeof SwitchPrimitives.Root>>;
14
+ }
15
+ ) => (<SwitchPrimitives.Root
16
+ className={cn(
17
+ "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-xs transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
18
+ className
19
+ )}
20
+ {...props}
21
+ ref={ref}
22
+ >
23
+ <SwitchPrimitives.Thumb
24
+ className={cn(
25
+ "pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
26
+ )}
27
+ />
28
+ </SwitchPrimitives.Root>)
29
+
30
+ export { Switch }
@@ -0,0 +1,152 @@
1
+ import React from 'react';
2
+
3
+ export interface TextFieldProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
4
+ /**
5
+ * Visual variant of the text field
6
+ * @default 'outlined'
7
+ */
8
+ variant?: 'outlined' | 'filled';
9
+
10
+ /**
11
+ * Size of the text field
12
+ * @default 'md'
13
+ */
14
+ size?: 'sm' | 'md' | 'lg';
15
+
16
+ /**
17
+ * Error state
18
+ * @default false
19
+ */
20
+ error?: boolean;
21
+
22
+ /**
23
+ * Helper text displayed below the input
24
+ */
25
+ helperText?: string;
26
+
27
+ /**
28
+ * Label for the input
29
+ */
30
+ label?: string;
31
+
32
+ /**
33
+ * Whether the field is required
34
+ * @default false
35
+ */
36
+ required?: boolean;
37
+ }
38
+
39
+ /**
40
+ * TextField Component
41
+ *
42
+ * A text input field with support for outlined and filled variants,
43
+ * multiple sizes, error states, and helper text.
44
+ *
45
+ * Features:
46
+ * - Two visual variants (outlined, filled)
47
+ * - Three size options (sm, md, lg)
48
+ * - Error state with red border
49
+ * - Optional label and helper text
50
+ * - Theme-aware colors using CSS variables
51
+ * - Full keyboard accessibility
52
+ * - Ref forwarding support
53
+ */
54
+ export const TextField = (
55
+ {
56
+ ref,
57
+ variant = 'outlined',
58
+ size = 'md',
59
+ error = false,
60
+ helperText,
61
+ label,
62
+ required = false,
63
+ className = '',
64
+ id,
65
+ ...props
66
+ }: TextFieldProps & {
67
+ ref?: React.Ref<HTMLInputElement>;
68
+ }
69
+ ) => {
70
+ // Generate unique ID if not provided
71
+ const inputId = id || `textfield-${Math.random().toString(36).substr(2, 9)}`;
72
+ const helperTextId = helperText ? `${inputId}-helper` : undefined;
73
+
74
+ // Size classes
75
+ const sizeClasses = {
76
+ sm: 'px-3 py-1.5 text-sm',
77
+ md: 'px-4 py-2 text-base',
78
+ lg: 'px-5 py-3 text-lg',
79
+ };
80
+
81
+ // Variant classes
82
+ const variantClasses = {
83
+ outlined: `
84
+ bg-[var(--color-surface)]
85
+ border-2
86
+ ${error ? 'border-[var(--color-error)]' : 'border-[var(--color-border)]'}
87
+ focus:border-[var(--color-primary)]
88
+ `,
89
+ filled: `
90
+ bg-[var(--color-surface)]
91
+ border-2 border-transparent
92
+ ${error ? 'border-[var(--color-error)]' : ''}
93
+ focus:border-[var(--color-primary)]
94
+ `,
95
+ };
96
+
97
+ const baseClasses = `
98
+ w-full
99
+ rounded-lg
100
+ text-[var(--color-text-primary)]
101
+ placeholder:text-[var(--color-text-muted)]
102
+ transition-colors
103
+ duration-200
104
+ focus:outline-none
105
+ focus:ring-2
106
+ focus:ring-[var(--color-focus)]
107
+ focus:ring-opacity-50
108
+ disabled:opacity-50
109
+ disabled:cursor-not-allowed
110
+ `;
111
+
112
+ return (
113
+ <div className="w-full">
114
+ {label && (
115
+ <label
116
+ htmlFor={inputId}
117
+ className="block mb-2 text-sm font-medium text-[var(--color-text-primary)]"
118
+ >
119
+ {label}
120
+ {required && <span className="text-[var(--color-error)] ml-1">*</span>}
121
+ </label>
122
+ )}
123
+
124
+ <input
125
+ ref={ref}
126
+ id={inputId}
127
+ aria-describedby={helperTextId}
128
+ aria-invalid={error}
129
+ aria-required={required}
130
+ className={`
131
+ ${baseClasses}
132
+ ${sizeClasses[size]}
133
+ ${variantClasses[variant]}
134
+ ${className}
135
+ `}
136
+ {...props}
137
+ />
138
+
139
+ {helperText && (
140
+ <p
141
+ id={helperTextId}
142
+ className={`
143
+ mt-2 text-sm
144
+ ${error ? 'text-[var(--color-error)]' : 'text-[var(--color-text-secondary)]'}
145
+ `}
146
+ >
147
+ {helperText}
148
+ </p>
149
+ )}
150
+ </div>
151
+ );
152
+ };
@@ -0,0 +1,41 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import userEvent from '@testing-library/user-event'
3
+ import { describe, it, expect, vi } from 'vitest'
4
+ import { createRef } from 'react'
5
+ import { Textarea } from './Textarea'
6
+
7
+ describe('Textarea', () => {
8
+ it('renders a textarea', () => {
9
+ render(<Textarea aria-label="Message" />)
10
+ expect(screen.getByRole('textbox')).toBeInTheDocument()
11
+ })
12
+
13
+ it('accepts placeholder text', () => {
14
+ render(<Textarea placeholder="Write here..." />)
15
+ expect(screen.getByPlaceholderText('Write here...')).toBeInTheDocument()
16
+ })
17
+
18
+ it('handles user input', async () => {
19
+ const user = userEvent.setup()
20
+ render(<Textarea aria-label="Message" />)
21
+
22
+ await user.type(screen.getByRole('textbox'), 'Hello world')
23
+ expect(screen.getByRole('textbox')).toHaveValue('Hello world')
24
+ })
25
+
26
+ it('can be disabled', () => {
27
+ render(<Textarea disabled aria-label="Message" />)
28
+ expect(screen.getByRole('textbox')).toBeDisabled()
29
+ })
30
+
31
+ it('applies custom className', () => {
32
+ render(<Textarea className="custom-textarea" aria-label="Message" />)
33
+ expect(screen.getByRole('textbox')).toHaveClass('custom-textarea')
34
+ })
35
+
36
+ it('forwards ref', () => {
37
+ const ref = createRef<HTMLTextAreaElement>()
38
+ render(<Textarea ref={ref} aria-label="Message" />)
39
+ expect(ref.current).toBeInstanceOf(HTMLTextAreaElement)
40
+ })
41
+ })
@@ -0,0 +1,29 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "../../lib/utils"
4
+
5
+ export interface TextareaProps
6
+ extends React.TextareaHTMLAttributes<HTMLTextAreaElement> { }
7
+
8
+ const Textarea = (
9
+ {
10
+ ref,
11
+ className,
12
+ ...props
13
+ }: TextareaProps & {
14
+ ref?: React.Ref<HTMLTextAreaElement>;
15
+ }
16
+ ) => {
17
+ return (
18
+ <textarea
19
+ className={cn(
20
+ "flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
21
+ className
22
+ )}
23
+ ref={ref}
24
+ {...props}
25
+ />
26
+ )
27
+ }
28
+
29
+ export { Textarea }
@@ -0,0 +1,290 @@
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+ import { useTheme } from '../../hooks/useTheme';
5
+ import { Switch } from '../forms/Switch';
6
+
7
+ export interface ThemeSwitcherProps {
8
+ /**
9
+ * Size of the switcher
10
+ * @default 'md'
11
+ */
12
+ size?: 'sm' | 'md' | 'lg';
13
+
14
+ /**
15
+ * Initial expanded state
16
+ * @default false
17
+ */
18
+ defaultExpanded?: boolean;
19
+
20
+ /**
21
+ * Additional CSS classes
22
+ */
23
+ className?: string;
24
+ }
25
+
26
+ /**
27
+ * ThemeSwitcher Molecule
28
+ *
29
+ * An enhanced theme control panel with expandable options for comprehensive theme management.
30
+ *
31
+ * Features:
32
+ * - Quick toggle between light and dark modes
33
+ * - Expandable panel with additional controls
34
+ * - System/Auto mode preference
35
+ * - Visual preview of current mode
36
+ * - Smooth animations and transitions
37
+ * - Full keyboard accessibility
38
+ * - Theme-aware colors
39
+ *
40
+ * Example:
41
+ * ```tsx
42
+ * // Basic usage
43
+ * <ThemeSwitcher />
44
+ *
45
+ * // Expanded by default
46
+ * <ThemeSwitcher defaultExpanded />
47
+ *
48
+ * // Large size
49
+ * <ThemeSwitcher size="lg" />
50
+ * ```
51
+ */
52
+ export const ThemeSwitcher: React.FC<ThemeSwitcherProps> = ({
53
+ size = 'md',
54
+ defaultExpanded = false,
55
+ className = '',
56
+ }) => {
57
+ const { mode, setMode } = useTheme();
58
+ const [isExpanded, setIsExpanded] = useState(defaultExpanded);
59
+ const [useSystemTheme, setUseSystemTheme] = useState(false);
60
+
61
+ const toggleMode = () => {
62
+ setMode(mode === 'light' ? 'dark' : 'light');
63
+ };
64
+
65
+ const sizeClasses = {
66
+ sm: 'text-xs',
67
+ md: 'text-sm',
68
+ lg: 'text-base',
69
+ };
70
+
71
+ const iconSizes = {
72
+ sm: 16,
73
+ md: 20,
74
+ lg: 24,
75
+ };
76
+
77
+ const iconSize = iconSizes[size];
78
+
79
+ return (
80
+ <div className={`relative ${className}`}>
81
+ {/* Main Toggle Button */}
82
+ <div className="flex items-center gap-2">
83
+ <button
84
+ onClick={toggleMode}
85
+ className={`
86
+ p-2.5
87
+ rounded-lg
88
+ bg-[var(--color-surface)]
89
+ border border-[var(--color-border)]
90
+ text-[var(--color-text-primary)]
91
+ hover:bg-[var(--color-hover)]
92
+ active:bg-[var(--color-active)]
93
+ transition-all duration-200
94
+ focus-visible:outline-none
95
+ focus-visible:ring-2
96
+ focus-visible:ring-[var(--color-focus)]
97
+ focus-visible:ring-offset-2
98
+ `}
99
+ aria-label={`Switch to ${mode === 'light' ? 'dark' : 'light'} mode`}
100
+ title={`Switch to ${mode === 'light' ? 'dark' : 'light'} mode`}
101
+ >
102
+ {/* Sun Icon (Light Mode) */}
103
+ {mode === 'light' && (
104
+ <svg
105
+ width={iconSize}
106
+ height={iconSize}
107
+ viewBox="0 0 24 24"
108
+ fill="none"
109
+ stroke="currentColor"
110
+ strokeWidth="2"
111
+ strokeLinecap="round"
112
+ strokeLinejoin="round"
113
+ className="transition-transform duration-200"
114
+ aria-hidden="true"
115
+ >
116
+ <circle cx="12" cy="12" r="5" />
117
+ <line x1="12" y1="1" x2="12" y2="3" />
118
+ <line x1="12" y1="21" x2="12" y2="23" />
119
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
120
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
121
+ <line x1="1" y1="12" x2="3" y2="12" />
122
+ <line x1="21" y1="12" x2="23" y2="12" />
123
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
124
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
125
+ </svg>
126
+ )}
127
+
128
+ {/* Moon Icon (Dark Mode) */}
129
+ {mode === 'dark' && (
130
+ <svg
131
+ width={iconSize}
132
+ height={iconSize}
133
+ viewBox="0 0 24 24"
134
+ fill="none"
135
+ stroke="currentColor"
136
+ strokeWidth="2"
137
+ strokeLinecap="round"
138
+ strokeLinejoin="round"
139
+ className="transition-transform duration-200"
140
+ aria-hidden="true"
141
+ >
142
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
143
+ </svg>
144
+ )}
145
+ </button>
146
+
147
+ {/* Expand Toggle Button */}
148
+ <button
149
+ onClick={() => setIsExpanded(!isExpanded)}
150
+ className={`
151
+ p-2.5
152
+ rounded-lg
153
+ bg-[var(--color-surface)]
154
+ border border-[var(--color-border)]
155
+ text-[var(--color-text-primary)]
156
+ hover:bg-[var(--color-hover)]
157
+ active:bg-[var(--color-active)]
158
+ transition-all duration-200
159
+ focus-visible:outline-none
160
+ focus-visible:ring-2
161
+ focus-visible:ring-[var(--color-focus)]
162
+ focus-visible:ring-offset-2
163
+ `}
164
+ aria-label={isExpanded ? 'Hide theme options' : 'Show theme options'}
165
+ aria-expanded={isExpanded}
166
+ >
167
+ <svg
168
+ width={iconSize}
169
+ height={iconSize}
170
+ viewBox="0 0 24 24"
171
+ fill="none"
172
+ stroke="currentColor"
173
+ strokeWidth="2"
174
+ strokeLinecap="round"
175
+ strokeLinejoin="round"
176
+ className={`transition-transform duration-200 ${isExpanded ? 'rotate-180' : ''}`}
177
+ aria-hidden="true"
178
+ >
179
+ <polyline points="6 9 12 15 18 9" />
180
+ </svg>
181
+ </button>
182
+ </div>
183
+
184
+ {/* Expandable Options Panel */}
185
+ {isExpanded && (
186
+ <div
187
+ className={`
188
+ absolute bottom-full right-0 mb-2
189
+ min-w-[280px]
190
+ max-h-[80vh]
191
+ overflow-y-auto
192
+ p-4
193
+ rounded-lg
194
+ bg-[var(--color-surface)]
195
+ border border-[var(--color-border)]
196
+ shadow-lg
197
+ animate-in fade-in slide-in-from-bottom-2
198
+ duration-200
199
+ z-50
200
+ ${sizeClasses[size]}
201
+ `}
202
+ >
203
+ <div className="space-y-4">
204
+ {/* Header */}
205
+ <div className="pb-3 border-b border-[var(--color-border)]">
206
+ <h3 className="font-semibold text-[var(--color-text-primary)]">
207
+ Theme Settings
208
+ </h3>
209
+ <p className="text-[var(--color-text-muted)] mt-1">
210
+ Customize your viewing experience
211
+ </p>
212
+ </div>
213
+
214
+ {/* Current Mode Display */}
215
+ <div className="space-y-2">
216
+ <label className="text-[var(--color-text-secondary)] font-medium">
217
+ Current Mode
218
+ </label>
219
+ <div className="flex gap-2">
220
+ <button
221
+ onClick={() => setMode('light')}
222
+ className={`
223
+ flex-1 py-2 px-3 rounded-md
224
+ transition-all duration-200
225
+ ${mode === 'light'
226
+ ? 'bg-[var(--color-primary)] text-[var(--color-primary-foreground)] shadow-xs'
227
+ : 'bg-[var(--color-background)] border border-[var(--color-border)] text-[var(--color-text-secondary)] hover:bg-[var(--color-hover)]'
228
+ }
229
+ `}
230
+ >
231
+ Light
232
+ </button>
233
+ <button
234
+ onClick={() => setMode('dark')}
235
+ className={`
236
+ flex-1 py-2 px-3 rounded-md
237
+ transition-all duration-200
238
+ ${mode === 'dark'
239
+ ? 'bg-[var(--color-primary)] text-[var(--color-primary-foreground)] shadow-xs'
240
+ : 'bg-[var(--color-background)] border border-[var(--color-border)] text-[var(--color-text-secondary)] hover:bg-[var(--color-hover)]'
241
+ }
242
+ `}
243
+ >
244
+ Dark
245
+ </button>
246
+ </div>
247
+ </div>
248
+
249
+ {/* System Theme Option */}
250
+ <div className="space-y-2">
251
+ <label className="flex items-center justify-between">
252
+ <span className="text-[var(--color-text-secondary)] font-medium">
253
+ Use System Theme
254
+ </span>
255
+ <Switch
256
+ checked={useSystemTheme}
257
+ onCheckedChange={setUseSystemTheme}
258
+ />
259
+ </label>
260
+ <p className="text-[var(--color-text-muted)]">
261
+ Automatically match your system's theme preference
262
+ </p>
263
+ </div>
264
+
265
+ {/* Preview */}
266
+ <div className="space-y-2">
267
+ <label className="text-[var(--color-text-secondary)] font-medium">
268
+ Preview
269
+ </label>
270
+ <div className={`
271
+ p-3 rounded-md border border-[var(--color-border)]
272
+ bg-[var(--color-background)]
273
+ `}>
274
+ <div className="flex items-center gap-2">
275
+ <div className="w-8 h-8 rounded bg-[var(--color-primary)]" />
276
+ <div className="flex-1 space-y-1">
277
+ <div className="h-2 bg-[var(--color-text-primary)] rounded w-3/4 opacity-70" />
278
+ <div className="h-2 bg-[var(--color-text-secondary)] rounded w-1/2 opacity-50" />
279
+ </div>
280
+ </div>
281
+ </div>
282
+ </div>
283
+ </div>
284
+ </div>
285
+ )}
286
+ </div>
287
+ );
288
+ };
289
+
290
+ ThemeSwitcher.displayName = 'ThemeSwitcher';