@snowcone-app/ui 0.1.43 → 0.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.
Files changed (192) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +18 -4
  3. package/package.json +9 -5
  4. package/src/components/CanvasIsolationBoundary.tsx +202 -0
  5. package/src/components/LoadingOverlayPrism.tsx +251 -0
  6. package/src/composed/AddToCart.tsx +229 -0
  7. package/src/composed/ArtAlignment.tsx +703 -0
  8. package/src/composed/ArtSelector.tsx +290 -0
  9. package/src/composed/ArtworkCustomizer.tsx +212 -0
  10. package/src/composed/CanvasEditor.tsx +79 -0
  11. package/src/composed/ColorPicker.tsx +111 -0
  12. package/src/composed/CurrentSelectionDisplay.tsx +86 -0
  13. package/src/composed/HeroProductImage.tsx +1071 -0
  14. package/src/composed/Lightbox.index.ts +2 -0
  15. package/src/composed/Lightbox.tsx +230 -0
  16. package/src/composed/PlacementClipShapeSelector.tsx +88 -0
  17. package/src/composed/PlacementTabs.tsx +179 -0
  18. package/src/composed/ProductCard.tsx +298 -0
  19. package/src/composed/ProductGallery.tsx +54 -0
  20. package/src/composed/ProductImage.tsx +129 -0
  21. package/src/composed/ProductList.tsx +147 -0
  22. package/src/composed/ProductOptions.tsx +305 -0
  23. package/src/composed/RealtimeMockup.tsx +121 -0
  24. package/src/composed/TileCount.tsx +348 -0
  25. package/src/composed/carousels/HeroCarousel.tsx +240 -0
  26. package/src/composed/carousels/MobileProductCarousel.tsx +1002 -0
  27. package/src/composed/carousels/index.ts +11 -0
  28. package/src/composed/carousels/types.ts +58 -0
  29. package/src/composed/grids/MasonryGrid.tsx +238 -0
  30. package/src/composed/grids/index.ts +9 -0
  31. package/src/composed/search/CurrentRefinements.tsx +80 -0
  32. package/src/composed/search/Filters.tsx +49 -0
  33. package/src/composed/search/FiltersButton.tsx +57 -0
  34. package/src/composed/search/FiltersDrawer.tsx +375 -0
  35. package/src/composed/search/ProductGrid.tsx +118 -0
  36. package/src/composed/search/ProductHit.tsx +56 -0
  37. package/src/composed/search/SearchBox.tsx +109 -0
  38. package/src/composed/search/SearchProvider.tsx +136 -0
  39. package/src/composed/search/facetConfig.ts +16 -0
  40. package/src/composed/search/index.ts +22 -0
  41. package/src/composed/search/meilisearchAdapter.ts +20 -0
  42. package/src/composed/search/types.ts +22 -0
  43. package/src/composed/zoom/EnhancedImageViewer.tsx +505 -0
  44. package/src/composed/zoom/ResponsiveZoom.tsx +134 -0
  45. package/src/composed/zoom/ZoomOverlay.tsx +194 -0
  46. package/src/composed/zoom/index.ts +12 -0
  47. package/src/composed/zoom/types.ts +12 -0
  48. package/src/design-system/ColorPalette.tsx +126 -0
  49. package/src/design-system/ColorSwatch.tsx +49 -0
  50. package/src/design-system/DesignSystemPage.tsx +130 -0
  51. package/src/design-system/ThemeSwitcher.tsx +181 -0
  52. package/src/design-system/TypographyScale.tsx +106 -0
  53. package/src/design-system/index.ts +5 -0
  54. package/src/ecommerce/stories/HeroProductImage.stories.tsx +66 -0
  55. package/src/ecommerce/stories/PDPHeroGallery.stories.tsx +105 -0
  56. package/src/ecommerce/stories/PDPInfoPanel.stories.tsx +472 -0
  57. package/src/ecommerce/stories/PDPLayout.stories.tsx +365 -0
  58. package/src/hooks/useBrand.ts +41 -0
  59. package/src/hooks/useCanvasContext.ts +127 -0
  60. package/src/hooks/useDeviceDetection.ts +64 -0
  61. package/src/hooks/useFocusTrap.ts +70 -0
  62. package/src/hooks/useImagePreloader.ts +268 -0
  63. package/src/hooks/useImageTransition.ts +608 -0
  64. package/src/hooks/usePlacementsProcessor.ts +74 -0
  65. package/src/hooks/useProductGallery.ts +193 -0
  66. package/src/hooks/useProductPage.ts +467 -0
  67. package/src/hooks/useRenderGuard.ts +96 -0
  68. package/src/hooks/useScrollDirection.ts +196 -0
  69. package/src/hooks/viewport/index.ts +25 -0
  70. package/src/hooks/viewport/useContainerWidth.ts +59 -0
  71. package/src/hooks/viewport/useMediaQuery.ts +52 -0
  72. package/src/hooks/viewport/useResponsiveImageCap.ts +149 -0
  73. package/src/hooks/viewport/useViewportDimensions.ts +135 -0
  74. package/src/hooks/viewport/useWideMonitorMode.ts +150 -0
  75. package/src/hooks/visibility/index.ts +15 -0
  76. package/src/hooks/visibility/observerPool.ts +150 -0
  77. package/src/index.ts +240 -0
  78. package/src/layouts/hero-zoom/HeroShrinkLayout.tsx +209 -0
  79. package/src/layouts/hero-zoom/HeroZoomLayout.tsx +351 -0
  80. package/src/layouts/hero-zoom/index.ts +30 -0
  81. package/src/layouts/hero-zoom/stories/HeroZoomLayout.stories.tsx +350 -0
  82. package/src/layouts/hero-zoom/types.ts +113 -0
  83. package/src/layouts/hero-zoom/useHeroZoomScales.ts +156 -0
  84. package/src/layouts/index.ts +9 -0
  85. package/src/layouts/pdp/EdgeBlurBox.tsx +210 -0
  86. package/src/layouts/pdp/ImageBlurExtension.tsx +215 -0
  87. package/src/layouts/pdp/ImageEdgeBlur.tsx +215 -0
  88. package/src/layouts/pdp/PDPLayout.tsx +246 -0
  89. package/src/layouts/pdp/SimpleImageBlur.tsx +140 -0
  90. package/src/layouts/pdp/index.ts +40 -0
  91. package/src/lib/env.ts +15 -0
  92. package/src/lib/locale.ts +167 -0
  93. package/src/lib/router.tsx +46 -0
  94. package/src/lib/utils.ts +6 -0
  95. package/src/lightbox/README.md +77 -0
  96. package/src/next/index.tsx +26 -0
  97. package/src/patterns/MockupPriorityProvider.tsx +1014 -0
  98. package/src/patterns/Product.tsx +850 -0
  99. package/src/patterns/ProductPageProvider.tsx +224 -0
  100. package/src/patterns/RealtimeProvider.tsx +1162 -0
  101. package/src/patterns/ShopProvider.tsx +603 -0
  102. package/src/personalization/PersonalizationBridge.tsx +235 -0
  103. package/src/personalization/PersonalizationContext.ts +29 -0
  104. package/src/personalization/PersonalizationInputs.tsx +110 -0
  105. package/src/personalization/PersonalizationProvider.tsx +407 -0
  106. package/src/personalization/canvas-stub.d.ts +22 -0
  107. package/src/personalization/index.ts +43 -0
  108. package/src/personalization/types.ts +48 -0
  109. package/src/personalization/usePersonalization.ts +32 -0
  110. package/src/personalization/usePersonalizationShimmer.ts +159 -0
  111. package/src/personalization/utils.ts +59 -0
  112. package/src/primitives/BrandLogo.tsx +65 -0
  113. package/src/primitives/BrandName.tsx +51 -0
  114. package/src/primitives/Button.tsx +123 -0
  115. package/src/primitives/ColorSwatch.tsx +221 -0
  116. package/src/primitives/DragHintAnimation.tsx +190 -0
  117. package/src/primitives/EdgeSwipeGuards.tsx +60 -0
  118. package/src/primitives/FloatingActionGroup.tsx +176 -0
  119. package/src/primitives/ProductPrice.tsx +171 -0
  120. package/src/primitives/ProgressiveBlur.tsx +295 -0
  121. package/src/primitives/ThemeToggle.tsx +125 -0
  122. package/src/primitives/__tests__/story-coverage.test.ts +98 -0
  123. package/src/primitives/accordion.tsx +280 -0
  124. package/src/primitives/badge.tsx +137 -0
  125. package/src/primitives/card.tsx +61 -0
  126. package/src/primitives/checkbox.tsx +56 -0
  127. package/src/primitives/collapsible.tsx +51 -0
  128. package/src/primitives/drawer.tsx +828 -0
  129. package/src/primitives/dropdown-menu.tsx +197 -0
  130. package/src/primitives/fieldset.tsx +73 -0
  131. package/src/primitives/index.ts +138 -0
  132. package/src/primitives/input.tsx +91 -0
  133. package/src/primitives/kbd.tsx +130 -0
  134. package/src/primitives/label.tsx +20 -0
  135. package/src/primitives/link.tsx +182 -0
  136. package/src/primitives/popover.tsx +80 -0
  137. package/src/primitives/radio-group.tsx +79 -0
  138. package/src/primitives/scroll-fade.tsx +159 -0
  139. package/src/primitives/select.tsx +170 -0
  140. package/src/primitives/separator.tsx +25 -0
  141. package/src/primitives/slider.tsx +221 -0
  142. package/src/primitives/spinner.tsx +72 -0
  143. package/src/primitives/stories/Accordion.stories.tsx +121 -0
  144. package/src/primitives/stories/Badge.stories.tsx +221 -0
  145. package/src/primitives/stories/Button.stories.tsx +185 -0
  146. package/src/primitives/stories/Card.stories.tsx +171 -0
  147. package/src/primitives/stories/Checkbox.stories.tsx +214 -0
  148. package/src/primitives/stories/Collapsible.stories.tsx +230 -0
  149. package/src/primitives/stories/Drawer.stories.tsx +378 -0
  150. package/src/primitives/stories/DropdownMenu.stories.tsx +182 -0
  151. package/src/primitives/stories/Fieldset.stories.tsx +212 -0
  152. package/src/primitives/stories/Input.stories.tsx +172 -0
  153. package/src/primitives/stories/Kbd.stories.tsx +183 -0
  154. package/src/primitives/stories/Label.stories.tsx +98 -0
  155. package/src/primitives/stories/Link.stories.tsx +260 -0
  156. package/src/primitives/stories/Popover.stories.tsx +178 -0
  157. package/src/primitives/stories/RadioGroup.stories.tsx +205 -0
  158. package/src/primitives/stories/Select.stories.tsx +222 -0
  159. package/src/primitives/stories/Separator.stories.tsx +134 -0
  160. package/src/primitives/stories/Slider.stories.tsx +203 -0
  161. package/src/primitives/stories/Spinner.stories.tsx +142 -0
  162. package/src/primitives/stories/Surface.stories.tsx +257 -0
  163. package/src/primitives/stories/Switch.stories.tsx +131 -0
  164. package/src/primitives/stories/Tabs.stories.tsx +275 -0
  165. package/src/primitives/stories/TextField.stories.tsx +139 -0
  166. package/src/primitives/stories/Textarea.stories.tsx +148 -0
  167. package/src/primitives/stories/Tooltip.stories.tsx +119 -0
  168. package/src/primitives/surface.tsx +86 -0
  169. package/src/primitives/switch.tsx +35 -0
  170. package/src/primitives/tabs.tsx +206 -0
  171. package/src/primitives/text-field.tsx +84 -0
  172. package/src/primitives/textarea.tsx +50 -0
  173. package/src/primitives/tooltip.tsx +58 -0
  174. package/src/services/CanvasExportService.ts +518 -0
  175. package/src/styles/base.css +380 -0
  176. package/src/styles/defaults.css +280 -0
  177. package/src/styles/globals.css +1242 -0
  178. package/src/styles/index.css +17 -0
  179. package/src/styles/ne-themes.css +4740 -0
  180. package/src/styles/tailwind.css +11 -0
  181. package/src/styles/tokens.css +117 -0
  182. package/src/styles/utilities.css +188 -0
  183. package/src/themes/apply-theme.ts +449 -0
  184. package/src/themes/getThemeStyles.ts +454 -0
  185. package/src/themes/index.ts +48 -0
  186. package/src/themes/oklch-theme.ts +283 -0
  187. package/src/themes/presets.ts +989 -0
  188. package/src/themes/types.ts +386 -0
  189. package/src/themes/useTheme.tsx +450 -0
  190. package/src/utils/dev-warnings.ts +161 -0
  191. package/src/utils/devWarnings.ts +153 -0
  192. package/dist/styles.css +0 -1
