@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.
- package/.claude/CLAUDE.md +239 -0
- package/README.md +161 -0
- package/dist/cli.mjs +151 -0
- package/dist/dates.d.mts +20 -0
- package/dist/dates.d.ts +20 -0
- package/dist/dates.js +240 -0
- package/dist/dates.js.map +1 -0
- package/dist/dates.mjs +203 -0
- package/dist/dates.mjs.map +1 -0
- package/dist/dnd.d.mts +126 -0
- package/dist/dnd.d.ts +126 -0
- package/dist/dnd.js +274 -0
- package/dist/dnd.js.map +1 -0
- package/dist/dnd.mjs +250 -0
- package/dist/dnd.mjs.map +1 -0
- package/dist/fontThemes-Dh8mtXES.d.mts +868 -0
- package/dist/fontThemes-Dh8mtXES.d.ts +868 -0
- package/dist/forms.d.mts +38 -0
- package/dist/forms.d.ts +38 -0
- package/dist/forms.js +198 -0
- package/dist/forms.js.map +1 -0
- package/dist/forms.mjs +159 -0
- package/dist/forms.mjs.map +1 -0
- package/dist/hooks-1b8WaQf1.d.mts +225 -0
- package/dist/hooks-CKW8vE9H.d.ts +225 -0
- package/dist/hooks.d.mts +3 -0
- package/dist/hooks.d.ts +3 -0
- package/dist/hooks.js +971 -0
- package/dist/hooks.js.map +1 -0
- package/dist/hooks.mjs +943 -0
- package/dist/hooks.mjs.map +1 -0
- package/dist/index-DscTIrZ2.d.mts +29 -0
- package/dist/index-DscTIrZ2.d.ts +29 -0
- package/dist/index.d.mts +3382 -0
- package/dist/index.d.ts +3382 -0
- package/dist/index.js +15146 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +14802 -0
- package/dist/index.mjs.map +1 -0
- package/dist/providers-CXPDMsl7.d.mts +30 -0
- package/dist/providers-Dn_Msjvz.d.ts +30 -0
- package/dist/providers.d.mts +3 -0
- package/dist/providers.d.ts +3 -0
- package/dist/providers.js +1885 -0
- package/dist/providers.js.map +1 -0
- package/dist/providers.mjs +1859 -0
- package/dist/providers.mjs.map +1 -0
- package/dist/tables.d.mts +10 -0
- package/dist/tables.d.ts +10 -0
- package/dist/tables.js +248 -0
- package/dist/tables.js.map +1 -0
- package/dist/tables.mjs +218 -0
- package/dist/tables.mjs.map +1 -0
- package/dist/tokens.d.mts +1065 -0
- package/dist/tokens.d.ts +1065 -0
- package/dist/tokens.js +2637 -0
- package/dist/tokens.js.map +1 -0
- package/dist/tokens.mjs +2555 -0
- package/dist/tokens.mjs.map +1 -0
- package/dist/utils-CIIM7dAC.d.ts +986 -0
- package/dist/utils-Cs04sxth.d.mts +986 -0
- package/dist/utils.d.mts +4 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +874 -0
- package/dist/utils.js.map +1 -0
- package/dist/utils.mjs +806 -0
- package/dist/utils.mjs.map +1 -0
- package/dist/validation-Bj1ye-v_.d.mts +114 -0
- package/dist/validation-Bj1ye-v_.d.ts +114 -0
- package/dist/webgl.d.mts +104 -0
- package/dist/webgl.d.ts +104 -0
- package/dist/webgl.js +226 -0
- package/dist/webgl.js.map +1 -0
- package/dist/webgl.mjs +195 -0
- package/dist/webgl.mjs.map +1 -0
- package/package.json +267 -0
- package/src/cli.ts +206 -0
- package/src/component-registry.ts +183 -0
- package/src/components/actions/Button.test.tsx +61 -0
- package/src/components/actions/Button.tsx +70 -0
- package/src/components/actions/Link.tsx +78 -0
- package/src/components/actions/Magnetic.tsx +68 -0
- package/src/components/actions/Toggle.test.tsx +40 -0
- package/src/components/actions/Toggle.tsx +47 -0
- package/src/components/actions/ToggleGroup.tsx +70 -0
- package/src/components/actions/index.ts +5 -0
- package/src/components/backgrounds/FaultyTerminal.tsx +426 -0
- package/src/components/backgrounds/OrbBackground.tsx +424 -0
- package/src/components/backgrounds/WarpBackground.tsx +358 -0
- package/src/components/backgrounds/index.ts +3 -0
- package/src/components/blocks/Hero.tsx +142 -0
- package/src/components/blocks/social/OpenGraphCard.tsx +243 -0
- package/src/components/cursor/SplashCursor.tsx +1315 -0
- package/src/components/cursor/TargetCursor.tsx +187 -0
- package/src/components/cursor/index.ts +2 -0
- package/src/components/data-display/AspectImage.tsx +73 -0
- package/src/components/data-display/Avatar.test.tsx +35 -0
- package/src/components/data-display/Avatar.tsx +55 -0
- package/src/components/data-display/Badge.test.tsx +43 -0
- package/src/components/data-display/Badge.tsx +84 -0
- package/src/components/data-display/Brand.tsx +123 -0
- package/src/components/data-display/Calendar.tsx +70 -0
- package/src/components/data-display/Card.test.tsx +92 -0
- package/src/components/data-display/Card.tsx +115 -0
- package/src/components/data-display/Code.tsx +210 -0
- package/src/components/data-display/CollapsibleCodeBlock.tsx +238 -0
- package/src/components/data-display/DataTable.tsx +119 -0
- package/src/components/data-display/DescriptionList.tsx +41 -0
- package/src/components/data-display/GitHubIcon.tsx +44 -0
- package/src/components/data-display/Heading.test.tsx +36 -0
- package/src/components/data-display/Heading.tsx +83 -0
- package/src/components/data-display/StatCard.tsx +195 -0
- package/src/components/data-display/Table.tsx +133 -0
- package/src/components/data-display/Text.test.tsx +48 -0
- package/src/components/data-display/Text.tsx +144 -0
- package/src/components/data-display/Timeline.tsx +194 -0
- package/src/components/data-display/TreeView.tsx +226 -0
- package/src/components/data-display/Typewriter.tsx +119 -0
- package/src/components/data-display/VariableWeightText.tsx +130 -0
- package/src/components/data-display/index.ts +19 -0
- package/src/components/feedback/Alert.test.tsx +44 -0
- package/src/components/feedback/Alert.tsx +65 -0
- package/src/components/feedback/EmptyState.tsx +113 -0
- package/src/components/feedback/Progress.test.tsx +60 -0
- package/src/components/feedback/Progress.tsx +30 -0
- package/src/components/feedback/ProgressBar.tsx +158 -0
- package/src/components/feedback/Skeleton.test.tsx +39 -0
- package/src/components/feedback/Skeleton.tsx +45 -0
- package/src/components/feedback/Sonner.tsx +28 -0
- package/src/components/feedback/Spinner.test.tsx +33 -0
- package/src/components/feedback/Spinner.tsx +99 -0
- package/src/components/feedback/Stepper.tsx +307 -0
- package/src/components/feedback/Toast/Toast.tsx +243 -0
- package/src/components/feedback/Toast/index.ts +2 -0
- package/src/components/feedback/index.ts +9 -0
- package/src/components/forms/Checkbox.test.tsx +40 -0
- package/src/components/forms/Checkbox.tsx +31 -0
- package/src/components/forms/ColorPicker.tsx +118 -0
- package/src/components/forms/Combobox.tsx +96 -0
- package/src/components/forms/DragDrop.tsx +440 -0
- package/src/components/forms/FileUpload.tsx +252 -0
- package/src/components/forms/FilterButton.tsx +65 -0
- package/src/components/forms/Form.tsx +197 -0
- package/src/components/forms/Input.test.tsx +46 -0
- package/src/components/forms/Input.tsx +43 -0
- package/src/components/forms/InputOTP.tsx +81 -0
- package/src/components/forms/Label.test.tsx +20 -0
- package/src/components/forms/Label.tsx +25 -0
- package/src/components/forms/RadioGroup.tsx +51 -0
- package/src/components/forms/SearchBar.tsx +215 -0
- package/src/components/forms/Select.test.tsx +118 -0
- package/src/components/forms/Select.tsx +274 -0
- package/src/components/forms/Slider.tsx +29 -0
- package/src/components/forms/Switch.test.tsx +76 -0
- package/src/components/forms/Switch.tsx +30 -0
- package/src/components/forms/TextField.tsx +152 -0
- package/src/components/forms/Textarea.test.tsx +41 -0
- package/src/components/forms/Textarea.tsx +29 -0
- package/src/components/forms/ThemeSwitcher.tsx +290 -0
- package/src/components/forms/ThemeToggle.tsx +151 -0
- package/src/components/forms/index.ts +19 -0
- package/src/components/layout/Accordion.test.tsx +66 -0
- package/src/components/layout/Accordion.tsx +64 -0
- package/src/components/layout/AspectRatio.tsx +7 -0
- package/src/components/layout/Carousel.tsx +277 -0
- package/src/components/layout/Collapsible.test.tsx +40 -0
- package/src/components/layout/Collapsible.tsx +31 -0
- package/src/components/layout/Container.test.tsx +45 -0
- package/src/components/layout/Container.tsx +99 -0
- package/src/components/layout/CustomizerPanel.tsx +400 -0
- package/src/components/layout/DatePicker.tsx +57 -0
- package/src/components/layout/Footer/Footer.tsx +175 -0
- package/src/components/layout/Footer/index.ts +2 -0
- package/src/components/layout/GlassSurface.tsx +82 -0
- package/src/components/layout/Grid.test.tsx +31 -0
- package/src/components/layout/Grid.tsx +130 -0
- package/src/components/layout/Header/Header.tsx +450 -0
- package/src/components/layout/Header/index.ts +2 -0
- package/src/components/layout/PageLayout.tsx +180 -0
- package/src/components/layout/PageTemplate.tsx +158 -0
- package/src/components/layout/Resizable.tsx +48 -0
- package/src/components/layout/ScrollArea.tsx +53 -0
- package/src/components/layout/Separator.test.tsx +28 -0
- package/src/components/layout/Separator.tsx +29 -0
- package/src/components/layout/Sidebar.tsx +171 -0
- package/src/components/layout/Stack.test.tsx +41 -0
- package/src/components/layout/Stack.tsx +89 -0
- package/src/components/layout/glass-surface.css +60 -0
- package/src/components/layout/index.ts +18 -0
- package/src/components/motion/AnimatedBeam.tsx +159 -0
- package/src/components/navigation/Breadcrumb.test.tsx +57 -0
- package/src/components/navigation/Breadcrumb.tsx +119 -0
- package/src/components/navigation/Breadcrumbs.tsx +221 -0
- package/src/components/navigation/Command.tsx +159 -0
- package/src/components/navigation/Menubar.tsx +115 -0
- package/src/components/navigation/NavLink.tsx +55 -0
- package/src/components/navigation/NavigationMenu.tsx +125 -0
- package/src/components/navigation/Pagination.tsx +121 -0
- package/src/components/navigation/SecondaryNav.tsx +100 -0
- package/src/components/navigation/Tabs.test.tsx +47 -0
- package/src/components/navigation/Tabs.tsx +60 -0
- package/src/components/navigation/TertiaryNav.tsx +90 -0
- package/src/components/navigation/index.ts +10 -0
- package/src/components/overlays/AlertDialog.test.tsx +69 -0
- package/src/components/overlays/AlertDialog.tsx +166 -0
- package/src/components/overlays/ContextMenu.tsx +243 -0
- package/src/components/overlays/Dialog.test.tsx +79 -0
- package/src/components/overlays/Dialog.tsx +158 -0
- package/src/components/overlays/Drawer.tsx +128 -0
- package/src/components/overlays/Dropdown.tsx +253 -0
- package/src/components/overlays/DropdownMenu.tsx +242 -0
- package/src/components/overlays/HoverCard.tsx +32 -0
- package/src/components/overlays/Modal.tsx +250 -0
- package/src/components/overlays/NotificationCenter.tsx +364 -0
- package/src/components/overlays/Popover.test.tsx +40 -0
- package/src/components/overlays/Popover.tsx +46 -0
- package/src/components/overlays/Sheet.tsx +163 -0
- package/src/components/overlays/Tooltip.test.tsx +33 -0
- package/src/components/overlays/Tooltip.tsx +32 -0
- package/src/components/overlays/index.ts +12 -0
- package/src/dates.ts +2 -0
- package/src/dnd.ts +1 -0
- package/src/forms.ts +1 -0
- package/src/globals.css +187 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useForm.ts +247 -0
- package/src/hooks/useMotionPreference.test.ts +102 -0
- package/src/hooks/useMotionPreference.ts +78 -0
- package/src/hooks/useTheme.ts +58 -0
- package/src/hooks.ts +9 -0
- package/src/index.ts +168 -0
- package/src/lib/animations.ts +356 -0
- package/src/lib/breadcrumbs.ts +94 -0
- package/src/lib/colors.ts +493 -0
- package/src/lib/store/customizer.ts +482 -0
- package/src/lib/store/index.ts +3 -0
- package/src/lib/store/theme.ts +55 -0
- package/src/lib/syntax-parser/index.ts +50 -0
- package/src/lib/syntax-parser/patterns.ts +64 -0
- package/src/lib/syntax-parser/tokenizer.ts +117 -0
- package/src/lib/syntax-parser/types.ts +27 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/validation.ts +204 -0
- package/src/lib/webgl/Color.ts +11 -0
- package/src/lib/webgl/Mesh.ts +41 -0
- package/src/lib/webgl/Program.ts +118 -0
- package/src/lib/webgl/Renderer.ts +51 -0
- package/src/lib/webgl/Triangle.ts +27 -0
- package/src/lib/webgl/Vec3.ts +18 -0
- package/src/lib/webgl/index.ts +13 -0
- package/src/nativewind-env.d.ts +1 -0
- package/src/providers/ThemeProvider.tsx +461 -0
- package/src/providers/index.ts +1 -0
- package/src/providers.ts +7 -0
- package/src/tables.ts +1 -0
- package/src/test/setup.ts +39 -0
- package/src/theme.css +158 -0
- package/src/tokens.ts +7 -0
- package/src/utils.ts +12 -0
- 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
|
+
}
|