@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,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';
|