@@ -0,0 +1,450 @@
1
+ "use client";
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * Shared Theme Hook
7
+ *
8
+ * This is the single source of truth for theme state management.
9
+ * All apps (next-ecommerce, docs, canvas) can use this same hook.
10
+ *
11
+ * Features:
12
+ * - State management for current theme
13
+ * - localStorage persistence
14
+ * - Light/dark toggle
15
+ * - Theme cycling
16
+ * - Calls applyTheme() on theme change
17
+ */
18
+
19
+ import { useState, useEffect, useCallback, useMemo, createContext, useContext, type ReactNode } from 'react';
20
+ import type { ThemePreset, FontPairing } from './types';
21
+ import { presetThemes, findTheme } from './presets';
22
+ import { applyTheme } from './apply-theme';
23
+
24
+ const STORAGE_KEY = 'snowcone-theme';
25
+ const COOKIE_NAME = 'snowcone-theme';
26
+
27
+ /**
28
+ * Set a cookie with the theme name
29
+ * Uses a 1-year expiry and sameSite=lax for security
30
+ */
31
+ function setThemeCookie(themeName: string): void {
32
+ if (typeof document === 'undefined') return;
33
+ const maxAge = 60 * 60 * 24 * 365; // 1 year
34
+ document.cookie = `${COOKIE_NAME}=${encodeURIComponent(themeName)}; path=/; max-age=${maxAge}; samesite=lax`;
35
+ }
36
+
37
+ export interface ThemeContextValue {
38
+ /** Current theme configuration */
39
+ currentTheme: ThemePreset;
40
+ /** Update the current theme */
41
+ setCurrentTheme: (theme: ThemePreset | ((prev: ThemePreset) => ThemePreset)) => void;
42
+ /** Current theme index in presetThemes array */
43
+ currentThemeIndex: number;
44
+ /** All available preset themes */
45
+ presetThemes: ThemePreset[];
46
+ /** Select a theme by index */
47
+ selectPresetTheme: (index: number, respectLightDark?: boolean) => void;
48
+ /** Toggle between light and dark variants of current theme */
49
+ toggleLightDark: () => void;
50
+ /** Get info about current theme's light/dark state */
51
+ getCurrentThemeVariant: () => { isDark: boolean; hasLightVariant: boolean; hasDarkVariant: boolean } | null;
52
+ /** Reset to default theme */
53
+ resetTheme: () => void;
54
+ /** Whether auto-cycling is enabled */
55
+ isAutoTheme: boolean;
56
+ /** Toggle auto-cycling mode */
57
+ toggleAutoTheme: () => void;
58
+ /** Selected font pairing (separate from theme's font) */
59
+ selectedFontPairing: FontPairing | null;
60
+ /** Set selected font pairing */
61
+ setSelectedFontPairing: (pairing: FontPairing | null) => void;
62
+ }
63
+
64
+ const ThemeContext = createContext<ThemeContextValue | null>(null);
65
+
66
+ export interface ThemeProviderProps {
67
+ children: ReactNode;
68
+ /** Default theme to use (defaults to first preset) */
69
+ defaultTheme?: ThemePreset;
70
+ /** Storage key for localStorage persistence */
71
+ storageKey?: string;
72
+ /** Auto-apply theme on mount and changes */
73
+ autoApply?: boolean;
74
+ }
75
+
76
+ /**
77
+ * Theme Provider Component
78
+ *
79
+ * Wrap your app with this provider to enable theme functionality.
80
+ *
81
+ * @example
82
+ * ```tsx
83
+ * <ThemeProvider>
84
+ * <App />
85
+ * </ThemeProvider>
86
+ * ```
87
+ */
88
+ export function ThemeProvider({
89
+ children,
90
+ defaultTheme = presetThemes[0],
91
+ storageKey = STORAGE_KEY,
92
+ autoApply = true,
93
+ }: ThemeProviderProps) {
94
+ const [currentTheme, setCurrentThemeState] = useState<ThemePreset>(defaultTheme);
95
+ const [currentThemeIndex, setCurrentThemeIndex] = useState(() => {
96
+ const idx = presetThemes.findIndex(t => t.name === defaultTheme.name);
97
+ return idx >= 0 ? idx : 0;
98
+ });
99
+ const [isAutoTheme, setIsAutoTheme] = useState(false);
100
+ const [selectedFontPairing, setSelectedFontPairing] = useState<FontPairing | null>(null);
101
+ const [isInitialized, setIsInitialized] = useState(false);
102
+
103
+ // Load saved theme from localStorage on mount
104
+ useEffect(() => {
105
+ if (typeof window === 'undefined') return;
106
+
107
+ try {
108
+ const saved = localStorage.getItem(storageKey);
109
+ if (saved) {
110
+ const parsed = JSON.parse(saved);
111
+ const savedTheme = findTheme(parsed.name);
112
+ // Only update state if localStorage theme differs from defaultTheme
113
+ // (defaultTheme may already be set correctly from server-side cookie)
114
+ if (savedTheme && savedTheme.name !== defaultTheme.name) {
115
+ setCurrentThemeState(savedTheme);
116
+ const idx = presetThemes.findIndex(t => t.name === savedTheme.name);
117
+ if (idx >= 0) setCurrentThemeIndex(idx);
118
+ }
119
+ }
120
+ } catch {
121
+ // Ignore localStorage errors
122
+ }
123
+ setIsInitialized(true);
124
+ }, [storageKey, defaultTheme.name]);
125
+
126
+ // Apply theme when it changes
127
+ useEffect(() => {
128
+ if (!isInitialized || !autoApply) return;
129
+
130
+ // Apply the theme
131
+ applyTheme(currentTheme);
132
+
133
+ // Save to localStorage
134
+ try {
135
+ localStorage.setItem(storageKey, JSON.stringify({ name: currentTheme.name }));
136
+ } catch {
137
+ // Ignore localStorage errors
138
+ }
139
+
140
+ // Save to cookie for SSR
141
+ setThemeCookie(currentTheme.name);
142
+ }, [currentTheme, isInitialized, autoApply, storageKey]);
143
+
144
+ const setCurrentTheme = useCallback((themeOrUpdater: ThemePreset | ((prev: ThemePreset) => ThemePreset)) => {
145
+ setCurrentThemeState(prev => {
146
+ const newTheme = typeof themeOrUpdater === 'function' ? themeOrUpdater(prev) : themeOrUpdater;
147
+ const idx = presetThemes.findIndex(t => t.name === newTheme.name);
148
+ if (idx >= 0) setCurrentThemeIndex(idx);
149
+ return newTheme;
150
+ });
151
+ }, []);
152
+
153
+ const selectPresetTheme = useCallback((index: number, respectLightDark = false) => {
154
+ if (index < 0 || index >= presetThemes.length) return;
155
+
156
+ let targetTheme = presetThemes[index];
157
+
158
+ // If respectLightDark is true, match the current light/dark mode
159
+ if (respectLightDark && currentThemeIndex >= 0) {
160
+ const currentIsDark = presetThemes[currentThemeIndex]?.isDark || false;
161
+ const targetIsDark = targetTheme.isDark || false;
162
+
163
+ if (currentIsDark !== targetIsDark) {
164
+ const variantName = currentIsDark ? targetTheme.darkVariant : targetTheme.lightVariant;
165
+ if (variantName) {
166
+ const variantIndex = presetThemes.findIndex(t => t.name === variantName);
167
+ if (variantIndex >= 0) {
168
+ targetTheme = presetThemes[variantIndex];
169
+ index = variantIndex;
170
+ }
171
+ }
172
+ }
173
+ }
174
+
175
+ setCurrentThemeState(targetTheme);
176
+ setCurrentThemeIndex(index);
177
+ setIsAutoTheme(false);
178
+ }, [currentThemeIndex]);
179
+
180
+ const toggleLightDark = useCallback(() => {
181
+ const current = presetThemes[currentThemeIndex];
182
+ if (!current) return;
183
+
184
+ const targetName = current.isDark ? current.lightVariant : current.darkVariant;
185
+ if (targetName) {
186
+ const targetIndex = presetThemes.findIndex(t => t.name === targetName);
187
+ if (targetIndex >= 0) {
188
+ selectPresetTheme(targetIndex);
189
+ }
190
+ }
191
+ }, [currentThemeIndex, selectPresetTheme]);
192
+
193
+ const getCurrentThemeVariant = useCallback(() => {
194
+ const current = presetThemes[currentThemeIndex];
195
+ if (!current) return null;
196
+
197
+ return {
198
+ isDark: current.isDark || false,
199
+ hasLightVariant: !!(current.isDark && current.lightVariant),
200
+ hasDarkVariant: !!(!current.isDark && current.darkVariant),
201
+ };
202
+ }, [currentThemeIndex]);
203
+
204
+ const resetTheme = useCallback(() => {
205
+ setCurrentThemeState(defaultTheme);
206
+ const idx = presetThemes.findIndex(t => t.name === defaultTheme.name);
207
+ setCurrentThemeIndex(idx >= 0 ? idx : 0);
208
+ setIsAutoTheme(false);
209
+ }, [defaultTheme]);
210
+
211
+ const toggleAutoTheme = useCallback(() => {
212
+ setIsAutoTheme(prev => !prev);
213
+ }, []);
214
+
215
+ // Auto theme cycling effect
216
+ useEffect(() => {
217
+ if (!isAutoTheme) return;
218
+
219
+ const interval = setInterval(() => {
220
+ setCurrentThemeIndex(prev => {
221
+ const nextIndex = (prev + 1) % presetThemes.length;
222
+ setCurrentThemeState(presetThemes[nextIndex]);
223
+ return nextIndex;
224
+ });
225
+ }, 3000);
226
+
227
+ return () => clearInterval(interval);
228
+ }, [isAutoTheme]);
229
+
230
+ const value = useMemo<ThemeContextValue>(() => ({
231
+ currentTheme,
232
+ setCurrentTheme,
233
+ currentThemeIndex,
234
+ presetThemes,
235
+ selectPresetTheme,
236
+ toggleLightDark,
237
+ getCurrentThemeVariant,
238
+ resetTheme,
239
+ isAutoTheme,
240
+ toggleAutoTheme,
241
+ selectedFontPairing,
242
+ setSelectedFontPairing,
243
+ }), [
244
+ currentTheme,
245
+ setCurrentTheme,
246
+ currentThemeIndex,
247
+ selectPresetTheme,
248
+ toggleLightDark,
249
+ getCurrentThemeVariant,
250
+ resetTheme,
251
+ isAutoTheme,
252
+ toggleAutoTheme,
253
+ selectedFontPairing,
254
+ ]);
255
+
256
+ return (
257
+ <ThemeContext.Provider value={value}>
258
+ {children}
259
+ </ThemeContext.Provider>
260
+ );
261
+ }
262
+
263
+ /**
264
+ * Hook to access theme context
265
+ *
266
+ * Must be used within a ThemeProvider.
267
+ *
268
+ * @example
269
+ * ```tsx
270
+ * function MyComponent() {
271
+ * const { currentTheme, toggleLightDark } = useTheme();
272
+ * return (
273
+ * <button onClick={toggleLightDark}>
274
+ * {currentTheme.isDark ? 'Light Mode' : 'Dark Mode'}
275
+ * </button>
276
+ * );
277
+ * }
278
+ * ```
279
+ */
280
+ export function useTheme(): ThemeContextValue {
281
+ const context = useContext(ThemeContext);
282
+ if (!context) {
283
+ throw new Error('useTheme must be used within a ThemeProvider');
284
+ }
285
+ return context;
286
+ }
287
+
288
+ /**
289
+ * Standalone hook for theme management without context
290
+ *
291
+ * Use this when you don't need a provider (e.g., single-page apps).
292
+ *
293
+ * @example
294
+ * ```tsx
295
+ * function App() {
296
+ * const theme = useThemeStandalone();
297
+ * return <div>Current theme: {theme.currentTheme.name}</div>;
298
+ * }
299
+ * ```
300
+ */
301
+ export function useThemeStandalone(options?: {
302
+ defaultTheme?: ThemePreset;
303
+ storageKey?: string;
304
+ autoApply?: boolean;
305
+ }): ThemeContextValue {
306
+ const { defaultTheme = presetThemes[0], storageKey = STORAGE_KEY, autoApply = true } = options || {};
307
+
308
+ const [currentTheme, setCurrentThemeState] = useState<ThemePreset>(defaultTheme);
309
+ const [currentThemeIndex, setCurrentThemeIndex] = useState(() => {
310
+ const idx = presetThemes.findIndex(t => t.name === defaultTheme.name);
311
+ return idx >= 0 ? idx : 0;
312
+ });
313
+ const [isAutoTheme, setIsAutoTheme] = useState(false);
314
+ const [selectedFontPairing, setSelectedFontPairing] = useState<FontPairing | null>(null);
315
+ const [isInitialized, setIsInitialized] = useState(false);
316
+
317
+ // Load saved theme from localStorage on mount
318
+ useEffect(() => {
319
+ if (typeof window === 'undefined') return;
320
+
321
+ try {
322
+ const saved = localStorage.getItem(storageKey);
323
+ if (saved) {
324
+ const parsed = JSON.parse(saved);
325
+ const savedTheme = findTheme(parsed.name);
326
+ // Only update state if localStorage theme differs from defaultTheme
327
+ if (savedTheme && savedTheme.name !== defaultTheme.name) {
328
+ setCurrentThemeState(savedTheme);
329
+ const idx = presetThemes.findIndex(t => t.name === savedTheme.name);
330
+ if (idx >= 0) setCurrentThemeIndex(idx);
331
+ }
332
+ }
333
+ } catch {
334
+ // Ignore localStorage errors
335
+ }
336
+ setIsInitialized(true);
337
+ }, [storageKey, defaultTheme.name]);
338
+
339
+ // Apply theme when it changes
340
+ useEffect(() => {
341
+ if (!isInitialized || !autoApply) return;
342
+ applyTheme(currentTheme);
343
+ try {
344
+ localStorage.setItem(storageKey, JSON.stringify({ name: currentTheme.name }));
345
+ } catch {
346
+ // Ignore localStorage errors
347
+ }
348
+ // Save to cookie for SSR
349
+ setThemeCookie(currentTheme.name);
350
+ }, [currentTheme, isInitialized, autoApply, storageKey]);
351
+
352
+ const setCurrentTheme = useCallback((themeOrUpdater: ThemePreset | ((prev: ThemePreset) => ThemePreset)) => {
353
+ setCurrentThemeState(prev => {
354
+ const newTheme = typeof themeOrUpdater === 'function' ? themeOrUpdater(prev) : themeOrUpdater;
355
+ const idx = presetThemes.findIndex(t => t.name === newTheme.name);
356
+ if (idx >= 0) setCurrentThemeIndex(idx);
357
+ return newTheme;
358
+ });
359
+ }, []);
360
+
361
+ const selectPresetTheme = useCallback((index: number, respectLightDark = false) => {
362
+ if (index < 0 || index >= presetThemes.length) return;
363
+ let targetTheme = presetThemes[index];
364
+ if (respectLightDark && currentThemeIndex >= 0) {
365
+ const currentIsDark = presetThemes[currentThemeIndex]?.isDark || false;
366
+ const targetIsDark = targetTheme.isDark || false;
367
+ if (currentIsDark !== targetIsDark) {
368
+ const variantName = currentIsDark ? targetTheme.darkVariant : targetTheme.lightVariant;
369
+ if (variantName) {
370
+ const variantIndex = presetThemes.findIndex(t => t.name === variantName);
371
+ if (variantIndex >= 0) {
372
+ targetTheme = presetThemes[variantIndex];
373
+ index = variantIndex;
374
+ }
375
+ }
376
+ }
377
+ }
378
+ setCurrentThemeState(targetTheme);
379
+ setCurrentThemeIndex(index);
380
+ setIsAutoTheme(false);
381
+ }, [currentThemeIndex]);
382
+
383
+ const toggleLightDark = useCallback(() => {
384
+ const current = presetThemes[currentThemeIndex];
385
+ if (!current) return;
386
+ const targetName = current.isDark ? current.lightVariant : current.darkVariant;
387
+ if (targetName) {
388
+ const targetIndex = presetThemes.findIndex(t => t.name === targetName);
389
+ if (targetIndex >= 0) selectPresetTheme(targetIndex);
390
+ }
391
+ }, [currentThemeIndex, selectPresetTheme]);
392
+
393
+ const getCurrentThemeVariant = useCallback(() => {
394
+ const current = presetThemes[currentThemeIndex];
395
+ if (!current) return null;
396
+ return {
397
+ isDark: current.isDark || false,
398
+ hasLightVariant: !!(current.isDark && current.lightVariant),
399
+ hasDarkVariant: !!(!current.isDark && current.darkVariant),
400
+ };
401
+ }, [currentThemeIndex]);
402
+
403
+ const resetTheme = useCallback(() => {
404
+ setCurrentThemeState(defaultTheme);
405
+ const idx = presetThemes.findIndex(t => t.name === defaultTheme.name);
406
+ setCurrentThemeIndex(idx >= 0 ? idx : 0);
407
+ setIsAutoTheme(false);
408
+ }, [defaultTheme]);
409
+
410
+ const toggleAutoTheme = useCallback(() => setIsAutoTheme(prev => !prev), []);
411
+
412
+ // Auto theme cycling
413
+ useEffect(() => {
414
+ if (!isAutoTheme) return;
415
+ const interval = setInterval(() => {
416
+ setCurrentThemeIndex(prev => {
417
+ const nextIndex = (prev + 1) % presetThemes.length;
418
+ setCurrentThemeState(presetThemes[nextIndex]);
419
+ return nextIndex;
420
+ });
421
+ }, 3000);
422
+ return () => clearInterval(interval);
423
+ }, [isAutoTheme]);
424
+
425
+ return useMemo(() => ({
426
+ currentTheme,
427
+ setCurrentTheme,
428
+ currentThemeIndex,
429
+ presetThemes,
430
+ selectPresetTheme,
431
+ toggleLightDark,
432
+ getCurrentThemeVariant,
433
+ resetTheme,
434
+ isAutoTheme,
435
+ toggleAutoTheme,
436
+ selectedFontPairing,
437
+ setSelectedFontPairing,
438
+ }), [
439
+ currentTheme,
440
+ setCurrentTheme,
441
+ currentThemeIndex,
442
+ selectPresetTheme,
443
+ toggleLightDark,
444
+ getCurrentThemeVariant,
445
+ resetTheme,
446
+ isAutoTheme,
447
+ toggleAutoTheme,
448
+ selectedFontPairing,
449
+ ]);
450
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Development-only warnings for detecting configuration issues
3
+ *
4
+ * This utility helps catch common setup problems early by checking
5
+ * if Tailwind styles are properly applied to components.
6
+ */
7
+
8
+ let hasCheckedConfig = false;
9
+ let configCheckPassed = false;
10
+
11
+ /**
12
+ * Check if Tailwind CSS is properly configured
13
+ * This runs once per page load in development mode
14
+ */
15
+ export function checkTailwindConfig() {
16
+ // Only run in development
17
+ if (process.env.NODE_ENV !== 'development') return true;
18
+
19
+ // Only check once
20
+ if (hasCheckedConfig) return configCheckPassed;
21
+ hasCheckedConfig = true;
22
+
23
+ try {
24
+ // Create a test element with Tailwind classes
25
+ const testElement = document.createElement('div');
26
+ testElement.className = 'bg-background text-foreground';
27
+ testElement.style.position = 'absolute';
28
+ testElement.style.visibility = 'hidden';
29
+ testElement.style.pointerEvents = 'none';
30
+ document.body.appendChild(testElement);
31
+
32
+ // Check if styles are applied
33
+ const styles = window.getComputedStyle(testElement);
34
+ const hasBackgroundColor = styles.backgroundColor && styles.backgroundColor !== 'rgba(0, 0, 0, 0)';
35
+ const hasTextColor = styles.color && styles.color !== 'rgb(0, 0, 0)';
36
+
37
+ // Clean up
38
+ document.body.removeChild(testElement);
39
+
40
+ if (!hasBackgroundColor || !hasTextColor) {
41
+ console.error(
42
+ '%c🚨 @snowcone-app/ui: Tailwind styles not detected!',
43
+ 'font-size: 14px; font-weight: bold; color: #ef4444;',
44
+ '\n\n' +
45
+ '❌ Your components may look unstyled.\n\n' +
46
+ 'Common causes:\n' +
47
+ ' 1. Missing or incorrect @source paths in globals.css\n' +
48
+ ' 2. Forgot to restart dev server after changing globals.css\n' +
49
+ ' 3. Tailwind CSS not imported in globals.css\n\n' +
50
+ 'Quick fix:\n' +
51
+ ' Run: npx snowcone-cli init\n\n' +
52
+ 'Or manually check:\n' +
53
+ ' https://developers.snowcone.app/llm-kit/latest/theme-setup.md\n'
54
+ );
55
+ configCheckPassed = false;
56
+ return false;
57
+ }
58
+
59
+ configCheckPassed = true;
60
+ return true;
61
+
62
+ } catch (error) {
63
+ console.warn('@snowcone-app/ui: Could not verify Tailwind configuration', error);
64
+ configCheckPassed = false;
65
+ return false;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Check if a specific component has styles applied
71
+ * Use this in individual components to provide more specific warnings
72
+ */
73
+ export function checkComponentStyles(
74
+ element: HTMLElement | null,
75
+ componentName: string
76
+ ): boolean {
77
+ // Only run in development
78
+ if (process.env.NODE_ENV !== 'development') return true;
79
+ if (!element) return false;
80
+
81
+ try {
82
+ const styles = window.getComputedStyle(element);
83
+
84
+ // Check if any Tailwind styles are applied
85
+ // Look for non-default values on common properties
86
+ const hasStyles =
87
+ (styles.backgroundColor && styles.backgroundColor !== 'rgba(0, 0, 0, 0)') ||
88
+ (styles.padding && styles.padding !== '0px') ||
89
+ (styles.margin && styles.margin !== '0px') ||
90
+ (styles.display && !['inline', 'block'].includes(styles.display));
91
+
92
+ if (!hasStyles) {
93
+ console.warn(
94
+ `%c⚠️ ${componentName}: Missing Tailwind styles`,
95
+ 'font-size: 12px; color: #f59e0b;',
96
+ '\n' +
97
+ 'Component may not render correctly.\n' +
98
+ 'Check @source configuration in globals.css or run: npx snowcone-cli init'
99
+ );
100
+ return false;
101
+ }
102
+
103
+ return true;
104
+
105
+ } catch (error) {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Check if required CSS variables (theme tokens) are defined
112
+ */
113
+ export function checkThemeTokens(): boolean {
114
+ // Only run in development
115
+ if (process.env.NODE_ENV !== 'development') return true;
116
+
117
+ try {
118
+ const requiredTokens = [
119
+ '--color-background',
120
+ '--color-foreground',
121
+ '--color-primary',
122
+ '--color-card',
123
+ ];
124
+
125
+ const rootStyles = getComputedStyle(document.documentElement);
126
+ const missingTokens = requiredTokens.filter(
127
+ (token) => !rootStyles.getPropertyValue(token)
128
+ );
129
+
130
+ if (missingTokens.length > 0) {
131
+ console.warn(
132
+ '%c⚠️ @snowcone-app/ui: Missing theme tokens',
133
+ 'font-size: 12px; color: #f59e0b;',
134
+ '\n' +
135
+ `Missing: ${missingTokens.join(', ')}\n\n` +
136
+ 'Components may not render correctly.\n' +
137
+ 'Add theme tokens to globals.css:\n' +
138
+ ' https://developers.snowcone.app/llm-kit/latest/themes.css'
139
+ );
140
+ return false;
141
+ }
142
+
143
+ return true;
144
+
145
+ } catch (error) {
146
+ return false;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Run all configuration checks
152
+ * Call this once when the app initializes
153
+ */
154
+ export function runConfigChecks(): boolean {
155
+ if (process.env.NODE_ENV !== 'development') return true;
156
+
157
+ const tailwindOk = checkTailwindConfig();
158
+ const themesOk = checkThemeTokens();
159
+
160
+ return tailwindOk && themesOk;
161
+ }