meno-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/bin/cli.ts +281 -0
  2. package/build-static.ts +298 -0
  3. package/bunfig.toml +39 -0
  4. package/entries/client-router.tsx +111 -0
  5. package/entries/server-router.tsx +71 -0
  6. package/lib/client/ClientInitializer.test.ts +9 -0
  7. package/lib/client/ClientInitializer.test.ts.skip +92 -0
  8. package/lib/client/ClientInitializer.ts +60 -0
  9. package/lib/client/ErrorBoundary.test.tsx +595 -0
  10. package/lib/client/ErrorBoundary.tsx +230 -0
  11. package/lib/client/componentRegistry.test.ts +165 -0
  12. package/lib/client/componentRegistry.ts +18 -0
  13. package/lib/client/contexts/ThemeContext.tsx +73 -0
  14. package/lib/client/core/ComponentBuilder.test.ts +677 -0
  15. package/lib/client/core/ComponentBuilder.ts +660 -0
  16. package/lib/client/core/ComponentRenderer.test.tsx +176 -0
  17. package/lib/client/core/ComponentRenderer.tsx +83 -0
  18. package/lib/client/core/cmsTemplateProcessor.ts +129 -0
  19. package/lib/client/elementRegistry.ts +81 -0
  20. package/lib/client/hmr/HMRManager.tsx +179 -0
  21. package/lib/client/hmr/index.ts +5 -0
  22. package/lib/client/hmrWebSocket.test.ts +9 -0
  23. package/lib/client/hmrWebSocket.ts +250 -0
  24. package/lib/client/hooks/useColorVariables.test.ts +166 -0
  25. package/lib/client/hooks/useColorVariables.ts +249 -0
  26. package/lib/client/hooks/usePropertyAutocomplete.test.ts +9 -0
  27. package/lib/client/hooks/usePropertyAutocomplete.ts +40 -0
  28. package/lib/client/hydration/HydrationUtils.test.ts +154 -0
  29. package/lib/client/hydration/HydrationUtils.ts +35 -0
  30. package/lib/client/i18nConfigService.test.ts +74 -0
  31. package/lib/client/i18nConfigService.ts +78 -0
  32. package/lib/client/index.ts +56 -0
  33. package/lib/client/navigation.test.ts +441 -0
  34. package/lib/client/navigation.ts +23 -0
  35. package/lib/client/responsiveStyleResolver.test.ts +491 -0
  36. package/lib/client/responsiveStyleResolver.ts +184 -0
  37. package/lib/client/routing/RouteLoader.test.ts +635 -0
  38. package/lib/client/routing/RouteLoader.ts +347 -0
  39. package/lib/client/routing/Router.tsx +382 -0
  40. package/lib/client/scripts/ScriptExecutor.test.ts +489 -0
  41. package/lib/client/scripts/ScriptExecutor.ts +171 -0
  42. package/lib/client/scripts/formHandler.ts +103 -0
  43. package/lib/client/styleProcessor.test.ts +126 -0
  44. package/lib/client/styleProcessor.ts +92 -0
  45. package/lib/client/styles/StyleInjector.test.ts +354 -0
  46. package/lib/client/styles/StyleInjector.ts +154 -0
  47. package/lib/client/templateEngine.test.ts +660 -0
  48. package/lib/client/templateEngine.ts +667 -0
  49. package/lib/client/theme.test.ts +173 -0
  50. package/lib/client/theme.ts +159 -0
  51. package/lib/client/utils/toast.ts +46 -0
  52. package/lib/server/createServer.ts +170 -0
  53. package/lib/server/cssGenerator.test.ts +172 -0
  54. package/lib/server/cssGenerator.ts +58 -0
  55. package/lib/server/fileWatcher.ts +134 -0
  56. package/lib/server/index.ts +55 -0
  57. package/lib/server/jsonLoader.test.ts +103 -0
  58. package/lib/server/jsonLoader.ts +350 -0
  59. package/lib/server/middleware/cors.test.ts +177 -0
  60. package/lib/server/middleware/cors.ts +69 -0
  61. package/lib/server/middleware/errorHandler.test.ts +208 -0
  62. package/lib/server/middleware/errorHandler.ts +63 -0
  63. package/lib/server/middleware/index.ts +9 -0
  64. package/lib/server/middleware/logger.test.ts +233 -0
  65. package/lib/server/middleware/logger.ts +99 -0
  66. package/lib/server/pageCache.test.ts +167 -0
  67. package/lib/server/pageCache.ts +97 -0
  68. package/lib/server/projectContext.ts +51 -0
  69. package/lib/server/providers/fileSystemCMSProvider.test.ts +292 -0
  70. package/lib/server/providers/fileSystemCMSProvider.ts +227 -0
  71. package/lib/server/providers/fileSystemPageProvider.ts +83 -0
  72. package/lib/server/routes/api/cms.test.ts +177 -0
  73. package/lib/server/routes/api/cms.ts +82 -0
  74. package/lib/server/routes/api/colors.ts +59 -0
  75. package/lib/server/routes/api/components.ts +70 -0
  76. package/lib/server/routes/api/config.test.ts +9 -0
  77. package/lib/server/routes/api/config.ts +28 -0
  78. package/lib/server/routes/api/core-routes.ts +182 -0
  79. package/lib/server/routes/api/functions.ts +170 -0
  80. package/lib/server/routes/api/index.ts +69 -0
  81. package/lib/server/routes/api/pages.ts +95 -0
  82. package/lib/server/routes/api/shared.test.ts +81 -0
  83. package/lib/server/routes/api/shared.ts +31 -0
  84. package/lib/server/routes/editor.test.ts +9 -0
  85. package/lib/server/routes/index.ts +104 -0
  86. package/lib/server/routes/pages.ts +161 -0
  87. package/lib/server/routes/static.ts +107 -0
  88. package/lib/server/services/ColorService.ts +193 -0
  89. package/lib/server/services/cmsService.test.ts +388 -0
  90. package/lib/server/services/cmsService.ts +296 -0
  91. package/lib/server/services/componentService.test.ts +276 -0
  92. package/lib/server/services/componentService.ts +346 -0
  93. package/lib/server/services/configService.ts +156 -0
  94. package/lib/server/services/fileWatcherService.ts +67 -0
  95. package/lib/server/services/index.ts +10 -0
  96. package/lib/server/services/pageService.test.ts +258 -0
  97. package/lib/server/services/pageService.ts +240 -0
  98. package/lib/server/ssrRenderer.test.ts +1005 -0
  99. package/lib/server/ssrRenderer.ts +878 -0
  100. package/lib/server/utilityClassGenerator.ts +11 -0
  101. package/lib/server/utils/index.ts +5 -0
  102. package/lib/server/utils/jsonLineMapper.test.ts +100 -0
  103. package/lib/server/utils/jsonLineMapper.ts +166 -0
  104. package/lib/server/validateStyleCoverage.test.ts +9 -0
  105. package/lib/server/validateStyleCoverage.ts +167 -0
  106. package/lib/server/websocketManager.test.ts +9 -0
  107. package/lib/server/websocketManager.ts +95 -0
  108. package/lib/shared/attributeNodeUtils.test.ts +152 -0
  109. package/lib/shared/attributeNodeUtils.ts +50 -0
  110. package/lib/shared/breakpoints.test.ts +166 -0
  111. package/lib/shared/breakpoints.ts +65 -0
  112. package/lib/shared/colorProperties.test.ts +111 -0
  113. package/lib/shared/colorProperties.ts +40 -0
  114. package/lib/shared/colorVariableUtils.test.ts +319 -0
  115. package/lib/shared/colorVariableUtils.ts +97 -0
  116. package/lib/shared/constants.test.ts +175 -0
  117. package/lib/shared/constants.ts +116 -0
  118. package/lib/shared/cssGeneration.ts +481 -0
  119. package/lib/shared/cssProperties.test.ts +252 -0
  120. package/lib/shared/cssProperties.ts +338 -0
  121. package/lib/shared/elementUtils.test.ts +245 -0
  122. package/lib/shared/elementUtils.ts +90 -0
  123. package/lib/shared/fontLoader.ts +97 -0
  124. package/lib/shared/i18n.test.ts +313 -0
  125. package/lib/shared/i18n.ts +286 -0
  126. package/lib/shared/index.ts +50 -0
  127. package/lib/shared/interfaces/contentProvider.test.ts +9 -0
  128. package/lib/shared/interfaces/contentProvider.ts +121 -0
  129. package/lib/shared/nodeUtils.test.ts +320 -0
  130. package/lib/shared/nodeUtils.ts +220 -0
  131. package/lib/shared/pathArrayUtils.test.ts +315 -0
  132. package/lib/shared/pathArrayUtils.ts +17 -0
  133. package/lib/shared/pathUtils.test.ts +260 -0
  134. package/lib/shared/pathUtils.ts +244 -0
  135. package/lib/shared/paths/Path.test.ts +74 -0
  136. package/lib/shared/paths/Path.ts +23 -0
  137. package/lib/shared/paths/PathConverter.test.ts +232 -0
  138. package/lib/shared/paths/PathConverter.ts +141 -0
  139. package/lib/shared/paths/PathUtils.ts +290 -0
  140. package/lib/shared/paths/PathValidator.test.ts +193 -0
  141. package/lib/shared/paths/PathValidator.ts +53 -0
  142. package/lib/shared/paths/index.ts +48 -0
  143. package/lib/shared/propResolver.test.ts +639 -0
  144. package/lib/shared/propResolver.ts +124 -0
  145. package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +190 -0
  146. package/lib/shared/registry/BaseNodeTypeRegistry.ts +200 -0
  147. package/lib/shared/registry/ClientNodeTypeRegistry.ts +34 -0
  148. package/lib/shared/registry/ClientRegistry.test.ts +26 -0
  149. package/lib/shared/registry/ClientRegistry.ts +15 -0
  150. package/lib/shared/registry/ComponentRegistry.test.ts +293 -0
  151. package/lib/shared/registry/ComponentRegistry.ts +100 -0
  152. package/lib/shared/registry/NodeTypeDefinition.ts +198 -0
  153. package/lib/shared/registry/NodeTypeManager.ts +94 -0
  154. package/lib/shared/registry/RegistryManager.test.ts +58 -0
  155. package/lib/shared/registry/RegistryManager.ts +60 -0
  156. package/lib/shared/registry/SSRNodeTypeRegistry.ts +33 -0
  157. package/lib/shared/registry/SSRRegistry.test.ts +26 -0
  158. package/lib/shared/registry/SSRRegistry.ts +15 -0
  159. package/lib/shared/registry/createNodeType.ts +175 -0
  160. package/lib/shared/registry/defineNodeType.ts +73 -0
  161. package/lib/shared/registry/fieldPresets.ts +109 -0
  162. package/lib/shared/registry/index.ts +50 -0
  163. package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +71 -0
  164. package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +61 -0
  165. package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +88 -0
  166. package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +66 -0
  167. package/lib/shared/registry/nodeTypes/ObjectLinkNodeType.ts +75 -0
  168. package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +49 -0
  169. package/lib/shared/registry/nodeTypes/TextNodeType.ts +52 -0
  170. package/lib/shared/registry/nodeTypes/index.ts +75 -0
  171. package/lib/shared/responsiveScaling.test.ts +268 -0
  172. package/lib/shared/responsiveScaling.ts +194 -0
  173. package/lib/shared/responsiveStyleUtils.test.ts +300 -0
  174. package/lib/shared/responsiveStyleUtils.ts +139 -0
  175. package/lib/shared/slugTranslator.test.ts +325 -0
  176. package/lib/shared/slugTranslator.ts +177 -0
  177. package/lib/shared/styleNodeUtils.test.ts +132 -0
  178. package/lib/shared/styleNodeUtils.ts +102 -0
  179. package/lib/shared/styleUtils.test.ts +238 -0
  180. package/lib/shared/styleUtils.ts +63 -0
  181. package/lib/shared/themeDefaults.test.ts +113 -0
  182. package/lib/shared/themeDefaults.ts +103 -0
  183. package/lib/shared/tree/PathBuilder.ts +383 -0
  184. package/lib/shared/treePathUtils.test.ts +539 -0
  185. package/lib/shared/treePathUtils.ts +339 -0
  186. package/lib/shared/types/api.ts +58 -0
  187. package/lib/shared/types/cms.ts +95 -0
  188. package/lib/shared/types/colors.ts +45 -0
  189. package/lib/shared/types/components.ts +121 -0
  190. package/lib/shared/types/errors.test.ts +103 -0
  191. package/lib/shared/types/errors.ts +69 -0
  192. package/lib/shared/types/index.ts +96 -0
  193. package/lib/shared/types/nodes.ts +20 -0
  194. package/lib/shared/types/rendering.ts +61 -0
  195. package/lib/shared/types/styles.ts +38 -0
  196. package/lib/shared/types.ts +11 -0
  197. package/lib/shared/utilityClassConfig.ts +287 -0
  198. package/lib/shared/utilityClassMapper.test.ts +140 -0
  199. package/lib/shared/utilityClassMapper.ts +229 -0
  200. package/lib/shared/utils/fileUtils.test.ts +99 -0
  201. package/lib/shared/utils/fileUtils.ts +56 -0
  202. package/lib/shared/utils.test.ts +261 -0
  203. package/lib/shared/utils.ts +84 -0
  204. package/lib/shared/validation/index.ts +7 -0
  205. package/lib/shared/validation/propValidator.test.ts +178 -0
  206. package/lib/shared/validation/propValidator.ts +238 -0
  207. package/lib/shared/validation/schemas.test.ts +177 -0
  208. package/lib/shared/validation/schemas.ts +401 -0
  209. package/lib/shared/validation/validators.test.ts +109 -0
  210. package/lib/shared/validation/validators.ts +304 -0
  211. package/lib/test-utils/dom-setup.ts +55 -0
  212. package/lib/test-utils/factories/ConsoleMockFactory.ts +200 -0
  213. package/lib/test-utils/factories/DomMockFactory.ts +487 -0
  214. package/lib/test-utils/factories/EventMockFactory.ts +244 -0
  215. package/lib/test-utils/factories/FetchMockFactory.ts +210 -0
  216. package/lib/test-utils/factories/ServerMockFactory.ts +223 -0
  217. package/lib/test-utils/factories/StoreMockFactory.ts +370 -0
  218. package/lib/test-utils/factories/index.ts +11 -0
  219. package/lib/test-utils/fixtures.ts +134 -0
  220. package/lib/test-utils/helpers/asyncHelpers.test.ts +112 -0
  221. package/lib/test-utils/helpers/asyncHelpers.ts +196 -0
  222. package/lib/test-utils/helpers/index.ts +6 -0
  223. package/lib/test-utils/helpers.test.ts +73 -0
  224. package/lib/test-utils/helpers.ts +90 -0
  225. package/lib/test-utils/index.ts +17 -0
  226. package/lib/test-utils/mockFactories.ts +92 -0
  227. package/lib/test-utils/mocks.ts +341 -0
  228. package/package.json +38 -0
  229. package/templates/index-router.html +34 -0
  230. package/tsconfig.json +14 -0
  231. package/vite.config.ts +43 -0
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Shared constants used across the application
3
+ */
4
+
5
+ // Read PORT from environment variable, fallback to 3000
6
+ // Safe for browser environments where process is not defined
7
+ export const SERVER_PORT = typeof process !== 'undefined' && process.env?.PORT
8
+ ? parseInt(process.env.PORT, 10)
9
+ : 3000;
10
+
11
+ export const API_ROUTES = {
12
+ PAGES: '/api/pages',
13
+ COMPONENTS: '/api/components',
14
+ YAML: '/api/yaml', // Legacy route name, now serves JSON
15
+ PAGE_DATA: '/api/page-data', // Returns parsed JSON for a specific page
16
+ COMPONENT_DATA: '/api/component-data', // Returns parsed JSON for a specific component
17
+ SAVE_PAGE: '/api/save-page',
18
+ SAVE_COMPONENT: '/api/save-component',
19
+ SAVE_COMPONENT_JS: '/api/save-component-js', // Save JavaScript to .js file
20
+ SAVE_COMPONENT_CSS: '/api/save-component-css', // Save CSS to .css file
21
+ COMPONENT_JS: '/api/component-js', // Get JavaScript from .js file
22
+ CONFIG: '/api/config', // Get project config
23
+ SAVE_CONFIG: '/api/save-config', // Save project config
24
+ SELECTION: '/api/selection', // Editor selection state (for AI integration)
25
+ // CMS API routes
26
+ CMS_COLLECTIONS: '/api/cms/collections', // List all CMS collections
27
+ CMS_BASE: '/api/cms', // Base path for CMS item operations
28
+ // Colors API routes
29
+ COLORS_CONFIG: '/api/colors-config', // Get full colors config
30
+ SAVE_COLORS: '/api/save-colors', // Save colors config
31
+ } as const;
32
+
33
+ export const HMR_ROUTE = '/hmr';
34
+
35
+ export const FILE_PATTERNS = {
36
+ PAGES: './pages',
37
+ COMPONENTS: './components',
38
+ } as const;
39
+
40
+ export const DEFAULT_TIMEOUT = 5000;
41
+
42
+ export const WEBSOCKET_STATES = {
43
+ CONNECTING: 0,
44
+ OPEN: 1,
45
+ CLOSING: 2,
46
+ CLOSED: 3,
47
+ } as const;
48
+
49
+ // Timeout constants
50
+ export const NOT_FOUND_TIMEOUT_MS = 300;
51
+ export const TAB_SWITCH_DELAY_MS = 100;
52
+ export const IFRAME_HIGHLIGHT_DELAY_MS = 100;
53
+ export const TREE_SCROLL_DELAY_MS = 200;
54
+ export const HOVER_HIGHLIGHT_DELAY_MS = 50; // Delay before highlighting tree item after expansion
55
+
56
+ // Server configuration
57
+ export const MAX_PORT_ATTEMPTS = 10;
58
+
59
+ // Message types for iframe-parent communication
60
+ export const IFRAME_MESSAGE_TYPES = {
61
+ DELETE_ELEMENT: 'DELETE_ELEMENT',
62
+ TOGGLE_SELECTING_MODE: 'TOGGLE_SELECTING_MODE',
63
+ SET_TAB: 'SET_TAB',
64
+ COPY_ELEMENT: 'COPY_ELEMENT',
65
+ PASTE_ELEMENT: 'PASTE_ELEMENT',
66
+ ARROW_UP: 'ARROW_UP',
67
+ ARROW_DOWN: 'ARROW_DOWN',
68
+ ARROW_LEFT: 'ARROW_LEFT',
69
+ ARROW_RIGHT: 'ARROW_RIGHT',
70
+ OPEN_COMMAND_PALETTE: 'OPEN_COMMAND_PALETTE',
71
+ EDIT_COMPONENT: 'EDIT_COMPONENT',
72
+ NAVIGATE_BACK: 'NAVIGATE_BACK',
73
+ TOGGLE_ADDING_STYLE: 'TOGGLE_ADDING_STYLE',
74
+ MOVE_ELEMENT_UP: 'MOVE_ELEMENT_UP',
75
+ MOVE_ELEMENT_DOWN: 'MOVE_ELEMENT_DOWN',
76
+ SELECTION_CHANGED: 'SELECTION_CHANGED',
77
+ CMS_CONTEXT_UPDATE: 'CMS_CONTEXT_UPDATE',
78
+ CMS_CONTEXT_REQUEST: 'CMS_CONTEXT_REQUEST',
79
+ } as const;
80
+
81
+ // Component node type constants
82
+ export const NODE_TYPE = {
83
+ NODE: 'node',
84
+ COMPONENT: 'component',
85
+ SLOT: 'slot',
86
+ EMBED: 'embed',
87
+ OBJECT_LINK: 'object-link',
88
+ LOCALE_LIST: 'locale-list',
89
+ TEXT: 'text',
90
+ } as const;
91
+
92
+ export type NodeType = typeof NODE_TYPE[keyof typeof NODE_TYPE];
93
+
94
+ // Special path identifiers for component editing
95
+ export const SPECIAL_PATHS = {
96
+ COMPONENT_INTERFACE: 'component_interface',
97
+ COMPONENT_JAVASCRIPT: 'component_javascript',
98
+ COMPONENT_CSS: 'component_css',
99
+ STRUCTURE_STYLE: 'structure_style',
100
+ } as const;
101
+
102
+ // Component type configuration
103
+ export interface ComponentTypeConfig {
104
+ id: string;
105
+ label: string;
106
+ color: string;
107
+ }
108
+
109
+ export const DEFAULT_COMPONENT_TYPES: ComponentTypeConfig[] = [
110
+ { id: 'ui', label: 'UI', color: '#3b82f6' },
111
+ { id: 'layout', label: 'Layout', color: '#10b981' },
112
+ { id: 'content', label: 'Content', color: '#f59e0b' },
113
+ ];
114
+
115
+ export const DEFAULT_ICON_COLOR = '#6b7280';
116
+
@@ -0,0 +1,481 @@
1
+ /**
2
+ * Shared CSS Generation Module
3
+ * Unified CSS generation logic for both editor and static render systems
4
+ * Used by both client and server to ensure consistent CSS output
5
+ */
6
+
7
+ import { prefixToCSSProperty, propertyMap } from './utilityClassConfig';
8
+ import type { BreakpointConfig } from './breakpoints';
9
+ import { DEFAULT_BREAKPOINTS } from './breakpoints';
10
+ import type { ResponsiveScales } from './responsiveScaling';
11
+ import { scalePropertyValue } from './responsiveScaling';
12
+
13
+ /**
14
+ * Escape special characters in CSS class names using backslash notation
15
+ * Characters that have special meaning in CSS selectors need to be escaped
16
+ */
17
+ function escapeCSSClassName(className: string): string {
18
+ // Escape special characters with backslash
19
+ // This includes: . # [ ] ( ) { } ; : , > + ~ etc.
20
+ return className.replace(/[.#[\](){};<>+~:,\s]/g, '\\$&');
21
+ }
22
+
23
+ // Define all possible utility class rules
24
+ const utilityClassRules: Record<string, string> = {
25
+ // Display utilities (short forms from specialValueMappings)
26
+ f: 'display: flex;',
27
+ 'fd-col': 'flex-direction: column;',
28
+ 'fd-row': 'flex-direction: row;',
29
+ g: 'display: grid;',
30
+ b: 'display: block;',
31
+ i: 'display: inline;',
32
+ ib: 'display: inline-block;',
33
+ h: 'display: none;',
34
+
35
+ // Justify Content (short forms)
36
+ 'jc-c': 'justify-content: center;',
37
+ 'jc-s': 'justify-content: flex-start;',
38
+ 'jc-e': 'justify-content: flex-end;',
39
+ 'jc-b': 'justify-content: space-between;',
40
+ 'jc-a': 'justify-content: space-around;',
41
+
42
+ // Align Items (short forms)
43
+ 'ai-c': 'align-items: center;',
44
+ 'ai-s': 'align-items: flex-start;',
45
+ 'ai-e': 'align-items: flex-end;',
46
+ 'ai-b': 'align-items: baseline;',
47
+
48
+ // Overflow (short forms)
49
+ 'o-h': 'overflow: hidden;',
50
+ 'o-a': 'overflow: auto;',
51
+ 'o-s': 'overflow: scroll;',
52
+ 'o-v': 'overflow: visible;',
53
+
54
+ // Cursor (full forms)
55
+ 'cursor-pointer': 'cursor: pointer;',
56
+ 'cursor-default': 'cursor: default;',
57
+ };
58
+
59
+ /**
60
+ * Extract CSS property and value from a utility class
61
+ * Returns { property: CSS property name, value: CSS value } or null if not a dynamic class
62
+ */
63
+ function extractPropertyAndValue(className: string): { property: string; value: string } | null {
64
+ // Parse prefix-value pattern (e.g., "p-10px", "fs-48px")
65
+ const knownPrefixes = Object.keys(prefixToCSSProperty).sort((a, b) => b.length - a.length);
66
+
67
+ for (const knownPrefix of knownPrefixes) {
68
+ if (className.startsWith(knownPrefix + '-')) {
69
+ const classValue = className.substring(knownPrefix.length + 1);
70
+ const cssProp = prefixToCSSProperty[knownPrefix];
71
+ if (cssProp) {
72
+ return { property: cssProp, value: classValue };
73
+ }
74
+ }
75
+ }
76
+
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * Generate CSS rule for a utility class
82
+ * Handles dynamic classes like p-10px, m-20px, fs-48px, etc.
83
+ */
84
+ export function generateRuleForClass(className: string): string | null {
85
+ // Check if it's a predefined rule
86
+ if (utilityClassRules[className]) {
87
+ return utilityClassRules[className];
88
+ }
89
+
90
+ // Parse prefix-value pattern (e.g., "p-10px", "fs-48px", "p-92px-0", "bgc-background-light")
91
+ // Try to match known prefixes first by checking longest prefixes first
92
+ const knownPrefixes = Object.keys(prefixToCSSProperty).sort((a, b) => b.length - a.length);
93
+
94
+ let prefix = '';
95
+ let classValue = '';
96
+
97
+ for (const knownPrefix of knownPrefixes) {
98
+ if (className.startsWith(knownPrefix + '-')) {
99
+ prefix = knownPrefix;
100
+ classValue = className.substring(knownPrefix.length + 1); // +1 for the hyphen
101
+ break;
102
+ }
103
+ }
104
+
105
+ if (!prefix || !classValue) return null;
106
+
107
+ // Look up the CSS property from prefix
108
+ const cssProp = prefixToCSSProperty[prefix];
109
+ if (!cssProp) return null;
110
+
111
+ // Handle border with special syntax BEFORE hyphen conversion (e.g., b-1px-solid-text)
112
+ // This needs the original hyphenated format to parse correctly
113
+ if (prefix === 'b' && classValue.includes('-')) {
114
+ const parts = classValue.split('-');
115
+ const width = parts[0];
116
+ const borderStyle = parts[1] || 'solid';
117
+ const borderColor = parts[2] ? `var(--${parts[2]})` : 'currentColor';
118
+ return `border: ${width} ${borderStyle} ${borderColor};`;
119
+ }
120
+
121
+ // Handle border-radius with CSS variables (e.g., br-background)
122
+ if (prefix === 'br' && classValue.includes('-') && !classValue.match(/^\d+px$/)) {
123
+ return `border-radius: var(--${classValue});`;
124
+ }
125
+
126
+ // Convert hyphenated values back to spaces, but preserve negative numbers
127
+ // Handle cases like:
128
+ // - "92px-0" → "92px 0" (space between values)
129
+ // - "-0.015" → "-0.015" (negative number)
130
+ // - "0-5px" → "0 5px" (space between values)
131
+ // - "1px-solid" → "1px solid" (border shorthand)
132
+ let value = classValue;
133
+
134
+ // Pattern to match CSS values that should be preceded by a space instead of hyphen
135
+ // Includes: digits, auto, inherit, initial, unset, border style keywords, and CSS function names
136
+ const valuePattern = /\d|auto|inherit|initial|unset|solid|dashed|dotted|double|groove|ridge|inset|outset|none|hidden|minmax|repeat|clamp|calc|min|max|fit-content|var/;
137
+
138
+ // If value starts with hyphen, preserve it as a negative number
139
+ if (value.startsWith('-')) {
140
+ // For negative values, only replace hyphens that come after the first character
141
+ // This way "-0.015" stays as is, but "-10px-5px" becomes "-10px 5px"
142
+ value = value[0] + value.substring(1).replace(new RegExp(`-(?=${valuePattern.source})`, 'g'), ' ');
143
+ } else {
144
+ // For positive values, replace hyphens normally
145
+ value = value.replace(new RegExp(`-(?=${valuePattern.source})`, 'g'), ' ');
146
+ }
147
+
148
+ // Convert 'p' suffix to '%' for percentage values (e.g., "50p" → "50%")
149
+ value = value.replace(/(\d+)p(?!\w)/g, '$1%');
150
+
151
+ // Handle special multi-property prefixes
152
+ if (prefix === 'px') {
153
+ // padding-left and padding-right
154
+ return `padding-left: ${value}; padding-right: ${value};`;
155
+ }
156
+ if (prefix === 'py') {
157
+ // padding-top and padding-bottom
158
+ return `padding-top: ${value}; padding-bottom: ${value};`;
159
+ }
160
+ if (prefix === 'mx') {
161
+ // margin-left and margin-right
162
+ return `margin-left: ${value}; margin-right: ${value};`;
163
+ }
164
+ if (prefix === 'my') {
165
+ // margin-top and margin-bottom
166
+ return `margin-top: ${value}; margin-bottom: ${value};`;
167
+ }
168
+
169
+ // Handle CSS variables for color properties
170
+ // For color (c), background-color (bgc), and border-color (bc), treat as CSS variables
171
+ // unless it's a hex color (#...) or RGB value
172
+ if ((prefix === 'bgc' || prefix === 'c' || prefix === 'bc')) {
173
+ // If it's not a hex color, RGB value, or pixel value, treat it as a CSS variable name
174
+ if (!value.startsWith('#') && !value.includes('rgb') && !value.includes('px')) {
175
+ return `${cssProp}: var(--${value});`;
176
+ }
177
+ }
178
+
179
+ // Standard case: prefix-value (e.g., p-10px, fs-48px, ta-center)
180
+ return `${cssProp}: ${value};`;
181
+ }
182
+
183
+ /**
184
+ * Generate CSS for all utility classes used in the application
185
+ * Scans through all classes and generates the necessary CSS rules
186
+ * Optionally applies responsive scaling based on configuration
187
+ */
188
+ export function generateUtilityCSS(
189
+ usedClasses: Set<string>,
190
+ breakpoints: BreakpointConfig = DEFAULT_BREAKPOINTS,
191
+ responsiveScales?: ResponsiveScales
192
+ ): string {
193
+ const css: string[] = [];
194
+ const baseClasses = new Set<string>();
195
+ const autoResponsiveClasses = new Set<string>(); // Classes that should get auto-scaling
196
+
197
+ // Create a map for responsive breakpoint classes
198
+ // Map from prefix (e.g., 't', 'm') to the class name and breakpoint info
199
+ type BreakpointClassMap = Record<string, { classes: Set<string>; breakpointName: string; value: number }>;
200
+ const responsiveClasses: BreakpointClassMap = {};
201
+
202
+ // Initialize responsive class sets for each breakpoint
203
+ for (const [breakpointName, breakpointValue] of Object.entries(breakpoints)) {
204
+ // Generate prefix from breakpoint name, avoiding conflicts with property prefixes
205
+ // For 'mobile', use 'mob' to avoid conflict with 'margin' (m-), etc.
206
+ let prefix = breakpointName.charAt(0).toLowerCase();
207
+ if (breakpointName.toLowerCase() === 'mobile') {
208
+ prefix = 'mob'; // Use 'mob' instead of 'm' to avoid margin conflict
209
+ }
210
+
211
+ responsiveClasses[prefix] = {
212
+ classes: new Set<string>(),
213
+ breakpointName,
214
+ value: breakpointValue
215
+ };
216
+ }
217
+
218
+ // Separate classes by breakpoint
219
+ for (const className of usedClasses) {
220
+ let matched = false;
221
+
222
+ // Try to match against any responsive breakpoint prefix
223
+ for (const prefix of Object.keys(responsiveClasses)) {
224
+ if (className.startsWith(`${prefix}-`) && className.length > prefix.length + 1) {
225
+ const potentialClass = className.substring(prefix.length + 1);
226
+ const rule = generateRuleForClass(potentialClass);
227
+
228
+ // If it generates a valid rule and doesn't look like a margin value, treat as breakpoint class
229
+ // Margin values are typically: auto, 0, or pixel/percentage values (like 10px, 92px, 50p)
230
+ if (rule && !potentialClass.match(/^(auto|0|[\d.]+px|[\d.]+p)$/)) {
231
+ responsiveClasses[prefix].classes.add(potentialClass);
232
+ matched = true;
233
+ break;
234
+ }
235
+ }
236
+ }
237
+
238
+ if (!matched) {
239
+ baseClasses.add(className);
240
+
241
+ // Check if this class should get auto-responsive scaling
242
+ if (responsiveScales?.enabled) {
243
+ const propValue = extractPropertyAndValue(className);
244
+ if (propValue) {
245
+ // Check if this property type has scaling configured
246
+ const typeMap: Record<string, string> = {
247
+ 'padding': 'padding',
248
+ 'padding-left': 'padding',
249
+ 'padding-right': 'padding',
250
+ 'padding-top': 'padding',
251
+ 'padding-bottom': 'padding',
252
+ 'margin': 'margin',
253
+ 'margin-left': 'margin',
254
+ 'margin-right': 'margin',
255
+ 'margin-top': 'margin',
256
+ 'margin-bottom': 'margin',
257
+ 'font-size': 'fontSize',
258
+ 'gap': 'gap',
259
+ 'row-gap': 'gap',
260
+ 'column-gap': 'gap',
261
+ };
262
+
263
+ const category = typeMap[propValue.property];
264
+ if (category && responsiveScales[category as keyof ResponsiveScales]) {
265
+ autoResponsiveClasses.add(className);
266
+ }
267
+ }
268
+ }
269
+ }
270
+ }
271
+
272
+ // Generate base rules
273
+ for (const className of baseClasses) {
274
+ const rule = generateRuleForClass(className);
275
+ if (rule) {
276
+ // Escape special characters in class name for CSS selector
277
+ const escapedClassName = escapeCSSClassName(className);
278
+ css.push(`.${escapedClassName} { ${rule} }`);
279
+ }
280
+ }
281
+
282
+ // Map to collect auto-responsive media queries by breakpoint
283
+ type MediaQueryMap = Record<string, { classes: Array<{ className: string; rule: string }>; value: number }>;
284
+ const autoResponsiveMediaQueries: MediaQueryMap = {};
285
+
286
+ // Generate auto-responsive rules for classes with enabled scaling
287
+ if (responsiveScales?.enabled) {
288
+ const typeMap: Record<string, string> = {
289
+ 'padding': 'padding',
290
+ 'padding-left': 'padding',
291
+ 'padding-right': 'padding',
292
+ 'padding-top': 'padding',
293
+ 'padding-bottom': 'padding',
294
+ 'margin': 'margin',
295
+ 'margin-left': 'margin',
296
+ 'margin-right': 'margin',
297
+ 'margin-top': 'margin',
298
+ 'margin-bottom': 'margin',
299
+ 'font-size': 'fontSize',
300
+ 'gap': 'gap',
301
+ 'row-gap': 'gap',
302
+ 'column-gap': 'gap',
303
+ };
304
+
305
+ for (const className of autoResponsiveClasses) {
306
+ const propValue = extractPropertyAndValue(className);
307
+ if (!propValue) continue;
308
+
309
+ const category = typeMap[propValue.property];
310
+ if (!category) continue;
311
+
312
+ const scaleConfig = responsiveScales[category as keyof ResponsiveScales] as any;
313
+ if (!scaleConfig) continue;
314
+
315
+ const baseRef = responsiveScales.baseReference || 16;
316
+ const escapedClassName = escapeCSSClassName(className);
317
+
318
+ // Generate scaled rules for each breakpoint
319
+ for (const [breakpointName, breakpointValue] of Object.entries(breakpoints)) {
320
+ const scaleType = breakpointName === 'mobile' ? 'mobile' : 'tablet';
321
+ const scale = scaleConfig[scaleType];
322
+ if (!scale) continue;
323
+
324
+ const scaledValue = scalePropertyValue(propValue.value, baseRef, scale);
325
+ if (!scaledValue) continue;
326
+
327
+ // Initialize media query entry if not exists
328
+ if (!autoResponsiveMediaQueries[breakpointName]) {
329
+ autoResponsiveMediaQueries[breakpointName] = {
330
+ classes: [],
331
+ value: breakpointValue
332
+ };
333
+ }
334
+
335
+ autoResponsiveMediaQueries[breakpointName].classes.push({
336
+ className: escapedClassName,
337
+ rule: `${propValue.property}: ${scaledValue};`
338
+ });
339
+ }
340
+ }
341
+ }
342
+
343
+ // Generate media queries in two separate sections:
344
+ // 1. Auto-responsive classes FIRST (baseline responsive behavior)
345
+ // 2. Manual breakpoint classes AFTER (explicit overrides)
346
+ // This ensures manual t-, mob- classes always override auto-responsive rules
347
+
348
+ // Sort auto-responsive media queries by breakpoint value (descending)
349
+ const sortedAutoResponsive = Object.entries(autoResponsiveMediaQueries)
350
+ .sort(([, a], [, b]) => b.value - a.value);
351
+
352
+ // Generate auto-responsive media queries first
353
+ for (const [breakpointName, mq] of sortedAutoResponsive) {
354
+ if (mq.classes.length === 0) continue;
355
+
356
+ css.push(`@media (max-width: ${mq.value}px) {`);
357
+ for (const { className, rule } of mq.classes) {
358
+ css.push(` .${className} { ${rule} }`);
359
+ }
360
+ css.push('}');
361
+ }
362
+
363
+ // Sort manual responsive classes by breakpoint value (descending)
364
+ const sortedManualResponsive = Object.entries(responsiveClasses)
365
+ .filter(([, info]) => info.classes.size > 0)
366
+ .sort(([, a], [, b]) => b.value - a.value);
367
+
368
+ // Generate manual breakpoint media queries after
369
+ for (const [prefix, breakpointInfo] of sortedManualResponsive) {
370
+ const rules: string[] = [];
371
+
372
+ for (const className of breakpointInfo.classes) {
373
+ const rule = generateRuleForClass(className);
374
+ if (rule) {
375
+ const escapedClassName = escapeCSSClassName(className);
376
+ rules.push(` .${prefix}-${escapedClassName} { ${rule} }`);
377
+ }
378
+ }
379
+
380
+ if (rules.length > 0) {
381
+ css.push(`@media (max-width: ${breakpointInfo.value}px) {`);
382
+ css.push(...rules);
383
+ css.push('}');
384
+ }
385
+ }
386
+
387
+ return css.join('\n');
388
+ }
389
+
390
+ /**
391
+ * Extract all utility classes from a rendered HTML string
392
+ * Identifies utility classes by checking against known prefixes from propertyMap
393
+ */
394
+ export function extractUtilityClassesFromHTML(html: string): Set<string> {
395
+ const classes = new Set<string>();
396
+ const classRegex = /class="([^"]*)"/g;
397
+ let match;
398
+
399
+ // Get all known utility prefixes from propertyMap
400
+ const knownPrefixes = new Set(Object.values(propertyMap));
401
+ // Also include special/short form prefixes from utilityClassRules
402
+ const specialPrefixes = new Set([
403
+ // Display short forms
404
+ 'f', 'fd-col', 'fd-row', 'g', 'b', 'i', 'ib', 'h',
405
+ // Justify content
406
+ 'jc-c', 'jc-s', 'jc-e', 'jc-b', 'jc-a',
407
+ // Align items
408
+ 'ai-c', 'ai-s', 'ai-e', 'ai-b',
409
+ // Overflow
410
+ 'o-h', 'o-a', 'o-s', 'o-v',
411
+ // Cursor
412
+ 'cursor-pointer', 'cursor-default',
413
+ ]);
414
+
415
+ while ((match = classRegex.exec(html)) !== null) {
416
+ const classString = match[1];
417
+ const classList = classString.split(/\s+/);
418
+
419
+ for (const className of classList) {
420
+ // Skip empty class names
421
+ if (!className || className.length === 0) continue;
422
+
423
+ // Check if it's a special/short form class
424
+ if (specialPrefixes.has(className)) {
425
+ classes.add(className);
426
+ continue;
427
+ }
428
+
429
+ // Check for responsive prefix (like 't-', 'mob-', 's-', etc.) and process accordingly
430
+ let classToCheck = className;
431
+ let hasResponsivePrefix = false;
432
+
433
+ // Extract responsive prefix if present
434
+ // Check for multi-letter prefix first (e.g., 'mob-' for mobile)
435
+ if (className.startsWith('mob-') && className.length > 4) {
436
+ classToCheck = className.substring(4); // Remove 'mob-' prefix
437
+ hasResponsivePrefix = true;
438
+ }
439
+ // Then check for single-letter prefixes (t-, s-, l-, x-, u-)
440
+ else if (className.length > 2 && className.charAt(1) === '-' && className.match(/^[a-z]-/)) {
441
+ const firstChar = className.charAt(0);
442
+ // Only treat as responsive prefix if it looks like a breakpoint indicator
443
+ // Common breakpoint prefixes: t (tablet), s (small), l (large), x (extra), u (ultra)
444
+ // NOTE: We exclude 'm' because it conflicts with margin prefix - use 'mob' instead
445
+ if (['t', 's', 'l', 'x', 'u'].includes(firstChar)) {
446
+ classToCheck = className.substring(2); // Remove responsive prefix
447
+ hasResponsivePrefix = true;
448
+ }
449
+ }
450
+
451
+ // Check if it starts with a known prefix
452
+ // Handle both single-letter (like 'p-', 'w-') and multi-letter (like 'jc-', 'ai-')
453
+ for (const prefix of knownPrefixes) {
454
+ if (classToCheck === prefix) {
455
+ // Exact match (like 'f' for flex, 'g' for grid)
456
+ classes.add(className);
457
+ break;
458
+ } else if (classToCheck.startsWith(prefix + '-')) {
459
+ // Prefix with hyphen separator (like 'p-10px', 'fs-16px', or 't-p-10px')
460
+ classes.add(className);
461
+ break;
462
+ }
463
+ }
464
+
465
+ // Also check special prefixes for responsive classes
466
+ if (hasResponsivePrefix && !classes.has(className)) {
467
+ for (const specialPrefix of specialPrefixes) {
468
+ if (classToCheck === specialPrefix) {
469
+ classes.add(className);
470
+ break;
471
+ } else if (classToCheck.startsWith(specialPrefix + '-')) {
472
+ classes.add(className);
473
+ break;
474
+ }
475
+ }
476
+ }
477
+ }
478
+ }
479
+
480
+ return classes;
481
+ }