@djangocfg/ui-nextjs 2.1.4 → 2.1.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-nextjs",
3
- "version": "2.1.4",
3
+ "version": "2.1.6",
4
4
  "description": "Next.js UI component library with Radix UI primitives, Tailwind CSS styling, charts, and form components",
5
5
  "keywords": [
6
6
  "ui-components",
@@ -58,10 +58,11 @@
58
58
  "check": "tsc --noEmit"
59
59
  },
60
60
  "peerDependencies": {
61
- "@djangocfg/api": "^2.1.4",
62
- "@djangocfg/ui-core": "^1.0.4",
61
+ "@djangocfg/api": "^2.1.6",
62
+ "@djangocfg/ui-core": "^2.1.6",
63
63
  "@types/react": "^19.1.0",
64
64
  "@types/react-dom": "^19.1.0",
65
+ "consola": "^3.4.2",
65
66
  "lucide-react": "^0.545.0",
66
67
  "moment": "^2.30.1",
67
68
  "next": "^15.5.7",
@@ -69,13 +70,9 @@
69
70
  "react-dom": "^19.1.0",
70
71
  "react-hook-form": "7.65.0",
71
72
  "tailwindcss": "^4.1.14",
72
- "zod": "^4.1.13",
73
- "consola": "^3.4.2"
73
+ "zod": "^4.1.13"
74
74
  },
75
75
  "dependencies": {
76
- "class-variance-authority": "^0.7.1",
77
- "libphonenumber-js": "^1.12.24",
78
- "sonner": "2.0.7",
79
76
  "@radix-ui/react-dropdown-menu": "^2.1.16",
80
77
  "@radix-ui/react-menubar": "^1.1.16",
81
78
  "@radix-ui/react-navigation-menu": "^1.2.14",
@@ -83,26 +80,30 @@
83
80
  "@rjsf/core": "^6.1.2",
84
81
  "@rjsf/utils": "^6.1.2",
85
82
  "@rjsf/validator-ajv8": "^6.1.2",
83
+ "@vidstack/react": "next",
86
84
  "@web3icons/react": "^4.0.26",
87
85
  "chart.js": "^4.5.0",
86
+ "class-variance-authority": "^0.7.1",
88
87
  "cytoscape": "^3.33.1",
89
88
  "cytoscape-cose-bilkent": "^4.1.0",
89
+ "libphonenumber-js": "^1.12.24",
90
+ "media-icons": "next",
90
91
  "mermaid": "^11.12.0",
91
92
  "next-themes": "^0.4.6",
92
93
  "prism-react-renderer": "^2.4.1",
93
94
  "react-chartjs-2": "^5.3.0",
95
+ "react-hotkeys-hook": "^5.2.1",
94
96
  "react-json-tree": "^0.20.0",
95
97
  "react-lottie-player": "^2.1.0",
96
98
  "react-markdown": "10.1.0",
97
- "remark-gfm": "4.0.1",
98
99
  "react-sticky-box": "^2.0.5",
99
100
  "recharts": "2.15.4",
100
- "@vidstack/react": "next",
101
- "media-icons": "next",
101
+ "remark-gfm": "4.0.1",
102
+ "sonner": "2.0.7",
102
103
  "vidstack": "next"
103
104
  },
