@gv-tech/design-system 2.1.1 → 2.2.0

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.
@@ -1 +1 @@
1
- {"version":3,"file":"ThemeToggleDocs.d.ts","sourceRoot":"","sources":["../../../src/pages/components/ThemeToggleDocs.tsx"],"names":[],"mappings":"AAKA,wBAAgB,eAAe,4CAoH9B"}
1
+ {"version":3,"file":"ThemeToggleDocs.d.ts","sourceRoot":"","sources":["../../../src/pages/components/ThemeToggleDocs.tsx"],"names":[],"mappings":"AAKA,wBAAgB,eAAe,4CA+I9B"}
@@ -6,7 +6,7 @@
6
6
  "files": [
7
7
  {
8
8
  "path": "ui/alert-dialog.test.tsx",
9
- "content": "import { render, screen, waitFor } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { describe, expect, it } from 'vitest';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n AlertDialogTrigger,\n} from './alert-dialog';\n\ndescribe('AlertDialog', () => {\n it('renders correctly', async () => {\n render(\n <AlertDialog>\n <AlertDialogTrigger>Open</AlertDialogTrigger>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Are you sure?</AlertDialogTitle>\n <AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Cancel</AlertDialogCancel>\n <AlertDialogAction>Continue</AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>,\n );\n\n expect(screen.getByText('Open')).toBeInTheDocument();\n expect(screen.queryByText('Are you sure?')).not.toBeInTheDocument();\n\n await userEvent.click(screen.getByText('Open'));\n\n await waitFor(() => {\n expect(screen.getByRole('alertdialog')).toBeInTheDocument();\n expect(screen.getByText('Are you sure?')).toBeInTheDocument();\n expect(screen.getByText('This action cannot be undone.')).toBeInTheDocument();\n });\n });\n\n it('closes when cancel is clicked', async () => {\n render(\n <AlertDialog>\n <AlertDialogTrigger>Open</AlertDialogTrigger>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Are you sure?</AlertDialogTitle>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Cancel</AlertDialogCancel>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>,\n );\n\n await userEvent.click(screen.getByText('Open'));\n await waitFor(() => expect(screen.getByRole('alertdialog')).toBeInTheDocument());\n\n await userEvent.click(screen.getByText('Cancel'));\n await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument());\n });\n\n it('closes when action is clicked', async () => {\n render(\n <AlertDialog>\n <AlertDialogTrigger>Open</AlertDialogTrigger>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Are you sure?</AlertDialogTitle>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogAction>Continue</AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>,\n );\n\n await userEvent.click(screen.getByText('Open'));\n await waitFor(() => expect(screen.getByRole('alertdialog')).toBeInTheDocument());\n\n await userEvent.click(screen.getByText('Continue'));\n await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument());\n });\n});\n",
9
+ "content": "import { render, screen, waitFor } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { describe, expect, it } from 'vitest';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n AlertDialogTrigger,\n} from './alert-dialog';\n\ndescribe('AlertDialog', () => {\n it('renders correctly', async () => {\n render(\n <AlertDialog>\n <AlertDialogTrigger>Open</AlertDialogTrigger>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Are you sure?</AlertDialogTitle>\n <AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Cancel</AlertDialogCancel>\n <AlertDialogAction>Continue</AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>,\n );\n\n expect(screen.getByText('Open')).toBeInTheDocument();\n expect(screen.queryByText('Are you sure?')).not.toBeInTheDocument();\n\n await userEvent.click(screen.getByText('Open'));\n\n await waitFor(() => {\n expect(screen.getByRole('alertdialog')).toBeInTheDocument();\n expect(screen.getByText('Are you sure?')).toBeInTheDocument();\n expect(screen.getByText('This action cannot be undone.')).toBeInTheDocument();\n });\n });\n\n it('closes when cancel is clicked', async () => {\n render(\n <AlertDialog>\n <AlertDialogTrigger>Open</AlertDialogTrigger>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Are you sure?</AlertDialogTitle>\n <AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Cancel</AlertDialogCancel>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>,\n );\n\n await userEvent.click(screen.getByText('Open'));\n await waitFor(() => expect(screen.getByRole('alertdialog')).toBeInTheDocument());\n\n await userEvent.click(screen.getByText('Cancel'));\n await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument());\n });\n\n it('closes when action is clicked', async () => {\n render(\n <AlertDialog>\n <AlertDialogTrigger>Open</AlertDialogTrigger>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Are you sure?</AlertDialogTitle>\n <AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogAction>Continue</AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>,\n );\n\n await userEvent.click(screen.getByText('Open'));\n await waitFor(() => expect(screen.getByRole('alertdialog')).toBeInTheDocument());\n\n await userEvent.click(screen.getByText('Continue'));\n await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument());\n });\n});\n",
10
10
  "type": "registry:ui"
11
11
  }
12
12
  ]
