@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.
- package/CHANGELOG.md +33 -0
- package/README.md +18 -4
- package/package.json +9 -5
- package/src/components/CanvasIsolationBoundary.tsx +202 -0
- package/src/components/LoadingOverlayPrism.tsx +251 -0
- package/src/composed/AddToCart.tsx +229 -0
- package/src/composed/ArtAlignment.tsx +703 -0
- package/src/composed/ArtSelector.tsx +290 -0
- package/src/composed/ArtworkCustomizer.tsx +212 -0
- package/src/composed/CanvasEditor.tsx +79 -0
- package/src/composed/ColorPicker.tsx +111 -0
- package/src/composed/CurrentSelectionDisplay.tsx +86 -0
- package/src/composed/HeroProductImage.tsx +1071 -0
- package/src/composed/Lightbox.index.ts +2 -0
- package/src/composed/Lightbox.tsx +230 -0
- package/src/composed/PlacementClipShapeSelector.tsx +88 -0
- package/src/composed/PlacementTabs.tsx +179 -0
- package/src/composed/ProductCard.tsx +298 -0
- package/src/composed/ProductGallery.tsx +54 -0
- package/src/composed/ProductImage.tsx +129 -0
- package/src/composed/ProductList.tsx +147 -0
- package/src/composed/ProductOptions.tsx +305 -0
- package/src/composed/RealtimeMockup.tsx +121 -0
- package/src/composed/TileCount.tsx +348 -0
- package/src/composed/carousels/HeroCarousel.tsx +240 -0
- package/src/composed/carousels/MobileProductCarousel.tsx +1002 -0
- package/src/composed/carousels/index.ts +11 -0
- package/src/composed/carousels/types.ts +58 -0
- package/src/composed/grids/MasonryGrid.tsx +238 -0
- package/src/composed/grids/index.ts +9 -0
- package/src/composed/search/CurrentRefinements.tsx +80 -0
- package/src/composed/search/Filters.tsx +49 -0
- package/src/composed/search/FiltersButton.tsx +57 -0
- package/src/composed/search/FiltersDrawer.tsx +375 -0
- package/src/composed/search/ProductGrid.tsx +118 -0
- package/src/composed/search/ProductHit.tsx +56 -0
- package/src/composed/search/SearchBox.tsx +109 -0
- package/src/composed/search/SearchProvider.tsx +136 -0
- package/src/composed/search/facetConfig.ts +16 -0
- package/src/composed/search/index.ts +22 -0
- package/src/composed/search/meilisearchAdapter.ts +20 -0
- package/src/composed/search/types.ts +22 -0
- package/src/composed/zoom/EnhancedImageViewer.tsx +505 -0
- package/src/composed/zoom/ResponsiveZoom.tsx +134 -0
- package/src/composed/zoom/ZoomOverlay.tsx +194 -0
- package/src/composed/zoom/index.ts +12 -0
- package/src/composed/zoom/types.ts +12 -0
- package/src/design-system/ColorPalette.tsx +126 -0
- package/src/design-system/ColorSwatch.tsx +49 -0
- package/src/design-system/DesignSystemPage.tsx +130 -0
- package/src/design-system/ThemeSwitcher.tsx +181 -0
- package/src/design-system/TypographyScale.tsx +106 -0
- package/src/design-system/index.ts +5 -0
- package/src/ecommerce/stories/HeroProductImage.stories.tsx +66 -0
- package/src/ecommerce/stories/PDPHeroGallery.stories.tsx +105 -0
- package/src/ecommerce/stories/PDPInfoPanel.stories.tsx +472 -0
- package/src/ecommerce/stories/PDPLayout.stories.tsx +365 -0
- package/src/hooks/useBrand.ts +41 -0
- package/src/hooks/useCanvasContext.ts +127 -0
- package/src/hooks/useDeviceDetection.ts +64 -0
- package/src/hooks/useFocusTrap.ts +70 -0
- package/src/hooks/useImagePreloader.ts +268 -0
- package/src/hooks/useImageTransition.ts +608 -0
- package/src/hooks/usePlacementsProcessor.ts +74 -0
- package/src/hooks/useProductGallery.ts +193 -0
- package/src/hooks/useProductPage.ts +467 -0
- package/src/hooks/useRenderGuard.ts +96 -0
- package/src/hooks/useScrollDirection.ts +196 -0
- package/src/hooks/viewport/index.ts +25 -0
- package/src/hooks/viewport/useContainerWidth.ts +59 -0
- package/src/hooks/viewport/useMediaQuery.ts +52 -0
- package/src/hooks/viewport/useResponsiveImageCap.ts +149 -0
- package/src/hooks/viewport/useViewportDimensions.ts +135 -0
- package/src/hooks/viewport/useWideMonitorMode.ts +150 -0
- package/src/hooks/visibility/index.ts +15 -0
- package/src/hooks/visibility/observerPool.ts +150 -0
- package/src/index.ts +240 -0
- package/src/layouts/hero-zoom/HeroShrinkLayout.tsx +209 -0
- package/src/layouts/hero-zoom/HeroZoomLayout.tsx +351 -0
- package/src/layouts/hero-zoom/index.ts +30 -0
- package/src/layouts/hero-zoom/stories/HeroZoomLayout.stories.tsx +350 -0
- package/src/layouts/hero-zoom/types.ts +113 -0
- package/src/layouts/hero-zoom/useHeroZoomScales.ts +156 -0
- package/src/layouts/index.ts +9 -0
- package/src/layouts/pdp/EdgeBlurBox.tsx +210 -0
- package/src/layouts/pdp/ImageBlurExtension.tsx +215 -0
- package/src/layouts/pdp/ImageEdgeBlur.tsx +215 -0
- package/src/layouts/pdp/PDPLayout.tsx +246 -0
- package/src/layouts/pdp/SimpleImageBlur.tsx +140 -0
- package/src/layouts/pdp/index.ts +40 -0
- package/src/lib/env.ts +15 -0
- package/src/lib/locale.ts +167 -0
- package/src/lib/router.tsx +46 -0
- package/src/lib/utils.ts +6 -0
- package/src/lightbox/README.md +77 -0
- package/src/next/index.tsx +26 -0
- package/src/patterns/MockupPriorityProvider.tsx +1014 -0
- package/src/patterns/Product.tsx +850 -0
- package/src/patterns/ProductPageProvider.tsx +224 -0
- package/src/patterns/RealtimeProvider.tsx +1162 -0
- package/src/patterns/ShopProvider.tsx +603 -0
- package/src/personalization/PersonalizationBridge.tsx +235 -0
- package/src/personalization/PersonalizationContext.ts +29 -0
- package/src/personalization/PersonalizationInputs.tsx +110 -0
- package/src/personalization/PersonalizationProvider.tsx +407 -0
- package/src/personalization/canvas-stub.d.ts +22 -0
- package/src/personalization/index.ts +43 -0
- package/src/personalization/types.ts +48 -0
- package/src/personalization/usePersonalization.ts +32 -0
- package/src/personalization/usePersonalizationShimmer.ts +159 -0
- package/src/personalization/utils.ts +59 -0
- package/src/primitives/BrandLogo.tsx +65 -0
- package/src/primitives/BrandName.tsx +51 -0
- package/src/primitives/Button.tsx +123 -0
- package/src/primitives/ColorSwatch.tsx +221 -0
- package/src/primitives/DragHintAnimation.tsx +190 -0
- package/src/primitives/EdgeSwipeGuards.tsx +60 -0
- package/src/primitives/FloatingActionGroup.tsx +176 -0
- package/src/primitives/ProductPrice.tsx +171 -0
- package/src/primitives/ProgressiveBlur.tsx +295 -0
- package/src/primitives/ThemeToggle.tsx +125 -0
- package/src/primitives/__tests__/story-coverage.test.ts +98 -0
- package/src/primitives/accordion.tsx +280 -0
- package/src/primitives/badge.tsx +137 -0
- package/src/primitives/card.tsx +61 -0
- package/src/primitives/checkbox.tsx +56 -0
- package/src/primitives/collapsible.tsx +51 -0
- package/src/primitives/drawer.tsx +828 -0
- package/src/primitives/dropdown-menu.tsx +197 -0
- package/src/primitives/fieldset.tsx +73 -0
- package/src/primitives/index.ts +138 -0
- package/src/primitives/input.tsx +91 -0
- package/src/primitives/kbd.tsx +130 -0
- package/src/primitives/label.tsx +20 -0
- package/src/primitives/link.tsx +182 -0
- package/src/primitives/popover.tsx +80 -0
- package/src/primitives/radio-group.tsx +79 -0
- package/src/primitives/scroll-fade.tsx +159 -0
- package/src/primitives/select.tsx +170 -0
- package/src/primitives/separator.tsx +25 -0
- package/src/primitives/slider.tsx +221 -0
- package/src/primitives/spinner.tsx +72 -0
- package/src/primitives/stories/Accordion.stories.tsx +121 -0
- package/src/primitives/stories/Badge.stories.tsx +221 -0
- package/src/primitives/stories/Button.stories.tsx +185 -0
- package/src/primitives/stories/Card.stories.tsx +171 -0
- package/src/primitives/stories/Checkbox.stories.tsx +214 -0
- package/src/primitives/stories/Collapsible.stories.tsx +230 -0
- package/src/primitives/stories/Drawer.stories.tsx +378 -0
- package/src/primitives/stories/DropdownMenu.stories.tsx +182 -0
- package/src/primitives/stories/Fieldset.stories.tsx +212 -0
- package/src/primitives/stories/Input.stories.tsx +172 -0
- package/src/primitives/stories/Kbd.stories.tsx +183 -0
- package/src/primitives/stories/Label.stories.tsx +98 -0
- package/src/primitives/stories/Link.stories.tsx +260 -0
- package/src/primitives/stories/Popover.stories.tsx +178 -0
- package/src/primitives/stories/RadioGroup.stories.tsx +205 -0
- package/src/primitives/stories/Select.stories.tsx +222 -0
- package/src/primitives/stories/Separator.stories.tsx +134 -0
- package/src/primitives/stories/Slider.stories.tsx +203 -0
- package/src/primitives/stories/Spinner.stories.tsx +142 -0
- package/src/primitives/stories/Surface.stories.tsx +257 -0
- package/src/primitives/stories/Switch.stories.tsx +131 -0
- package/src/primitives/stories/Tabs.stories.tsx +275 -0
- package/src/primitives/stories/TextField.stories.tsx +139 -0
- package/src/primitives/stories/Textarea.stories.tsx +148 -0
- package/src/primitives/stories/Tooltip.stories.tsx +119 -0
- package/src/primitives/surface.tsx +86 -0
- package/src/primitives/switch.tsx +35 -0
- package/src/primitives/tabs.tsx +206 -0
- package/src/primitives/text-field.tsx +84 -0
- package/src/primitives/textarea.tsx +50 -0
- package/src/primitives/tooltip.tsx +58 -0
- package/src/services/CanvasExportService.ts +518 -0
- package/src/styles/base.css +380 -0
- package/src/styles/defaults.css +280 -0
- package/src/styles/globals.css +1242 -0
- package/src/styles/index.css +17 -0
- package/src/styles/ne-themes.css +4740 -0
- package/src/styles/tailwind.css +11 -0
- package/src/styles/tokens.css +117 -0
- package/src/styles/utilities.css +188 -0
- package/src/themes/apply-theme.ts +449 -0
- package/src/themes/getThemeStyles.ts +454 -0
- package/src/themes/index.ts +48 -0
- package/src/themes/oklch-theme.ts +283 -0
- package/src/themes/presets.ts +989 -0
- package/src/themes/types.ts +386 -0
- package/src/themes/useTheme.tsx +450 -0
- package/src/utils/dev-warnings.ts +161 -0
- package/src/utils/devWarnings.ts +153 -0
- 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';
|