meno-core 1.0.23 → 1.0.25

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 (95) hide show
  1. package/bin/cli.ts +1 -0
  2. package/build-static.ts +9 -5
  3. package/bunfig.toml +4 -1
  4. package/entries/client-router.tsx +83 -1
  5. package/entries/server-router.tsx +6 -2
  6. package/lib/client/core/ComponentBuilder.ts +2 -0
  7. package/lib/client/core/builders/embedBuilder.ts +5 -1
  8. package/lib/client/core/builders/linkNodeBuilder.ts +3 -0
  9. package/lib/client/core/builders/listBuilder.ts +3 -1
  10. package/lib/client/core/builders/localeListBuilder.ts +7 -0
  11. package/lib/client/core/cmsTemplateProcessor.ts +10 -9
  12. package/lib/client/hmr/HMRManager.tsx +17 -19
  13. package/lib/client/hooks/useVariables.ts +101 -0
  14. package/lib/client/index.ts +1 -0
  15. package/lib/client/responsiveStyleResolver.test.ts +14 -9
  16. package/lib/client/routing/RouteLoader.test.ts +7 -3
  17. package/lib/client/routing/RouteLoader.ts +4 -2
  18. package/lib/client/routing/Router.tsx +45 -21
  19. package/lib/client/services/PrefetchService.ts +1 -1
  20. package/lib/client/styles/StyleInjector.test.ts +20 -8
  21. package/lib/client/styles/StyleInjector.ts +103 -108
  22. package/lib/client/styles/UtilityClassCollector.ts +208 -0
  23. package/lib/client/templateEngine.ts +61 -3
  24. package/lib/server/__integration__/cms-integration.test.ts +2 -2
  25. package/lib/server/createServer.ts +1 -0
  26. package/lib/server/cssGenerator.ts +91 -3
  27. package/lib/server/fileWatcher.ts +81 -3
  28. package/lib/server/index.ts +8 -3
  29. package/lib/server/migrateTemplates.ts +22 -0
  30. package/lib/server/projectContext.ts +3 -1
  31. package/lib/server/providers/fileSystemCMSProvider.test.ts +6 -7
  32. package/lib/server/providers/fileSystemCMSProvider.ts +6 -11
  33. package/lib/server/providers/fileSystemPageProvider.ts +86 -40
  34. package/lib/server/routes/api/colors.test.ts +103 -0
  35. package/lib/server/routes/api/colors.ts +10 -42
  36. package/lib/server/routes/api/core-routes.ts +42 -2
  37. package/lib/server/routes/api/enums.test.ts +53 -0
  38. package/lib/server/routes/api/enums.ts +16 -0
  39. package/lib/server/routes/api/pages.ts +3 -2
  40. package/lib/server/routes/api/variables.test.ts +74 -0
  41. package/lib/server/routes/api/variables.ts +32 -0
  42. package/lib/server/routes/index.ts +1 -1
  43. package/lib/server/routes/pages.ts +4 -4
  44. package/lib/server/routes/static.ts +3 -1
  45. package/lib/server/services/CachedConfigLoader.ts +55 -0
  46. package/lib/server/services/ColorService.test.ts +216 -0
  47. package/lib/server/services/ColorService.ts +11 -30
  48. package/lib/server/services/EnumService.test.ts +192 -0
  49. package/lib/server/services/EnumService.ts +118 -0
  50. package/lib/server/services/VariableService.test.ts +183 -0
  51. package/lib/server/services/VariableService.ts +100 -0
  52. package/lib/server/services/cmsService.test.ts +11 -11
  53. package/lib/server/services/cmsService.ts +18 -8
  54. package/lib/server/services/configService.test.ts +1 -95
  55. package/lib/server/services/configService.ts +0 -27
  56. package/lib/server/services/fileWatcherService.ts +33 -4
  57. package/lib/server/services/pageService.ts +140 -2
  58. package/lib/server/ssr/cmsSSRProcessor.ts +3 -2
  59. package/lib/server/ssr/htmlGenerator.ts +35 -7
  60. package/lib/server/ssr/imageMetadata.ts +5 -1
  61. package/lib/server/ssr/ssrRenderer.test.ts +183 -117
  62. package/lib/server/ssr/ssrRenderer.ts +159 -272
  63. package/lib/server/websocketManager.ts +36 -0
  64. package/lib/shared/colorConversions.test.ts +140 -0
  65. package/lib/shared/colorConversions.ts +181 -0
  66. package/lib/shared/constants.test.ts +1 -1
  67. package/lib/shared/constants.ts +33 -2
  68. package/lib/shared/cssGeneration.ts +167 -47
  69. package/lib/shared/cssProperties.ts +22 -0
  70. package/lib/shared/fontLoader.test.ts +11 -2
  71. package/lib/shared/fontLoader.ts +20 -9
  72. package/lib/shared/gradientUtils.test.ts +183 -0
  73. package/lib/shared/gradientUtils.ts +184 -0
  74. package/lib/shared/index.ts +6 -0
  75. package/lib/shared/libraryLoader.test.ts +20 -0
  76. package/lib/shared/libraryLoader.ts +49 -3
  77. package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +6 -4
  78. package/lib/shared/registry/nodeTypes/ListNodeType.ts +4 -32
  79. package/lib/shared/responsiveScaling.ts +3 -3
  80. package/lib/shared/treePathUtils.ts +8 -0
  81. package/lib/shared/types/api.ts +1 -1
  82. package/lib/shared/types/cms.ts +4 -6
  83. package/lib/shared/types/colors.ts +10 -0
  84. package/lib/shared/types/components.ts +4 -0
  85. package/lib/shared/types/index.ts +21 -0
  86. package/lib/shared/types/libraries.ts +9 -0
  87. package/lib/shared/types/styles.ts +10 -0
  88. package/lib/shared/types/variables.test.ts +132 -0
  89. package/lib/shared/types/variables.ts +215 -0
  90. package/lib/shared/utilityClassConfig.ts +4 -0
  91. package/lib/shared/validation/propValidator.ts +3 -2
  92. package/lib/shared/validation/schemas.ts +59 -47
  93. package/lib/test-utils/dom-setup.ts +1 -0
  94. package/package.json +1 -1
  95. package/templates/index-router.html +1 -1
