@snowcone-app/ui 0.1.42 → 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 +33 -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,454 @@
1
+ /**
2
+ * Get Theme Styles
3
+ *
4
+ * Generates inline CSS styles (as a Record) from a theme configuration.
5
+ * This is useful for isolated previews where you can't set CSS classes
6
+ * on the document root (like LivePreview iframes or sandboxed components).
7
+ *
8
+ * OKLCH-BASED COLOR GENERATION:
9
+ * -----------------------------
10
+ * When an OKLCH theme preset is available, colors are generated using OKLCH
11
+ * color space for consistent contrast ratios and mathematically correct
12
+ * color relationships.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * const styles = getThemeStyles(currentTheme);
17
+ * return <div style={styles}>{children}</div>;
18
+ * ```
19
+ */
20
+
21
+ import type { ThemeConfig, RadiusConfig, RadiusPresetId, RadiusSize } from './types';
22
+ import { RADIUS_PRESETS } from './types';
23
+ import { getOklchThemeStyles } from './oklch-theme';
24
+ import { findOklchTheme } from './presets';
25
+
26
+ /**
27
+ * Primitive radius tokens (CSS variable values)
28
+ */
29
+ const RADIUS_VALUES: Record<RadiusSize, string> = {
30
+ none: '0',
31
+ sm: '2px',
32
+ md: '4px',
33
+ lg: '6px',
34
+ xl: '8px',
35
+ '2xl': '12px',
36
+ '3xl': '20px',
37
+ '4xl': '24px',
38
+ full: '9999px',
39
+ };
40
+
41
+ /**
42
+ * Font family presets mapping
43
+ * Inter is loaded from rsms.me in layout.tsx for docs site
44
+ */
45
+ const FONT_FAMILY_MAP: Record<string, string> = {
46
+ sans: '"Inter", "InterVariable", ui-sans-serif, system-ui, sans-serif',
47
+ serif: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
48
+ mono: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
49
+ display: '"Inter", "InterVariable", ui-sans-serif, system-ui, sans-serif',
50
+ mixed: '"Inter", "InterVariable", ui-sans-serif, system-ui, sans-serif',
51
+ 'montserrat-combo': '"Inter", "InterVariable", ui-sans-serif, system-ui, sans-serif',
52
+ 'playfair-combo': '"Inter", "InterVariable", ui-sans-serif, system-ui, sans-serif',
53
+ 'helvetica-combo': '"Inter", "InterVariable", "Helvetica Neue", Helvetica, Arial, ui-sans-serif, system-ui, sans-serif',
54
+ };
55
+
56
+ /**
57
+ * Default font weights
58
+ */
59
+ const DEFAULT_WEIGHTS = {
60
+ body: '400',
61
+ heading: '600',
62
+ button: '500',
63
+ label: '500',
64
+ caption: '400',
65
+ display: '700',
66
+ };
67
+
68
+ /**
69
+ * Map legacy borderRadius to a radius preset
70
+ */
71
+ function legacyBorderRadiusToPreset(borderRadius: string | undefined): RadiusPresetId {
72
+ switch (borderRadius) {
73
+ case 'none': return 'brutalist';
74
+ case 'sm': return 'sharp';
75
+ case 'full': return 'soft';
76
+ case 'xl': return 'playful';
77
+ case 'md':
78
+ case 'lg':
79
+ default:
80
+ return 'default';
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Resolve radius preset to a config object
86
+ */
87
+ function resolveRadiusConfig(preset: RadiusPresetId | RadiusConfig | undefined): Required<RadiusConfig> {
88
+ if (!preset) return RADIUS_PRESETS.default;
89
+ if (typeof preset === 'string') return RADIUS_PRESETS[preset] || RADIUS_PRESETS.default;
90
+ return { ...RADIUS_PRESETS.default, ...preset };
91
+ }
92
+
93
+ /**
94
+ * Get heading font for a font family preset
95
+ */
96
+ function getHeadingFont(fontFamily: string | undefined, bodyFont: string): string {
97
+ switch (fontFamily) {
98
+ case 'mixed':
99
+ case 'playfair-combo':
100
+ return 'ui-serif, Georgia, Cambria, serif';
101
+ case 'serif':
102
+ return 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif';
103
+ default:
104
+ return bodyFont;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Generate inline CSS styles from a theme configuration
110
+ *
111
+ * Returns a Record of CSS variable names to values that can be spread into
112
+ * a style prop for isolated theme previews.
113
+ *
114
+ * @param config - Theme configuration
115
+ * @returns Record of CSS variable names to values
116
+ */
117
+ export function getThemeStyles(config: ThemeConfig): Record<string, string> {
118
+ // Try to find OKLCH config for this theme
119
+ const oklchConfig = config.name ? findOklchTheme(config.name) : undefined;
120
+
121
+ if (oklchConfig) {
122
+ // Use OKLCH-based style generation (preferred path)
123
+ return getOklchBasedStyles(oklchConfig, config);
124
+ }
125
+
126
+ // Fall back to legacy hex-based generation
127
+ return getLegacyStyles(config);
128
+ }
129
+
130
+ /**
131
+ * Generate styles using OKLCH color generation
132
+ */
133
+ function getOklchBasedStyles(
134
+ oklchConfig: ReturnType<typeof findOklchTheme>,
135
+ legacyConfig: ThemeConfig
136
+ ): Record<string, string> {
137
+ if (!oklchConfig) {
138
+ return getLegacyStyles(legacyConfig);
139
+ }
140
+
141
+ // Get OKLCH-generated color variables
142
+ const colorStyles = getOklchThemeStyles(oklchConfig);
143
+
144
+ const styles: Record<string, string> = {
145
+ ...colorStyles,
146
+ };
147
+
148
+ // =========================================================================
149
+ // PRIMITIVE RADIUS TOKENS
150
+ // =========================================================================
151
+ styles['--radius-sm'] = RADIUS_VALUES.sm;
152
+ styles['--radius-md'] = RADIUS_VALUES.md;
153
+ styles['--radius-lg'] = RADIUS_VALUES.lg;
154
+ styles['--radius-xl'] = RADIUS_VALUES.xl;
155
+ styles['--radius-2xl'] = RADIUS_VALUES['2xl'];
156
+ styles['--radius-3xl'] = RADIUS_VALUES['3xl'];
157
+ styles['--radius-4xl'] = RADIUS_VALUES['4xl'];
158
+ styles['--radius-full'] = RADIUS_VALUES.full;
159
+
160
+ // =========================================================================
161
+ // SEMANTIC RADIUS
162
+ // =========================================================================
163
+ const radiusConfig = resolveRadiusConfig(oklchConfig.radiusPreset);
164
+ styles['--radius-button'] = RADIUS_VALUES[radiusConfig.button];
165
+ styles['--radius-input'] = RADIUS_VALUES[radiusConfig.input];
166
+ styles['--radius-card'] = RADIUS_VALUES[radiusConfig.card];
167
+ styles['--radius-modal'] = RADIUS_VALUES[radiusConfig.modal];
168
+ styles['--radius-badge'] = RADIUS_VALUES[radiusConfig.badge];
169
+ styles['--radius-avatar'] = RADIUS_VALUES[radiusConfig.avatar];
170
+ styles['--radius-tooltip'] = RADIUS_VALUES[radiusConfig.tooltip];
171
+ styles['--radius-image'] = RADIUS_VALUES[radiusConfig.image];
172
+
173
+ // =========================================================================
174
+ // FONTS
175
+ // =========================================================================
176
+ const fontPairing = oklchConfig.fontPairing || legacyConfig.fontPairing;
177
+ const fontFamily = oklchConfig.fontFamily || legacyConfig.fontFamily;
178
+
179
+ if (fontPairing) {
180
+ styles['--font-heading'] = fontPairing.headingFont;
181
+ styles['--font-body'] = fontPairing.bodyFont;
182
+ styles['--font-label'] = fontPairing.labelFont || fontPairing.bodyFont;
183
+ styles['--font-button'] = fontPairing.buttonFont || fontPairing.bodyFont;
184
+ styles['--font-caption'] = fontPairing.captionFont || fontPairing.bodyFont;
185
+ styles['--font-display'] = fontPairing.displayFont || fontPairing.headingFont;
186
+ styles['--font-accent'] = fontPairing.accentFont || fontPairing.headingFont;
187
+
188
+ const weights = fontPairing.weights;
189
+ styles['--font-weight-body'] = weights?.bodyWeight || DEFAULT_WEIGHTS.body;
190
+ styles['--font-weight-heading'] = weights?.headingWeight || DEFAULT_WEIGHTS.heading;
191
+ styles['--font-weight-button'] = weights?.buttonWeight || DEFAULT_WEIGHTS.button;
192
+ styles['--font-weight-label'] = weights?.labelWeight || DEFAULT_WEIGHTS.label;
193
+ styles['--font-weight-caption'] = weights?.captionWeight || DEFAULT_WEIGHTS.caption;
194
+ styles['--font-weight-display'] = weights?.displayWeight || DEFAULT_WEIGHTS.display;
195
+
196
+ } else {
197
+ const bodyFont = FONT_FAMILY_MAP[fontFamily || 'sans'] || FONT_FAMILY_MAP.sans;
198
+ const headingFont = getHeadingFont(fontFamily, bodyFont);
199
+
200
+ styles['--font-heading'] = headingFont;
201
+ styles['--font-body'] = bodyFont;
202
+ styles['--font-label'] = bodyFont;
203
+ styles['--font-button'] = bodyFont;
204
+ styles['--font-caption'] = bodyFont;
205
+ styles['--font-display'] = headingFont;
206
+ styles['--font-accent'] = headingFont;
207
+
208
+ styles['--font-weight-body'] = DEFAULT_WEIGHTS.body;
209
+ styles['--font-weight-heading'] = DEFAULT_WEIGHTS.heading;
210
+ styles['--font-weight-button'] = DEFAULT_WEIGHTS.button;
211
+ styles['--font-weight-label'] = DEFAULT_WEIGHTS.label;
212
+ styles['--font-weight-caption'] = DEFAULT_WEIGHTS.caption;
213
+ styles['--font-weight-display'] = DEFAULT_WEIGHTS.display;
214
+ }
215
+
216
+ // =========================================================================
217
+ // ICON STROKE WIDTH
218
+ // =========================================================================
219
+ const strokeWidthMap: Record<string, string> = {
220
+ thin: '1',
221
+ normal: '1.5',
222
+ thick: '2',
223
+ 'extra-thick': '2.5',
224
+ };
225
+ const iconStrokeWidth = oklchConfig.iconStrokeWidth || legacyConfig.iconStrokeWidth;
226
+ styles['--icon-stroke-width'] = strokeWidthMap[iconStrokeWidth || 'normal'];
227
+
228
+ // Allow explicit primaryColor to override the OKLCH-generated value
229
+ if (legacyConfig.primaryColor) {
230
+ styles['--primary'] = legacyConfig.primaryColor;
231
+ styles['--color-primary'] = legacyConfig.primaryColor;
232
+ const hex = legacyConfig.primaryColor.replace('#', '');
233
+ const r = parseInt(hex.substr(0, 2), 16);
234
+ const g = parseInt(hex.substr(2, 2), 16);
235
+ const b = parseInt(hex.substr(4, 2), 16);
236
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
237
+ const primaryForeground = luminance > 0.6 ? '#0a0a0a' : '#ffffff';
238
+ styles['--primary-foreground'] = primaryForeground;
239
+ styles['--color-primary-foreground'] = primaryForeground;
240
+ styles['--accent'] = legacyConfig.primaryColor;
241
+ styles['--color-accent'] = legacyConfig.primaryColor;
242
+ styles['--color-ring'] = legacyConfig.primaryColor;
243
+ }
244
+
245
+ return styles;
246
+ }
247
+
248
+ /**
249
+ * Generate legacy hex-based styles (fallback)
250
+ */
251
+ function getLegacyStyles(config: ThemeConfig): Record<string, string> {
252
+ const styles: Record<string, string> = {};
253
+
254
+ // =========================================================================
255
+ // COLOR VARIABLES
256
+ // =========================================================================
257
+
258
+ const backgroundColor = config.backgroundColor || '#ffffff';
259
+ const surfaceColor = config.isDark ? '#18181b' : '#ffffff';
260
+
261
+ if (backgroundColor) {
262
+ styles['backgroundColor'] = backgroundColor;
263
+ styles['--background'] = backgroundColor;
264
+ styles['--color-background'] = backgroundColor;
265
+ styles['--surface'] = surfaceColor;
266
+ styles['--color-card'] = surfaceColor;
267
+ }
268
+
269
+ // IMPORTANT: Use neutral foreground colors for maximum readability
270
+ const textColor = config.isDark ? '#fafafa' : '#171717';
271
+ styles['color'] = textColor;
272
+ // ALL foreground variables should be the same for consistency
273
+ styles['--foreground'] = textColor;
274
+ styles['--color-foreground'] = textColor;
275
+ styles['--color-heading'] = textColor;
276
+ styles['--color-card-foreground'] = textColor;
277
+ styles['--default-foreground'] = textColor;
278
+ styles['--color-default-foreground'] = textColor;
279
+ styles['--surface-foreground'] = textColor;
280
+ styles['--color-popover-foreground'] = textColor;
281
+ styles['--field-foreground'] = textColor;
282
+
283
+ // Color scheme for proper dark mode styling
284
+ if (config.isDark !== undefined) {
285
+ styles['colorScheme'] = config.isDark ? 'dark' : 'light';
286
+ }
287
+
288
+ if (config.primaryColor) {
289
+ styles['--primary'] = config.primaryColor;
290
+ styles['--color-primary'] = config.primaryColor;
291
+ // Calculate primary foreground based on luminance
292
+ const hex = config.primaryColor.replace('#', '');
293
+ const r = parseInt(hex.substr(0, 2), 16);
294
+ const g = parseInt(hex.substr(2, 2), 16);
295
+ const b = parseInt(hex.substr(4, 2), 16);
296
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
297
+ const primaryForeground = luminance > 0.6 ? '#0a0a0a' : '#ffffff';
298
+ styles['--primary-foreground'] = primaryForeground;
299
+ styles['--color-primary-foreground'] = primaryForeground;
300
+ styles['--accent'] = config.primaryColor;
301
+ styles['--color-accent'] = config.primaryColor;
302
+ styles['--accent-foreground'] = primaryForeground;
303
+ styles['--color-accent-foreground'] = primaryForeground;
304
+ styles['--color-ring'] = config.primaryColor;
305
+ }
306
+
307
+ if (config.secondaryColor) {
308
+ styles['--secondary'] = config.secondaryColor;
309
+ styles['--color-secondary'] = config.secondaryColor;
310
+ }
311
+
312
+ // Icon color
313
+ const iconColor = config.iconColor || config.primaryColor;
314
+ if (iconColor) {
315
+ styles['--color-icon'] = iconColor;
316
+ }
317
+
318
+ // Accent text colors
319
+ if (config.accentTextDark) {
320
+ styles['--color-accent-text-overlay'] = config.accentTextDark;
321
+ } else {
322
+ styles['--color-accent-text-overlay'] = config.isDark ? '#d6d3d1' : '#737373';
323
+ }
324
+ if (config.accentTextLight) {
325
+ styles['--color-accent-text-page'] = config.accentTextLight;
326
+ } else {
327
+ styles['--color-accent-text-page'] = config.isDark ? '#a1a1aa' : '#57534e';
328
+ }
329
+
330
+ // Derived colors
331
+ if (backgroundColor && config.textColor) {
332
+ const mutedBg = `color-mix(in srgb, ${surfaceColor} 97%, ${config.textColor})`;
333
+ styles['--default'] = `color-mix(in srgb, ${surfaceColor} 94%, ${config.textColor})`;
334
+ styles['--color-default'] = styles['--default'];
335
+ styles['--muted'] = mutedBg;
336
+ styles['--color-muted'] = mutedBg;
337
+
338
+ const mutedForeground = `color-mix(in srgb, ${config.textColor} 60%, transparent)`;
339
+ styles['--muted-foreground'] = mutedForeground;
340
+ styles['--color-muted-foreground'] = mutedForeground;
341
+
342
+ const dividerColor = config.isDark
343
+ ? `color-mix(in srgb, ${config.textColor} 30%, transparent)`
344
+ : `color-mix(in srgb, ${config.textColor} 20%, transparent)`;
345
+ styles['--color-border'] = dividerColor;
346
+ styles['--border'] = dividerColor;
347
+ styles['--divider'] = dividerColor;
348
+ styles['--color-divider'] = dividerColor;
349
+
350
+ // Semantic surface layers (replaces legacy content1-4)
351
+ styles['--color-surface-raised'] = config.isDark
352
+ ? `color-mix(in srgb, ${surfaceColor} 85%, ${config.textColor})`
353
+ : `color-mix(in srgb, ${backgroundColor} 97%, ${config.textColor})`;
354
+ }
355
+
356
+ // Form field colors (context-aware)
357
+ styles['--color-field'] = config.isDark ? surfaceColor : '#ffffff';
358
+ styles['--field-background'] = config.isDark ? surfaceColor : '#ffffff';
359
+ styles['--color-field-on-surface'] = config.isDark
360
+ ? `color-mix(in srgb, ${surfaceColor} 80%, ${config.textColor})`
361
+ : `color-mix(in srgb, ${backgroundColor} 96%, ${config.textColor})`;
362
+ styles['--color-popover'] = surfaceColor;
363
+ styles['--overlay'] = surfaceColor;
364
+
365
+ // =========================================================================
366
+ // PRIMITIVE RADIUS TOKENS
367
+ // =========================================================================
368
+ styles['--radius-sm'] = RADIUS_VALUES.sm;
369
+ styles['--radius-md'] = RADIUS_VALUES.md;
370
+ styles['--radius-lg'] = RADIUS_VALUES.lg;
371
+ styles['--radius-xl'] = RADIUS_VALUES.xl;
372
+ styles['--radius-2xl'] = RADIUS_VALUES['2xl'];
373
+ styles['--radius-3xl'] = RADIUS_VALUES['3xl'];
374
+ styles['--radius-4xl'] = RADIUS_VALUES['4xl'];
375
+ styles['--radius-full'] = RADIUS_VALUES.full;
376
+
377
+ // =========================================================================
378
+ // SEMANTIC RADIUS
379
+ // =========================================================================
380
+ let radiusConfig: Required<RadiusConfig>;
381
+ if (config.radiusPreset) {
382
+ radiusConfig = resolveRadiusConfig(config.radiusPreset);
383
+ } else if (config.borderRadius) {
384
+ const presetId = legacyBorderRadiusToPreset(config.borderRadius);
385
+ radiusConfig = RADIUS_PRESETS[presetId];
386
+ } else {
387
+ radiusConfig = RADIUS_PRESETS.default;
388
+ }
389
+
390
+ styles['--radius-button'] = RADIUS_VALUES[radiusConfig.button];
391
+ styles['--radius-input'] = RADIUS_VALUES[radiusConfig.input];
392
+ styles['--radius-card'] = RADIUS_VALUES[radiusConfig.card];
393
+ styles['--radius-modal'] = RADIUS_VALUES[radiusConfig.modal];
394
+ styles['--radius-badge'] = RADIUS_VALUES[radiusConfig.badge];
395
+ styles['--radius-avatar'] = RADIUS_VALUES[radiusConfig.avatar];
396
+ styles['--radius-tooltip'] = RADIUS_VALUES[radiusConfig.tooltip];
397
+ styles['--radius-image'] = RADIUS_VALUES[radiusConfig.image];
398
+
399
+ // =========================================================================
400
+ // FONTS
401
+ // =========================================================================
402
+ const bodyFont = FONT_FAMILY_MAP[config.fontFamily || 'sans'] || FONT_FAMILY_MAP.sans;
403
+ const headingFont = config.fontPairing?.headingFont || getHeadingFont(config.fontFamily, bodyFont);
404
+
405
+ // Core fonts
406
+ styles['--font-heading'] = headingFont;
407
+ styles['--font-body'] = config.fontPairing?.bodyFont || bodyFont;
408
+
409
+ // Semantic font roles
410
+ styles['--font-label'] = config.fontPairing?.labelFont || bodyFont;
411
+ styles['--font-button'] = config.fontPairing?.buttonFont || bodyFont;
412
+ styles['--font-caption'] = config.fontPairing?.captionFont || bodyFont;
413
+ styles['--font-display'] = config.fontPairing?.displayFont || headingFont;
414
+ styles['--font-accent'] = config.fontPairing?.accentFont || headingFont;
415
+
416
+ // Font weights
417
+ const weights = config.fontPairing?.weights;
418
+ styles['--font-weight-body'] = weights?.bodyWeight || DEFAULT_WEIGHTS.body;
419
+ styles['--font-weight-heading'] = weights?.headingWeight || DEFAULT_WEIGHTS.heading;
420
+ styles['--font-weight-button'] = weights?.buttonWeight || DEFAULT_WEIGHTS.button;
421
+ styles['--font-weight-label'] = weights?.labelWeight || DEFAULT_WEIGHTS.label;
422
+ styles['--font-weight-caption'] = weights?.captionWeight || DEFAULT_WEIGHTS.caption;
423
+ styles['--font-weight-display'] = weights?.displayWeight || DEFAULT_WEIGHTS.display;
424
+
425
+ // =========================================================================
426
+ // ICON STROKE WIDTH
427
+ // =========================================================================
428
+ const strokeWidthMap: Record<string, string> = {
429
+ thin: '1',
430
+ normal: '1.5',
431
+ thick: '2',
432
+ 'extra-thick': '2.5',
433
+ };
434
+ styles['--icon-stroke-width'] = strokeWidthMap[config.iconStrokeWidth || 'normal'];
435
+
436
+ return styles;
437
+ }
438
+
439
+ /**
440
+ * Convert theme styles to a CSS string
441
+ *
442
+ * Useful for injecting styles into a <style> tag or style attribute.
443
+ *
444
+ * @param config - Theme configuration
445
+ * @param selector - CSS selector to scope the styles (default: ':root')
446
+ * @returns CSS string
447
+ */
448
+ export function getThemeStylesCSS(config: ThemeConfig, selector = ':root'): string {
449
+ const styles = getThemeStyles(config);
450
+ const rules = Object.entries(styles)
451
+ .map(([key, value]) => ` ${key}: ${value};`)
452
+ .join('\n');
453
+ return `${selector} {\n${rules}\n}`;
454
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Theme System
3
+ *
4
+ * TWO APPROACHES — pick one:
5
+ *
6
+ * 1. CSS-ONLY (recommended for new apps):
7
+ * Import defaults.css, override variables in your CSS. No JS needed.
8
+ * See CUSTOMIZATION.md.
9
+ *
10
+ * @import "@snowcone-app/ui/styles/defaults.css";
11
+ * @import "@snowcone-app/ui/styles/base.css";
12
+ * :root { --color-primary: #4f46e5; }
13
+ *
14
+ * 2. RUNTIME (for Storybook, theme picker UIs, dynamic switching):
15
+ * Use ThemeProvider + applyTheme() + ne-themes.css.
16
+ * This is used by Storybook configs and the canvas package.
17
+ */
18
+
19
+ // Types
20
+ export type {
21
+ ThemeConfig,
22
+ ThemePreset,
23
+ FontPairing,
24
+ FontWeights,
25
+ RadiusSize,
26
+ RadiusPresetId,
27
+ RadiusConfig,
28
+ } from './types';
29
+ export { RADIUS_PRESETS } from './types';
30
+
31
+ // Presets & lookup
32
+ export {
33
+ presetThemes,
34
+ baseThemes,
35
+ getThemeVariant,
36
+ findTheme,
37
+ findThemeById,
38
+ getBaseThemeName,
39
+ } from './presets';
40
+
41
+ // Runtime theme application
42
+ export { applyTheme, clearTheme, toThemeId, getCurrentThemeId } from './apply-theme';
43
+
44
+ // React context for runtime theme state
45
+ export { ThemeProvider, useTheme, useThemeStandalone, type ThemeContextValue, type ThemeProviderProps } from './useTheme';
46
+
47
+ // Style generation (for SSR inline styles)
48
+ export { getThemeStyles, getThemeStylesCSS } from './getThemeStyles';