@@ -587,6 +587,13 @@
587
587
  ],
588
588
  "type": "registry:ui"
589
589
  },
590
+ {
591
+ "name": "theme-toggle.test",
592
+ "files": [
593
+ "ui/theme-toggle.test.tsx"
594
+ ],
595
+ "type": "registry:ui"
596
+ },
590
597
  {
591
598
  "name": "theme-toggle",
592
599
  "files": [
@@ -6,7 +6,7 @@
6
6
  "files": [
7
7
  {
8
8
  "path": "ui/theme-toggle.tsx",
9
- "content": "import { Button } from '@/components/ui/button';\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu';\nimport { cn } from '@/lib/utils';\nimport { Moon, Sun, SunMoon } from 'lucide-react';\nimport { useTheme } from 'next-themes';\n\nexport interface ThemeToggleProps {\n /**\n * The mode of the theme toggle. 'binary' allows toggling between light and dark. 'ternary' allows choosing between\n * light, dark, and system.\n *\n * @default 'binary'\n */\n variant?: 'binary' | 'ternary';\n /** Optional callback when the theme changes. */\n onThemeChange?: (theme: string) => void;\n /** Optional current theme value for external control. */\n customTheme?: string;\n /** Optional className for the button. */\n className?: string;\n}\n\nexport function ThemeToggle({ variant = 'binary', onThemeChange, customTheme, className }: ThemeToggleProps) {\n const { theme: nextTheme, setTheme: setNextTheme, resolvedTheme } = useTheme();\n\n // Use customTheme if provided, otherwise fallback to next-themes\n const currentTheme = customTheme ?? nextTheme;\n\n // Determine the effective theme for icon rendering\n const effectiveTheme = customTheme ? customTheme : resolvedTheme;\n const isDark = effectiveTheme === 'dark';\n const isSystem = currentTheme === 'system';\n\n const handleThemeChange = (newTheme: string) => {\n if (onThemeChange) {\n onThemeChange(newTheme);\n } else {\n setNextTheme(newTheme);\n }\n };\n\n const IconToggle = () => (\n <>\n <Sun\n className={cn(\n 'h-[1.2rem] w-[1.2rem] transition-all',\n !isSystem && !isDark ? 'rotate-0 scale-100' : '-rotate-90 scale-0',\n )}\n />\n <Moon\n className={cn(\n 'absolute h-[1.2rem] w-[1.2rem] transition-all',\n !isSystem && isDark ? 'rotate-0 scale-100' : 'rotate-90 scale-0',\n )}\n />\n <SunMoon\n className={cn(\n 'absolute h-[1.2rem] w-[1.2rem] transition-all',\n isSystem ? 'rotate-0 scale-100' : 'rotate-90 scale-0',\n )}\n />\n <span className=\"sr-only\">Toggle theme</span>\n </>\n );\n\n if (variant === 'ternary') {\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"icon\" className={cn('relative h-9 w-9', className)}>\n <IconToggle />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem onClick={() => handleThemeChange('light')}>\n <Sun className=\"mr-2 h-4 w-4\" />\n <span>Light</span>\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => handleThemeChange('dark')}>\n <Moon className=\"mr-2 h-4 w-4\" />\n <span>Dark</span>\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => handleThemeChange('system')}>\n <SunMoon className=\"mr-2 h-4 w-4\" />\n <span>System</span>\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n }\n\n return (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className={cn('relative h-9 w-9', className)}\n onClick={() => handleThemeChange(currentTheme === 'dark' ? 'light' : 'dark')}\n aria-label=\"Toggle theme\"\n >\n <IconToggle />\n </Button>\n );\n}\n",
9
+ "content": "import { Button } from '@/components/ui/button';\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu';\nimport { useTheme } from '@/hooks/use-theme';\nimport { cn } from '@/lib/utils';\nimport { Moon, Sun, SunMoon } from 'lucide-react';\n\nexport interface ThemeToggleProps {\n /**\n * The mode of the theme toggle. 'binary' allows toggling between light and dark. 'ternary' allows choosing between\n * light, dark, and system.\n *\n * @default 'binary'\n */\n variant?: 'binary' | 'ternary';\n /** Optional callback when the theme changes. */\n onThemeChange?: (theme: string) => void;\n /** Optional current theme value for external control. */\n customTheme?: string;\n /** Optional className for the button. */\n className?: string;\n}\n\nexport function ThemeToggle({ variant = 'binary', onThemeChange, customTheme, className }: ThemeToggleProps) {\n const { theme: nextTheme, setTheme: setNextTheme, resolvedTheme } = useTheme();\n\n // Use customTheme if provided, otherwise fallback to next-themes\n const currentTheme = customTheme ?? nextTheme;\n\n // Determine the effective theme for icon rendering\n const effectiveTheme = customTheme ? customTheme : resolvedTheme;\n const isDark = effectiveTheme === 'dark';\n const isSystem = currentTheme === 'system';\n\n const handleThemeChange = (newTheme: string) => {\n if (onThemeChange) {\n onThemeChange(newTheme);\n } else {\n setNextTheme(newTheme);\n }\n };\n\n const IconToggle = () => (\n <>\n <Sun\n className={cn(\n 'h-[1.2rem] w-[1.2rem] transition-all',\n !isSystem && !isDark ? 'rotate-0 scale-100' : '-rotate-90 scale-0',\n )}\n />\n <Moon\n className={cn(\n 'absolute h-[1.2rem] w-[1.2rem] transition-all',\n !isSystem && isDark ? 'rotate-0 scale-100' : 'rotate-90 scale-0',\n )}\n />\n <SunMoon\n className={cn(\n 'absolute h-[1.2rem] w-[1.2rem] transition-all',\n isSystem ? 'rotate-0 scale-100' : 'rotate-90 scale-0',\n )}\n />\n <span className=\"sr-only\">Toggle theme</span>\n </>\n );\n\n if (variant === 'ternary') {\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"icon\" className={cn('relative h-9 w-9', className)}>\n <IconToggle />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem onClick={() => handleThemeChange('light')}>\n <Sun className=\"mr-2 h-4 w-4\" />\n <span>Light</span>\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => handleThemeChange('dark')}>\n <Moon className=\"mr-2 h-4 w-4\" />\n <span>Dark</span>\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => handleThemeChange('system')}>\n <SunMoon className=\"mr-2 h-4 w-4\" />\n <span>System</span>\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n }\n\n return (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className={cn('relative h-9 w-9', className)}\n onClick={() => handleThemeChange(currentTheme === 'dark' ? 'light' : 'dark')}\n aria-label=\"Toggle theme\"\n >\n <IconToggle />\n </Button>\n );\n}\n",
10
10
  "type": "registry:ui"
11
11
  }
12
12
  ]
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "theme-toggle.test",
3
+ "type": "registry:ui",
4
+ "dependencies": [],
5
+ "registryDependencies": [],
6
+ "files": [
7
+ {
8
+ "path": "ui/theme-toggle.test.tsx",
9
+ "content": "import { ThemeToggle } from '@/components/ui/theme-toggle';\nimport { render, screen } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { ThemeProvider } from 'next-themes';\nimport { describe, expect, it, vi } from 'vitest';\n\n// Mock the useTheme hook to control its return values\nvi.mock('@/hooks/use-theme', async () => {\n const actual = await vi.importActual('@/hooks/use-theme');\n return {\n ...actual,\n useTheme: () => ({\n theme: 'light',\n setTheme: vi.fn(),\n resolvedTheme: 'light',\n tokens: {},\n }),\n };\n});\n\ndescribe('ThemeToggle', () => {\n it('renders binary toggle by default', () => {\n render(\n <ThemeProvider>\n <ThemeToggle />\n </ThemeProvider>,\n );\n // Use role button which is accessible\n const button = screen.getByRole('button', { name: /toggle theme/i });\n expect(button).toBeInTheDocument();\n });\n\n it('renders ternary toggle with dropdown', async () => {\n const user = userEvent.setup();\n render(\n <ThemeProvider>\n <ThemeToggle variant=\"ternary\" />\n </ThemeProvider>,\n );\n const button = screen.getByRole('button'); // Dropdown trigger\n expect(button).toBeInTheDocument();\n\n // Open dropdown\n await user.click(button);\n expect(await screen.findByText('Light')).toBeInTheDocument();\n expect(screen.getByText('Dark')).toBeInTheDocument();\n expect(screen.getByText('System')).toBeInTheDocument();\n });\n});\n",
10
+ "type": "registry:ui"
11
+ }
12
+ ]
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gv-tech/design-system",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "Garcia Ventures react design system",
5
5
  "repository": "git@github.com:Garcia-Ventures/gvtech-design.git",
6
6
  "license": "MIT",
@@ -15,6 +15,7 @@ const steps = [
15
15
  cmd: fix ? 'yarn lint:fix' : 'yarn lint',
16
16
  },
17
17
  { name: 'TypeScript type check', cmd: 'npx tsc --noEmit' },
18
+ { name: 'Test (vitest)', cmd: 'yarn test:ci' },
18
19
  { name: 'Build (vite)', cmd: 'yarn build' },
19
20
  ];
20
21
 
@@ -50,6 +50,7 @@ describe('AlertDialog', () => {
50
50
  <AlertDialogContent>
51
51
  <AlertDialogHeader>
52
52
  <AlertDialogTitle>Are you sure?</AlertDialogTitle>
53
+ <AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>
53
54
  </AlertDialogHeader>
54
55
  <AlertDialogFooter>
55
56
  <AlertDialogCancel>Cancel</AlertDialogCancel>
@@ -72,6 +73,7 @@ describe('AlertDialog', () => {
72
73
  <AlertDialogContent>
73
74
  <AlertDialogHeader>
74
75
  <AlertDialogTitle>Are you sure?</AlertDialogTitle>
76
+ <AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>
75
77
  </AlertDialogHeader>
76
78
  <AlertDialogFooter>
77
79
  <AlertDialogAction>Continue</AlertDialogAction>
@@ -0,0 +1,49 @@
1
+ import { ThemeToggle } from '@/components/ui/theme-toggle';
2
+ import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { ThemeProvider } from 'next-themes';
5
+ import { describe, expect, it, vi } from 'vitest';
6
+
7
+ // Mock the useTheme hook to control its return values
8
+ vi.mock('@/hooks/use-theme', async () => {
9
+ const actual = await vi.importActual('@/hooks/use-theme');
10
+ return {
11
+ ...actual,
12
+ useTheme: () => ({
13
+ theme: 'light',
14
+ setTheme: vi.fn(),
15
+ resolvedTheme: 'light',
16
+ tokens: {},
17
+ }),
18
+ };
19
+ });
20
+
21
+ describe('ThemeToggle', () => {
22
+ it('renders binary toggle by default', () => {
23
+ render(
24
+ <ThemeProvider>
25
+ <ThemeToggle />
26
+ </ThemeProvider>,
27
+ );
28
+ // Use role button which is accessible
29
+ const button = screen.getByRole('button', { name: /toggle theme/i });
30
+ expect(button).toBeInTheDocument();
31
+ });
32
+
33
+ it('renders ternary toggle with dropdown', async () => {
34
+ const user = userEvent.setup();
35
+ render(
36
+ <ThemeProvider>
37
+ <ThemeToggle variant="ternary" />
38
+ </ThemeProvider>,
39
+ );
40
+ const button = screen.getByRole('button'); // Dropdown trigger
41
+ expect(button).toBeInTheDocument();
42
+
43
+ // Open dropdown
44
+ await user.click(button);
45
+ expect(await screen.findByText('Light')).toBeInTheDocument();
46
+ expect(screen.getByText('Dark')).toBeInTheDocument();
47
+ expect(screen.getByText('System')).toBeInTheDocument();
48
+ });
49
+ });
@@ -5,9 +5,9 @@ import {
5
5
  DropdownMenuItem,
6
6
  DropdownMenuTrigger,
7
7
  } from '@/components/ui/dropdown-menu';
8
+ import { useTheme } from '@/hooks/use-theme';
8
9
  import { cn } from '@/lib/utils';
9
10
  import { Moon, Sun, SunMoon } from 'lucide-react';
10
- import { useTheme } from 'next-themes';
11
11
 
12
12
  export interface ThemeToggleProps {
13
13
  /**
@@ -0,0 +1,27 @@
1
+ import { useTheme } from '@/hooks/use-theme';
2
+ import { theme } from '@/theme/tokens';
3
+ import { renderHook } from '@testing-library/react';
4
+ import { ThemeProvider } from 'next-themes';
5
+ import { describe, expect, it } from 'vitest';
6
+
7
+ describe('useTheme', () => {
8
+ it('returns default light tokens when no theme is set', () => {
9
+ const { result } = renderHook(() => useTheme(), {
10
+ wrapper: ({ children }) => <ThemeProvider>{children}</ThemeProvider>,
11
+ });
12
+
13
+ expect(result.current.tokens).toEqual(theme.light);
14
+ });
15
+
16
+ it('returns dark tokens when theme is dark', () => {
17
+ const { result } = renderHook(() => useTheme(), {
18
+ wrapper: ({ children }) => (
19
+ <ThemeProvider defaultTheme="dark" enableSystem={false}>
20
+ {children}
21
+ </ThemeProvider>
22
+ ),
23
+ });
24
+
25
+ expect(result.current.tokens).toEqual(theme.dark);
26
+ });
27
+ });
@@ -0,0 +1,15 @@
1
+ import { theme } from '@/theme/tokens';
2
+ import { useTheme as useNextTheme } from 'next-themes';
3
+
4
+ export function useTheme() {
5
+ const context = useNextTheme();
6
+ const { resolvedTheme } = context;
7
+
8
+ // Default to light theme tokens if resolvedTheme is undefined or invalid
9
+ const activeTokens = resolvedTheme === 'dark' ? theme.dark : theme.light;
10
+
11
+ return {
12
+ ...context,
13
+ tokens: activeTokens,
14
+ };
15
+ }
package/src/index.ts CHANGED
@@ -53,4 +53,5 @@ export * from './components/ui/toggle-group';
53
53
  export * from './components/ui/tooltip';
54
54
 
55
55
  // Hooks
56
+ export * from './hooks/use-theme';
56
57
  export * from './hooks/use-toast';
@@ -30,23 +30,48 @@ export function ThemeToggleDocs() {
30
30
  </ComponentShowcase>
31
31
 
32
32
  <ComponentShowcase
33
- title="Custom State Integration"
34
- description="You can control the theme externally by passing customTheme and onThemeChange props."
33
+ title="Controlled Mode"
34
+ description="You can control the theme externally by passing customTheme and onThemeChange props. This is useful for testing or when using a different theme provider."
35
35
  code={`const [theme, setTheme] = useState('light');
36
36
 
37
37
  <ThemeToggle
38
38
  customTheme={theme}
39
- onThemeChange={(newTheme) => setTheme(newTheme)}
39
+ onThemeChange={setTheme}
40
40
  />
41
41
 
42
42
  <p>Current Theme: {theme}</p>`}
43
43
  >
44
44
  <div className="flex flex-col items-center gap-4">
45
- <ThemeToggle customTheme={customTheme} onThemeChange={(newTheme) => setCustomTheme(newTheme)} />
45
+ <ThemeToggle customTheme={customTheme} onThemeChange={setCustomTheme} />
46
46
  <p className="text-sm font-medium">Current Selection: {customTheme}</p>
47
47
  </div>
48
48
  </ComponentShowcase>
49
49
 
50
+ <div className="space-y-4">
51
+ <h3 className="text-xl font-semibold">useTheme Hook</h3>
52
+ <p className="text-sm text-muted-foreground">
53
+ The `useTheme` hook provides access to the current theme and the active design tokens.
54
+ </p>
55
+ <div className="rounded-md border bg-muted p-4">
56
+ <pre className="text-xs">
57
+ <code>
58
+ {`import { useTheme } from '@gv-tech/design-system';
59
+
60
+ export function MyComponent() {
61
+ const { theme, setTheme, tokens } = useTheme();
62
+
63
+ return (
64
+ <div style={{ backgroundColor: tokens.background }}>
65
+ <p>Current theme: {theme}</p>
66
+ <button onClick={() => setTheme('dark')}>Dark Mode</button>
67
+ </div>
68
+ );
69
+ }`}
70
+ </code>
71
+ </pre>
72
+ </div>
73
+ </div>
74
+
50
75
  <div className="space-y-4">
51
76
  <h3 className="text-xl font-semibold">ThemeToggle Props</h3>
52
77
  <PropsTable
@@ -104,14 +129,16 @@ export function ThemeToggleDocs() {
104
129
  </div>
105
130
 
106
131
  <div className="rounded-lg border bg-muted/50 p-6">
107
- <h4 className="font-medium text-foreground">Custom Provider</h4>
132
+ <h4 className="font-medium text-foreground">Controlled / Custom State</h4>
108
133
  <p className="mt-1 text-sm text-muted-foreground">
109
134
  Pass your own theme state and change handler to integrate with custom logic or external storage.
110
135
  </p>
111
136
  <pre className="mt-4 overflow-x-auto rounded-md bg-background p-4 text-xs">
112
- <code>{`<ThemeToggle
113
- customTheme={myTheme}
114
- onThemeChange={(t) => updateMyTheme(t)}
137
+ <code>{`const [theme, setTheme] = useState("light")
138
+
139
+ <ThemeToggle
140
+ customTheme={theme}
141
+ onThemeChange={setTheme}
115
142
  />`}</code>
116
143
  </pre>
117
144
  </div>