@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.
- package/CHANGELOG.md +26 -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,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply Theme
|
|
3
|
+
*
|
|
4
|
+
* This is the single source of truth for theme application logic.
|
|
5
|
+
* All apps (next-ecommerce, docs, canvas) use these same functions.
|
|
6
|
+
*
|
|
7
|
+
* COLOR STRATEGY (Build-Time OKLCH):
|
|
8
|
+
* ----------------------------------
|
|
9
|
+
* All color variables are defined in ne-themes.css using OKLCH color space.
|
|
10
|
+
* This function ONLY sets the data-theme attribute to activate those CSS rules.
|
|
11
|
+
* No runtime color calculation is needed!
|
|
12
|
+
*
|
|
13
|
+
* RUNTIME VARIABLES (Fonts, Radius, Icons):
|
|
14
|
+
* -----------------------------------------
|
|
15
|
+
* These are set at runtime because CSS selector-based approach doesn't reliably
|
|
16
|
+
* override Tailwind's utilities. The runtime applies:
|
|
17
|
+
* - Font family and weight variables
|
|
18
|
+
* - Semantic radius variables
|
|
19
|
+
* - Icon stroke width
|
|
20
|
+
*
|
|
21
|
+
* ## Semantic Font Roles
|
|
22
|
+
*
|
|
23
|
+
* The theme system supports granular font customization through semantic roles:
|
|
24
|
+
* - **heading**: h1-h6, titles
|
|
25
|
+
* - **body**: paragraphs, default text
|
|
26
|
+
* - **label**: form labels, field names
|
|
27
|
+
* - **button**: button text, CTAs
|
|
28
|
+
* - **caption**: small text, descriptions
|
|
29
|
+
* - **display**: hero text, prices
|
|
30
|
+
* - **accent**: emphasis, blockquotes
|
|
31
|
+
*
|
|
32
|
+
* ## Semantic Radius Roles
|
|
33
|
+
*
|
|
34
|
+
* The theme system supports granular border-radius customization:
|
|
35
|
+
* - **button**: buttons, chips, toggles
|
|
36
|
+
* - **input**: text inputs, selects, textareas
|
|
37
|
+
* - **card**: cards, panels, surfaces
|
|
38
|
+
* - **modal**: modals, dialogs, sheets
|
|
39
|
+
* - **badge**: badges, tags, indicators
|
|
40
|
+
* - **avatar**: avatars, profile images
|
|
41
|
+
* - **tooltip**: tooltips, popovers, dropdowns
|
|
42
|
+
* - **image**: product images, thumbnails
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import type { ThemeConfig, FontPairing, RadiusConfig, RadiusPresetId, RadiusSize } from './types';
|
|
46
|
+
import { RADIUS_PRESETS } from './types';
|
|
47
|
+
import { findOklchTheme } from './presets';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a safe CSS identifier from theme name
|
|
51
|
+
* Must match the pattern used in scripts/generate-theme-css.ts
|
|
52
|
+
*/
|
|
53
|
+
export function toThemeId(name: string): string {
|
|
54
|
+
return `ne-${name.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get the current theme ID from the document
|
|
59
|
+
*/
|
|
60
|
+
export function getCurrentThemeId(): string | null {
|
|
61
|
+
if (typeof document === 'undefined') return null;
|
|
62
|
+
return document.documentElement.getAttribute('data-theme');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Font family presets mapping
|
|
67
|
+
* Inter is loaded from rsms.me in layout.tsx for docs site
|
|
68
|
+
*/
|
|
69
|
+
const fontFamilyMap: Record<string, string> = {
|
|
70
|
+
sans: '"Inter", "InterVariable", ui-sans-serif, system-ui, sans-serif',
|
|
71
|
+
serif: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
|
|
72
|
+
mono: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
|
|
73
|
+
display: '"Inter", "InterVariable", ui-sans-serif, system-ui, sans-serif',
|
|
74
|
+
mixed: '"Inter", "InterVariable", ui-sans-serif, system-ui, sans-serif',
|
|
75
|
+
'montserrat-combo': '"Inter", "InterVariable", ui-sans-serif, system-ui, sans-serif',
|
|
76
|
+
'playfair-combo': '"Inter", "InterVariable", ui-sans-serif, system-ui, sans-serif',
|
|
77
|
+
'helvetica-combo':
|
|
78
|
+
'"Inter", "InterVariable", "Helvetica Neue", Helvetica, Arial, ui-sans-serif, system-ui, sans-serif',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get heading font for a font family preset
|
|
83
|
+
*/
|
|
84
|
+
function getHeadingFont(fontFamily: string | undefined, bodyFont: string): string {
|
|
85
|
+
switch (fontFamily) {
|
|
86
|
+
case 'mixed':
|
|
87
|
+
case 'playfair-combo':
|
|
88
|
+
return 'ui-serif, Georgia, Cambria, serif';
|
|
89
|
+
case 'serif':
|
|
90
|
+
return 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif';
|
|
91
|
+
default:
|
|
92
|
+
return bodyFont;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Default font weights
|
|
98
|
+
*/
|
|
99
|
+
const DEFAULT_WEIGHTS = {
|
|
100
|
+
body: '400',
|
|
101
|
+
heading: '600',
|
|
102
|
+
button: '500',
|
|
103
|
+
label: '500',
|
|
104
|
+
caption: '400',
|
|
105
|
+
display: '700',
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Apply semantic font variables to the document root
|
|
110
|
+
*/
|
|
111
|
+
function applySemanticFonts(root: HTMLElement, fontPairing: FontPairing): void {
|
|
112
|
+
const { headingFont, bodyFont, weights } = fontPairing;
|
|
113
|
+
|
|
114
|
+
// Core fonts (required)
|
|
115
|
+
root.style.setProperty('--font-heading', headingFont);
|
|
116
|
+
root.style.setProperty('--font-body', bodyFont);
|
|
117
|
+
|
|
118
|
+
// Semantic font roles - use explicit override or cascade from core fonts
|
|
119
|
+
root.style.setProperty('--font-label', fontPairing.labelFont || bodyFont);
|
|
120
|
+
root.style.setProperty('--font-button', fontPairing.buttonFont || bodyFont);
|
|
121
|
+
root.style.setProperty('--font-caption', fontPairing.captionFont || bodyFont);
|
|
122
|
+
root.style.setProperty('--font-display', fontPairing.displayFont || headingFont);
|
|
123
|
+
root.style.setProperty('--font-accent', fontPairing.accentFont || headingFont);
|
|
124
|
+
|
|
125
|
+
// Font weights
|
|
126
|
+
root.style.setProperty('--font-weight-body', weights?.bodyWeight || DEFAULT_WEIGHTS.body);
|
|
127
|
+
root.style.setProperty('--font-weight-heading', weights?.headingWeight || DEFAULT_WEIGHTS.heading);
|
|
128
|
+
root.style.setProperty('--font-weight-button', weights?.buttonWeight || DEFAULT_WEIGHTS.button);
|
|
129
|
+
root.style.setProperty('--font-weight-label', weights?.labelWeight || DEFAULT_WEIGHTS.label);
|
|
130
|
+
root.style.setProperty('--font-weight-caption', weights?.captionWeight || DEFAULT_WEIGHTS.caption);
|
|
131
|
+
root.style.setProperty('--font-weight-display', weights?.displayWeight || DEFAULT_WEIGHTS.display);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Apply default fonts derived from fontFamily preset
|
|
136
|
+
*/
|
|
137
|
+
function applyDefaultFonts(root: HTMLElement, bodyFont: string, headingFont: string): void {
|
|
138
|
+
root.style.setProperty('--font-heading', headingFont);
|
|
139
|
+
root.style.setProperty('--font-body', bodyFont);
|
|
140
|
+
root.style.setProperty('--font-label', bodyFont);
|
|
141
|
+
root.style.setProperty('--font-button', bodyFont);
|
|
142
|
+
root.style.setProperty('--font-caption', bodyFont);
|
|
143
|
+
root.style.setProperty('--font-display', headingFont);
|
|
144
|
+
root.style.setProperty('--font-accent', headingFont);
|
|
145
|
+
root.style.setProperty('--font-weight-body', DEFAULT_WEIGHTS.body);
|
|
146
|
+
root.style.setProperty('--font-weight-heading', DEFAULT_WEIGHTS.heading);
|
|
147
|
+
root.style.setProperty('--font-weight-button', DEFAULT_WEIGHTS.button);
|
|
148
|
+
root.style.setProperty('--font-weight-label', DEFAULT_WEIGHTS.label);
|
|
149
|
+
root.style.setProperty('--font-weight-caption', DEFAULT_WEIGHTS.caption);
|
|
150
|
+
root.style.setProperty('--font-weight-display', DEFAULT_WEIGHTS.display);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* =============================================================================
|
|
154
|
+
* SEMANTIC RADIUS SYSTEM
|
|
155
|
+
* =============================================================================
|
|
156
|
+
*/
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Map RadiusSize to CSS value
|
|
160
|
+
*/
|
|
161
|
+
function radiusSizeToCss(size: RadiusSize): string {
|
|
162
|
+
switch (size) {
|
|
163
|
+
case 'none':
|
|
164
|
+
return '0';
|
|
165
|
+
case 'sm':
|
|
166
|
+
return 'var(--radius-sm)';
|
|
167
|
+
case 'md':
|
|
168
|
+
return 'var(--radius-md)';
|
|
169
|
+
case 'lg':
|
|
170
|
+
return 'var(--radius-lg)';
|
|
171
|
+
case 'xl':
|
|
172
|
+
return 'var(--radius-xl)';
|
|
173
|
+
case '2xl':
|
|
174
|
+
return 'var(--radius-2xl)';
|
|
175
|
+
case '3xl':
|
|
176
|
+
return 'var(--radius-3xl)';
|
|
177
|
+
case '4xl':
|
|
178
|
+
return 'var(--radius-4xl)';
|
|
179
|
+
case 'full':
|
|
180
|
+
return 'var(--radius-full)';
|
|
181
|
+
default:
|
|
182
|
+
return 'var(--radius-md)';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Resolve radius preset - handles both preset IDs and custom configs
|
|
188
|
+
*/
|
|
189
|
+
function resolveRadiusConfig(preset: RadiusPresetId | RadiusConfig | undefined): Required<RadiusConfig> {
|
|
190
|
+
if (!preset) {
|
|
191
|
+
return RADIUS_PRESETS.default;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (typeof preset === 'string') {
|
|
195
|
+
return RADIUS_PRESETS[preset] || RADIUS_PRESETS.default;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
...RADIUS_PRESETS.default,
|
|
200
|
+
...preset,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Map legacy borderRadius to a radius preset
|
|
206
|
+
*/
|
|
207
|
+
function legacyBorderRadiusToPreset(borderRadius: string | undefined): RadiusPresetId {
|
|
208
|
+
switch (borderRadius) {
|
|
209
|
+
case 'none':
|
|
210
|
+
return 'brutalist';
|
|
211
|
+
case 'sm':
|
|
212
|
+
return 'sharp';
|
|
213
|
+
case 'full':
|
|
214
|
+
return 'soft';
|
|
215
|
+
case 'xl':
|
|
216
|
+
return 'playful';
|
|
217
|
+
case 'md':
|
|
218
|
+
case 'lg':
|
|
219
|
+
default:
|
|
220
|
+
return 'default';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Apply semantic radius variables to the document root
|
|
226
|
+
*/
|
|
227
|
+
function applySemanticRadius(root: HTMLElement, config: Required<RadiusConfig>): void {
|
|
228
|
+
root.style.setProperty('--radius-button', radiusSizeToCss(config.button));
|
|
229
|
+
root.style.setProperty('--radius-input', radiusSizeToCss(config.input));
|
|
230
|
+
root.style.setProperty('--radius-card', radiusSizeToCss(config.card));
|
|
231
|
+
root.style.setProperty('--radius-modal', radiusSizeToCss(config.modal));
|
|
232
|
+
root.style.setProperty('--radius-badge', radiusSizeToCss(config.badge));
|
|
233
|
+
root.style.setProperty('--radius-avatar', radiusSizeToCss(config.avatar));
|
|
234
|
+
root.style.setProperty('--radius-tooltip', radiusSizeToCss(config.tooltip));
|
|
235
|
+
root.style.setProperty('--radius-image', radiusSizeToCss(config.image));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Apply icon stroke width variable
|
|
240
|
+
*/
|
|
241
|
+
function applyIconStrokeWidth(root: HTMLElement, strokeWidth: string | undefined): void {
|
|
242
|
+
const strokeWidthMap: Record<string, string> = {
|
|
243
|
+
thin: '1',
|
|
244
|
+
normal: '1.5',
|
|
245
|
+
thick: '2',
|
|
246
|
+
'extra-thick': '2.5',
|
|
247
|
+
};
|
|
248
|
+
root.style.setProperty('--icon-stroke-width', strokeWidthMap[strokeWidth || 'normal']);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Clear inline color styles that were set during SSR
|
|
253
|
+
*
|
|
254
|
+
* These inline styles are set by getThemeInlineStyles() to prevent FOIT,
|
|
255
|
+
* but they need to be cleared when switching themes so the CSS selector-based
|
|
256
|
+
* styles from ne-themes.css can take effect.
|
|
257
|
+
*/
|
|
258
|
+
function clearInlineColorStyles(root: HTMLElement): void {
|
|
259
|
+
const colorVars = [
|
|
260
|
+
// Core semantic colors
|
|
261
|
+
'--background',
|
|
262
|
+
'--foreground',
|
|
263
|
+
'--primary',
|
|
264
|
+
'--primary-foreground',
|
|
265
|
+
'--secondary',
|
|
266
|
+
'--secondary-foreground',
|
|
267
|
+
'--accent',
|
|
268
|
+
'--accent-foreground',
|
|
269
|
+
'--muted',
|
|
270
|
+
'--muted-foreground',
|
|
271
|
+
'--border',
|
|
272
|
+
'--surface',
|
|
273
|
+
// Prefixed versions
|
|
274
|
+
'--color-background',
|
|
275
|
+
'--color-foreground',
|
|
276
|
+
'--color-primary',
|
|
277
|
+
'--color-primary-foreground',
|
|
278
|
+
'--color-secondary',
|
|
279
|
+
'--color-secondary-foreground',
|
|
280
|
+
'--color-accent',
|
|
281
|
+
'--color-accent-foreground',
|
|
282
|
+
'--color-muted',
|
|
283
|
+
'--color-muted-foreground',
|
|
284
|
+
'--color-border',
|
|
285
|
+
'--color-card',
|
|
286
|
+
'--color-card-foreground',
|
|
287
|
+
'--color-popover',
|
|
288
|
+
'--color-popover-foreground',
|
|
289
|
+
'--color-surface',
|
|
290
|
+
'--color-surface-raised',
|
|
291
|
+
'--color-ring',
|
|
292
|
+
'--color-field',
|
|
293
|
+
'--color-field-on-surface',
|
|
294
|
+
'--color-divider',
|
|
295
|
+
'--color-heading',
|
|
296
|
+
'--color-icon',
|
|
297
|
+
'--color-accent-text-overlay',
|
|
298
|
+
'--color-accent-text-page',
|
|
299
|
+
// Code block colors
|
|
300
|
+
'--color-code-bg',
|
|
301
|
+
'--color-code-bg-solid',
|
|
302
|
+
'--color-code-fg',
|
|
303
|
+
// Legacy aliases
|
|
304
|
+
'--divider',
|
|
305
|
+
'--focus',
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
for (const v of colorVars) {
|
|
309
|
+
root.style.removeProperty(v);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Apply a theme to the document
|
|
315
|
+
*
|
|
316
|
+
* Sets data-theme attribute to activate CSS color variables from ne-themes.css.
|
|
317
|
+
* Then applies runtime-only variables: fonts, radius, icon stroke width.
|
|
318
|
+
*
|
|
319
|
+
* NOTE: Colors are NOT set at runtime - they come from the CSS.
|
|
320
|
+
* This eliminates hydration mismatches and runtime overhead.
|
|
321
|
+
*/
|
|
322
|
+
export function applyTheme(config: ThemeConfig): void {
|
|
323
|
+
const root = document.documentElement;
|
|
324
|
+
const themeId = toThemeId(config.name || 'minimal');
|
|
325
|
+
|
|
326
|
+
// Clear any inline color styles that were set during SSR
|
|
327
|
+
// This allows the CSS selector-based styles from ne-themes.css to take effect
|
|
328
|
+
clearInlineColorStyles(root);
|
|
329
|
+
|
|
330
|
+
// Set the data-theme attribute - this activates colors from ne-themes.css
|
|
331
|
+
root.setAttribute('data-theme', themeId);
|
|
332
|
+
|
|
333
|
+
// Find the OKLCH config for additional theme properties
|
|
334
|
+
const oklchConfig = config.name ? findOklchTheme(config.name) : undefined;
|
|
335
|
+
|
|
336
|
+
// Determine dark mode: explicit config takes precedence, then OKLCH config
|
|
337
|
+
const isDark = config.isDark ?? oklchConfig?.isDark ?? false;
|
|
338
|
+
|
|
339
|
+
// Toggle dark mode class and color scheme
|
|
340
|
+
// This activates the correct CSS selectors in ne-themes.css:
|
|
341
|
+
// - Light themes: [data-theme='ne-xxx']
|
|
342
|
+
// - Dark themes: .dark[data-theme='ne-xxx'], [data-theme='ne-xxx'] .dark
|
|
343
|
+
if (isDark) {
|
|
344
|
+
root.classList.add('dark');
|
|
345
|
+
root.classList.remove('light');
|
|
346
|
+
root.style.colorScheme = 'dark';
|
|
347
|
+
} else {
|
|
348
|
+
root.classList.remove('dark');
|
|
349
|
+
root.classList.add('light');
|
|
350
|
+
root.style.colorScheme = 'light';
|
|
351
|
+
}
|
|
352
|
+
// Clean up any stale .dark class on body from previous code
|
|
353
|
+
document.body.classList.remove('dark');
|
|
354
|
+
|
|
355
|
+
// Set code block background colors based on dark/light mode
|
|
356
|
+
// These must be set at runtime because they're not in ne-themes.css
|
|
357
|
+
const codeBg = isDark ? 'rgba(255, 255, 255, 0.13)' : 'rgba(0, 0, 0, 0.03)';
|
|
358
|
+
const codeBgSolid = isDark ? 'rgb(26, 26, 26)' : 'rgb(255, 255, 255)';
|
|
359
|
+
const codeFg = isDark ? 'rgba(255, 255, 255, 0.9)' : 'rgba(0, 0, 0, 0.8)';
|
|
360
|
+
const codeBorder = isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.08)';
|
|
361
|
+
root.style.setProperty('--color-code-bg', codeBg);
|
|
362
|
+
root.style.setProperty('--color-code-bg-solid', codeBgSolid);
|
|
363
|
+
root.style.setProperty('--color-code-fg', codeFg);
|
|
364
|
+
root.style.setProperty('--color-code-border', codeBorder);
|
|
365
|
+
|
|
366
|
+
// Apply font variables (runtime - CSS selectors don't work well with Tailwind)
|
|
367
|
+
const fontPairing = oklchConfig?.fontPairing || config.fontPairing;
|
|
368
|
+
if (fontPairing) {
|
|
369
|
+
applySemanticFonts(root, fontPairing);
|
|
370
|
+
} else {
|
|
371
|
+
const fontFamily = oklchConfig?.fontFamily || config.fontFamily;
|
|
372
|
+
const bodyFont = fontFamilyMap[fontFamily || 'sans'] || fontFamilyMap.sans;
|
|
373
|
+
const headingFont = getHeadingFont(fontFamily, bodyFont);
|
|
374
|
+
applyDefaultFonts(root, bodyFont, headingFont);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Apply radius variables (runtime - CSS selectors don't work well with Tailwind)
|
|
378
|
+
let radiusConfig: Required<RadiusConfig>;
|
|
379
|
+
const radiusPreset = oklchConfig?.radiusPreset || config.radiusPreset;
|
|
380
|
+
|
|
381
|
+
if (radiusPreset) {
|
|
382
|
+
radiusConfig = resolveRadiusConfig(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
|
+
applySemanticRadius(root, radiusConfig);
|
|
391
|
+
|
|
392
|
+
// Apply icon stroke width
|
|
393
|
+
const iconStrokeWidth = oklchConfig?.iconStrokeWidth || config.iconStrokeWidth;
|
|
394
|
+
applyIconStrokeWidth(root, iconStrokeWidth);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Clear theme
|
|
399
|
+
* Removes the data-theme attribute and all theme-related CSS variables
|
|
400
|
+
*/
|
|
401
|
+
export function clearTheme(): void {
|
|
402
|
+
const root = document.documentElement;
|
|
403
|
+
|
|
404
|
+
// Only clear if it's a NE theme (starts with "ne-")
|
|
405
|
+
const currentTheme = root.getAttribute('data-theme');
|
|
406
|
+
if (currentTheme?.startsWith('ne-')) {
|
|
407
|
+
root.removeAttribute('data-theme');
|
|
408
|
+
|
|
409
|
+
// Clear font variables (runtime-applied)
|
|
410
|
+
const fontVars = [
|
|
411
|
+
'--font-heading',
|
|
412
|
+
'--font-body',
|
|
413
|
+
'--font-label',
|
|
414
|
+
'--font-button',
|
|
415
|
+
'--font-caption',
|
|
416
|
+
'--font-display',
|
|
417
|
+
'--font-accent',
|
|
418
|
+
'--font-weight-body',
|
|
419
|
+
'--font-weight-heading',
|
|
420
|
+
'--font-weight-button',
|
|
421
|
+
'--font-weight-label',
|
|
422
|
+
'--font-weight-caption',
|
|
423
|
+
'--font-weight-display',
|
|
424
|
+
];
|
|
425
|
+
|
|
426
|
+
for (const v of fontVars) {
|
|
427
|
+
root.style.removeProperty(v);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Clear radius variables (runtime-applied)
|
|
431
|
+
const radiusVars = [
|
|
432
|
+
'--radius-button',
|
|
433
|
+
'--radius-input',
|
|
434
|
+
'--radius-card',
|
|
435
|
+
'--radius-modal',
|
|
436
|
+
'--radius-badge',
|
|
437
|
+
'--radius-avatar',
|
|
438
|
+
'--radius-tooltip',
|
|
439
|
+
'--radius-image',
|
|
440
|
+
];
|
|
441
|
+
|
|
442
|
+
for (const v of radiusVars) {
|
|
443
|
+
root.style.removeProperty(v);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Clear icon stroke width
|
|
447
|
+
root.style.removeProperty('--icon-stroke-width');
|
|
448
|
+
}
|
|
449
|
+
}
|