meno-core 1.0.54 → 1.1.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 (296) hide show
  1. package/bin/cli.ts +19 -205
  2. package/build-astro.ts +31 -27
  3. package/dist/bin/cli.js +15 -154
  4. package/dist/bin/cli.js.map +2 -2
  5. package/dist/chunks/{chunk-7HWQUVTU.js → chunk-2IIQK7T3.js} +1353 -347
  6. package/dist/chunks/chunk-2IIQK7T3.js.map +7 -0
  7. package/dist/chunks/{chunk-3XER4E5W.js → chunk-4ZRU52J2.js} +6 -5
  8. package/dist/chunks/{chunk-3XER4E5W.js.map → chunk-4ZRU52J2.js.map} +2 -2
  9. package/dist/chunks/{chunk-NVRBTSQG.js → chunk-5X4HCN7J.js} +4 -4
  10. package/dist/chunks/chunk-5X4HCN7J.js.map +7 -0
  11. package/dist/chunks/{chunk-FZITJSSS.js → chunk-7CCVOL5M.js} +14 -14
  12. package/dist/chunks/chunk-7CCVOL5M.js.map +7 -0
  13. package/dist/chunks/{chunk-HIZMY3EP.js → chunk-AR7LHIFY.js} +8 -2
  14. package/dist/chunks/chunk-AR7LHIFY.js.map +7 -0
  15. package/dist/chunks/{chunk-AE3QK5QW.js → chunk-NUP7H7D3.js} +31 -26
  16. package/dist/chunks/chunk-NUP7H7D3.js.map +7 -0
  17. package/dist/chunks/{chunk-5ETZFREW.js → chunk-QWTQZHG3.js} +186 -79
  18. package/dist/chunks/chunk-QWTQZHG3.js.map +7 -0
  19. package/dist/chunks/{chunk-STDY3OVM.js → chunk-XTKNX4FW.js} +3 -3
  20. package/dist/chunks/{chunk-STDY3OVM.js.map → chunk-XTKNX4FW.js.map} +2 -2
  21. package/dist/chunks/{fs-ZI5JEU7V.js → fs-G3ANWEFP.js} +2 -2
  22. package/dist/lib/client/index.js +381 -159
  23. package/dist/lib/client/index.js.map +3 -3
  24. package/dist/lib/server/index.js +14345 -6265
  25. package/dist/lib/server/index.js.map +4 -4
  26. package/dist/lib/shared/index.js +292 -87
  27. package/dist/lib/shared/index.js.map +4 -4
  28. package/dist/lib/shared/richtext/index.js +1 -1
  29. package/dist/lib/test-utils/index.js +27 -21
  30. package/dist/lib/test-utils/index.js.map +2 -2
  31. package/lib/client/ErrorBoundary.test.tsx +147 -90
  32. package/lib/client/ErrorBoundary.tsx +8 -5
  33. package/lib/client/componentRegistry.ts +0 -1
  34. package/lib/client/core/ComponentBuilder.test.ts +150 -0
  35. package/lib/client/core/ComponentBuilder.ts +112 -12
  36. package/lib/client/core/ComponentRenderer.test.tsx +1 -2
  37. package/lib/client/core/builders/embedBuilder.ts +1 -1
  38. package/lib/client/core/builders/linkBuilder.ts +2 -2
  39. package/lib/client/core/builders/linkNodeBuilder.ts +1 -1
  40. package/lib/client/core/builders/listBuilder.ts +41 -4
  41. package/lib/client/core/builders/localeListBuilder.ts +2 -2
  42. package/lib/client/core/cmsTemplateProcessor.ts +1 -2
  43. package/lib/client/hmr/HMRManager.tsx +39 -30
  44. package/lib/client/hmrCssReload.ts +35 -6
  45. package/lib/client/hmrWebSocket.ts +5 -5
  46. package/lib/client/hooks/useColorVariables.ts +12 -9
  47. package/lib/client/hooks/usePropertyAutocomplete.ts +1 -1
  48. package/lib/client/hooks/useVariables.ts +3 -1
  49. package/lib/client/meno-filter/MenoFilter.test.ts +28 -28
  50. package/lib/client/meno-filter/MenoFilter.ts +9 -6
  51. package/lib/client/meno-filter/bindings.ts +3 -3
  52. package/lib/client/meno-filter/init.ts +3 -3
  53. package/lib/client/meno-filter/renderer.ts +8 -4
  54. package/lib/client/meno-filter/ui.ts +1 -1
  55. package/lib/client/meno-filter/updates.ts +3 -2
  56. package/lib/client/meno-filter/utils.ts +1 -1
  57. package/lib/client/navigation.test.ts +40 -44
  58. package/lib/client/responsiveStyleResolver.knownTokens.test.ts +71 -0
  59. package/lib/client/responsiveStyleResolver.test.ts +5 -5
  60. package/lib/client/responsiveStyleResolver.ts +61 -4
  61. package/lib/client/routing/RouteLoader.test.ts +1 -1
  62. package/lib/client/routing/RouteLoader.ts +8 -3
  63. package/lib/client/routing/Router.tsx +181 -152
  64. package/lib/client/scripts/ScriptExecutor.ts +3 -3
  65. package/lib/client/services/PrefetchService.test.ts +1 -1
  66. package/lib/client/services/PrefetchService.ts +13 -5
  67. package/lib/client/styleProcessor.ts +9 -3
  68. package/lib/client/styles/StyleInjector.ts +1 -1
  69. package/lib/client/styles/UtilityClassCollector.ts +54 -13
  70. package/lib/client/templateEngine.test.ts +17 -23
  71. package/lib/client/templateEngine.ts +7 -13
  72. package/lib/client/theme.test.ts +1 -1
  73. package/lib/server/__integration__/server-lifecycle.test.ts +3 -1
  74. package/lib/server/__integration__/static-assets.test.ts +2 -2
  75. package/lib/server/__integration__/test-helpers.ts +3 -4
  76. package/lib/server/astro/cmsPageEmitter.ts +4 -5
  77. package/lib/server/astro/componentEmitter.ts +11 -8
  78. package/lib/server/astro/nodeToAstro.test.ts +4 -4
  79. package/lib/server/astro/nodeToAstro.ts +41 -36
  80. package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +11 -11
  81. package/lib/server/astro/normalizeOrphanTemplateProps.ts +7 -6
  82. package/lib/server/astro/pageEmitter.ts +1 -1
  83. package/lib/server/astro/templateTransformer.ts +3 -3
  84. package/lib/server/createServer.ts +2 -2
  85. package/lib/server/cssGenerator.ts +2 -2
  86. package/lib/server/fileWatcher.test.ts +41 -4
  87. package/lib/server/fileWatcher.ts +102 -23
  88. package/lib/server/index.ts +7 -16
  89. package/lib/server/jsonLoader.test.ts +3 -3
  90. package/lib/server/jsonLoader.ts +8 -5
  91. package/lib/server/middleware/cors.test.ts +1 -1
  92. package/lib/server/middleware/cors.ts +2 -2
  93. package/lib/server/middleware/errorHandler.test.ts +2 -2
  94. package/lib/server/middleware/logger.test.ts +3 -3
  95. package/lib/server/middleware/logger.ts +7 -7
  96. package/lib/server/migrateTemplates.ts +3 -3
  97. package/lib/server/projectContext.ts +1 -1
  98. package/lib/server/providers/fileSystemCMSProvider.test.ts +18 -4
  99. package/lib/server/providers/fileSystemCMSProvider.ts +27 -18
  100. package/lib/server/routes/api/cms.ts +1 -1
  101. package/lib/server/routes/api/colors.test.ts +2 -2
  102. package/lib/server/routes/api/components.ts +1 -1
  103. package/lib/server/routes/api/config.ts +3 -3
  104. package/lib/server/routes/api/core-routes.ts +2 -2
  105. package/lib/server/routes/api/enums.test.ts +2 -2
  106. package/lib/server/routes/api/functions.ts +1 -1
  107. package/lib/server/routes/api/variables.test.ts +2 -2
  108. package/lib/server/routes/index.ts +2 -2
  109. package/lib/server/routes/pages.ts +1 -2
  110. package/lib/server/routes/static.ts +1 -2
  111. package/lib/server/runtime/bundler.ts +65 -43
  112. package/lib/server/runtime/fs.ts +13 -13
  113. package/lib/server/runtime/httpServer.ts +3 -3
  114. package/lib/server/services/ColorService.test.ts +3 -3
  115. package/lib/server/services/ColorService.ts +5 -2
  116. package/lib/server/services/EnumService.test.ts +2 -2
  117. package/lib/server/services/EnumService.ts +4 -1
  118. package/lib/server/services/VariableService.test.ts +7 -7
  119. package/lib/server/services/VariableService.ts +5 -2
  120. package/lib/server/services/cmsService.test.ts +44 -44
  121. package/lib/server/services/cmsService.ts +17 -97
  122. package/lib/server/services/componentService.test.ts +10 -16
  123. package/lib/server/services/componentService.ts +20 -12
  124. package/lib/server/services/configService.test.ts +13 -14
  125. package/lib/server/services/configService.ts +30 -0
  126. package/lib/server/services/fileWatcherService.ts +2 -2
  127. package/lib/server/services/pageService.test.ts +7 -7
  128. package/lib/server/services/pageService.ts +18 -5
  129. package/lib/server/ssr/attributeBuilder.ts +2 -2
  130. package/lib/server/ssr/cmsSSRProcessor.ts +1 -2
  131. package/lib/server/ssr/errorOverlay.ts +1 -1
  132. package/lib/server/ssr/htmlGenerator.nonce.test.ts +6 -5
  133. package/lib/server/ssr/htmlGenerator.test.ts +0 -1
  134. package/lib/server/ssr/htmlGenerator.ts +7 -7
  135. package/lib/server/ssr/imageMetadata.ts +2 -2
  136. package/lib/server/ssr/jsCollector.test.ts +5 -5
  137. package/lib/server/ssr/liveReloadIntegration.test.ts +7 -8
  138. package/lib/server/ssr/ssrRenderer.branches.test.ts +9 -9
  139. package/lib/server/ssr/ssrRenderer.test.ts +125 -15
  140. package/lib/server/ssr/ssrRenderer.ts +206 -38
  141. package/lib/server/ssrRenderer.test.ts +6 -6
  142. package/lib/server/utils/jsonLineMapper.ts +7 -7
  143. package/lib/server/validateStyleCoverage.ts +0 -1
  144. package/lib/server/websocketManager.ts +12 -0
  145. package/lib/shared/breakpoints.test.ts +5 -5
  146. package/lib/shared/cmsQuery.test.ts +81 -0
  147. package/lib/shared/cmsQuery.ts +92 -0
  148. package/lib/shared/cmsQueryParser.ts +11 -7
  149. package/lib/shared/colorConversions.ts +9 -9
  150. package/lib/shared/colorVariableUtils.test.ts +22 -0
  151. package/lib/shared/colorVariableUtils.ts +19 -5
  152. package/lib/shared/constants.ts +4 -0
  153. package/lib/shared/cssGeneration.test.ts +23 -0
  154. package/lib/shared/cssGeneration.ts +40 -27
  155. package/lib/shared/cssProperties.test.ts +64 -64
  156. package/lib/shared/cssProperties.ts +2 -2
  157. package/lib/shared/elementClassName.ts +2 -2
  158. package/lib/shared/errorLogger.test.ts +12 -12
  159. package/lib/shared/expressionEvaluator.test.ts +27 -1
  160. package/lib/shared/expressionEvaluator.ts +9 -0
  161. package/lib/shared/fontLoader.test.ts +13 -13
  162. package/lib/shared/fontLoader.ts +2 -2
  163. package/lib/shared/gradientUtils.test.ts +2 -2
  164. package/lib/shared/gradientUtils.ts +15 -8
  165. package/lib/shared/hrefRefs.test.ts +2 -2
  166. package/lib/shared/hrefRefs.ts +1 -1
  167. package/lib/shared/i18n.test.ts +3 -3
  168. package/lib/shared/i18n.ts +5 -4
  169. package/lib/shared/index.ts +23 -0
  170. package/lib/shared/inlineSvgStyleRules.ts +12 -9
  171. package/lib/shared/interactiveStyleMappings.test.ts +21 -21
  172. package/lib/shared/interactiveStyleMappings.ts +2 -8
  173. package/lib/shared/itemTemplateUtils.test.ts +5 -5
  174. package/lib/shared/itemTemplateUtils.ts +9 -5
  175. package/lib/shared/jsonRepair.ts +8 -8
  176. package/lib/shared/libraryLoader.test.ts +6 -6
  177. package/lib/shared/linkUtils.ts +1 -1
  178. package/lib/shared/logger.test.ts +66 -0
  179. package/lib/shared/logger.ts +94 -0
  180. package/lib/shared/markdown.ts +25 -0
  181. package/lib/shared/nodeUtils.test.ts +13 -9
  182. package/lib/shared/nodeUtils.ts +27 -4
  183. package/lib/shared/pathSecurity.ts +1 -1
  184. package/lib/shared/pathUtils.ts +1 -2
  185. package/lib/shared/paths/PathConverter.ts +3 -3
  186. package/lib/shared/paths/PathUtils.ts +4 -3
  187. package/lib/shared/paths/PathValidator.ts +1 -1
  188. package/lib/shared/permissions.test.ts +168 -0
  189. package/lib/shared/permissions.ts +162 -0
  190. package/lib/shared/propResolver.test.ts +1 -1
  191. package/lib/shared/propResolver.ts +1 -1
  192. package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +5 -3
  193. package/lib/shared/registry/ClientRegistry.ts +0 -1
  194. package/lib/shared/registry/ComponentRegistry.test.ts +3 -3
  195. package/lib/shared/registry/ComponentRegistry.ts +6 -1
  196. package/lib/shared/registry/NodeTypeDefinition.ts +2 -2
  197. package/lib/shared/registry/SSRRegistry.ts +0 -1
  198. package/lib/shared/registry/fieldPresets.ts +3 -1
  199. package/lib/shared/registry/nodeTypes/CustomNodeType.ts +103 -0
  200. package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +1 -1
  201. package/lib/shared/registry/nodeTypes/IslandNodeType.ts +129 -0
  202. package/lib/shared/registry/nodeTypes/ListNodeType.ts +43 -7
  203. package/lib/shared/registry/nodeTypes/MarkdownNodeType.ts +78 -0
  204. package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +6 -1
  205. package/lib/shared/registry/nodeTypes/index.ts +16 -1
  206. package/lib/shared/responsiveScaling.test.ts +1 -3
  207. package/lib/shared/responsiveScaling.ts +6 -5
  208. package/lib/shared/responsiveStyleUtils.test.ts +0 -1
  209. package/lib/shared/richtext/htmlToTiptap.test.ts +116 -116
  210. package/lib/shared/richtext/htmlToTiptap.ts +1 -1
  211. package/lib/shared/styleNodeUtils.ts +17 -14
  212. package/lib/shared/themeDefaults.test.ts +0 -3
  213. package/lib/shared/tree/PathBuilder.test.ts +1 -7
  214. package/lib/shared/tree/PathBuilder.ts +15 -13
  215. package/lib/shared/treePathUtils.test.ts +49 -2
  216. package/lib/shared/treePathUtils.ts +40 -4
  217. package/lib/shared/types/api.ts +105 -7
  218. package/lib/shared/types/cms.test.ts +34 -0
  219. package/lib/shared/types/cms.ts +65 -13
  220. package/lib/shared/types/components.ts +24 -0
  221. package/lib/shared/types/errors.test.ts +1 -1
  222. package/lib/shared/types/index.ts +23 -0
  223. package/lib/shared/types/permissions.ts +139 -0
  224. package/lib/shared/types/variables.ts +3 -3
  225. package/lib/shared/utilityClassConfig.ts +103 -0
  226. package/lib/shared/utilityClassMapper.durability.test.ts +117 -0
  227. package/lib/shared/utilityClassMapper.test.ts +253 -26
  228. package/lib/shared/utilityClassMapper.ts +160 -29
  229. package/lib/shared/utilityClassNames.test.ts +38 -0
  230. package/lib/shared/utilityClassNames.ts +155 -23
  231. package/lib/shared/validation/index.ts +1 -0
  232. package/lib/shared/validation/permissionsValidators.test.ts +49 -0
  233. package/lib/shared/validation/permissionsValidators.ts +49 -0
  234. package/lib/shared/validation/propValidator.test.ts +1 -1
  235. package/lib/shared/validation/propValidator.ts +1 -1
  236. package/lib/shared/validation/schemas.test.ts +41 -0
  237. package/lib/shared/validation/schemas.ts +159 -13
  238. package/lib/shared/validation/validators.test.ts +2 -2
  239. package/lib/shared/viewportUnits.test.ts +13 -0
  240. package/lib/shared/viewportUnits.ts +12 -5
  241. package/lib/test-utils/factories/ConsoleMockFactory.ts +7 -7
  242. package/lib/test-utils/factories/DomMockFactory.ts +10 -4
  243. package/lib/test-utils/factories/ServerMockFactory.ts +1 -1
  244. package/lib/test-utils/factories/StoreMockFactory.ts +5 -5
  245. package/lib/test-utils/mockFactories.ts +1 -1
  246. package/lib/test-utils/mocks.ts +3 -3
  247. package/package.json +3 -5
  248. package/scripts/build-for-publish.mjs +2 -4
  249. package/scripts/build-meno-filter.ts +3 -3
  250. package/tsconfig.json +1 -0
  251. package/vite.config.ts +1 -1
  252. package/build-next.ts +0 -1374
  253. package/build-static.test.ts +0 -453
  254. package/build-static.ts +0 -1072
  255. package/dist/build-static.js +0 -37
  256. package/dist/chunks/chunk-2FN4UOVO.js +0 -6465
  257. package/dist/chunks/chunk-2FN4UOVO.js.map +0 -7
  258. package/dist/chunks/chunk-5ETZFREW.js.map +0 -7
  259. package/dist/chunks/chunk-7E4IF5L7.js +0 -245
  260. package/dist/chunks/chunk-7E4IF5L7.js.map +0 -7
  261. package/dist/chunks/chunk-7HWQUVTU.js.map +0 -7
  262. package/dist/chunks/chunk-AE3QK5QW.js.map +0 -7
  263. package/dist/chunks/chunk-F6KTJYGV.js +0 -322
  264. package/dist/chunks/chunk-F6KTJYGV.js.map +0 -7
  265. package/dist/chunks/chunk-FZITJSSS.js.map +0 -7
  266. package/dist/chunks/chunk-GSYYA5GX.js +0 -77
  267. package/dist/chunks/chunk-GSYYA5GX.js.map +0 -7
  268. package/dist/chunks/chunk-HIZMY3EP.js.map +0 -7
  269. package/dist/chunks/chunk-I2WEGYA7.js +0 -424
  270. package/dist/chunks/chunk-I2WEGYA7.js.map +0 -7
  271. package/dist/chunks/chunk-JNO3CNLJ.js +0 -464
  272. package/dist/chunks/chunk-JNO3CNLJ.js.map +0 -7
  273. package/dist/chunks/chunk-NVRBTSQG.js.map +0 -7
  274. package/dist/chunks/chunk-Q4OBWKXG.js +0 -1143
  275. package/dist/chunks/chunk-Q4OBWKXG.js.map +0 -7
  276. package/dist/chunks/chunk-QTE32Y53.js +0 -3829
  277. package/dist/chunks/chunk-QTE32Y53.js.map +0 -7
  278. package/dist/chunks/configService-PRJZF7Y6.js +0 -14
  279. package/dist/chunks/configService-PRJZF7Y6.js.map +0 -7
  280. package/dist/chunks/constants-KIQEYMAM.js +0 -46
  281. package/dist/chunks/constants-KIQEYMAM.js.map +0 -7
  282. package/dist/chunks/fs-ZI5JEU7V.js.map +0 -7
  283. package/dist/entries/server-router.js +0 -62
  284. package/dist/entries/server-router.js.map +0 -7
  285. package/entries/server-router.tsx +0 -75
  286. package/lib/server/providers/fileSystemPageProvider.test.ts +0 -84
  287. package/lib/server/providers/fileSystemPageProvider.ts +0 -179
  288. package/lib/server/webflow/buildWebflow.ts +0 -623
  289. package/lib/server/webflow/index.ts +0 -23
  290. package/lib/server/webflow/nodeToWebflow.test.ts +0 -3102
  291. package/lib/server/webflow/nodeToWebflow.ts +0 -2372
  292. package/lib/server/webflow/styleMapper.test.ts +0 -348
  293. package/lib/server/webflow/styleMapper.ts +0 -687
  294. package/lib/server/webflow/templateWrapper.ts +0 -49
  295. package/lib/server/webflow/types.ts +0 -389
  296. /package/dist/{build-static.js.map → chunks/fs-G3ANWEFP.js.map} +0 -0
