@djangocfg/ui-nextjs 1.4.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -0
  3. package/package.json +110 -0
  4. package/src/animations/AnimatedBackground.tsx +645 -0
  5. package/src/animations/index.ts +2 -0
  6. package/src/blocks/ArticleCard.tsx +94 -0
  7. package/src/blocks/ArticleList.tsx +95 -0
  8. package/src/blocks/CTASection.tsx +136 -0
  9. package/src/blocks/FeatureSection.tsx +104 -0
  10. package/src/blocks/Hero.tsx +102 -0
  11. package/src/blocks/NewsletterSection.tsx +119 -0
  12. package/src/blocks/StatsSection.tsx +103 -0
  13. package/src/blocks/SuperHero.tsx +328 -0
  14. package/src/blocks/TestimonialSection.tsx +122 -0
  15. package/src/blocks/index.ts +9 -0
  16. package/src/components/README.md +2018 -0
  17. package/src/components/breadcrumb-navigation.tsx +127 -0
  18. package/src/components/breadcrumb.tsx +132 -0
  19. package/src/components/button-download.tsx +275 -0
  20. package/src/components/dropdown-menu.tsx +219 -0
  21. package/src/components/index.ts +86 -0
  22. package/src/components/markdown/MarkdownMessage.tsx +338 -0
  23. package/src/components/markdown/index.ts +5 -0
  24. package/src/components/menubar.tsx +274 -0
  25. package/src/components/multi-select-pro/async.tsx +608 -0
  26. package/src/components/multi-select-pro/helpers.tsx +84 -0
  27. package/src/components/multi-select-pro/index.tsx +622 -0
  28. package/src/components/navigation-menu.tsx +153 -0
  29. package/src/components/pagination-static.tsx +348 -0
  30. package/src/components/pagination.tsx +138 -0
  31. package/src/components/phone-input.tsx +276 -0
  32. package/src/components/sidebar.tsx +866 -0
  33. package/src/components/sonner.tsx +31 -0
  34. package/src/components/ssr-pagination.tsx +237 -0
  35. package/src/hooks/index.ts +19 -0
  36. package/src/hooks/useCfgRouter.ts +153 -0
  37. package/src/hooks/useLocalStorage.ts +221 -0
  38. package/src/hooks/useQueryParams.ts +73 -0
  39. package/src/hooks/useSessionStorage.ts +188 -0
  40. package/src/hooks/useTheme.ts +57 -0
  41. package/src/index.ts +24 -0
  42. package/src/lib/index.ts +2 -0
  43. package/src/styles/index.css +2 -0
  44. package/src/theme/ForceTheme.tsx +115 -0
  45. package/src/theme/ThemeProvider.tsx +82 -0
  46. package/src/theme/ThemeToggle.tsx +52 -0
  47. package/src/theme/index.ts +3 -0
  48. package/src/tools/JsonForm/JsonSchemaForm.tsx +199 -0
  49. package/src/tools/JsonForm/examples/BotConfigExample.tsx +245 -0
  50. package/src/tools/JsonForm/examples/RealBotConfigExample.tsx +157 -0
  51. package/src/tools/JsonForm/index.ts +46 -0
  52. package/src/tools/JsonForm/templates/ArrayFieldItemTemplate.tsx +46 -0
  53. package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +73 -0
  54. package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +106 -0
  55. package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +34 -0
  56. package/src/tools/JsonForm/templates/FieldTemplate.tsx +61 -0
  57. package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +43 -0
  58. package/src/tools/JsonForm/templates/index.ts +12 -0
  59. package/src/tools/JsonForm/types.ts +83 -0
  60. package/src/tools/JsonForm/utils.ts +212 -0
  61. package/src/tools/JsonForm/widgets/CheckboxWidget.tsx +36 -0
  62. package/src/tools/JsonForm/widgets/NumberWidget.tsx +88 -0
  63. package/src/tools/JsonForm/widgets/SelectWidget.tsx +100 -0
  64. package/src/tools/JsonForm/widgets/SwitchWidget.tsx +34 -0
  65. package/src/tools/JsonForm/widgets/TextWidget.tsx +95 -0
  66. package/src/tools/JsonForm/widgets/index.ts +12 -0
  67. package/src/tools/JsonTree/index.tsx +252 -0
  68. package/src/tools/LottiePlayer/LottiePlayer.client.tsx +212 -0
  69. package/src/tools/LottiePlayer/index.tsx +54 -0
  70. package/src/tools/LottiePlayer/types.ts +108 -0
  71. package/src/tools/LottiePlayer/useLottie.ts +163 -0
  72. package/src/tools/Mermaid/Mermaid.client.tsx +341 -0
  73. package/src/tools/Mermaid/index.tsx +40 -0
  74. package/src/tools/OpenapiViewer/components/EndpointInfo.tsx +144 -0
  75. package/src/tools/OpenapiViewer/components/EndpointsLibrary.tsx +255 -0
  76. package/src/tools/OpenapiViewer/components/PlaygroundLayout.tsx +123 -0
  77. package/src/tools/OpenapiViewer/components/PlaygroundStepper.tsx +98 -0
  78. package/src/tools/OpenapiViewer/components/RequestBuilder.tsx +164 -0
  79. package/src/tools/OpenapiViewer/components/RequestParametersForm.tsx +253 -0
  80. package/src/tools/OpenapiViewer/components/ResponseViewer.tsx +169 -0
  81. package/src/tools/OpenapiViewer/components/VersionSelector.tsx +64 -0
  82. package/src/tools/OpenapiViewer/components/index.ts +14 -0
  83. package/src/tools/OpenapiViewer/constants.ts +39 -0
  84. package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +338 -0
  85. package/src/tools/OpenapiViewer/hooks/index.ts +8 -0
  86. package/src/tools/OpenapiViewer/hooks/useMobile.ts +10 -0
  87. package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +203 -0
  88. package/src/tools/OpenapiViewer/index.tsx +36 -0
  89. package/src/tools/OpenapiViewer/types.ts +152 -0
  90. package/src/tools/OpenapiViewer/utils/apiKeyManager.ts +149 -0
  91. package/src/tools/OpenapiViewer/utils/formatters.ts +71 -0
  92. package/src/tools/OpenapiViewer/utils/index.ts +9 -0
  93. package/src/tools/OpenapiViewer/utils/versionManager.ts +161 -0
  94. package/src/tools/PrettyCode/PrettyCode.client.tsx +217 -0
  95. package/src/tools/PrettyCode/index.tsx +43 -0
  96. package/src/tools/VideoPlayer/README.md +239 -0
  97. package/src/tools/VideoPlayer/VideoControls.tsx +138 -0
  98. package/src/tools/VideoPlayer/VideoPlayer.tsx +230 -0
  99. package/src/tools/VideoPlayer/index.ts +9 -0
  100. package/src/tools/VideoPlayer/types.ts +62 -0
  101. package/src/tools/index.ts +43 -0
