@snowcone-app/ui 0.1.43 → 0.2.1

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 (196) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +18 -4
  3. package/dist/index.cjs +5 -2
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.js +5 -2
  6. package/dist/index.js.map +1 -1
  7. package/package.json +9 -5
  8. package/src/components/CanvasIsolationBoundary.tsx +202 -0
  9. package/src/components/LoadingOverlayPrism.tsx +251 -0
  10. package/src/composed/AddToCart.tsx +229 -0
  11. package/src/composed/ArtAlignment.tsx +703 -0
  12. package/src/composed/ArtSelector.tsx +290 -0
  13. package/src/composed/ArtworkCustomizer.tsx +212 -0
  14. package/src/composed/CanvasEditor.tsx +79 -0
  15. package/src/composed/ColorPicker.tsx +111 -0
  16. package/src/composed/CurrentSelectionDisplay.tsx +86 -0
  17. package/src/composed/HeroProductImage.tsx +1079 -0
  18. package/src/composed/Lightbox.index.ts +2 -0
  19. package/src/composed/Lightbox.tsx +230 -0
  20. package/src/composed/PlacementClipShapeSelector.tsx +88 -0
  21. package/src/composed/PlacementTabs.tsx +179 -0
  22. package/src/composed/ProductCard.tsx +298 -0
  23. package/src/composed/ProductGallery.tsx +54 -0
  24. package/src/composed/ProductImage.tsx +129 -0
  25. package/src/composed/ProductList.tsx +147 -0
  26. package/src/composed/ProductOptions.tsx +305 -0
  27. package/src/composed/RealtimeMockup.tsx +121 -0
  28. package/src/composed/TileCount.tsx +348 -0
  29. package/src/composed/carousels/HeroCarousel.tsx +240 -0
  30. package/src/composed/carousels/MobileProductCarousel.tsx +1002 -0
  31. package/src/composed/carousels/index.ts +11 -0
  32. package/src/composed/carousels/types.ts +58 -0
  33. package/src/composed/grids/MasonryGrid.tsx +238 -0
  34. package/src/composed/grids/index.ts +9 -0
  35. package/src/composed/search/CurrentRefinements.tsx +80 -0
  36. package/src/composed/search/Filters.tsx +49 -0
  37. package/src/composed/search/FiltersButton.tsx +57 -0
  38. package/src/composed/search/FiltersDrawer.tsx +375 -0
  39. package/src/composed/search/ProductGrid.tsx +118 -0
  40. package/src/composed/search/ProductHit.tsx +56 -0
  41. package/src/composed/search/SearchBox.tsx +109 -0
  42. package/src/composed/search/SearchProvider.tsx +136 -0
  43. package/src/composed/search/facetConfig.ts +16 -0
  44. package/src/composed/search/index.ts +22 -0
  45. package/src/composed/search/meilisearchAdapter.ts +20 -0
  46. package/src/composed/search/types.ts +22 -0
  47. package/src/composed/zoom/EnhancedImageViewer.tsx +505 -0
  48. package/src/composed/zoom/ResponsiveZoom.tsx +134 -0
  49. package/src/composed/zoom/ZoomOverlay.tsx +194 -0
  50. package/src/composed/zoom/index.ts +12 -0
  51. package/src/composed/zoom/types.ts +12 -0
  52. package/src/design-system/ColorPalette.tsx +126 -0
  53. package/src/design-system/ColorSwatch.tsx +49 -0
  54. package/src/design-system/DesignSystemPage.tsx +130 -0
  55. package/src/design-system/ThemeSwitcher.tsx +181 -0
  56. package/src/design-system/TypographyScale.tsx +106 -0
  57. package/src/design-system/index.ts +5 -0
  58. package/src/ecommerce/stories/HeroProductImage.stories.tsx +66 -0
  59. package/src/ecommerce/stories/PDPHeroGallery.stories.tsx +105 -0
  60. package/src/ecommerce/stories/PDPInfoPanel.stories.tsx +472 -0
  61. package/src/ecommerce/stories/PDPLayout.stories.tsx +365 -0
  62. package/src/hooks/useBrand.ts +41 -0
  63. package/src/hooks/useCanvasContext.ts +127 -0
  64. package/src/hooks/useDeviceDetection.ts +64 -0
  65. package/src/hooks/useFocusTrap.ts +70 -0
  66. package/src/hooks/useImagePreloader.ts +268 -0
  67. package/src/hooks/useImageTransition.ts +608 -0
  68. package/src/hooks/usePlacementsProcessor.ts +74 -0
  69. package/src/hooks/useProductGallery.ts +193 -0
  70. package/src/hooks/useProductPage.ts +467 -0
  71. package/src/hooks/useRenderGuard.ts +96 -0
  72. package/src/hooks/useScrollDirection.ts +196 -0
  73. package/src/hooks/viewport/index.ts +25 -0
  74. package/src/hooks/viewport/useContainerWidth.ts +59 -0
  75. package/src/hooks/viewport/useMediaQuery.ts +52 -0
  76. package/src/hooks/viewport/useResponsiveImageCap.ts +149 -0
  77. package/src/hooks/viewport/useViewportDimensions.ts +135 -0
  78. package/src/hooks/viewport/useWideMonitorMode.ts +150 -0
  79. package/src/hooks/visibility/index.ts +15 -0
  80. package/src/hooks/visibility/observerPool.ts +150 -0
  81. package/src/index.ts +240 -0
  82. package/src/layouts/hero-zoom/HeroShrinkLayout.tsx +209 -0
  83. package/src/layouts/hero-zoom/HeroZoomLayout.tsx +351 -0
  84. package/src/layouts/hero-zoom/index.ts +30 -0
  85. package/src/layouts/hero-zoom/stories/HeroZoomLayout.stories.tsx +350 -0
  86. package/src/layouts/hero-zoom/types.ts +113 -0
  87. package/src/layouts/hero-zoom/useHeroZoomScales.ts +156 -0
  88. package/src/layouts/index.ts +9 -0
  89. package/src/layouts/pdp/EdgeBlurBox.tsx +210 -0
  90. package/src/layouts/pdp/ImageBlurExtension.tsx +215 -0
  91. package/src/layouts/pdp/ImageEdgeBlur.tsx +215 -0
  92. package/src/layouts/pdp/PDPLayout.tsx +246 -0
  93. package/src/layouts/pdp/SimpleImageBlur.tsx +140 -0
  94. package/src/layouts/pdp/index.ts +40 -0
  95. package/src/lib/env.ts +15 -0
  96. package/src/lib/locale.ts +167 -0
  97. package/src/lib/router.tsx +46 -0
  98. package/src/lib/utils.ts +6 -0
  99. package/src/lightbox/README.md +77 -0
  100. package/src/next/index.tsx +26 -0
  101. package/src/patterns/MockupPriorityProvider.tsx +1014 -0
  102. package/src/patterns/Product.tsx +850 -0
  103. package/src/patterns/ProductPageProvider.tsx +224 -0
  104. package/src/patterns/RealtimeProvider.tsx +1162 -0
  105. package/src/patterns/ShopProvider.tsx +603 -0
  106. package/src/personalization/PersonalizationBridge.tsx +235 -0
  107. package/src/personalization/PersonalizationContext.ts +29 -0
  108. package/src/personalization/PersonalizationInputs.tsx +110 -0
  109. package/src/personalization/PersonalizationProvider.tsx +407 -0
  110. package/src/personalization/canvas-stub.d.ts +22 -0
  111. package/src/personalization/index.ts +43 -0
  112. package/src/personalization/types.ts +48 -0
  113. package/src/personalization/usePersonalization.ts +32 -0
  114. package/src/personalization/usePersonalizationShimmer.ts +159 -0
  115. package/src/personalization/utils.ts +59 -0
  116. package/src/primitives/BrandLogo.tsx +65 -0
  117. package/src/primitives/BrandName.tsx +51 -0
  118. package/src/primitives/Button.tsx +123 -0
  119. package/src/primitives/ColorSwatch.tsx +221 -0
  120. package/src/primitives/DragHintAnimation.tsx +190 -0
  121. package/src/primitives/EdgeSwipeGuards.tsx +60 -0
  122. package/src/primitives/FloatingActionGroup.tsx +176 -0
  123. package/src/primitives/ProductPrice.tsx +171 -0
  124. package/src/primitives/ProgressiveBlur.tsx +295 -0
  125. package/src/primitives/ThemeToggle.tsx +125 -0
  126. package/src/primitives/__tests__/story-coverage.test.ts +98 -0
  127. package/src/primitives/accordion.tsx +280 -0
  128. package/src/primitives/badge.tsx +137 -0
  129. package/src/primitives/card.tsx +61 -0
  130. package/src/primitives/checkbox.tsx +56 -0
  131. package/src/primitives/collapsible.tsx +51 -0
  132. package/src/primitives/drawer.tsx +828 -0
  133. package/src/primitives/dropdown-menu.tsx +197 -0
  134. package/src/primitives/fieldset.tsx +73 -0
  135. package/src/primitives/index.ts +138 -0
  136. package/src/primitives/input.tsx +91 -0
  137. package/src/primitives/kbd.tsx +130 -0
  138. package/src/primitives/label.tsx +20 -0
  139. package/src/primitives/link.tsx +182 -0
  140. package/src/primitives/popover.tsx +80 -0
  141. package/src/primitives/radio-group.tsx +79 -0
  142. package/src/primitives/scroll-fade.tsx +159 -0
  143. package/src/primitives/select.tsx +170 -0
  144. package/src/primitives/separator.tsx +25 -0
  145. package/src/primitives/slider.tsx +221 -0
  146. package/src/primitives/spinner.tsx +72 -0
  147. package/src/primitives/stories/Accordion.stories.tsx +121 -0
  148. package/src/primitives/stories/Badge.stories.tsx +221 -0
  149. package/src/primitives/stories/Button.stories.tsx +185 -0
  150. package/src/primitives/stories/Card.stories.tsx +171 -0
  151. package/src/primitives/stories/Checkbox.stories.tsx +214 -0
  152. package/src/primitives/stories/Collapsible.stories.tsx +230 -0
  153. package/src/primitives/stories/Drawer.stories.tsx +378 -0
  154. package/src/primitives/stories/DropdownMenu.stories.tsx +182 -0
  155. package/src/primitives/stories/Fieldset.stories.tsx +212 -0
  156. package/src/primitives/stories/Input.stories.tsx +172 -0
  157. package/src/primitives/stories/Kbd.stories.tsx +183 -0
  158. package/src/primitives/stories/Label.stories.tsx +98 -0
  159. package/src/primitives/stories/Link.stories.tsx +260 -0
  160. package/src/primitives/stories/Popover.stories.tsx +178 -0
  161. package/src/primitives/stories/RadioGroup.stories.tsx +205 -0
  162. package/src/primitives/stories/Select.stories.tsx +222 -0
  163. package/src/primitives/stories/Separator.stories.tsx +134 -0
  164. package/src/primitives/stories/Slider.stories.tsx +203 -0
  165. package/src/primitives/stories/Spinner.stories.tsx +142 -0
  166. package/src/primitives/stories/Surface.stories.tsx +257 -0
  167. package/src/primitives/stories/Switch.stories.tsx +131 -0
  168. package/src/primitives/stories/Tabs.stories.tsx +275 -0
  169. package/src/primitives/stories/TextField.stories.tsx +139 -0
  170. package/src/primitives/stories/Textarea.stories.tsx +148 -0
  171. package/src/primitives/stories/Tooltip.stories.tsx +119 -0
  172. package/src/primitives/surface.tsx +86 -0
  173. package/src/primitives/switch.tsx +35 -0
  174. package/src/primitives/tabs.tsx +206 -0
  175. package/src/primitives/text-field.tsx +84 -0
  176. package/src/primitives/textarea.tsx +50 -0
  177. package/src/primitives/tooltip.tsx +58 -0
  178. package/src/services/CanvasExportService.ts +518 -0
  179. package/src/styles/base.css +380 -0
  180. package/src/styles/defaults.css +280 -0
  181. package/src/styles/globals.css +1242 -0
  182. package/src/styles/index.css +17 -0
  183. package/src/styles/ne-themes.css +4740 -0
  184. package/src/styles/tailwind.css +11 -0
  185. package/src/styles/tokens.css +117 -0
  186. package/src/styles/utilities.css +188 -0
  187. package/src/themes/apply-theme.ts +449 -0
  188. package/src/themes/getThemeStyles.ts +454 -0
  189. package/src/themes/index.ts +48 -0
  190. package/src/themes/oklch-theme.ts +283 -0
  191. package/src/themes/presets.ts +989 -0
  192. package/src/themes/types.ts +386 -0
  193. package/src/themes/useTheme.tsx +450 -0
  194. package/src/utils/dev-warnings.ts +161 -0
  195. package/src/utils/devWarnings.ts +153 -0
  196. 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
+ }