@@ -6,7 +6,6 @@
6
6
  import type { ReactElement } from 'react';
7
7
  import type { ZodType } from 'zod';
8
8
  import type { ComponentNode } from '../types/nodes';
9
- import type { StyleValue } from '../types/styles';
10
9
 
11
10
  /**
12
11
  * Tree icon types for node display
@@ -21,7 +20,8 @@ export type TreeIcon =
21
20
  | 'ARRAY'
22
21
  | 'UNKNOWN'
23
22
  | 'FORM'
24
- | 'LINK';
23
+ | 'LINK'
24
+ | 'CODE';
25
25
 
26
26
  /**
27
27
  * Node category for grouping in command palette
@@ -4,7 +4,6 @@
4
4
  */
5
5
 
6
6
  import { BaseComponentRegistry } from './ComponentRegistry';
7
- import type { ComponentDefinition } from '../types';
8
7
 
9
8
  export class SSRRegistry extends BaseComponentRegistry {
10
9
  /**
@@ -33,7 +33,9 @@ interface TextFieldConfig extends BaseFieldConfig {
33
33
  }
34
34
 
35
35
  interface SelectFieldConfig extends BaseFieldConfig {
36
- options: string[];
36
+ // Bare strings are value-is-label options; the object form carries a distinct
37
+ // display label (e.g. an empty-value `{ value: '', label: 'None' }` entry).
38
+ options: (string | { value: string | boolean; label: string })[];
37
39
  }
38
40
 
39
41
  // ============================================
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Custom Node Type Definition
3
+ *
4
+ * Represents a hand-authored, opaque `.astro` component (under `src/custom/`) that Meno
5
+ * treats as a BLACK BOX: it renders server-side in a real `astro build` / play preview
6
+ * (the meno-astro codec emits `<Fancy … />` + the import), but Meno doesn't model its
7
+ * internals — you can place it, pass props, and slot children, but not edit what's inside.
8
+ * The native-Astro sibling of an island (no framework renderer, no hydration).
9
+ *
10
+ * meno-core can't execute a foreign `.astro` file, so the editor canvas renders a quiet
11
+ * placeholder (its slotted children, or an inline marker) — real rendering is play/build
12
+ * only. The on-disk `.astro` form and the round-trip codec live in `meno-astro`; this file
13
+ * only models the node so it validates, persists, and renders a placeholder.
14
+ */
15
+
16
+ import { z } from 'zod';
17
+ import { createElement as h } from 'react';
18
+ import type { ComponentNode } from '../../types/nodes';
19
+ import { IfConditionSchema } from '../../validation/schemas';
20
+ import { createNodeType } from '../createNodeType';
21
+ import { textField } from '../fieldPresets';
22
+
23
+ // Schema is the SINGLE source of truth
24
+ const CustomNodeSchemaInternal = z
25
+ .object({
26
+ type: z.literal('custom'),
27
+ // Path to the hand-authored .astro component relative to `src/custom/`
28
+ // (e.g. "Fancy.astro", "widgets/Banner.astro").
29
+ src: z.string(),
30
+ props: z.record(z.string(), z.any()).optional(),
31
+ if: IfConditionSchema.optional(), // Conditional rendering - skip node when false
32
+ // Slotted children (rendered server-side into the component's default <slot/>).
33
+ children: z.union([z.array(z.any()), z.string()]).optional(),
34
+ })
35
+ .passthrough();
36
+
37
+ // TypeScript type inferred from schema
38
+ export type CustomNode = z.infer<typeof CustomNodeSchemaInternal>;
39
+
40
+ // Export schema for validation/schemas.ts
41
+ export const CustomNodeSchema = CustomNodeSchemaInternal;
42
+
43
+ /** Basename of the custom source (drops directory + `.astro` extension) for display. */
44
+ function customName(src: string): string {
45
+ const base = src.split('/').pop() ?? src;
46
+ return base.replace(/\.astro$/i, '') || base;
47
+ }
48
+
49
+ /** Slotted children, normalized to the shape `buildChildren`/`renderChildren` accept. */
50
+ function customChildren(node: CustomNode): Array<ComponentNode | string> | string | undefined {
51
+ const { children } = node;
52
+ if (Array.isArray(children)) return children.length > 0 ? (children as Array<ComponentNode | string>) : undefined;
53
+ if (typeof children === 'string') return children.length > 0 ? children : undefined;
54
+ return undefined;
55
+ }
56
+
57
+ export const CustomNodeType = createNodeType({
58
+ type: 'custom',
59
+ displayName: 'Custom Component',
60
+ category: 'special',
61
+ schema: CustomNodeSchemaInternal,
62
+
63
+ defaultValues: {
64
+ src: '',
65
+ },
66
+
67
+ treeDisplay: {
68
+ icon: 'COMPONENT',
69
+ getLabel: (node) => `Custom: ${customName(node.src) || '?'}`,
70
+ },
71
+
72
+ editableFields: [textField({ name: 'src', label: 'Custom .astro file', required: true, placeholder: 'Fancy.astro' })],
73
+
74
+ // meno-core can't run a foreign `.astro` file. Render the slotted children as normal
75
+ // content and fall back to a quiet inline marker when there are none. The wrapper keeps
76
+ // `data-custom` for selection/identification only. Real rendering is play/build only.
77
+ clientRenderer: (node, context) => {
78
+ const children = customChildren(node);
79
+ const inner = children
80
+ ? context.buildChildren(children, context.elementPath)
81
+ : h('span', { style: { color: '#9ca3af', fontSize: '13px' } }, customName(node.src) || '?');
82
+ return h('div', { key: context.key, 'data-custom': node.src, style: { display: 'contents' } }, inner);
83
+ },
84
+
85
+ ssrRenderer: (node, context) => {
86
+ const children = customChildren(node);
87
+ const inner = children
88
+ ? context.renderChildren(children, context)
89
+ : `<span style="color:#9ca3af;font-size:13px">${escapeHtml(customName(node.src) || '?')}</span>`;
90
+ return `<div data-custom="${escapeHtml(node.src)}" style="display:contents">${inner}</div>`;
91
+ },
92
+
93
+ capabilities: {
94
+ canHaveChildren: true,
95
+ canHaveStyle: false,
96
+ canHaveAttributes: false,
97
+ requiresProps: ['src'],
98
+ },
99
+ });
100
+
101
+ function escapeHtml(value: string): string {
102
+ return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
103
+ }
@@ -51,7 +51,7 @@ export const HtmlNodeType = createNodeType({
51
51
  },
52
52
 
53
53
  clientRenderer: (node, context) => {
54
- const props: Record<string, any> = { key: context.key };
54
+ const props: Record<string, unknown> = { key: context.key };
55
55
 
56
56
  // Prevent form submission in editor
57
57
  if (node.tag === 'form') {
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Island Node Type Definition
3
+ *
4
+ * Represents a BYO framework component (React/Preact/Vue/Svelte) rendered as an
5
+ * Astro island. The actual hydration only happens in a real `astro build` / play
6
+ * preview (the meno-astro codec emits `<Counter client:visible … />`). meno-core
7
+ * itself can't run a framework component, so the editor canvas renders a visible
8
+ * placeholder — design-time positioning works, interactivity is play/build only.
9
+ *
10
+ * The on-disk `.astro` form and the round-trip codec live in `meno-astro`; this
11
+ * file only models the node so it validates, persists, and renders a placeholder.
12
+ */
13
+
14
+ import { z } from 'zod';
15
+ import { createElement as h } from 'react';
16
+ import type { ComponentNode } from '../../types/nodes';
17
+ import { IfConditionSchema } from '../../validation/schemas';
18
+ import { createNodeType } from '../createNodeType';
19
+ import { textField, selectField } from '../fieldPresets';
20
+
21
+ /**
22
+ * Client (hydration) directive for an island.
23
+ * `directive` maps to Astro's `client:*` directive; `value` carries the media
24
+ * query (`client:media="(max-width: 50em)"`) or the framework name
25
+ * (`client:only="react"`). When omitted entirely, the island is server-rendered
26
+ * with no JS (a valid, zero-hydration Astro island).
27
+ */
28
+ export const IslandClientDirectiveSchema = z
29
+ .object({
30
+ directive: z.enum(['load', 'idle', 'visible', 'media', 'only']),
31
+ value: z.string().optional(),
32
+ })
33
+ .passthrough();
34
+
35
+ // Schema is the SINGLE source of truth
36
+ const IslandNodeSchemaInternal = z
37
+ .object({
38
+ type: z.literal('island'),
39
+ // Path to the framework component file relative to `src/islands/`
40
+ // (e.g. "Counter.tsx", "widgets/Chart.vue"). The extension derives the
41
+ // framework, which drives the Astro integration that gets provisioned.
42
+ src: z.string(),
43
+ client: IslandClientDirectiveSchema.optional(),
44
+ props: z.record(z.string(), z.any()).optional(),
45
+ if: IfConditionSchema.optional(), // Conditional rendering - skip node when false
46
+ // Slotted children (rendered server-side into the island's <slot/>).
47
+ children: z.union([z.array(z.any()), z.string()]).optional(),
48
+ })
49
+ .passthrough();
50
+
51
+ // TypeScript type inferred from schema
52
+ export type IslandNode = z.infer<typeof IslandNodeSchemaInternal>;
53
+
54
+ // Export schema for validation/schemas.ts
55
+ export const IslandNodeSchema = IslandNodeSchemaInternal;
56
+
57
+ /** Basename of the island source (drops directory + extension) for display. */
58
+ function islandName(src: string): string {
59
+ const base = src.split('/').pop() ?? src;
60
+ return base.replace(/\.(tsx|jsx|vue|svelte)$/i, '') || base;
61
+ }
62
+
63
+ /** Slotted children, normalized to the shape `buildChildren`/`renderChildren` accept. */
64
+ function islandChildren(node: IslandNode): Array<ComponentNode | string> | string | undefined {
65
+ const { children } = node;
66
+ if (Array.isArray(children)) return children.length > 0 ? (children as Array<ComponentNode | string>) : undefined;
67
+ if (typeof children === 'string') return children.length > 0 ? children : undefined;
68
+ return undefined;
69
+ }
70
+
71
+ export const IslandNodeType = createNodeType({
72
+ type: 'island',
73
+ displayName: 'Island',
74
+ category: 'special',
75
+ schema: IslandNodeSchemaInternal,
76
+
77
+ defaultValues: {
78
+ src: '',
79
+ client: { directive: 'load' },
80
+ },
81
+
82
+ treeDisplay: {
83
+ icon: 'COMPONENT',
84
+ getLabel: (node) => `Island: ${islandName(node.src) || '?'}`,
85
+ },
86
+
87
+ editableFields: [
88
+ textField({ name: 'src', label: 'Island file', required: true, placeholder: 'Counter.tsx' }),
89
+ selectField({
90
+ name: 'client.directive',
91
+ label: 'Hydration',
92
+ // The empty-value entry clears `client` entirely → a server-rendered island
93
+ // with zero JS (a valid Astro pattern); the rest map to `client:<directive>`.
94
+ options: [{ value: '', label: 'None (server-only, no JS)' }, 'load', 'idle', 'visible', 'media', 'only'],
95
+ }),
96
+ ],
97
+
98
+ // meno-core can't run a framework component. Instead of a loud highlight box
99
+ // (which made island-heavy pages unreadable), render the island's slotted
100
+ // children as normal content and fall back to a quiet inline marker when there
101
+ // are none. The wrapper keeps `data-island` for selection/identification only.
102
+ // Real hydration is play/build only.
103
+ clientRenderer: (node, context) => {
104
+ const children = islandChildren(node);
105
+ const inner = children
106
+ ? context.buildChildren(children, context.elementPath)
107
+ : h('span', { style: { color: '#9ca3af', fontSize: '13px' } }, islandName(node.src) || '?');
108
+ return h('div', { key: context.key, 'data-island': node.src, style: { display: 'contents' } }, inner);
109
+ },
110
+
111
+ ssrRenderer: (node, context) => {
112
+ const children = islandChildren(node);
113
+ const inner = children
114
+ ? context.renderChildren(children, context)
115
+ : `<span style="color:#9ca3af;font-size:13px">${escapeHtml(islandName(node.src) || '?')}</span>`;
116
+ return `<div data-island="${escapeHtml(node.src)}" style="display:contents">${inner}</div>`;
117
+ },
118
+
119
+ capabilities: {
120
+ canHaveChildren: true,
121
+ canHaveStyle: false,
122
+ canHaveAttributes: false,
123
+ requiresProps: ['src'],
124
+ },
125
+ });
126
+
127
+ function escapeHtml(value: string): string {
128
+ return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
129
+ }
@@ -41,14 +41,38 @@ const ListNodeSchemaInternal = z
41
41
  * Data source type:
42
42
  * - 'prop': Read items from component props (default)
43
43
  * - 'collection': Query items from CMS collection
44
+ * - 'remote': Fetch items from a public HTTP/JSON endpoint at build/SSR time
45
+ * - 'sanity': Fetch a Sanity document type via GROQ at build/SSR time (read-only, public dataset)
46
+ * - 'expression': Iterate a verbatim JS expression (`source`) that yields an array — a
47
+ * `.map()` over arbitrary frontmatter data (e.g. an Astro Action / supabase query) the
48
+ * dialect can't model as a prop/collection. The item template (`children` + `itemAs`) IS
49
+ * editable; the source itself is a read-only black box (edit it in the .astro frontmatter).
50
+ * The editor has no data for it, so it renders empty in the design canvas (real Astro play
51
+ * renders it for real at build/SSR).
44
52
  */
45
- sourceType: z.enum(['prop', 'collection']).default('prop'),
53
+ sourceType: z.enum(['prop', 'collection', 'remote', 'sanity', 'expression']).default('prop'),
46
54
  /**
47
- * Source identifier:
55
+ * Source identifier (sourceType 'prop'/'collection'/'expression'; 'remote' uses `url`):
48
56
  * - For sourceType 'prop': Prop name (e.g., "items") or template expression (e.g., "{{category.items}}")
49
57
  * - For sourceType 'collection': Collection name (e.g., "posts", "authors")
58
+ * - For sourceType 'expression': the verbatim JS expression iterated by `.map()` (e.g. "notes?")
50
59
  */
51
- source: z.string(),
60
+ source: z.string().optional(),
61
+
62
+ // Remote-only options (sourceType: 'remote')
63
+ /** HTTP(S) endpoint returning JSON, fetched at build/SSR time (e.g. a public REST API). */
64
+ url: z.string().optional(),
65
+ /**
66
+ * Dot-path into the JSON response to reach the items array (e.g. "data.items").
67
+ * Empty/absent = the response body is itself the array.
68
+ */
69
+ path: z.string().optional(),
70
+
71
+ // Sanity-only options (sourceType: 'sanity')
72
+ /** Sanity document `_type` to fetch (`*[_type == "<documentType>"]`). projectId/dataset come from project config. */
73
+ documentType: z.string().optional(),
74
+ /** Field used as the URL slug (for synthesizing item links); defaults to 'slug'. */
75
+ slugField: z.string().optional(),
52
76
  label: z.string().optional(), // Custom label displayed in structure tree
53
77
  if: IfConditionSchema.optional(), // Conditional rendering - skip node when false
54
78
  /**
@@ -111,8 +135,19 @@ export const ListNodeType = createNodeType({
111
135
  getLabel: (node) => {
112
136
  const listNode = node as ListNode;
113
137
  const sourceType = listNode.sourceType || 'prop';
114
- const prefix = sourceType === 'collection' ? 'CMS List' : 'List';
115
- return listNode.source ? `${prefix}: ${listNode.source}` : prefix;
138
+ const prefix =
139
+ sourceType === 'collection'
140
+ ? 'CMS List'
141
+ : sourceType === 'remote'
142
+ ? 'Remote List'
143
+ : sourceType === 'sanity'
144
+ ? 'Sanity List'
145
+ : sourceType === 'expression'
146
+ ? 'Loop'
147
+ : 'List';
148
+ const src =
149
+ sourceType === 'remote' ? listNode.url : sourceType === 'sanity' ? listNode.documentType : listNode.source;
150
+ return src ? `${prefix}: ${src}` : prefix;
116
151
  },
117
152
  },
118
153
 
@@ -132,7 +167,8 @@ export const ListNodeType = createNodeType({
132
167
  key: context.key,
133
168
  'data-list': 'true',
134
169
  'data-source-type': sourceType,
135
- 'data-source': listNode.source,
170
+ 'data-source':
171
+ sourceType === 'remote' ? listNode.url : sourceType === 'sanity' ? listNode.documentType : listNode.source,
136
172
  style: {
137
173
  padding: '8px 12px',
138
174
  background: bgColor,
@@ -162,7 +198,7 @@ export const ListNodeType = createNodeType({
162
198
  label: 'Source Type',
163
199
  type: 'select',
164
200
  required: false,
165
- options: ['prop', 'collection'],
201
+ options: ['prop', 'collection', 'remote', 'sanity'],
166
202
  },
167
203
  {
168
204
  name: 'source',
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Markdown Node Type Definition
3
+ * Renders a verbatim Markdown source string as HTML (Astro `.md`-style authoring).
4
+ *
5
+ * The body is stored byte-exact in `node.source` (whitespace-significant Markdown). meno-core
6
+ * renders it via `renderMarkdown` for the canvas + SSR; the meno-astro codec hoists the source
7
+ * to a backtick const and renders it through the runtime `renderMarkdown` at build (see
8
+ * dialect/emit/emitNode.ts:renderMarkdown). Modeled on the Embed node (verbatim payload).
9
+ */
10
+
11
+ import { z } from 'zod';
12
+ import { createElement as h } from 'react';
13
+ import { renderMarkdown } from '../../markdown';
14
+ import { StyleValueSchema, InteractiveStylesSchema, IfConditionSchema } from '../../validation/schemas';
15
+ import { createNodeType } from '../createNodeType';
16
+ import { textareaField } from '../fieldPresets';
17
+
18
+ // Schema is the SINGLE source of truth
19
+ const MarkdownNodeSchemaInternal = z
20
+ .object({
21
+ type: z.literal('markdown'),
22
+ source: z.string(), // verbatim Markdown source (whitespace-significant)
23
+ label: z.string().optional(), // Custom label displayed in structure tree
24
+ if: IfConditionSchema.optional(), // Conditional rendering - skip node when false
25
+ style: StyleValueSchema.optional(),
26
+ interactiveStyles: InteractiveStylesSchema.optional(),
27
+ generateElementClass: z.boolean().optional(),
28
+ attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
29
+ })
30
+ .passthrough();
31
+
32
+ // TypeScript type inferred from schema
33
+ export type MarkdownNode = z.infer<typeof MarkdownNodeSchemaInternal>;
34
+
35
+ // Export schema for validation/schemas.ts
36
+ export const MarkdownNodeSchema = MarkdownNodeSchemaInternal;
37
+
38
+ export const MarkdownNodeType = createNodeType({
39
+ type: 'markdown',
40
+ displayName: 'Markdown',
41
+ category: 'content',
42
+ schema: MarkdownNodeSchemaInternal,
43
+
44
+ defaultValues: {
45
+ source: '',
46
+ style: { base: {} },
47
+ },
48
+
49
+ treeDisplay: {
50
+ icon: 'HTML_ELEMENT',
51
+ getLabel: () => 'Markdown',
52
+ },
53
+
54
+ editableFields: [
55
+ textareaField({
56
+ name: 'source',
57
+ label: 'Markdown',
58
+ required: true,
59
+ placeholder: '# Heading\n\nWrite **markdown** here…',
60
+ }),
61
+ ],
62
+
63
+ clientRenderer: (node, context) => {
64
+ return h('div', {
65
+ key: context.key,
66
+ dangerouslySetInnerHTML: { __html: renderMarkdown((node as { source?: unknown }).source) },
67
+ });
68
+ },
69
+
70
+ ssrRenderer: (node, _context) => {
71
+ return `<div>${renderMarkdown((node as { source?: unknown }).source)}</div>`;
72
+ },
73
+
74
+ capabilities: {
75
+ canHaveChildren: false,
76
+ requiresProps: ['source'],
77
+ },
78
+ });
@@ -32,7 +32,12 @@ export const SlotMarkerType = createNodeType({
32
32
 
33
33
  treeDisplay: {
34
34
  icon: 'SLOT_MARKER',
35
- getLabel: () => 'slot',
35
+ // Show the slot's name so multiple named slots in a component are distinguishable
36
+ // (e.g. `slot: header`). Unnamed (default) slots stay just `slot`.
37
+ getLabel: (node) => {
38
+ const name = (node as { name?: unknown }).name;
39
+ return typeof name === 'string' && name ? `slot: ${name}` : 'slot';
40
+ },
36
41
  },
37
42
 
38
43
  clientRenderer: (_node, _context) => {
@@ -21,6 +21,9 @@ import { EmbedNodeType, EmbedNodeSchema, type EmbedNode } from './EmbedNodeType'
21
21
  import { LinkNodeType, LinkNodeSchema, type LinkNode } from './LinkNodeType';
22
22
  import { LocaleListNodeType, LocaleListNodeSchema, type LocaleListNode } from './LocaleListNodeType';
23
23
  import { ListNodeType, ListNodeSchema, type ListNode } from './ListNodeType';
24
+ import { IslandNodeType, IslandNodeSchema, type IslandNode } from './IslandNodeType';
25
+ import { MarkdownNodeType, MarkdownNodeSchema, type MarkdownNode } from './MarkdownNodeType';
26
+ import { CustomNodeType, CustomNodeSchema, type CustomNode } from './CustomNodeType';
24
27
 
25
28
  /**
26
29
  * All built-in node type definitions
@@ -34,6 +37,9 @@ export const builtInNodeTypes: NodeTypeDefinition[] = [
34
37
  LinkNodeType as NodeTypeDefinition,
35
38
  LocaleListNodeType as NodeTypeDefinition,
36
39
  ListNodeType as NodeTypeDefinition,
40
+ IslandNodeType as NodeTypeDefinition,
41
+ MarkdownNodeType as NodeTypeDefinition,
42
+ CustomNodeType as NodeTypeDefinition,
37
43
  ];
38
44
 
39
45
  /**
@@ -47,6 +53,9 @@ export const nodeSchemas = {
47
53
  link: LinkNodeSchema,
48
54
  localeList: LocaleListNodeSchema,
49
55
  list: ListNodeSchema,
56
+ island: IslandNodeSchema,
57
+ markdown: MarkdownNodeSchema,
58
+ custom: CustomNodeSchema,
50
59
  };
51
60
 
52
61
  /**
@@ -69,6 +78,9 @@ export { EmbedNodeType, EmbedNodeSchema, type EmbedNode } from './EmbedNodeType'
69
78
  export { LinkNodeType, LinkNodeSchema, type LinkNode } from './LinkNodeType';
70
79
  export { LocaleListNodeType, LocaleListNodeSchema, type LocaleListNode } from './LocaleListNodeType';
71
80
  export { ListNodeType, ListNodeSchema, type ListNode } from './ListNodeType';
81
+ export { IslandNodeType, IslandNodeSchema, type IslandNode } from './IslandNodeType';
82
+ export { MarkdownNodeType, MarkdownNodeSchema, type MarkdownNode } from './MarkdownNodeType';
83
+ export { CustomNodeType, CustomNodeSchema, type CustomNode } from './CustomNodeType';
72
84
 
73
85
  /**
74
86
  * Discriminated union of all component node types
@@ -81,4 +93,7 @@ export type ComponentNode =
81
93
  | EmbedNode
82
94
  | LinkNode
83
95
  | LocaleListNode
84
- | ListNode;
96
+ | ListNode
97
+ | IslandNode
98
+ | MarkdownNode
99
+ | CustomNode;
@@ -15,8 +15,6 @@ import {
15
15
  buildSiteMarginClamp,
16
16
  getSmallestBreakpointName,
17
17
  type ResponsiveScales,
18
- type CSSPropertyType,
19
- type BreakpointScales,
20
18
  } from './responsiveScaling';
21
19
 
22
20
  describe('responsiveScaling', () => {
@@ -454,7 +452,7 @@ describe('responsiveScaling', () => {
454
452
  test('multi-value padding "20px 40px" → both tokens scaled', () => {
455
453
  const out = buildFluidPropertyValue('20px 40px', 0.5, 320, 1440);
456
454
  expect(out).not.toBeNull();
457
- const parts = out!.split(' ');
455
+ const _parts = out!.split(' ');
458
456
  // The two tokens get joined by space — but each clamp() also has spaces inside.
459
457
  // Check both clamp(...) substrings present.
460
458
  expect((out!.match(/clamp\(/g) ?? []).length).toBe(2);
@@ -111,8 +111,8 @@ export function parseValue(valueStr: string): { value: number; unit: string } |
111
111
  const match = valueStr.trim().match(/^(-?[\d.]+)(px|rem|em|%|pt)$/);
112
112
  if (!match) return null;
113
113
  return {
114
- value: parseFloat(match[1]),
115
- unit: match[2],
114
+ value: parseFloat(match[1] ?? ''),
115
+ unit: match[2] ?? '',
116
116
  };
117
117
  }
118
118
 
@@ -339,9 +339,10 @@ export function getSmallestBreakpointName(
339
339
  ): string | null {
340
340
  if (!breakpoints) return null;
341
341
  const entries = Object.entries(breakpoints);
342
- if (entries.length === 0) return null;
343
- let smallestName = entries[0][0];
344
- let smallestWidth = entries[0][1].breakpoint;
342
+ const firstEntry = entries[0];
343
+ if (entries.length === 0 || firstEntry === undefined) return null;
344
+ let smallestName = firstEntry[0];
345
+ let smallestWidth = firstEntry[1].breakpoint;
345
346
  for (const [name, cfg] of entries) {
346
347
  if (cfg.breakpoint < smallestWidth) {
347
348
  smallestWidth = cfg.breakpoint;
@@ -4,7 +4,6 @@ import {
4
4
  normalizeStyle,
5
5
  getAvailableBreakpoints,
6
6
  breakpointToViewportWidth,
7
- type MergeStrategy,
8
7
  } from './responsiveStyleUtils';
9
8
  import type { ResponsiveStyleObject, StyleObject } from './types';
10
9
  import type { BreakpointConfig } from './breakpoints';