@@ -0,0 +1,73 @@
1
+ /**
2
+ * useQueryParams Hook
3
+ *
4
+ * Safe hook to access URL query parameters without requiring Suspense boundary.
5
+ * Works on client-side only, returns empty URLSearchParams during SSR/prerendering.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * const params = useQueryParams();
10
+ * const flow = params.get('flow');
11
+ * const hasFlow = params.has('flow');
12
+ * const allTags = params.getAll('tags');
13
+ * ```
14
+ */
15
+
16
+ 'use client';
17
+
18
+ import { useState, useEffect, useRef } from 'react';
19
+ import { usePathname } from 'next/navigation';
20
+
21
+ /**
22
+ * Hook to safely access URL query parameters without useSearchParams()
23
+ *
24
+ * This hook reads query parameters directly from window.location.search,
25
+ * avoiding the need for Suspense boundaries that useSearchParams() requires.
26
+ *
27
+ * Automatically updates when URL changes (navigation, back/forward, etc.)
28
+ * Uses pathname from Next.js to detect route changes and polls for query param changes.
29
+ *
30
+ * Returns a URLSearchParams object with get(), getAll(), has(), etc.
31
+ *
32
+ * @returns URLSearchParams object (empty during SSR)
33
+ */
34
+ export function useQueryParams(): URLSearchParams {
35
+ const pathname = usePathname();
36
+ const [queryParams, setQueryParams] = useState<URLSearchParams>(() => {
37
+ if (typeof window === 'undefined') {
38
+ return new URLSearchParams();
39
+ }
40
+ return new URLSearchParams(window.location.search);
41
+ });
42
+ const lastSearchRef = useRef<string>('');
43
+
44
+ useEffect(() => {
45
+ if (typeof window === 'undefined') return;
46
+
47
+ const updateQueryParams = () => {
48
+ const currentSearch = window.location.search;
49
+ if (currentSearch !== lastSearchRef.current) {
50
+ lastSearchRef.current = currentSearch;
51
+ setQueryParams(new URLSearchParams(currentSearch));
52
+ }
53
+ };
54
+
55
+ // Update when pathname changes (Next.js navigation)
56
+ updateQueryParams();
57
+
58
+ // Listen to popstate (back/forward navigation)
59
+ window.addEventListener('popstate', updateQueryParams);
60
+
61
+ // Poll for query param changes (for router.push with same pathname)
62
+ // This handles cases where Next.js router.push updates query params without changing pathname
63
+ const intervalId = setInterval(updateQueryParams, 100);
64
+
65
+ return () => {
66
+ window.removeEventListener('popstate', updateQueryParams);
67
+ clearInterval(intervalId);
68
+ };
69
+ }, [pathname]);
70
+
71
+ return queryParams;
72
+ }
73
+
@@ -0,0 +1,188 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+
5
+ /**
6
+ * Simple sessionStorage hook with better error handling
7
+ * @param key - Storage key
8
+ * @param initialValue - Default value if key doesn't exist
9
+ * @returns [value, setValue, removeValue] - Current value, setter function, and remove function
10
+ */
11
+ export function useSessionStorage<T>(key: string, initialValue: T) {
12
+ // Get initial value from sessionStorage or use provided initialValue
13
+ const [storedValue, setStoredValue] = useState<T>(() => {
14
+ if (typeof window === 'undefined') {
15
+ return initialValue;
16
+ }
17
+
18
+ try {
19
+ const item = window.sessionStorage.getItem(key);
20
+ return item ? JSON.parse(item) : initialValue;
21
+ } catch (error) {
22
+ console.error(`Error reading sessionStorage key "${key}":`, error);
23
+ return initialValue;
24
+ }
25
+ });
26
+
27
+ // Check data size and limit
28
+ const checkDataSize = (data: any): boolean => {
29
+ try {
30
+ const jsonString = JSON.stringify(data);
31
+ const sizeInBytes = new Blob([jsonString]).size;
32
+ const sizeInKB = sizeInBytes / 1024;
33
+
34
+ // Limit to 1MB per item
35
+ if (sizeInKB > 1024) {
36
+ console.warn(`Data size (${sizeInKB.toFixed(2)}KB) exceeds 1MB limit for key "${key}"`);
37
+ return false;
38
+ }
39
+
40
+ return true;
41
+ } catch (error) {
42
+ console.error(`Error checking data size for key "${key}":`, error);
43
+ return false;
44
+ }
45
+ };
46
+
47
+ // Clear old data when sessionStorage is full
48
+ const clearOldData = () => {
49
+ try {
50
+ const keys = Object.keys(sessionStorage).filter(key => key && typeof key === 'string');
51
+ // Remove oldest items if we have more than 50 items
52
+ if (keys.length > 50) {
53
+ const itemsToRemove = Math.ceil(keys.length * 0.2);
54
+ for (let i = 0; i < itemsToRemove; i++) {
55
+ try {
56
+ const key = keys[i];
57
+ if (key) {
58
+ sessionStorage.removeItem(key);
59
+ sessionStorage.removeItem(`${key}_timestamp`);
60
+ }
61
+ } catch {
62
+ // Ignore errors when removing items
63
+ }
64
+ }
65
+ }
66
+ } catch (error) {
67
+ console.error('Error clearing old sessionStorage data:', error);
68
+ }
69
+ };
70
+
71
+ // Force clear all data if quota is exceeded
72
+ const forceClearAll = () => {
73
+ try {
74
+ const keys = Object.keys(sessionStorage);
75
+ for (const key of keys) {
76
+ try {
77
+ sessionStorage.removeItem(key);
78
+ } catch {
79
+ // Ignore errors when removing items
80
+ }
81
+ }
82
+ } catch (error) {
83
+ console.error('Error force clearing sessionStorage:', error);
84
+ }
85
+ };
86
+
87
+ // Update sessionStorage when value changes
88
+ const setValue = (value: T | ((val: T) => T)) => {
89
+ try {
90
+ const valueToStore = value instanceof Function ? value(storedValue) : value;
91
+
92
+ // Check data size before attempting to save
93
+ if (!checkDataSize(valueToStore)) {
94
+ console.warn(`Data size too large for key "${key}", removing key`);
95
+ // Remove the key if data is too large
96
+ try {
97
+ window.sessionStorage.removeItem(key);
98
+ window.sessionStorage.removeItem(`${key}_timestamp`);
99
+ } catch {
100
+ // Ignore errors when removing
101
+ }
102
+ // Still update the state
103
+ setStoredValue(valueToStore);
104
+ return;
105
+ }
106
+
107
+ setStoredValue(valueToStore);
108
+
109
+ if (typeof window !== 'undefined') {
110
+ // Try to set the value
111
+ try {
112
+ window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
113
+ // Add timestamp for cleanup
114
+ window.sessionStorage.setItem(`${key}_timestamp`, Date.now().toString());
115
+ } catch (storageError: any) {
116
+ // If quota exceeded, clear old data and try again
117
+ if (storageError.name === 'QuotaExceededError' ||
118
+ storageError.code === 22 ||
119
+ storageError.message?.includes('quota')) {
120
+ console.warn('sessionStorage quota exceeded, clearing old data...');
121
+ clearOldData();
122
+
123
+ // Try again after clearing
124
+ try {
125
+ window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
126
+ window.sessionStorage.setItem(`${key}_timestamp`, Date.now().toString());
127
+ } catch (retryError) {
128
+ console.error(`Failed to set sessionStorage key "${key}" after clearing old data:`, retryError);
129
+ // If still fails, force clear all and try one more time
130
+ try {
131
+ forceClearAll();
132
+ window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
133
+ window.sessionStorage.setItem(`${key}_timestamp`, Date.now().toString());
134
+ } catch (finalError) {
135
+ console.error(`Failed to set sessionStorage key "${key}" after force clearing:`, finalError);
136
+ // If still fails, just update the state without sessionStorage
137
+ setStoredValue(valueToStore);
138
+ }
139
+ }
140
+ } else {
141
+ throw storageError;
142
+ }
143
+ }
144
+ }
145
+ } catch (error) {
146
+ console.error(`Error setting sessionStorage key "${key}":`, error);
147
+ // Still update the state even if sessionStorage fails
148
+ const valueToStore = value instanceof Function ? value(storedValue) : value;
149
+ setStoredValue(valueToStore);
150
+ }
151
+ };
152
+
153
+ // Remove value from sessionStorage
154
+ const removeValue = () => {
155
+ try {
156
+ setStoredValue(initialValue);
157
+ if (typeof window !== 'undefined') {
158
+ try {
159
+ window.sessionStorage.removeItem(key);
160
+ window.sessionStorage.removeItem(`${key}_timestamp`);
161
+ } catch (removeError: any) {
162
+ // If removal fails due to quota, try to clear some data first
163
+ if (removeError.name === 'QuotaExceededError' ||
164
+ removeError.code === 22 ||
165
+ removeError.message?.includes('quota')) {
166
+ console.warn('sessionStorage quota exceeded during removal, clearing old data...');
167
+ clearOldData();
168
+
169
+ try {
170
+ window.sessionStorage.removeItem(key);
171
+ window.sessionStorage.removeItem(`${key}_timestamp`);
172
+ } catch (retryError) {
173
+ console.error(`Failed to remove sessionStorage key "${key}" after clearing:`, retryError);
174
+ // If still fails, force clear all
175
+ forceClearAll();
176
+ }
177
+ } else {
178
+ throw removeError;
179
+ }
180
+ }
181
+ }
182
+ } catch (error) {
183
+ console.error(`Error removing sessionStorage key "${key}":`, error);
184
+ }
185
+ };
186
+
187
+ return [storedValue, setValue, removeValue] as const;
188
+ }
@@ -0,0 +1,57 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+
5
+ export type Theme = 'light' | 'dark';
6
+
7
+ /**
8
+ * Hook to detect and track the current theme
9
+ * Supports both manual theme switching and system preference
10
+ */
11
+ export const useTheme = (): Theme => {
12
+ const [theme, setTheme] = useState<Theme>('light');
13
+
14
+ useEffect(() => {
15
+ const checkTheme = (): Theme => {
16
+ // Check if dark class is applied to html element (manual theme)
17
+ if (document.documentElement.classList.contains('dark')) {
18
+ return 'dark';
19
+ }
20
+
21
+ // Check system preference
22
+ if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
23
+ return 'dark';
24
+ }
25
+
26
+ return 'light';
27
+ };
28
+
29
+ // Set initial theme
30
+ setTheme(checkTheme());
31
+
32
+ // Listen for manual theme changes (class changes on html element)
33
+ const observer = new MutationObserver(() => {
34
+ setTheme(checkTheme());
35
+ });
36
+
37
+ observer.observe(document.documentElement, {
38
+ attributes: true,
39
+ attributeFilter: ['class']
40
+ });
41
+
42
+ // Listen for system theme changes
43
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
44
+ const handleMediaChange = () => {
45
+ setTheme(checkTheme());
46
+ };
47
+
48
+ mediaQuery.addEventListener('change', handleMediaChange);
49
+
50
+ return () => {
51
+ observer.disconnect();
52
+ mediaQuery.removeEventListener('change', handleMediaChange);
53
+ };
54
+ }, []);
55
+
56
+ return theme;
57
+ };
package/src/index.ts ADDED
@@ -0,0 +1,24 @@
1
+ // ============================================================================
2
+ // @djangocfg/ui - Main Export File
3
+ // ============================================================================
4
+
5
+ // Re-export everything from components
6
+ export * from './components';
7
+
8
+ // Re-export hooks
9
+ export * from './hooks';
10
+
11
+ // Re-export blocks
12
+ export * from './blocks';
13
+
14
+ // Re-export animations
15
+ export * from './animations';
16
+
17
+ // Re-export lib utilities
18
+ export * from './lib';
19
+
20
+ // Re-export tools
21
+ export * from './tools';
22
+
23
+ // Re-export theme
24
+ export * from './theme';
@@ -0,0 +1,2 @@
1
+ // Re-export from ui-core
2
+ export * from '@djangocfg/ui-core/lib';
@@ -0,0 +1,2 @@
1
+ /* Re-export styles from ui-core */
2
+ @import '@djangocfg/ui-core/styles';
@@ -0,0 +1,115 @@
1
+ /**
2
+ * ForceTheme - Force a specific theme for a section
3
+ *
4
+ * Wraps content to override the global theme setting.
5
+ * Works by adding both the theme class and inline CSS variables
6
+ * to ensure proper theme application regardless of parent context.
7
+ */
8
+
9
+ 'use client';
10
+
11
+ import React, { ReactNode } from 'react';
12
+ import { cn } from '@djangocfg/ui-core/lib';
13
+
14
+ interface ForceThemeProps {
15
+ theme: 'light' | 'dark';
16
+ children: ReactNode;
17
+ className?: string;
18
+ }
19
+
20
+ // Dark theme CSS variables
21
+ const darkThemeVars = {
22
+ // Base HSL values
23
+ '--background': '0 0% 4%',
24
+ '--foreground': '0 0% 98%',
25
+ '--card': '0 0% 8%',
26
+ '--card-foreground': '0 0% 98%',
27
+ '--popover': '0 0% 12%',
28
+ '--popover-foreground': '0 0% 98%',
29
+ '--primary': '217 91% 60%',
30
+ '--primary-foreground': '0 0% 100%',
31
+ '--secondary': '0 0% 98%',
32
+ '--secondary-foreground': '0 0% 9%',
33
+ '--muted': '0 0% 10%',
34
+ '--muted-foreground': '0 0% 60%',
35
+ '--accent': '0 0% 15%',
36
+ '--accent-foreground': '0 0% 98%',
37
+ '--destructive': '0 84% 60%',
38
+ '--destructive-foreground': '0 0% 98%',
39
+ '--border': '0 0% 15%',
40
+ '--input': '0 0% 15%',
41
+ '--ring': '217 91% 60%',
42
+ // Tailwind color tokens (used by bg-*, text-*, etc)
43
+ '--color-background': 'hsl(0 0% 4%)',
44
+ '--color-foreground': 'hsl(0 0% 98%)',
45
+ '--color-card': 'hsl(0 0% 8%)',
46
+ '--color-card-foreground': 'hsl(0 0% 98%)',
47
+ '--color-primary': 'hsl(217 91% 60%)',
48
+ '--color-primary-foreground': 'hsl(0 0% 100%)',
49
+ '--color-secondary': 'hsl(0 0% 98%)',
50
+ '--color-secondary-foreground': 'hsl(0 0% 9%)',
51
+ '--color-muted': 'hsl(0 0% 10%)',
52
+ '--color-muted-foreground': 'hsl(0 0% 60%)',
53
+ '--color-accent': 'hsl(0 0% 15%)',
54
+ '--color-accent-foreground': 'hsl(0 0% 98%)',
55
+ '--color-destructive': 'hsl(0 84% 60%)',
56
+ '--color-destructive-foreground': 'hsl(0 0% 98%)',
57
+ '--color-border': 'hsl(0 0% 15%)',
58
+ '--color-input': 'hsl(0 0% 15%)',
59
+ '--color-ring': 'hsl(217 91% 60%)',
60
+ } as React.CSSProperties;
61
+
62
+ // Light theme CSS variables
63
+ const lightThemeVars = {
64
+ // Base HSL values
65
+ '--background': '0 0% 96%',
66
+ '--foreground': '0 0% 9%',
67
+ '--card': '0 0% 100%',
68
+ '--card-foreground': '0 0% 9%',
69
+ '--popover': '0 0% 100%',
70
+ '--popover-foreground': '0 0% 9%',
71
+ '--primary': '217 91% 60%',
72
+ '--primary-foreground': '0 0% 100%',
73
+ '--secondary': '0 0% 9%',
74
+ '--secondary-foreground': '0 0% 98%',
75
+ '--muted': '0 0% 96%',
76
+ '--muted-foreground': '0 0% 40%',
77
+ '--accent': '0 0% 92%',
78
+ '--accent-foreground': '0 0% 9%',
79
+ '--destructive': '0 84% 60%',
80
+ '--destructive-foreground': '0 0% 98%',
81
+ '--border': '0 0% 90%',
82
+ '--input': '0 0% 90%',
83
+ '--ring': '217 91% 60%',
84
+ // Tailwind color tokens (used by bg-*, text-*, etc)
85
+ '--color-background': 'hsl(0 0% 96%)',
86
+ '--color-foreground': 'hsl(0 0% 9%)',
87
+ '--color-card': 'hsl(0 0% 100%)',
88
+ '--color-card-foreground': 'hsl(0 0% 9%)',
89
+ '--color-primary': 'hsl(217 91% 60%)',
90
+ '--color-primary-foreground': 'hsl(0 0% 100%)',
91
+ '--color-secondary': 'hsl(0 0% 9%)',
92
+ '--color-secondary-foreground': 'hsl(0 0% 98%)',
93
+ '--color-muted': 'hsl(0 0% 96%)',
94
+ '--color-muted-foreground': 'hsl(0 0% 40%)',
95
+ '--color-accent': 'hsl(0 0% 92%)',
96
+ '--color-accent-foreground': 'hsl(0 0% 9%)',
97
+ '--color-destructive': 'hsl(0 84% 60%)',
98
+ '--color-destructive-foreground': 'hsl(0 0% 98%)',
99
+ '--color-border': 'hsl(0 0% 90%)',
100
+ '--color-input': 'hsl(0 0% 90%)',
101
+ '--color-ring': 'hsl(217 91% 60%)',
102
+ } as React.CSSProperties;
103
+
104
+ export function ForceTheme({ theme, children, className }: ForceThemeProps) {
105
+ const themeVars = theme === 'dark' ? darkThemeVars : lightThemeVars;
106
+
107
+ return (
108
+ <div
109
+ className={cn(theme, className)}
110
+ style={themeVars}
111
+ >
112
+ {children}
113
+ </div>
114
+ );
115
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * ThemeProvider - Universal theme management
3
+ *
4
+ * Provides theme context for the entire application with localStorage persistence.
5
+ */
6
+
7
+ 'use client';
8
+
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
+ // ─────────────────────────────────────────────────────────────────────────
27
+
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
+ }
39
+
40
+ export function ThemeProvider({
41
+ children,
42
+ defaultTheme = 'light',
43
+ storageKey = 'theme'
44
+ }: ThemeProviderProps) {
45
+ const [theme, setTheme] = useLocalStorage<Theme>(storageKey, defaultTheme);
46
+
47
+ useEffect(() => {
48
+ const root = window.document.documentElement;
49
+ root.classList.remove('light', 'dark');
50
+ root.classList.add(theme);
51
+ }, [theme]);
52
+
53
+ const toggleTheme = () => {
54
+ setTheme(theme === 'light' ? 'dark' : 'light');
55
+ };
56
+
57
+ const value: ThemeContextValue = {
58
+ theme,
59
+ setTheme,
60
+ toggleTheme,
61
+ };
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
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * ThemeToggle - Theme switcher component
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.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import { ThemeToggle } from '@djangocfg/ui-nextjs';
10
+ *
11
+ * <ThemeToggle />
12
+ * ```
13
+ */
14
+
15
+ 'use client';
16
+
17
+ import { useEffect, useState } from 'react';
18
+ import { Moon, Sun } from 'lucide-react';
19
+ import { Button } from '@djangocfg/ui-core/components';
20
+ import { useThemeContext } from './ThemeProvider';
21
+
22
+ export function ThemeToggle() {
23
+ const { theme, toggleTheme } = useThemeContext();
24
+ const [isMounted, setIsMounted] = useState(false);
25
+
26
+ // Prevent hydration mismatch by only rendering after mount
27
+ useEffect(() => {
28
+ setIsMounted(true);
29
+ }, []);
30
+
31
+ // Don't render anything during SSR
32
+ if (!isMounted) {
33
+ return null;
34
+ }
35
+
36
+ return (
37
+ <Button
38
+ variant="ghost"
39
+ size="icon"
40
+ onClick={toggleTheme}
41
+ className="h-9 w-9"
42
+ title={`Switch to ${theme === 'light' ? 'dark' : 'light'} theme`}
43
+ >
44
+ {theme === 'light' ? (
45
+ <Sun className="h-4 w-4" />
46
+ ) : (
47
+ <Moon className="h-4 w-4" />
48
+ )}
49
+ <span className="sr-only">Toggle theme</span>
50
+ </Button>
51
+ );
52
+ }
@@ -0,0 +1,3 @@
1
+ export { ThemeProvider, useThemeContext } from './ThemeProvider';
2
+ export { ThemeToggle } from './ThemeToggle';
3
+ export { ForceTheme } from './ForceTheme';