104
105
  "devDependencies": {
105
- "@djangocfg/typescript-config": "^2.1.4",
106
+ "@djangocfg/typescript-config": "^2.1.6",
106
107
  "@types/node": "^24.7.2",
107
108
  "eslint": "^9.37.0",
108
109
  "tailwindcss-animate": "1.0.7",
@@ -5,10 +5,8 @@ import ReactMarkdown from 'react-markdown';
5
5
  import remarkGfm from 'remark-gfm';
6
6
  import Mermaid from '../../tools/Mermaid';
7
7
  import PrettyCode from '../../tools/PrettyCode';
8
- import { Button } from '@djangocfg/ui-core/components';
9
- import { useCopy } from '@djangocfg/ui-core/hooks';
10
- import { useTheme } from '../../hooks/useTheme';
11
- import { Copy } from 'lucide-react';
8
+ import { CopyButton } from '@djangocfg/ui-core/components';
9
+ import { useResolvedTheme } from '../../hooks/useResolvedTheme';
12
10
  import type { Components } from 'react-markdown';
13
11
 
14
12
  // Helper function to extract text content from React children
@@ -52,32 +50,24 @@ interface CodeBlockProps {
52
50
  }
53
51
 
54
52
  const CodeBlock: React.FC<CodeBlockProps> = ({ code, language, isUser }) => {
55
- const { copyToClipboard } = useCopy();
56
- const theme = useTheme();
57
-
58
- const handleCopy = () => {
59
- copyToClipboard(code, "Code copied to clipboard!");
60
- };
53
+ const theme = useResolvedTheme();
61
54
 
62
55
  return (
63
56
  <div className="relative group my-3">
64
57
  {/* Copy button */}
65
- <Button
58
+ <CopyButton
59
+ value={code}
66
60
  variant="ghost"
67
- size="sm"
68
- onClick={handleCopy}
69
61
  className={`
70
62
  absolute top-2 right-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity
71
- h-8 w-8 p-0
63
+ h-8 w-8
72
64
  ${isUser
73
65
  ? 'hover:bg-white/20 text-white'
74
66
  : 'hover:bg-muted-foreground/20 text-muted-foreground hover:text-foreground'
75
67
  }
76
68
  `}
77
69
  title="Copy code"
78
- >
79
- <Copy className="h-4 w-4" />
80
- </Button>
70
+ />
81
71
 
82
72
  {/* Code content */}
83
73
  <PrettyCode
@@ -195,24 +185,19 @@ const createMarkdownComponents = (isUser: boolean = false, isCompact: boolean =
195
185
  console.warn('CodeBlock failed, using fallback:', error);
196
186
  return (
197
187
  <div className="relative group my-3">
198
- <Button
188
+ <CopyButton
189
+ value={codeContent}
199
190
  variant="ghost"
200
- size="sm"
201
- onClick={() => {
202
- navigator.clipboard.writeText(codeContent);
203
- }}
204
191
  className={`
205
192
  absolute top-2 right-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity
206
- h-8 w-8 p-0
193
+ h-8 w-8
207
194
  ${isUser
208
195
  ? 'hover:bg-white/20 text-white'
209
196
  : 'hover:bg-muted-foreground/20 text-muted-foreground hover:text-foreground'
210
197
  }
211
198
  `}
212
199
  title="Copy code"
213
- >
214
- <Copy className="h-4 w-4" />
215
- </Button>
200
+ />
216
201
  <pre className={`
217
202
  p-3 rounded text-xs font-mono overflow-x-auto
218
203
  ${isUser
@@ -440,7 +440,9 @@ const SidebarFooter = React.forwardRef<
440
440
  })
441
441
  SidebarFooter.displayName = "SidebarFooter"
442
442
 
443
- const SidebarSeparator = React.forwardRef<
443
+ const SidebarSeparator: React.ForwardRefExoticComponent<
444
+ React.ComponentProps<typeof Separator> & React.RefAttributes<React.ElementRef<typeof Separator>>
445
+ > = React.forwardRef<
444
446
  React.ElementRef<typeof Separator>,
445
447
  React.ComponentProps<typeof Separator>
446
448
  >(({ className, ...props }, ref) => {
@@ -2,7 +2,6 @@
2
2
 
3
3
  import React from 'react';
4
4
  import { usePathname } from 'next/navigation';
5
- import Link from 'next/link';
6
5
  import { cn } from '@djangocfg/ui-core/lib';
7
6
  import { useIsMobile } from '@djangocfg/ui-core/hooks';
8
7
  import { useQueryParams } from '../hooks';
@@ -29,7 +28,6 @@ interface SSRPaginationProps {
29
28
  baseUrl?: string;
30
29
  pathname?: string;
31
30
  preserveQuery?: boolean;
32
- prefetch?: boolean;
33
31
  }
34
32
 
35
33
  export const SSRPagination: React.FC<SSRPaginationProps> = ({
@@ -45,7 +43,6 @@ export const SSRPagination: React.FC<SSRPaginationProps> = ({
45
43
  baseUrl,
46
44
  pathname: propPathname,
47
45
  preserveQuery = true,
48
- prefetch = true,
49
46
  }) => {
50
47
  const queryParams = useQueryParams();
51
48
  const pathname = usePathname();
@@ -186,17 +183,10 @@ export const SSRPagination: React.FC<SSRPaginationProps> = ({
186
183
  <PaginationContent>
187
184
  {/* Previous Button */}
188
185
  <PaginationItem>
189
- {actualHasPreviousPage ? (
190
- <Link href={getPageUrl(actualCurrentPage - 1)} prefetch={prefetch} passHref legacyBehavior>
191
- <PaginationPrevious />
192
- </Link>
193
- ) : (
194
- <PaginationPrevious
195
- href="#"
196
- className="pointer-events-none opacity-50"
197
- onClick={(e) => e.preventDefault()}
198
- />
199
- )}
186
+ <PaginationPrevious
187
+ href={actualHasPreviousPage ? getPageUrl(actualCurrentPage - 1) : undefined}
188
+ className={!actualHasPreviousPage ? "pointer-events-none opacity-50" : undefined}
189
+ />
200
190
  </PaginationItem>
201
191
 
202
192
  {/* Page Numbers */}
@@ -205,28 +195,22 @@ export const SSRPagination: React.FC<SSRPaginationProps> = ({
205
195
  {page === 'ellipsis' ? (
206
196
  <PaginationEllipsis />
207
197
  ) : (
208
- <Link href={getPageUrl(page)} prefetch={prefetch} passHref legacyBehavior>
209
- <PaginationLink isActive={page === actualCurrentPage}>
210
- {page}
211
- </PaginationLink>
212
- </Link>
198
+ <PaginationLink
199
+ href={getPageUrl(page)}
200
+ isActive={page === actualCurrentPage}
201
+ >
202
+ {page}
203
+ </PaginationLink>
213
204
  )}
214
205
  </PaginationItem>
215
206
  ))}
216
207
 
217
208
  {/* Next Button */}
218
209
  <PaginationItem>
219
- {hasNextPage ? (
220
- <Link href={getPageUrl(actualCurrentPage + 1)} prefetch={prefetch} passHref legacyBehavior>
221
- <PaginationNext />
222
- </Link>
223
- ) : (
224
- <PaginationNext
225
- href="#"
226
- className="pointer-events-none opacity-50"
227
- onClick={(e) => e.preventDefault()}
228
- />
229
- )}
210
+ <PaginationNext
211
+ href={hasNextPage ? getPageUrl(actualCurrentPage + 1) : undefined}
212
+ className={!hasNextPage ? "pointer-events-none opacity-50" : undefined}
213
+ />
230
214
  </PaginationItem>
231
215
  </PaginationContent>
232
216
  </Pagination>
@@ -11,9 +11,14 @@ export * from '@djangocfg/ui-core/hooks';
11
11
  export { useLocalStorage } from './useLocalStorage';
12
12
  export { useSessionStorage } from './useSessionStorage';
13
13
 
14
- // Theme hook (uses next-themes)
15
- export { useTheme } from './useTheme';
14
+ // Theme hook (standalone, no provider required)
15
+ export { useResolvedTheme } from './useResolvedTheme';
16
+ export type { ResolvedTheme } from './useResolvedTheme';
16
17
 
17
18
  // Next.js router hooks
18
19
  export { useQueryParams } from './useQueryParams';
19
20
  export { useCfgRouter } from './useCfgRouter';
21
+
22
+ // Keyboard shortcuts
23
+ export { useHotkey, useHotkeysContext, HotkeysProvider, isHotkeyPressed } from './useHotkey';
24
+ export type { UseHotkeyOptions, HotkeyCallback, Keys } from './useHotkey';
@@ -0,0 +1,103 @@
1
+ 'use client';
2
+
3
+ import type { RefObject } from 'react';
4
+ import { useHotkeys, Options as HotkeysOptions } from 'react-hotkeys-hook';
5
+ import type { HotkeyCallback, Keys } from 'react-hotkeys-hook';
6
+
7
+ /**
8
+ * Options for the useHotkey hook
9
+ */
10
+ export interface UseHotkeyOptions extends Omit<HotkeysOptions, 'enabled'> {
11
+ /** Whether the hotkey is enabled (default: true) */
12
+ enabled?: boolean;
13
+ /** Scope for the hotkey - useful for context-specific shortcuts */
14
+ scope?: string;
15
+ /** Only trigger when focus is within a specific element */
16
+ scopes?: string[];
17
+ /** Prevent default browser behavior */
18
+ preventDefault?: boolean;
19
+ /** Enable in input fields and textareas */
20
+ enableOnFormTags?: boolean | readonly ('input' | 'textarea' | 'select')[];
21
+ /** Enable when contentEditable element is focused */
22
+ enableOnContentEditable?: boolean;
23
+ /** Split key for multiple hotkey combinations (default: ',') */
24
+ splitKey?: string;
25
+ /** Key up/down events */
26
+ keyup?: boolean;
27
+ keydown?: boolean;
28
+ /** Description for the hotkey (useful for help dialogs) */
29
+ description?: string;
30
+ }
31
+
32
+ /**
33
+ * Simple wrapper hook for react-hotkeys-hook
34
+ *
35
+ * @example
36
+ * // Single key
37
+ * useHotkey('escape', () => closeModal());
38
+ *
39
+ * @example
40
+ * // Key combination
41
+ * useHotkey('ctrl+s', (e) => {
42
+ * e.preventDefault();
43
+ * saveDocument();
44
+ * });
45
+ *
46
+ * @example
47
+ * // Multiple keys (any of them will trigger)
48
+ * useHotkey(['ArrowLeft', '['], () => goToPrevious());
49
+ * useHotkey(['ArrowRight', ']'], () => goToNext());
50
+ *
51
+ * @example
52
+ * // With options
53
+ * useHotkey('/', () => focusSearch(), {
54
+ * preventDefault: true,
55
+ * enableOnFormTags: false,
56
+ * description: 'Focus search input'
57
+ * });
58
+ *
59
+ * @example
60
+ * // Scoped hotkeys
61
+ * useHotkey('delete', () => deleteItem(), { scopes: ['list-view'] });
62
+ *
63
+ * @param keys - Hotkey or array of hotkeys (e.g., 'ctrl+s', 'ArrowLeft', ['[', 'ArrowLeft'])
64
+ * @param callback - Function to call when hotkey is pressed
65
+ * @param options - Configuration options
66
+ * @returns Ref to attach to element for scoped hotkeys
67
+ */
68
+ export function useHotkey<T extends HTMLElement = HTMLElement>(
69
+ keys: Keys,
70
+ callback: HotkeyCallback,
71
+ options: UseHotkeyOptions = {}
72
+ ): RefObject<T | null> {
73
+ const {
74
+ enabled = true,
75
+ preventDefault = false,
76
+ enableOnFormTags = false,
77
+ enableOnContentEditable = false,
78
+ description: _description,
79
+ ...restOptions
80
+ } = options;
81
+
82
+ return useHotkeys<T>(
83
+ keys,
84
+ (event, handler) => {
85
+ if (preventDefault) {
86
+ event.preventDefault();
87
+ }
88
+ callback(event, handler);
89
+ },
90
+ {
91
+ enabled,
92
+ enableOnFormTags,
93
+ enableOnContentEditable,
94
+ ...restOptions,
95
+ }
96
+ );
97
+ }
98
+
99
+ // Re-export useful utilities from react-hotkeys-hook
100
+ export { useHotkeysContext, HotkeysProvider, isHotkeyPressed } from 'react-hotkeys-hook';
101
+
102
+ // Re-export types
103
+ export type { HotkeyCallback, Keys } from 'react-hotkeys-hook';
@@ -2,56 +2,67 @@
2
2
 
3
3
  import { useEffect, useState } from 'react';
4
4
 
5
- export type Theme = 'light' | 'dark';
5
+ export type ResolvedTheme = 'light' | 'dark';
6
6
 
7
7
  /**
8
- * Hook to detect and track the current theme
9
- * Supports both manual theme switching and system preference
8
+ * Hook to detect the current resolved theme (light or dark)
9
+ *
10
+ * Standalone hook - doesn't require ThemeProvider.
11
+ * Detects theme from:
12
+ * 1. 'dark' class on html element
13
+ * 2. System preference (prefers-color-scheme)
14
+ *
15
+ * For full theme control (setTheme, toggleTheme), use useThemeContext instead.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * const theme = useResolvedTheme(); // 'light' | 'dark'
20
+ * ```
10
21
  */
11
- export const useTheme = (): Theme => {
12
- const [theme, setTheme] = useState<Theme>('light');
13
-
22
+ export const useResolvedTheme = (): ResolvedTheme => {
23
+ const [theme, setTheme] = useState<ResolvedTheme>('light');
24
+
14
25
  useEffect(() => {
15
- const checkTheme = (): Theme => {
16
- // Check if dark class is applied to html element (manual theme)
26
+ const checkTheme = (): ResolvedTheme => {
27
+ // Check if dark class is applied to html element
17
28
  if (document.documentElement.classList.contains('dark')) {
18
29
  return 'dark';
19
30
  }
20
-
31
+
21
32
  // Check system preference
22
33
  if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
23
34
  return 'dark';
24
35
  }
25
-
36
+
26
37
  return 'light';
27
38
  };
28
-
39
+
29
40
  // Set initial theme
30
41
  setTheme(checkTheme());
31
-
32
- // Listen for manual theme changes (class changes on html element)
42
+
43
+ // Listen for class changes on html element
33
44
  const observer = new MutationObserver(() => {
34
45
  setTheme(checkTheme());
35
46
  });
36
-
47
+
37
48
  observer.observe(document.documentElement, {
38
49
  attributes: true,
39
50
  attributeFilter: ['class']
40
51
  });
41
-
52
+
42
53
  // Listen for system theme changes
43
54
  const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
44
55
  const handleMediaChange = () => {
45
56
  setTheme(checkTheme());
46
57
  };
47
-
58
+
48
59
  mediaQuery.addEventListener('change', handleMediaChange);
49
-
60
+
50
61
  return () => {
51
62
  observer.disconnect();
52
63
  mediaQuery.removeEventListener('change', handleMediaChange);
53
64
  };
54
65
  }, []);
55
-
66
+
56
67
  return theme;
57
68
  };
@@ -1,82 +1,76 @@
1
1
  /**
2
2
  * ThemeProvider - Universal theme management
3
3
  *
4
- * Provides theme context for the entire application with localStorage persistence.
4
+ * Re-exports next-themes ThemeProvider with sensible defaults.
5
+ * Supports light, dark, and system themes with localStorage persistence.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * // In app/layout.tsx
10
+ * import { ThemeProvider } from '@djangocfg/ui-nextjs';
11
+ *
12
+ * <ThemeProvider
13
+ * attribute="class"
14
+ * defaultTheme="system"
15
+ * enableSystem
16
+ * >
17
+ * {children}
18
+ * </ThemeProvider>
19
+ * ```
5
20
  */
6
21
 
7
22
  'use client';
8
23
 
9
- import React, { createContext, useContext, useEffect, ReactNode } from 'react';
10
- import { useLocalStorage } from '../hooks/useLocalStorage';
11
-
12
- // ─────────────────────────────────────────────────────────────────────────
13
- // Types
14
- // ─────────────────────────────────────────────────────────────────────────
15
-
16
- type Theme = 'light' | 'dark';
17
-
18
- interface ThemeContextValue {
19
- theme: Theme;
20
- setTheme: (theme: Theme) => void;
21
- toggleTheme: () => void;
22
- }
23
-
24
- // ─────────────────────────────────────────────────────────────────────────
25
- // Create Context
26
- // ─────────────────────────────────────────────────────────────────────────
24
+ import { ThemeProvider as NextThemesProvider, useTheme as useNextTheme } from 'next-themes';
25
+ import type { ThemeProviderProps as NextThemesProviderProps } from 'next-themes';
27
26
 
28
- const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
29
-
30
- // ─────────────────────────────────────────────────────────────────────────
31
- // Provider Component
32
- // ─────────────────────────────────────────────────────────────────────────
33
-
34
- interface ThemeProviderProps {
35
- children: ReactNode;
36
- defaultTheme?: Theme;
37
- storageKey?: string;
38
- }
27
+ // Re-export types
28
+ export type Theme = 'light' | 'dark' | 'system';
29
+ export type ThemeProviderProps = NextThemesProviderProps;
39
30
 
31
+ /**
32
+ * ThemeProvider wraps next-themes with sensible defaults
33
+ */
40
34
  export function ThemeProvider({
41
35
  children,
42
- defaultTheme = 'light',
43
- storageKey = 'theme'
36
+ attribute = 'class',
37
+ defaultTheme = 'system',
38
+ enableSystem = true,
39
+ disableTransitionOnChange = true,
40
+ ...props
44
41
  }: ThemeProviderProps) {
45
- const [theme, setTheme] = useLocalStorage<Theme>(storageKey, defaultTheme);
42
+ return (
43
+ <NextThemesProvider
44
+ attribute={attribute}
45
+ defaultTheme={defaultTheme}
46
+ enableSystem={enableSystem}
47
+ disableTransitionOnChange={disableTransitionOnChange}
48
+ {...props}
49
+ >
50
+ {children}
51
+ </NextThemesProvider>
52
+ );
53
+ }
46
54
 
47
- useEffect(() => {
48
- const root = window.document.documentElement;
49
- root.classList.remove('light', 'dark');
50
- root.classList.add(theme);
51
- }, [theme]);
55
+ /**
56
+ * Hook to access theme context
57
+ * Returns theme, setTheme, resolvedTheme, systemTheme, and themes
58
+ */
59
+ export function useThemeContext() {
60
+ const { theme, setTheme, resolvedTheme, systemTheme, themes } = useNextTheme();
52
61
 
53
62
  const toggleTheme = () => {
54
- setTheme(theme === 'light' ? 'dark' : 'light');
63
+ // Toggle between light and dark (ignore system)
64
+ const currentResolved = resolvedTheme || 'light';
65
+ setTheme(currentResolved === 'light' ? 'dark' : 'light');
55
66
  };
56
67
 
57
- const value: ThemeContextValue = {
58
- theme,
68
+ return {
69
+ theme: theme as Theme | undefined,
59
70
  setTheme,
71
+ resolvedTheme: resolvedTheme as 'light' | 'dark' | undefined,
72
+ systemTheme: systemTheme as 'light' | 'dark' | undefined,
73
+ themes,
60
74
  toggleTheme,
61
75
  };
62
-
63
- return (
64
- <ThemeContext.Provider value={value}>
65
- {children}
66
- </ThemeContext.Provider>
67
- );
68
- }
69
-
70
- // ─────────────────────────────────────────────────────────────────────────
71
- // Custom Hook
72
- // ─────────────────────────────────────────────────────────────────────────
73
-
74
- export function useThemeContext(): ThemeContextValue {
75
- const context = useContext(ThemeContext);
76
-
77
- if (context === undefined) {
78
- throw new Error('useThemeContext must be used within ThemeProvider');
79
- }
80
-
81
- return context;
82
76
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * ThemeToggle - Theme switcher component
3
3
  *
4
- * Switches between light and dark themes by toggling the 'dark' class on the html element.
5
- * Uses localStorage to persist the user's theme preference.
4
+ * Switches between light and dark themes.
5
+ * Must be used within ThemeProvider.
6
6
  *
7
7
  * @example
8
8
  * ```tsx
@@ -20,17 +20,21 @@ import { Button } from '@djangocfg/ui-core/components';
20
20
  import { useThemeContext } from './ThemeProvider';
21
21
 
22
22
  export function ThemeToggle() {
23
- const { theme, toggleTheme } = useThemeContext();
24
- const [isMounted, setIsMounted] = useState(false);
23
+ const { resolvedTheme, toggleTheme } = useThemeContext();
24
+ const [mounted, setMounted] = useState(false);
25
25
 
26
- // Prevent hydration mismatch by only rendering after mount
26
+ // Prevent hydration mismatch
27
27
  useEffect(() => {
28
- setIsMounted(true);
28
+ setMounted(true);
29
29
  }, []);
30
30
 
31
- // Don't render anything during SSR
32
- if (!isMounted) {
33
- return null;
31
+ if (!mounted) {
32
+ return (
33
+ <Button variant="ghost" size="icon" className="h-9 w-9" disabled>
34
+ <Sun className="h-4 w-4" />
35
+ <span className="sr-only">Toggle theme</span>
36
+ </Button>
37
+ );
34
38
  }
35
39
 
36
40
  return (
@@ -39,9 +43,9 @@ export function ThemeToggle() {
39
43
  size="icon"
40
44
  onClick={toggleTheme}
41
45
  className="h-9 w-9"
42
- title={`Switch to ${theme === 'light' ? 'dark' : 'light'} theme`}
46
+ title={`Switch to ${resolvedTheme === 'light' ? 'dark' : 'light'} theme`}
43
47
  >
44
- {theme === 'light' ? (
48
+ {resolvedTheme === 'light' ? (
45
49
  <Sun className="h-4 w-4" />
46
50
  ) : (
47
51
  <Moon className="h-4 w-4" />
@@ -1,3 +1,4 @@
1
1
  export { ThemeProvider, useThemeContext } from './ThemeProvider';
2
+ export type { Theme, ThemeProviderProps } from './ThemeProvider';
2
3
  export { ThemeToggle } from './ThemeToggle';
3
- export { ForceTheme } from './ForceTheme';
4
+ export { ForceTheme } from './ForceTheme';