package/bin/cli.ts CHANGED
@@ -102,6 +102,7 @@ async function startStaticServer(distPath: string) {
102
102
 
103
103
  const server = Bun.serve({
104
104
  port: SERVE_PORT,
105
+ hostname: 'localhost',
105
106
  async fetch(req: Request) {
106
107
  const url = new URL(req.url);
107
108
  let pathname = url.pathname;
package/build-static.ts CHANGED
@@ -30,6 +30,7 @@ import type { SlugMap } from "./lib/shared/slugTranslator";
30
30
  import { buildItemUrl } from "./lib/shared/itemTemplateUtils";
31
31
  import { generateMiddleware, generateTrackFunction, generateResultsFunction } from "./lib/server/ab/generateFunctions";
32
32
  import { generateTrackingScript } from "./lib/server/ab/trackingScript";
33
+ import { migrateTemplatesDirectory } from "./lib/server/migrateTemplates";
33
34
 
34
35
  /**
35
36
  * Collect build errors for error overlay
@@ -387,7 +388,7 @@ async function generateStaticDataFiles(
387
388
  }
388
389
 
389
390
  /**
390
- * Build CMS templates from pages/templates/ directory
391
+ * Build CMS templates from root templates/ directory
391
392
  */
392
393
  async function buildCMSTemplates(
393
394
  templatesDir: string,
@@ -566,7 +567,7 @@ async function buildCMSTemplates(
566
567
 
567
568
  console.error(`❌ Error processing ${file}:`, error);
568
569
  buildErrors.push({
569
- file: `pages/templates/${file}`,
570
+ file: `templates/${file}`,
570
571
  message: errorMessage,
571
572
  type: errorMessage.includes('minification') || errorMessage.includes('minify') ? 'minify' : 'cms',
572
573
  });
@@ -664,6 +665,9 @@ export async function buildStaticPages(): Promise<void> {
664
665
  const i18nConfig = await loadI18nConfig();
665
666
  console.log(`🌐 Locales: ${i18nConfig.locales.map(l => l.code).join(", ")} (default: ${i18nConfig.defaultLocale})\n`);
666
667
 
668
+ // Auto-migrate pages/templates/ → templates/ if needed
669
+ await migrateTemplatesDirectory();
670
+
667
671
  // Clean dist directory (removes editor files, old HTML)
668
672
  cleanDist();
669
673
 
@@ -770,7 +774,7 @@ export async function buildStaticPages(): Promise<void> {
770
774
  console.log(`✅ Loaded ${components.size} global component(s)\n`);
771
775
 
772
776
  // Initialize CMS service for CMSList rendering
773
- const cmsProvider = new FileSystemCMSProvider(projectPaths.pages(), projectPaths.cms());
777
+ const cmsProvider = new FileSystemCMSProvider(projectPaths.templates(), projectPaths.cms());
774
778
  const cmsService = new CMSService(cmsProvider);
775
779
  await cmsService.initialize();
776
780
  console.log(`✅ CMS service initialized\n`);
@@ -947,8 +951,8 @@ export async function buildStaticPages(): Promise<void> {
947
951
  }
948
952
  }
949
953
 
950
- // Build CMS templates from pages/templates/
951
- const templatesDir = join(pagesDir, 'templates');
954
+ // Build CMS templates from root templates/ directory
955
+ const templatesDir = projectPaths.templates();
952
956
  const staticCollections = new Map<string, ClientDataCollection>();
953
957
  const cmsResult = await buildCMSTemplates(
954
958
  templatesDir,
package/bunfig.toml CHANGED
@@ -32,7 +32,10 @@ testNamePattern = ".*"
32
32
 
33
33
  # Skip patterns to exclude from test discovery
34
34
  # tests/ contains Playwright E2E tests that should be run with `bunx playwright test`
35
- testPathIgnorePatterns = ["node_modules", "dist", "\\.next", "tests/"]
35
+ # __integration__ tests run in a separate process (bun test:integration)
36
+ # because Bun's mock.module is process-global and unit tests that mock core modules
37
+ # (e.g. ssrRenderer, configService) would contaminate integration tests.
38
+ testPathIgnorePatterns = ["node_modules", "dist", "\\.next", "tests/", "__integration__"]
36
39
 
37
40
  # Coverage reporter options
38
41
  # Generates coverage reports for analysis
@@ -7,6 +7,7 @@ import type { PrefetchConfig } from "../lib/shared/types/prefetch";
7
7
  declare global {
8
8
  interface Window {
9
9
  __hmrColorsInitialized?: boolean;
10
+ __hmrVariablesInitialized?: boolean;
10
11
  __MENO_CONFIG__?: {
11
12
  prefetch?: Partial<PrefetchConfig>;
12
13
  };
@@ -91,8 +92,89 @@ async function injectUpdatedThemeCSS() {
91
92
  }
92
93
  }
93
94
 
94
- // Initialize HMR colors listener
95
+ // Setup HMR variables update listener immediately on app load
96
+ function setupVariablesHMR() {
97
+ if (typeof window === 'undefined') return;
98
+
99
+ if (window.__hmrVariablesInitialized) return;
100
+ window.__hmrVariablesInitialized = true;
101
+
102
+ document.addEventListener('hmr-variables-update', async () => {
103
+ await injectUpdatedVariablesCSS();
104
+ });
105
+ }
106
+
107
+ // Fetch and inject updated variables CSS
108
+ async function injectUpdatedVariablesCSS() {
109
+ try {
110
+ const response = await fetch('/api/variables-css');
111
+ if (!response.ok) return;
112
+
113
+ const css = await response.text();
114
+
115
+ let styleTag = document.getElementById('hmr-css-variables');
116
+ if (!styleTag) {
117
+ styleTag = document.createElement('style');
118
+ styleTag.id = 'hmr-css-variables';
119
+ document.head.appendChild(styleTag);
120
+ }
121
+
122
+ styleTag.textContent = css;
123
+ } catch (error) {
124
+ // Silently fail - not critical if CSS injection doesn't work
125
+ }
126
+ }
127
+
128
+ // Setup HMR fonts update listener immediately on app load
129
+ function setupFontsHMR() {
130
+ if (typeof window === 'undefined') return;
131
+
132
+ if ((window as any).__hmrFontsCSSInitialized) return;
133
+ (window as any).__hmrFontsCSSInitialized = true;
134
+
135
+ document.addEventListener('hmr-fonts-update', async () => {
136
+ await injectUpdatedFontsCSS();
137
+ });
138
+ }
139
+
140
+ // Fetch and inject updated fonts CSS
141
+ async function injectUpdatedFontsCSS() {
142
+ try {
143
+ const response = await fetch('/api/fonts-css');
144
+ if (!response.ok) return;
145
+
146
+ const css = await response.text();
147
+
148
+ let styleTag = document.getElementById('hmr-fonts-css');
149
+ if (!styleTag) {
150
+ styleTag = document.createElement('style');
151
+ styleTag.id = 'hmr-fonts-css';
152
+ document.head.appendChild(styleTag);
153
+ }
154
+
155
+ styleTag.textContent = css;
156
+ } catch (error) {
157
+ // Silently fail
158
+ }
159
+ }
160
+
161
+ // Setup HMR libraries update listener - triggers full page reload
162
+ function setupLibrariesHMR() {
163
+ if (typeof window === 'undefined') return;
164
+
165
+ if ((window as any).__hmrLibrariesInitialized) return;
166
+ (window as any).__hmrLibrariesInitialized = true;
167
+
168
+ document.addEventListener('hmr-libraries-update', () => {
169
+ location.reload();
170
+ });
171
+ }
172
+
173
+ // Initialize HMR listeners
95
174
  setupColorsHMR();
175
+ setupVariablesHMR();
176
+ setupFontsHMR();
177
+ setupLibrariesHMR();
96
178
 
97
179
  // Render app with HMR support and prefetching enabled
98
180
  const rootElement = document.getElementById('root');
@@ -14,16 +14,20 @@ import { FileSystemPageProvider } from '../lib/server/providers/fileSystemPagePr
14
14
  import { FileSystemCMSProvider } from '../lib/server/providers/fileSystemCMSProvider';
15
15
  import { configService } from '../lib/server/services/configService';
16
16
  import { projectPaths } from '../lib/server/projectContext';
17
+ import { migrateTemplatesDirectory } from '../lib/server/migrateTemplates';
18
+
19
+ // Auto-migrate pages/templates/ → templates/ if needed
20
+ await migrateTemplatesDirectory();
17
21
 
18
22
  // Initialize services
19
23
  const pageCache = new PageCache();
20
- const pageProvider = new FileSystemPageProvider(projectPaths.pages());
24
+ const pageProvider = new FileSystemPageProvider(projectPaths.pages(), projectPaths.templates());
21
25
  const pageService = new PageService(pageCache, pageProvider);
22
26
  const componentService = new ComponentService();
23
27
  const wsManager = new WebSocketManager();
24
28
 
25
29
  // Initialize CMS services
26
- const cmsProvider = new FileSystemCMSProvider(projectPaths.pages(), projectPaths.cms());
30
+ const cmsProvider = new FileSystemCMSProvider(projectPaths.templates(), projectPaths.cms());
27
31
  const cmsService = new CMSService(cmsProvider);
28
32
 
29
33
  // Initialize file watcher with CMS service for template change detection
@@ -27,6 +27,7 @@ import { processItemTemplate, processItemPropsTemplate, hasItemTemplates, type V
27
27
  import { DEFAULT_I18N_CONFIG, resolveI18nValue } from "../../shared/i18n";
28
28
  import { getChildPath, pathToString } from "../../shared/pathArrayUtils";
29
29
  import { responsiveStylesToClasses } from "../../shared/utilityClassMapper";
30
+ import { UtilityClassCollector } from "../styles/UtilityClassCollector";
30
31
  import { processCMSTemplate, processCMSPropsTemplate, RAW_HTML_PREFIX } from "./cmsTemplateProcessor";
31
32
  import type { PrefetchService } from "../services/PrefetchService";
32
33
  import { generateElementClassName, type ElementClassContext } from "../../shared/elementClassName";
@@ -95,6 +96,7 @@ export class ComponentBuilder {
95
96
  cached = responsiveStylesToClasses(style as any);
96
97
  this.styleClassCache.set(style, cached);
97
98
  }
99
+ UtilityClassCollector.collect(cached);
98
100
  return cached;
99
101
  }
100
102
 
@@ -12,6 +12,7 @@ import { pathToString } from "../../../shared/pathArrayUtils";
12
12
  import { generateElementClassName, type ElementClassContext } from "../../../shared/elementClassName";
13
13
  import { InteractiveStylesRegistry } from "../../InteractiveStylesRegistry";
14
14
  import { hasInteractiveStyleMappings, extractInteractiveStyleMappings, resolveExtractedMappings } from "../../../shared/interactiveStyleMappings";
15
+ import { UtilityClassCollector } from "../../styles/UtilityClassCollector";
15
16
  import DOMPurify from "isomorphic-dompurify";
16
17
  import type { ElementRegistry } from "../../elementRegistry";
17
18
  import type { BuilderContext } from "./types";
@@ -53,7 +54,8 @@ export function buildEmbed(
53
54
  const { key, elementPath, parentComponentName, componentContext, componentRootPath, cmsItemIndexPath } = ctx;
54
55
 
55
56
  // Process templates in html property before sanitization (matching SSR behavior)
56
- let htmlContent = node.html;
57
+ // Mappings should already be resolved by processStructure, this is just a type safety guard
58
+ let htmlContent = typeof node.html === 'string' ? node.html : '';
57
59
 
58
60
  // Process item template strings (for List context) - e.g., {{item.icon}}, {{feature.title}}
59
61
  if (ctx.templateContext && hasItemTemplates(htmlContent)) {
@@ -125,6 +127,7 @@ export function buildEmbed(
125
127
  ) as StyleObject | ResponsiveStyleObject;
126
128
  }
127
129
  const utilityClasses = responsiveStylesToClasses(processedStyle as StyleObject | ResponsiveStyleObject);
130
+ UtilityClassCollector.collect(utilityClasses);
128
131
  classNames.push(...utilityClasses);
129
132
  }
130
133
 
@@ -172,6 +175,7 @@ export function buildEmbed(
172
175
  for (const rule of nodeInteractiveStyles) {
173
176
  if (rule.previewProp && ctx.componentResolvedProps[rule.previewProp] === true) {
174
177
  const styleClasses = responsiveStylesToClasses(rule.style as StyleObject | ResponsiveStyleObject);
178
+ UtilityClassCollector.collect(styleClasses);
175
179
  previewClasses.push(...styleClasses);
176
180
  }
177
181
  }
@@ -13,6 +13,7 @@ import { generateElementClassName, type ElementClassContext } from "../../../sha
13
13
  import { InteractiveStylesRegistry } from "../../InteractiveStylesRegistry";
14
14
  import { hasInteractiveStyleMappings, extractInteractiveStyleMappings, resolveExtractedMappings } from "../../../shared/interactiveStyleMappings";
15
15
  import { processItemPropsTemplate, type ValueResolver } from "../../../shared/itemTemplateUtils";
16
+ import { UtilityClassCollector } from "../../styles/UtilityClassCollector";
16
17
  import { resolveI18nValue, DEFAULT_I18N_CONFIG } from "../../../shared/i18n";
17
18
  import { isCurrentLink } from "../../../shared/linkUtils";
18
19
  import type { ElementRegistry } from "../../elementRegistry";
@@ -101,6 +102,7 @@ export function buildLinkNode(
101
102
  ) as StyleObject | ResponsiveStyleObject;
102
103
  }
103
104
  const utilityClasses = responsiveStylesToClasses(processedStyle as StyleObject | ResponsiveStyleObject);
105
+ UtilityClassCollector.collect(utilityClasses);
104
106
  classNames.push(...utilityClasses);
105
107
  }
106
108
 
@@ -148,6 +150,7 @@ export function buildLinkNode(
148
150
  for (const rule of nodeInteractiveStyles) {
149
151
  if (rule.previewProp && ctx.componentResolvedProps[rule.previewProp] === true) {
150
152
  const styleClasses = responsiveStylesToClasses(rule.style as StyleObject | ResponsiveStyleObject);
153
+ UtilityClassCollector.collect(styleClasses);
151
154
  previewClasses.push(...styleClasses);
152
155
  }
153
156
  }
@@ -11,6 +11,7 @@ import type { ListNode } from "../../../shared/registry/nodeTypes/ListNodeType";
11
11
  import type { CMSItem, CMSFilterCondition, CMSSortConfig, CMSFilterOperator } from "../../../shared/types/cms";
12
12
  import type { InteractiveStyles, StyleObject, ResponsiveStyleObject } from "../../../shared/types";
13
13
  import { singularize } from "../../../shared/types/cms";
14
+ import { UtilityClassCollector } from "../../styles/UtilityClassCollector";
14
15
  import { buildTemplateContext, resolveItemsTemplate, getNestedValue } from "../../../shared/itemTemplateUtils";
15
16
  import type { TemplateContext } from "../../../shared/types/cms";
16
17
  import { pathToString, getChildPath } from "../../../shared/pathArrayUtils";
@@ -152,6 +153,7 @@ export function buildList(
152
153
  for (const rule of nodeInteractiveStyles) {
153
154
  if (rule.previewProp && ctx.componentResolvedProps[rule.previewProp] === true) {
154
155
  const styleClasses = responsiveStylesToClasses(rule.style as StyleObject | ResponsiveStyleObject);
156
+ UtilityClassCollector.collect(styleClasses);
155
157
  previewClasses.push(...styleClasses);
156
158
  }
157
159
  }
@@ -222,7 +224,7 @@ export function buildList(
222
224
  const label = isCollectionMode ? 'CMS List' : 'List';
223
225
 
224
226
  // Use configurable tag (defaults to 'div') - null means fragment mode (no wrapper)
225
- const tag = node.tag === false ? null : (node.tag || 'div');
227
+ const tag = typeof node.tag === 'string' ? node.tag : null;
226
228
 
227
229
  if (!source && !sourceIsResolved) {
228
230
  // No source - render empty container with placeholder
@@ -12,6 +12,7 @@ import { pathToString } from "../../../shared/pathArrayUtils";
12
12
  import { generateElementClassName, type ElementClassContext } from "../../../shared/elementClassName";
13
13
  import { InteractiveStylesRegistry } from "../../InteractiveStylesRegistry";
14
14
  import { hasInteractiveStyleMappings, extractInteractiveStyleMappings, resolveExtractedMappings } from "../../../shared/interactiveStyleMappings";
15
+ import { UtilityClassCollector } from "../../styles/UtilityClassCollector";
15
16
  import type { ElementRegistry } from "../../elementRegistry";
16
17
  import type { BuilderContext } from "./types";
17
18
 
@@ -75,6 +76,7 @@ export function buildLocaleList(
75
76
  // Convert container styles to utility classes
76
77
  if (node.style) {
77
78
  const utilityClasses = responsiveStylesToClasses(node.style as StyleObject | ResponsiveStyleObject);
79
+ UtilityClassCollector.collect(utilityClasses);
78
80
  classNames.push(...utilityClasses);
79
81
  }
80
82
 
@@ -122,6 +124,7 @@ export function buildLocaleList(
122
124
  for (const rule of nodeInteractiveStyles) {
123
125
  if (rule.previewProp && ctx.componentResolvedProps[rule.previewProp] === true) {
124
126
  const styleClasses = responsiveStylesToClasses(rule.style as StyleObject | ResponsiveStyleObject);
127
+ UtilityClassCollector.collect(styleClasses);
125
128
  previewClasses.push(...styleClasses);
126
129
  }
127
130
  }
@@ -170,6 +173,10 @@ export function buildLocaleList(
170
173
  const activeItemClasses = node.activeItemStyle ? responsiveStylesToClasses(node.activeItemStyle as StyleObject | ResponsiveStyleObject) : [];
171
174
  const separatorClasses = node.separatorStyle ? responsiveStylesToClasses(node.separatorStyle as StyleObject | ResponsiveStyleObject) : [];
172
175
  const flagClasses = node.flagStyle ? responsiveStylesToClasses(node.flagStyle as StyleObject | ResponsiveStyleObject) : [];
176
+ UtilityClassCollector.collect(itemClasses);
177
+ UtilityClassCollector.collect(activeItemClasses);
178
+ UtilityClassCollector.collect(separatorClasses);
179
+ UtilityClassCollector.collect(flagClasses);
173
180
 
174
181
  // Build locale links from config
175
182
  const linkElements: ReactElement[] = [];
@@ -29,21 +29,21 @@ function isI18nValue(value: unknown): value is I18nValue {
29
29
  * Resolve an I18nValue to a string for the given locale
30
30
  * Falls back to default locale, then first available translation
31
31
  */
32
- function resolveI18nValue(value: I18nValue, locale: string, config: I18nConfig): string {
32
+ function resolveI18nValue(value: I18nValue, locale: string, config: I18nConfig): unknown {
33
33
  // Try exact locale match
34
- if (typeof value[locale] === 'string') {
35
- return value[locale] as string;
34
+ if (value[locale] !== undefined) {
35
+ return value[locale];
36
36
  }
37
37
 
38
38
  // Try default locale
39
- if (typeof value[config.defaultLocale] === 'string') {
40
- return value[config.defaultLocale] as string;
39
+ if (value[config.defaultLocale] !== undefined) {
40
+ return value[config.defaultLocale];
41
41
  }
42
42
 
43
43
  // Get first available translation (skip _i18n marker)
44
44
  for (const key of Object.keys(value)) {
45
- if (key !== '_i18n' && typeof value[key] === 'string') {
46
- return value[key] as string;
45
+ if (key !== '_i18n' && value[key] !== undefined) {
46
+ return value[key];
47
47
  }
48
48
  }
49
49
 
@@ -82,9 +82,10 @@ export function processCMSTemplate(
82
82
  }
83
83
  }
84
84
 
85
- // Handle i18n values
85
+ // Handle i18n values - resolve to locale-specific value, then continue
86
+ // through rich-text detection (don't early-return, as resolved value may be Tiptap JSON)
86
87
  if (isI18nValue(value)) {
87
- return resolveI18nValue(value, effectiveLocale, config);
88
+ value = resolveI18nValue(value, effectiveLocale, config);
88
89
  }
89
90
 
90
91
  // Return string representation
@@ -3,7 +3,7 @@
3
3
  * Manages Hot Module Replacement WebSocket connection, status tracking, and visual indicators.
4
4
  */
5
5
 
6
- import { createElement as h, useState, useEffect, useRef, useCallback } from "react";
6
+ import { createElement as h, useState, useEffect, useRef } from "react";
7
7
  import type { ReactElement } from "react";
8
8
  import { HMRWebSocket } from "../hmrWebSocket";
9
9
 
@@ -53,6 +53,7 @@ export function HMRManager({
53
53
  // Track callbacks via refs to avoid stale closures and prevent WebSocket recreation
54
54
  const currentPathRef = useRef(currentPath);
55
55
  const onCMSUpdateRef = useRef(onCMSUpdate);
56
+ const onReloadRef = useRef(onReload);
56
57
 
57
58
  useEffect(() => {
58
59
  currentPathRef.current = currentPath;
@@ -62,24 +63,15 @@ export function HMRManager({
62
63
  onCMSUpdateRef.current = onCMSUpdate;
63
64
  }, [onCMSUpdate]);
64
65
 
65
- // Helper function to show HMR indicator
66
- const showHMRIndicator = useCallback(() => {
67
- const indicator = document.getElementById('hmr-indicator');
68
- if (indicator) {
69
- indicator.style.display = 'block';
70
- setTimeout(() => {
71
- indicator.style.display = 'none';
72
- }, 2000);
73
- }
74
- }, []);
66
+ useEffect(() => { onReloadRef.current = onReload; }, [onReload]);
75
67
 
76
68
  // Define message handler - reads from refs to always get latest values
77
69
  const createMessageHandler = () => (data: any) => {
78
70
  if (data.type === 'hmr:update') {
79
71
  // Always use current path (preserves locale) when reloading
80
72
  // The HMR path tells us which page changed, but we reload with current locale
81
- if (onReload) {
82
- onReload(currentPathRef.current);
73
+ if (onReloadRef.current) {
74
+ onReloadRef.current(currentPathRef.current);
83
75
  }
84
76
 
85
77
  // Call update callback with the original HMR path
@@ -94,8 +86,15 @@ export function HMRManager({
94
86
  } else if (data.type === 'hmr:colors-update') {
95
87
  // Dispatch custom event to notify color hooks of the update
96
88
  document.dispatchEvent(new CustomEvent('hmr-colors-update'));
97
- // Show HMR indicator
98
- showHMRIndicator();
89
+ } else if (data.type === 'hmr:variables-update') {
90
+ // Dispatch custom event to notify variable hooks of the update
91
+ document.dispatchEvent(new CustomEvent('hmr-variables-update'));
92
+ } else if (data.type === 'hmr:fonts-update') {
93
+ // Dispatch custom event to notify font CSS injection
94
+ document.dispatchEvent(new CustomEvent('hmr-fonts-update'));
95
+ } else if (data.type === 'hmr:libraries-update') {
96
+ // Dispatch custom event to trigger full page reload for library changes
97
+ document.dispatchEvent(new CustomEvent('hmr-libraries-update'));
99
98
  }
100
99
  };
101
100
 
@@ -145,7 +144,7 @@ export function HMRManager({
145
144
  hmrWs.close();
146
145
  }
147
146
  };
148
- }, [onUpdate, onStatusChange, onReload]);
147
+ }, [onUpdate, onStatusChange]);
149
148
 
150
149
  // Bun HMR API integration
151
150
  useEffect(() => {
@@ -157,14 +156,13 @@ export function HMRManager({
157
156
  });
158
157
 
159
158
  import.meta.hot.on('bun:afterUpdate', () => {
160
- // Show visual indicator
161
- showHMRIndicator();
159
+ // HMR update complete
162
160
  });
163
161
 
164
162
  import.meta.hot.on('bun:error', () => {
165
163
  });
166
164
  }
167
- }, [showHMRIndicator]);
165
+ }, []);
168
166
 
169
167
  // Render indicators
170
168
  return h(HMRIndicator, { status });
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Hook for fetching and caching CSS variables (variables.json)
3
+ */
4
+
5
+ import { useState, useEffect, useRef } from 'react';
6
+ import type { CSSVariable } from '../../shared/types/variables';
7
+
8
+ let cachedVariables: CSSVariable[] | null = null;
9
+ let hmrCallbacks: Set<() => void> = new Set();
10
+
11
+ // Setup HMR listener immediately when module loads
12
+ function initializeHMRListener() {
13
+ if (typeof window === 'undefined') return;
14
+
15
+ // Only setup once
16
+ if ((window as any).__hmrVariablesInitialized) return;
17
+ (window as any).__hmrVariablesInitialized = true;
18
+
19
+ // Listen for custom HMR events from HMRManager
20
+ document.addEventListener('hmr-variables-update', () => {
21
+ // Clear cache
22
+ cachedVariables = null;
23
+
24
+ // Notify all listeners to refresh their data
25
+ hmrCallbacks.forEach(callback => callback());
26
+ });
27
+ }
28
+
29
+ // Initialize immediately
30
+ if (typeof window !== 'undefined') {
31
+ initializeHMRListener();
32
+ }
33
+
34
+ export function useVariables() {
35
+ const [variables, setVariables] = useState<CSSVariable[] | null>(cachedVariables);
36
+ const [loading, setLoading] = useState(!cachedVariables);
37
+ const [error, setError] = useState<Error | null>(null);
38
+ const callbackRef = useRef<(() => void) | null>(null);
39
+
40
+ useEffect(() => {
41
+ // Create a callback to refresh variables when HMR update is received
42
+ const refreshCallback = async () => {
43
+ try {
44
+ const response = await fetch('/api/variables-status');
45
+ if (!response.ok) {
46
+ throw new Error('Failed to fetch variables');
47
+ }
48
+ const data = await response.json() as { status: string; config: { variables: CSSVariable[] } };
49
+ cachedVariables = data.config.variables;
50
+ setVariables(cachedVariables);
51
+ setError(null);
52
+ } catch (err) {
53
+ setError(err instanceof Error ? err : new Error('Unknown error'));
54
+ }
55
+ };
56
+
57
+ // Register callback for HMR updates
58
+ callbackRef.current = refreshCallback;
59
+ hmrCallbacks.add(refreshCallback);
60
+
61
+ // Return cached variables immediately
62
+ if (cachedVariables) {
63
+ setVariables(cachedVariables);
64
+ setLoading(false);
65
+ return () => {
66
+ if (callbackRef.current) {
67
+ hmrCallbacks.delete(callbackRef.current);
68
+ }
69
+ };
70
+ }
71
+
72
+ // Fetch variables
73
+ const fetchVariables = async () => {
74
+ try {
75
+ const response = await fetch('/api/variables-status');
76
+ if (!response.ok) {
77
+ throw new Error('Failed to fetch variables');
78
+ }
79
+ const data = await response.json() as { status: string; config: { variables: CSSVariable[] } };
80
+ cachedVariables = data.config.variables;
81
+ setVariables(cachedVariables);
82
+ setError(null);
83
+ } catch (err) {
84
+ setError(err instanceof Error ? err : new Error('Unknown error'));
85
+ setVariables(null);
86
+ } finally {
87
+ setLoading(false);
88
+ }
89
+ };
90
+
91
+ fetchVariables();
92
+
93
+ return () => {
94
+ if (callbackRef.current) {
95
+ hmrCallbacks.delete(callbackRef.current);
96
+ }
97
+ };
98
+ }, []);
99
+
100
+ return { variables, loading, error };
101
+ }
@@ -52,6 +52,7 @@ export * from './navigation';
52
52
  // Hooks
53
53
  export * from './hooks/useColorVariables';
54
54
  export * from './hooks/usePropertyAutocomplete';
55
+ export * from './hooks/useVariables';
55
56
 
56
57
  // Template engine
57
58
  export * from './templateEngine';
@@ -1,4 +1,7 @@
1
- import { test, expect, describe, beforeEach, afterEach } from "bun:test";
1
+ import { test, expect, describe, beforeEach, afterEach, afterAll } from "bun:test";
2
+
3
+ // Save original fetch before any test can modify it
4
+ const _originalFetch = globalThis.fetch;
2
5
  import {
3
6
  resolveResponsiveStyleSync,
4
7
  resolveResponsiveStyle,
@@ -334,10 +337,8 @@ describe("Responsive Style Resolver - resolveResponsiveStyleSync", () => {
334
337
  });
335
338
 
336
339
  describe("Responsive Style Resolver - resolveResponsiveStyle (async)", () => {
337
- // Mock fetch for async tests
338
- beforeEach(() => {
339
- // Reset global fetch mock
340
- global.fetch = global.fetch || (() => Promise.reject(new Error('fetch not implemented'))) as any;
340
+ afterEach(() => {
341
+ globalThis.fetch = _originalFetch;
341
342
  });
342
343
 
343
344
  test("should resolve non-responsive styles asynchronously", async () => {
@@ -352,7 +353,7 @@ describe("Responsive Style Resolver - resolveResponsiveStyle (async)", () => {
352
353
 
353
354
  test("should resolve responsive styles with default breakpoints on fetch error", async () => {
354
355
  // Mock fetch to fail
355
- global.fetch = (() => Promise.reject(new Error('Network error'))) as unknown as typeof fetch;
356
+ globalThis.fetch = (() => Promise.reject(new Error('Network error'))) as unknown as typeof fetch;
356
357
 
357
358
  const style: ResponsiveStyleObject = {
358
359
  base: {
@@ -371,7 +372,7 @@ describe("Responsive Style Resolver - resolveResponsiveStyle (async)", () => {
371
372
 
372
373
  test("should handle async breakpoint config loading", async () => {
373
374
  // Mock successful fetch
374
- global.fetch = (() => Promise.resolve({
375
+ globalThis.fetch = (() => Promise.resolve({
375
376
  json: () => Promise.resolve({
376
377
  breakpoints: {
377
378
  tablet: 900,
@@ -396,9 +397,13 @@ describe("Responsive Style Resolver - resolveResponsiveStyle (async)", () => {
396
397
  });
397
398
 
398
399
  describe("Responsive Style Resolver - initializeBreakpoints", () => {
400
+ afterEach(() => {
401
+ globalThis.fetch = _originalFetch;
402
+ });
403
+
399
404
  test("should initialize breakpoint config", async () => {
400
405
  // Mock successful fetch
401
- global.fetch = (() => Promise.resolve({
406
+ globalThis.fetch = (() => Promise.resolve({
402
407
  json: () => Promise.resolve({
403
408
  breakpoints: {
404
409
  tablet: 900,
@@ -420,7 +425,7 @@ describe("Responsive Style Resolver - initializeBreakpoints", () => {
420
425
 
421
426
  test("should handle initialization error gracefully", async () => {
422
427
  // Mock fetch to fail
423
- global.fetch = (() => Promise.reject(new Error('Network error'))) as unknown as typeof fetch;
428
+ globalThis.fetch = (() => Promise.reject(new Error('Network error'))) as unknown as typeof fetch;
424
429
 
425
430
  // Should not throw
426
431
  await expect(initializeBreakpoints()).resolves.toBeUndefined();