@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,92 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import { describe, it, expect } from 'vitest'
3
+ import { createRef } from 'react'
4
+ import { Card, CardHeader, CardTitle, CardContent, CardFooter } from './Card'
5
+
6
+ describe('Card', () => {
7
+ it('renders with children', () => {
8
+ render(<Card>Card content</Card>)
9
+ expect(screen.getByText('Card content')).toBeInTheDocument()
10
+ })
11
+
12
+ it('renders CardHeader, CardTitle, CardContent, and CardFooter', () => {
13
+ render(
14
+ <Card>
15
+ <CardHeader>
16
+ <CardTitle>My Title</CardTitle>
17
+ </CardHeader>
18
+ <CardContent>Body text</CardContent>
19
+ <CardFooter>Footer text</CardFooter>
20
+ </Card>
21
+ )
22
+
23
+ expect(screen.getByText('My Title')).toBeInTheDocument()
24
+ expect(screen.getByText('Body text')).toBeInTheDocument()
25
+ expect(screen.getByText('Footer text')).toBeInTheDocument()
26
+ })
27
+
28
+ it('renders CardTitle as an h3 element', () => {
29
+ render(<CardTitle>Heading</CardTitle>)
30
+ const heading = screen.getByRole('heading', { level: 3 })
31
+ expect(heading).toBeInTheDocument()
32
+ expect(heading).toHaveTextContent('Heading')
33
+ })
34
+
35
+ it('applies default variant classes', () => {
36
+ const { container } = render(<Card>Default</Card>)
37
+ const card = container.firstChild as HTMLElement
38
+ expect(card).toHaveClass('bg-surface')
39
+ expect(card).toHaveClass('border-border')
40
+ })
41
+
42
+ it('applies glass variant classes', () => {
43
+ const { container } = render(<Card variant="glass">Glass</Card>)
44
+ const card = container.firstChild as HTMLElement
45
+ expect(card).toHaveClass('bg-glass')
46
+ expect(card).toHaveClass('backdrop-blur-md')
47
+ })
48
+
49
+ it('applies outline variant classes', () => {
50
+ const { container } = render(<Card variant="outline">Outline</Card>)
51
+ const card = container.firstChild as HTMLElement
52
+ expect(card).toHaveClass('bg-transparent')
53
+ expect(card).toHaveClass('border-border')
54
+ })
55
+
56
+ it('applies hover effect classes when hoverEffect is true', () => {
57
+ const { container } = render(<Card hoverEffect>Hover</Card>)
58
+ const card = container.firstChild as HTMLElement
59
+ expect(card).toHaveClass('hover:shadow-lg')
60
+ expect(card).toHaveClass('hover:-translate-y-1')
61
+ })
62
+
63
+ it('forwards ref', () => {
64
+ const ref = createRef<HTMLDivElement>()
65
+ render(<Card ref={ref}>Ref Card</Card>)
66
+ expect(ref.current).toBeInstanceOf(HTMLDivElement)
67
+ })
68
+
69
+ it('applies custom className', () => {
70
+ const { container } = render(<Card className="my-custom">Custom</Card>)
71
+ const card = container.firstChild as HTMLElement
72
+ expect(card).toHaveClass('my-custom')
73
+ })
74
+
75
+ it('forwards ref on CardHeader', () => {
76
+ const ref = createRef<HTMLDivElement>()
77
+ render(<CardHeader ref={ref}>Header</CardHeader>)
78
+ expect(ref.current).toBeInstanceOf(HTMLDivElement)
79
+ })
80
+
81
+ it('forwards ref on CardContent', () => {
82
+ const ref = createRef<HTMLDivElement>()
83
+ render(<CardContent ref={ref}>Content</CardContent>)
84
+ expect(ref.current).toBeInstanceOf(HTMLDivElement)
85
+ })
86
+
87
+ it('forwards ref on CardFooter', () => {
88
+ const ref = createRef<HTMLDivElement>()
89
+ render(<CardFooter ref={ref}>Footer</CardFooter>)
90
+ expect(ref.current).toBeInstanceOf(HTMLDivElement)
91
+ })
92
+ })
@@ -0,0 +1,115 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { cn } from "../../lib/utils"
4
+
5
+ const cardVariants = cva(
6
+ "rounded-2xl border bg-surface text-foreground shadow-xs",
7
+ {
8
+ variants: {
9
+ hoverEffect: {
10
+ true: "transition-all duration-300 hover:shadow-lg hover:-translate-y-1 hover:border-primary",
11
+ false: "",
12
+ },
13
+ variant: {
14
+ default: "bg-surface border-border",
15
+ glass: "bg-glass border-glass-border backdrop-blur-md",
16
+ outline: "bg-transparent border-border",
17
+ }
18
+ },
19
+ defaultVariants: {
20
+ variant: "default",
21
+ hoverEffect: false,
22
+ },
23
+ }
24
+ )
25
+
26
+ export interface CardProps
27
+ extends React.HTMLAttributes<HTMLDivElement>,
28
+ VariantProps<typeof cardVariants> { }
29
+
30
+ const Card = (
31
+ {
32
+ ref,
33
+ className,
34
+ variant,
35
+ hoverEffect,
36
+ ...props
37
+ }: CardProps & {
38
+ ref?: React.Ref<HTMLDivElement>;
39
+ }
40
+ ) => (<div
41
+ ref={ref}
42
+ className={cn(cardVariants({ variant, hoverEffect, className }))}
43
+ {...props}
44
+ />)
45
+
46
+ const CardHeader = (
47
+ {
48
+ ref,
49
+ className,
50
+ ...props
51
+ }: React.HTMLAttributes<HTMLDivElement> & {
52
+ ref?: React.Ref<HTMLDivElement>;
53
+ }
54
+ ) => (<div
55
+ ref={ref}
56
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
57
+ {...props}
58
+ />)
59
+
60
+ const CardTitle = (
61
+ {
62
+ ref,
63
+ className,
64
+ ...props
65
+ }: React.HTMLAttributes<HTMLHeadingElement> & {
66
+ ref?: React.Ref<HTMLParagraphElement>;
67
+ }
68
+ ) => (<h3
69
+ ref={ref}
70
+ className={cn(
71
+ "text-2xl font-semibold leading-none tracking-tight font-heading",
72
+ className
73
+ )}
74
+ {...props}
75
+ />)
76
+
77
+ const CardDescription = (
78
+ {
79
+ ref,
80
+ className,
81
+ ...props
82
+ }: React.HTMLAttributes<HTMLParagraphElement> & {
83
+ ref?: React.Ref<HTMLParagraphElement>;
84
+ }
85
+ ) => (<p
86
+ ref={ref}
87
+ className={cn("text-sm text-foreground-secondary", className)}
88
+ {...props}
89
+ />)
90
+
91
+ const CardContent = (
92
+ {
93
+ ref,
94
+ className,
95
+ ...props
96
+ }: React.HTMLAttributes<HTMLDivElement> & {
97
+ ref?: React.Ref<HTMLDivElement>;
98
+ }
99
+ ) => (<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />)
100
+
101
+ const CardFooter = (
102
+ {
103
+ ref,
104
+ className,
105
+ ...props
106
+ }: React.HTMLAttributes<HTMLDivElement> & {
107
+ ref?: React.Ref<HTMLDivElement>;
108
+ }
109
+ ) => (<div
110
+ ref={ref}
111
+ className={cn("flex items-center p-6 pt-0", className)}
112
+ {...props}
113
+ />)
114
+
115
+ export { Card, cardVariants, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
@@ -0,0 +1,210 @@
1
+ 'use client';
2
+
3
+ import { ReactNode, useState, useMemo } from 'react';
4
+ import type { SyntaxType } from '../../lib/syntax-parser';
5
+ import { parseCode } from '../../lib/syntax-parser';
6
+
7
+ export interface CodeProps {
8
+ /** The code content to display */
9
+ children: ReactNode;
10
+ /** Optional syntax highlighting type */
11
+ syntax?: SyntaxType;
12
+ /** Whether to render as inline code (default) or block */
13
+ inline?: boolean;
14
+ /** Show copy button for block code (default: true) */
15
+ showCopy?: boolean;
16
+ /** Additional className for custom styling */
17
+ className?: string;
18
+ }
19
+
20
+ /**
21
+ * Code Atom
22
+ *
23
+ * A semantic code wrapper with automatic syntax highlighting and enhanced visual styling.
24
+ * Features distinct treatments for inline vs block code, with copy-on-hover for blocks.
25
+ *
26
+ * **Visual Design:**
27
+ * - Inline: Pale amber background (#FEF3E7 light / #252525 dark) with subtle border
28
+ * - Block: Cool gray background (#F8F9FA light / #1E1E1E dark) with generous padding
29
+ * - Copy button appears on hover for block code with tooltip feedback
30
+ * - Accessible contrast ratios (WCAG AA 4.5:1)
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * // Inline code - perfect for text snippets
35
+ * <Code>example</Code>
36
+ * <Code syntax="keyword">const</Code>
37
+ * <Code syntax="function">useState()</Code>
38
+ *
39
+ * // Block code - for larger code examples
40
+ * <Code inline={false}>const example = "value";</Code>
41
+ * <Code inline={false} syntax="keyword" showCopy={false}>
42
+ * const greeting = "Hello World";
43
+ * </Code>
44
+ * ```
45
+ */
46
+ export function Code({
47
+ children,
48
+ syntax = 'plain',
49
+ inline = true,
50
+ showCopy = true,
51
+ className = '',
52
+ }: CodeProps) {
53
+ const [copied, setCopied] = useState(false);
54
+ const [showTooltip, setShowTooltip] = useState(false);
55
+
56
+ // Auto-parse code for block syntax highlighting
57
+ const tokens = useMemo(() => {
58
+ if (!inline && typeof children === 'string') {
59
+ return parseCode(children);
60
+ }
61
+ return null;
62
+ }, [children, inline]);
63
+
64
+ // Handle copy to clipboard for block code
65
+ const handleCopy = async () => {
66
+ const text = typeof children === 'string' ? children : String(children);
67
+ try {
68
+ await navigator.clipboard.writeText(text);
69
+ setCopied(true);
70
+ setTimeout(() => setCopied(false), 2000);
71
+ } catch (err) {
72
+ console.error('Failed to copy code:', err);
73
+ }
74
+ };
75
+
76
+ // Inline code - subtle, integrated into text
77
+ if (inline) {
78
+ return (
79
+ <code
80
+ className={`font-mono text-[0.875em] px-[0.375rem] py-[0.125rem] rounded border ${className}`}
81
+ style={{
82
+ backgroundColor: 'var(--code-inline-bg)',
83
+ borderColor: 'var(--code-border)',
84
+ color: `var(--syntax-${syntax})`,
85
+ }}
86
+ >
87
+ {children}
88
+ </code>
89
+ );
90
+ }
91
+
92
+ // Block code - prominent with copy button on hover
93
+ return (
94
+ <div className={`relative group ${className}`}>
95
+ <pre
96
+ className="font-mono text-sm p-6 rounded-lg border overflow-x-auto"
97
+ style={{
98
+ backgroundColor: 'var(--code-block-bg)',
99
+ borderColor: 'var(--code-border)',
100
+ }}
101
+ >
102
+ <code>
103
+ {tokens ? (
104
+ // Automatic syntax highlighting with parsed tokens
105
+ tokens.map((token, index) => (
106
+ <span
107
+ key={index}
108
+ style={{
109
+ color: `var(--syntax-${token.type})`,
110
+ }}
111
+ >
112
+ {token.text}
113
+ </span>
114
+ ))
115
+ ) : (
116
+ // Fallback to single color for non-string children
117
+ <span
118
+ style={{
119
+ color: `var(--syntax-${syntax})`,
120
+ }}
121
+ >
122
+ {children}
123
+ </span>
124
+ )}
125
+ </code>
126
+ </pre>
127
+
128
+ {/* Copy Button - appears on hover for block code */}
129
+ {showCopy && (
130
+ <div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
131
+ <div className="relative">
132
+ <button
133
+ onClick={handleCopy}
134
+ onMouseEnter={() => setShowTooltip(true)}
135
+ onMouseLeave={() => setShowTooltip(false)}
136
+ className="flex items-center gap-2 px-3 py-2 text-xs rounded-md transition-all duration-200 border"
137
+ style={{
138
+ backgroundColor: 'var(--color-surface)',
139
+ borderColor: 'var(--color-border)',
140
+ color: 'var(--color-text-primary)',
141
+ }}
142
+ aria-label={copied ? 'Copied!' : 'Copy code'}
143
+ >
144
+ {copied ? (
145
+ <>
146
+ <svg
147
+ className="w-3.5 h-3.5"
148
+ fill="none"
149
+ stroke="currentColor"
150
+ viewBox="0 0 24 24"
151
+ aria-hidden="true"
152
+ >
153
+ <path
154
+ strokeLinecap="round"
155
+ strokeLinejoin="round"
156
+ strokeWidth={2}
157
+ d="M5 13l4 4L19 7"
158
+ />
159
+ </svg>
160
+ <span>Copied!</span>
161
+ </>
162
+ ) : (
163
+ <>
164
+ <svg
165
+ className="w-3.5 h-3.5"
166
+ fill="none"
167
+ stroke="currentColor"
168
+ viewBox="0 0 24 24"
169
+ aria-hidden="true"
170
+ >
171
+ <path
172
+ strokeLinecap="round"
173
+ strokeLinejoin="round"
174
+ strokeWidth={2}
175
+ d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2 2v8a2 2 0 002 2z"
176
+ />
177
+ </svg>
178
+ <span>Copy</span>
179
+ </>
180
+ )}
181
+ </button>
182
+
183
+ {/* Tooltip */}
184
+ {showTooltip && !copied && (
185
+ <div
186
+ role="tooltip"
187
+ className="absolute bottom-full right-0 mb-2 px-2 py-1 text-xs rounded whitespace-nowrap pointer-events-none"
188
+ style={{
189
+ backgroundColor: 'var(--color-text-primary)',
190
+ color: 'var(--color-background)',
191
+ }}
192
+ >
193
+ Copy code
194
+ {/* Arrow */}
195
+ <div
196
+ className="absolute top-full right-4 w-0 h-0"
197
+ style={{
198
+ borderLeft: '4px solid transparent',
199
+ borderRight: '4px solid transparent',
200
+ borderTop: '4px solid var(--color-text-primary)',
201
+ }}
202
+ />
203
+ </div>
204
+ )}
205
+ </div>
206
+ </div>
207
+ )}
208
+ </div>
209
+ );
210
+ }
@@ -0,0 +1,238 @@
1
+ 'use client';
2
+
3
+ import { useState, useMemo } from 'react';
4
+ import { useTheme } from '../../hooks/useTheme';
5
+ import { syntaxColors } from '@thesage/tokens';
6
+ import { parseCode, type SyntaxToken } from '../../lib/syntax-parser';
7
+
8
+ export interface CollapsibleCodeBlockProps {
9
+ /** Unique identifier for the code block (required for animation) */
10
+ id: string;
11
+ /** Title/label for the code block */
12
+ title?: string;
13
+ /** Code to display - can be string or array of syntax tokens */
14
+ code: string | SyntaxToken[];
15
+ /** Language identifier (e.g., 'typescript', 'css', 'html') */
16
+ language?: string;
17
+ /** Initial collapsed state */
18
+ defaultCollapsed?: boolean;
19
+ /** Show copy button */
20
+ showCopy?: boolean;
21
+ /** Custom className for container */
22
+ className?: string;
23
+ }
24
+
25
+ /**
26
+ * CollapsibleCodeBlock Organism
27
+ *
28
+ * A reusable code block component with:
29
+ * - Smooth spring animation for expand/collapse
30
+ * - Syntax highlighting with light/dark theme support
31
+ * - Copy to clipboard functionality
32
+ * - Preview mode showing first 3 lines
33
+ * - Gradient overlay in collapsed state
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * <CollapsibleCodeBlock
38
+ * id="example-code"
39
+ * title="TypeScript Example"
40
+ * code={[
41
+ * { text: 'const', type: 'keyword' },
42
+ * { text: ' example ', type: 'plain' },
43
+ * { text: '=', type: 'operator' },
44
+ * { text: ' "Hello"', type: 'string' },
45
+ * ]}
46
+ * language="typescript"
47
+ * />
48
+ * ```
49
+ */
50
+ export function CollapsibleCodeBlock({
51
+ id,
52
+ title,
53
+ code,
54
+ language,
55
+ defaultCollapsed = true,
56
+ showCopy = true,
57
+ className = '',
58
+ }: CollapsibleCodeBlockProps) {
59
+ const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
60
+ const [copySuccess, setCopySuccess] = useState(false);
61
+ const { mode } = useTheme();
62
+
63
+ // Get the appropriate color scheme based on current theme mode
64
+ const colors = mode === 'dark' ? syntaxColors.dark : syntaxColors.light;
65
+
66
+ // Auto-tokenize string code for syntax highlighting
67
+ const tokens = useMemo(() => {
68
+ return typeof code === 'string' ? parseCode(code) : code;
69
+ }, [code]);
70
+
71
+ // Convert tokens to string for copying
72
+ const codeString = tokens.map(token => token.text).join('');
73
+
74
+ // Handle copy to clipboard
75
+ const handleCopy = async () => {
76
+ try {
77
+ await navigator.clipboard.writeText(codeString);
78
+ setCopySuccess(true);
79
+ setTimeout(() => setCopySuccess(false), 2000);
80
+ } catch (err) {
81
+ console.error('Failed to copy:', err);
82
+ }
83
+ };
84
+
85
+ // Handle toggle animation
86
+ const handleToggle = () => {
87
+ const preview = document.getElementById(`${id}-preview`);
88
+ const codeBlock = document.getElementById(`${id}-code`);
89
+ const icon = document.getElementById(`${id}-icon`);
90
+
91
+ if (preview && codeBlock && icon) {
92
+ const isHidden = codeBlock.classList.contains('hidden');
93
+
94
+ if (isHidden) {
95
+ // Opening: start at preview height, expand to full
96
+ preview.classList.add('hidden');
97
+ codeBlock.classList.remove('hidden');
98
+ codeBlock.style.maxHeight = '6.6rem'; // Match preview height (3 lines)
99
+ void codeBlock.offsetHeight; // Force reflow
100
+ codeBlock.style.maxHeight = codeBlock.scrollHeight + 'px';
101
+ } else {
102
+ // Closing: collapse to preview height, then swap
103
+ codeBlock.style.maxHeight = '6.6rem';
104
+ setTimeout(() => {
105
+ codeBlock.classList.add('hidden');
106
+ preview.classList.remove('hidden');
107
+ }, 500);
108
+ }
109
+
110
+ icon.classList.toggle('rotate-90');
111
+ setIsCollapsed(!isCollapsed);
112
+ }
113
+ };
114
+
115
+ // Render syntax-highlighted code with inline colors from tokens
116
+ const renderCode = (tokensToRender: SyntaxToken[]) => {
117
+ return tokensToRender.map((token, index) => {
118
+ // Get color from syntax tokens based on token type
119
+ const color = token.type ? colors[token.type] : colors.plain;
120
+
121
+ return (
122
+ <span
123
+ key={index}
124
+ style={{ color }}
125
+ >
126
+ {token.text}
127
+ </span>
128
+ );
129
+ });
130
+ };
131
+
132
+ // Get preview tokens (first ~3 lines worth)
133
+ const previewTokens = useMemo(() => {
134
+ // Count characters to approximate 3 lines (~120 chars)
135
+ let charCount = 0;
136
+ const maxChars = 120;
137
+ const preview: SyntaxToken[] = [];
138
+
139
+ for (const token of tokens) {
140
+ if (charCount >= maxChars) break;
141
+ preview.push(token);
142
+ charCount += token.text.length;
143
+ }
144
+
145
+ return preview;
146
+ }, [tokens]);
147
+
148
+ return (
149
+ <div className={`w-full min-w-0 ${className}`}>
150
+ {/* Title */}
151
+ {title && (
152
+ <h3 className="text-lg font-semibold mb-3 text-[var(--color-text-primary)]">
153
+ {title}
154
+ </h3>
155
+ )}
156
+
157
+ {/* Action Buttons */}
158
+ <div className="flex gap-2 mb-4">
159
+ <button
160
+ onClick={handleToggle}
161
+ className="flex items-center gap-2 px-3 py-2 text-xs text-[var(--color-text-primary)] bg-[var(--color-surface)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-foreground)] hover:scale-105 hover:shadow-lg active:scale-95 border border-[var(--color-border)] rounded-md transition-all duration-200"
162
+ aria-expanded={!isCollapsed}
163
+ aria-controls={`${id}-code`}
164
+ aria-label={isCollapsed ? 'Show code' : 'Hide code'}
165
+ >
166
+ <svg
167
+ id={`${id}-icon`}
168
+ className="w-3 h-3 transition-transform duration-200"
169
+ fill="none"
170
+ stroke="currentColor"
171
+ viewBox="0 0 24 24"
172
+ aria-hidden="true"
173
+ >
174
+ <path
175
+ strokeLinecap="round"
176
+ strokeLinejoin="round"
177
+ strokeWidth={2}
178
+ d="M9 5l7 7-7 7"
179
+ />
180
+ </svg>
181
+ {isCollapsed ? 'Show Code' : 'Hide Code'}
182
+ </button>
183
+
184
+ {showCopy && (
185
+ <button
186
+ onClick={handleCopy}
187
+ aria-label={copySuccess ? 'Copied to clipboard' : 'Copy code to clipboard'}
188
+ className="flex items-center gap-2 px-3 py-2 text-xs text-[var(--color-text-primary)] bg-[var(--color-surface)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-foreground)] hover:scale-105 hover:shadow-lg active:scale-95 border border-[var(--color-border)] rounded-md transition-all duration-200"
189
+ >
190
+ {copySuccess ? (
191
+ <>
192
+ <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
193
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
194
+ </svg>
195
+ Copied!
196
+ </>
197
+ ) : (
198
+ <>
199
+ <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
200
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012-2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
201
+ </svg>
202
+ Copy
203
+ </>
204
+ )}
205
+ </button>
206
+ )}
207
+ </div>
208
+
209
+ {/* Code Preview (visible when collapsed) */}
210
+ <div
211
+ id={`${id}-preview`}
212
+ className={`bg-[var(--color-background)] p-4 rounded border border-[var(--color-border)] overflow-hidden mb-4 w-full max-w-full ${isCollapsed ? '' : 'hidden'}`}
213
+ style={{ height: '6.6rem' }}
214
+ >
215
+ <div className="relative w-full max-w-full min-w-0">
216
+ <pre className="text-sm font-mono overflow-x-auto w-full max-w-full whitespace-pre">
217
+ <code>{renderCode(previewTokens)}</code>
218
+ </pre>
219
+ <div className="absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-[var(--color-background)] to-transparent pointer-events-none" />
220
+ </div>
221
+ </div>
222
+
223
+ {/* Full Code (hidden by default) */}
224
+ <div
225
+ id={`${id}-code`}
226
+ className={`transition-all duration-500 ease-out overflow-hidden bg-[var(--color-background)] p-4 rounded border border-[var(--color-border)] w-full max-w-full ${isCollapsed ? 'hidden' : ''}`}
227
+ style={{
228
+ maxHeight: isCollapsed ? '0px' : 'none',
229
+ transition: 'max-height 0.5s cubic-bezier(0.34, 1.56, 0.64, 1)',
230
+ }}
231
+ >
232
+ <pre className="text-sm font-mono overflow-x-auto w-full max-w-full whitespace-pre">
233
+ <code>{renderCode(tokens)}</code>
234
+ </pre>
235
+ </div>
236
+ </div>
237
+ );
238
+ }