@cmssy/cli 0.22.3 → 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 (260) hide show
  1. package/README.md +49 -650
  2. package/dist/cli.js +706 -328
  3. package/package.json +26 -73
  4. package/templates/block/Component.module.css.tpl +12 -0
  5. package/templates/block/Component.tsx.tpl +15 -0
  6. package/templates/block/block.ts.tpl +11 -0
  7. package/templates/init/app/[[...path]]/page.tsx +19 -0
  8. package/templates/init/app/api/draft/route.ts +5 -0
  9. package/templates/init/blocks/hero/Hero.module.css +131 -0
  10. package/templates/init/blocks/hero/Hero.tsx +100 -0
  11. package/templates/init/blocks/hero/block.ts +21 -0
  12. package/templates/init/cmssy/blocks.ts +5 -0
  13. package/templates/init/cmssy/editor.tsx +10 -0
  14. package/templates/init/cmssy.config.ts +8 -0
  15. package/templates/init/env.example +4 -0
  16. package/templates/init/next.config.mjs +8 -0
  17. package/templates/init/proxy.ts +32 -0
  18. package/config.d.ts +0 -3
  19. package/config.js +0 -4
  20. package/dist/cli.d.ts +0 -3
  21. package/dist/cli.d.ts.map +0 -1
  22. package/dist/cli.js.map +0 -1
  23. package/dist/commands/add-source.d.ts +0 -13
  24. package/dist/commands/add-source.d.ts.map +0 -1
  25. package/dist/commands/add-source.js +0 -273
  26. package/dist/commands/add-source.js.map +0 -1
  27. package/dist/commands/build.d.ts +0 -7
  28. package/dist/commands/build.d.ts.map +0 -1
  29. package/dist/commands/build.js +0 -127
  30. package/dist/commands/build.js.map +0 -1
  31. package/dist/commands/codegen.d.ts +0 -7
  32. package/dist/commands/codegen.d.ts.map +0 -1
  33. package/dist/commands/codegen.js +0 -150
  34. package/dist/commands/codegen.js.map +0 -1
  35. package/dist/commands/configure.d.ts +0 -6
  36. package/dist/commands/configure.d.ts.map +0 -1
  37. package/dist/commands/configure.js +0 -39
  38. package/dist/commands/configure.js.map +0 -1
  39. package/dist/commands/create.d.ts +0 -18
  40. package/dist/commands/create.d.ts.map +0 -1
  41. package/dist/commands/create.js +0 -403
  42. package/dist/commands/create.js.map +0 -1
  43. package/dist/commands/dev.d.ts +0 -6
  44. package/dist/commands/dev.d.ts.map +0 -1
  45. package/dist/commands/dev.js +0 -144
  46. package/dist/commands/dev.js.map +0 -1
  47. package/dist/commands/doctor.d.ts +0 -2
  48. package/dist/commands/doctor.d.ts.map +0 -1
  49. package/dist/commands/doctor.js +0 -230
  50. package/dist/commands/doctor.js.map +0 -1
  51. package/dist/commands/init.d.ts +0 -6
  52. package/dist/commands/init.d.ts.map +0 -1
  53. package/dist/commands/init.js +0 -414
  54. package/dist/commands/init.js.map +0 -1
  55. package/dist/commands/lib.d.ts +0 -17
  56. package/dist/commands/lib.d.ts.map +0 -1
  57. package/dist/commands/lib.js +0 -132
  58. package/dist/commands/lib.js.map +0 -1
  59. package/dist/commands/link.d.ts +0 -8
  60. package/dist/commands/link.d.ts.map +0 -1
  61. package/dist/commands/link.js +0 -116
  62. package/dist/commands/link.js.map +0 -1
  63. package/dist/commands/migrate.d.ts +0 -2
  64. package/dist/commands/migrate.d.ts.map +0 -1
  65. package/dist/commands/migrate.js +0 -231
  66. package/dist/commands/migrate.js.map +0 -1
  67. package/dist/commands/package.d.ts +0 -14
  68. package/dist/commands/package.d.ts.map +0 -1
  69. package/dist/commands/package.js +0 -137
  70. package/dist/commands/package.js.map +0 -1
  71. package/dist/commands/publish-block-buildtime.d.ts +0 -8
  72. package/dist/commands/publish-block-buildtime.d.ts.map +0 -1
  73. package/dist/commands/publish-block-buildtime.js +0 -290
  74. package/dist/commands/publish-block-buildtime.js.map +0 -1
  75. package/dist/commands/publish-template.d.ts +0 -22
  76. package/dist/commands/publish-template.d.ts.map +0 -1
  77. package/dist/commands/publish-template.js +0 -345
  78. package/dist/commands/publish-template.js.map +0 -1
  79. package/dist/commands/publish.d.ts +0 -15
  80. package/dist/commands/publish.d.ts.map +0 -1
  81. package/dist/commands/publish.js +0 -1003
  82. package/dist/commands/publish.js.map +0 -1
  83. package/dist/commands/skills.d.ts +0 -11
  84. package/dist/commands/skills.d.ts.map +0 -1
  85. package/dist/commands/skills.js +0 -178
  86. package/dist/commands/skills.js.map +0 -1
  87. package/dist/commands/sync.d.ts +0 -6
  88. package/dist/commands/sync.d.ts.map +0 -1
  89. package/dist/commands/sync.js +0 -208
  90. package/dist/commands/sync.js.map +0 -1
  91. package/dist/commands/test.d.ts +0 -8
  92. package/dist/commands/test.d.ts.map +0 -1
  93. package/dist/commands/test.js +0 -60
  94. package/dist/commands/test.js.map +0 -1
  95. package/dist/commands/upload.d.ts +0 -11
  96. package/dist/commands/upload.d.ts.map +0 -1
  97. package/dist/commands/upload.js +0 -128
  98. package/dist/commands/upload.js.map +0 -1
  99. package/dist/commands/workspaces.d.ts +0 -2
  100. package/dist/commands/workspaces.d.ts.map +0 -1
  101. package/dist/commands/workspaces.js +0 -62
  102. package/dist/commands/workspaces.js.map +0 -1
  103. package/dist/dev-ui/app.js +0 -1284
  104. package/dist/dev-ui/index.html +0 -1511
  105. package/dist/dev-ui-react/App.tsx +0 -164
  106. package/dist/dev-ui-react/__tests__/previewData.test.ts +0 -193
  107. package/dist/dev-ui-react/components/BlocksList.tsx +0 -232
  108. package/dist/dev-ui-react/components/Editor.tsx +0 -469
  109. package/dist/dev-ui-react/components/Preview.tsx +0 -146
  110. package/dist/dev-ui-react/hooks/useBlocks.ts +0 -80
  111. package/dist/dev-ui-react/index.html +0 -13
  112. package/dist/dev-ui-react/main.tsx +0 -8
  113. package/dist/dev-ui-react/styles.css +0 -856
  114. package/dist/dev-ui-react/types.ts +0 -45
  115. package/dist/skills/claude/cmssy-block.md +0 -454
  116. package/dist/skills/claude/cmssy-mcp-content.md +0 -231
  117. package/dist/test-helpers/index.d.ts +0 -23
  118. package/dist/test-helpers/index.d.ts.map +0 -1
  119. package/dist/test-helpers/index.js +0 -50
  120. package/dist/test-helpers/index.js.map +0 -1
  121. package/dist/types/block-config.d.ts +0 -6
  122. package/dist/types/block-config.d.ts.map +0 -1
  123. package/dist/types/block-config.js +0 -12
  124. package/dist/types/block-config.js.map +0 -1
  125. package/dist/types/publish-job.d.ts +0 -61
  126. package/dist/types/publish-job.d.ts.map +0 -1
  127. package/dist/types/publish-job.js +0 -13
  128. package/dist/types/publish-job.js.map +0 -1
  129. package/dist/utils/api-error.d.ts +0 -10
  130. package/dist/utils/api-error.d.ts.map +0 -1
  131. package/dist/utils/api-error.js +0 -55
  132. package/dist/utils/api-error.js.map +0 -1
  133. package/dist/utils/block-config.d.ts +0 -87
  134. package/dist/utils/block-config.d.ts.map +0 -1
  135. package/dist/utils/block-config.js +0 -214
  136. package/dist/utils/block-config.js.map +0 -1
  137. package/dist/utils/blocks-meta-cache.d.ts +0 -28
  138. package/dist/utils/blocks-meta-cache.d.ts.map +0 -1
  139. package/dist/utils/blocks-meta-cache.js +0 -72
  140. package/dist/utils/blocks-meta-cache.js.map +0 -1
  141. package/dist/utils/builder.d.ts +0 -34
  142. package/dist/utils/builder.d.ts.map +0 -1
  143. package/dist/utils/builder.js +0 -140
  144. package/dist/utils/builder.js.map +0 -1
  145. package/dist/utils/cmssy-config.d.ts +0 -16
  146. package/dist/utils/cmssy-config.d.ts.map +0 -1
  147. package/dist/utils/cmssy-config.js +0 -19
  148. package/dist/utils/cmssy-config.js.map +0 -1
  149. package/dist/utils/config.d.ts +0 -9
  150. package/dist/utils/config.d.ts.map +0 -1
  151. package/dist/utils/config.js +0 -51
  152. package/dist/utils/config.js.map +0 -1
  153. package/dist/utils/dependency-check.d.ts +0 -21
  154. package/dist/utils/dependency-check.d.ts.map +0 -1
  155. package/dist/utils/dependency-check.js +0 -56
  156. package/dist/utils/dependency-check.js.map +0 -1
  157. package/dist/utils/dev-generator/api-routes/blocks.d.ts +0 -3
  158. package/dist/utils/dev-generator/api-routes/blocks.d.ts.map +0 -1
  159. package/dist/utils/dev-generator/api-routes/blocks.js +0 -194
  160. package/dist/utils/dev-generator/api-routes/blocks.js.map +0 -1
  161. package/dist/utils/dev-generator/api-routes/config.d.ts +0 -2
  162. package/dist/utils/dev-generator/api-routes/config.d.ts.map +0 -1
  163. package/dist/utils/dev-generator/api-routes/config.js +0 -107
  164. package/dist/utils/dev-generator/api-routes/config.js.map +0 -1
  165. package/dist/utils/dev-generator/api-routes/preview.d.ts +0 -2
  166. package/dist/utils/dev-generator/api-routes/preview.d.ts.map +0 -1
  167. package/dist/utils/dev-generator/api-routes/preview.js +0 -50
  168. package/dist/utils/dev-generator/api-routes/preview.js.map +0 -1
  169. package/dist/utils/dev-generator/api-routes/workspaces.d.ts +0 -2
  170. package/dist/utils/dev-generator/api-routes/workspaces.d.ts.map +0 -1
  171. package/dist/utils/dev-generator/api-routes/workspaces.js +0 -128
  172. package/dist/utils/dev-generator/api-routes/workspaces.js.map +0 -1
  173. package/dist/utils/dev-generator/helpers.d.ts +0 -23
  174. package/dist/utils/dev-generator/helpers.d.ts.map +0 -1
  175. package/dist/utils/dev-generator/helpers.js +0 -128
  176. package/dist/utils/dev-generator/helpers.js.map +0 -1
  177. package/dist/utils/dev-generator/home-page.d.ts +0 -2
  178. package/dist/utils/dev-generator/home-page.d.ts.map +0 -1
  179. package/dist/utils/dev-generator/home-page.js +0 -643
  180. package/dist/utils/dev-generator/home-page.js.map +0 -1
  181. package/dist/utils/dev-generator/index.d.ts +0 -11
  182. package/dist/utils/dev-generator/index.d.ts.map +0 -1
  183. package/dist/utils/dev-generator/index.js +0 -85
  184. package/dist/utils/dev-generator/index.js.map +0 -1
  185. package/dist/utils/dev-generator/layout.d.ts +0 -3
  186. package/dist/utils/dev-generator/layout.d.ts.map +0 -1
  187. package/dist/utils/dev-generator/layout.js +0 -63
  188. package/dist/utils/dev-generator/layout.js.map +0 -1
  189. package/dist/utils/dev-generator/next-config.d.ts +0 -3
  190. package/dist/utils/dev-generator/next-config.d.ts.map +0 -1
  191. package/dist/utils/dev-generator/next-config.js +0 -111
  192. package/dist/utils/dev-generator/next-config.js.map +0 -1
  193. package/dist/utils/dev-generator/preview-pages.d.ts +0 -3
  194. package/dist/utils/dev-generator/preview-pages.d.ts.map +0 -1
  195. package/dist/utils/dev-generator/preview-pages.js +0 -833
  196. package/dist/utils/dev-generator/preview-pages.js.map +0 -1
  197. package/dist/utils/dev-generator.d.ts +0 -12
  198. package/dist/utils/dev-generator.d.ts.map +0 -1
  199. package/dist/utils/dev-generator.js +0 -1752
  200. package/dist/utils/dev-generator.js.map +0 -1
  201. package/dist/utils/field-schema.d.ts +0 -13
  202. package/dist/utils/field-schema.d.ts.map +0 -1
  203. package/dist/utils/field-schema.js +0 -182
  204. package/dist/utils/field-schema.js.map +0 -1
  205. package/dist/utils/graphql.d.ts +0 -39
  206. package/dist/utils/graphql.d.ts.map +0 -1
  207. package/dist/utils/graphql.js +0 -138
  208. package/dist/utils/graphql.js.map +0 -1
  209. package/dist/utils/lib-manifest.d.ts +0 -7
  210. package/dist/utils/lib-manifest.d.ts.map +0 -1
  211. package/dist/utils/lib-manifest.js +0 -59
  212. package/dist/utils/lib-manifest.js.map +0 -1
  213. package/dist/utils/publish-helpers.d.ts +0 -43
  214. package/dist/utils/publish-helpers.d.ts.map +0 -1
  215. package/dist/utils/publish-helpers.js +0 -215
  216. package/dist/utils/publish-helpers.js.map +0 -1
  217. package/dist/utils/publish-helpers.test.d.ts +0 -2
  218. package/dist/utils/publish-helpers.test.d.ts.map +0 -1
  219. package/dist/utils/publish-helpers.test.js +0 -24
  220. package/dist/utils/publish-helpers.test.js.map +0 -1
  221. package/dist/utils/resolve-workspace.d.ts +0 -6
  222. package/dist/utils/resolve-workspace.d.ts.map +0 -1
  223. package/dist/utils/resolve-workspace.js +0 -32
  224. package/dist/utils/resolve-workspace.js.map +0 -1
  225. package/dist/utils/scanner.d.ts +0 -42
  226. package/dist/utils/scanner.d.ts.map +0 -1
  227. package/dist/utils/scanner.js +0 -156
  228. package/dist/utils/scanner.js.map +0 -1
  229. package/dist/utils/schema-diff.d.ts +0 -22
  230. package/dist/utils/schema-diff.d.ts.map +0 -1
  231. package/dist/utils/schema-diff.js +0 -107
  232. package/dist/utils/schema-diff.js.map +0 -1
  233. package/dist/utils/source-collector.d.ts +0 -19
  234. package/dist/utils/source-collector.d.ts.map +0 -1
  235. package/dist/utils/source-collector.js +0 -565
  236. package/dist/utils/source-collector.js.map +0 -1
  237. package/dist/utils/theme-builder.d.ts +0 -12
  238. package/dist/utils/theme-builder.d.ts.map +0 -1
  239. package/dist/utils/theme-builder.js +0 -117
  240. package/dist/utils/theme-builder.js.map +0 -1
  241. package/dist/utils/type-generator.d.ts +0 -9
  242. package/dist/utils/type-generator.d.ts.map +0 -1
  243. package/dist/utils/type-generator.js +0 -86
  244. package/dist/utils/type-generator.js.map +0 -1
  245. package/dist/utils/version.d.ts +0 -10
  246. package/dist/utils/version.d.ts.map +0 -1
  247. package/dist/utils/version.js +0 -30
  248. package/dist/utils/version.js.map +0 -1
  249. package/dist/utils/versions.d.ts.map +0 -1
  250. package/dist/utils/versions.js.map +0 -1
  251. package/src/dev-app/app/api/blocks/[name]/config/route.ts +0 -140
  252. package/src/dev-app/app/api/blocks/route.ts +0 -61
  253. package/src/dev-app/app/api/config/route.ts +0 -98
  254. package/src/dev-app/app/api/context/route.ts +0 -126
  255. package/src/dev-app/app/api/preview/[name]/route.ts +0 -137
  256. package/src/dev-app/app/api/workspaces/route.ts +0 -124
  257. package/src/dev-app/app/layout.tsx +0 -18
  258. package/src/dev-app/app/page.tsx +0 -1785
  259. package/test.d.ts +0 -1
  260. package/test.js +0 -2
@@ -1,1752 +0,0 @@
1
- import { execSync } from "child_process";
2
- import fs from "fs-extra";
3
- import path from "path";
4
- const DEV_DIR = ".cmssy/dev";
5
- /**
6
- * Generate the .cmssy/dev/ Next.js app structure for cmssy dev.
7
- * This creates a minimal Next.js app that imports blocks directly,
8
- * enabling "use client" boundaries, next/image, and SSR in dev preview.
9
- */
10
- export function generateDevApp(projectRoot, resources) {
11
- const devRoot = path.join(projectRoot, DEV_DIR);
12
- // Clean and recreate
13
- fs.removeSync(devRoot);
14
- fs.mkdirSync(path.join(devRoot, "app/preview"), { recursive: true });
15
- fs.mkdirSync(path.join(devRoot, "app/api/blocks"), { recursive: true });
16
- fs.mkdirSync(path.join(devRoot, "app/api/preview"), { recursive: true });
17
- fs.mkdirSync(path.join(devRoot, "app/api/workspaces"), { recursive: true });
18
- // Generate all files
19
- generateNextConfig(devRoot, projectRoot);
20
- generateTsConfig(devRoot, projectRoot);
21
- generateRootLayout(devRoot);
22
- generateGlobalsCss(devRoot, projectRoot);
23
- generateHomePage(devRoot);
24
- generateBlocksApiRoute(devRoot);
25
- generateBlockConfigApiRoute(devRoot);
26
- generatePreviewApiRoute(devRoot);
27
- generateWorkspacesApiRoute(devRoot);
28
- generatePreviewPages(devRoot, projectRoot, resources);
29
- return devRoot;
30
- }
31
- /**
32
- * Regenerate only the preview pages (called when new blocks are detected).
33
- */
34
- export function regeneratePreviewPages(projectRoot, resources) {
35
- const devRoot = path.join(projectRoot, DEV_DIR);
36
- const previewDir = path.join(devRoot, "app/preview");
37
- // Remove old preview pages
38
- if (fs.existsSync(previewDir)) {
39
- fs.removeSync(previewDir);
40
- }
41
- fs.mkdirSync(previewDir, { recursive: true });
42
- generatePreviewPages(devRoot, projectRoot, resources);
43
- }
44
- // ============================================================================
45
- // File Generators
46
- // ============================================================================
47
- function generateNextConfig(devRoot, projectRoot) {
48
- const rel = path.relative(devRoot, projectRoot);
49
- const content = `import { resolve, dirname } from "path";
50
- import { fileURLToPath } from "url";
51
-
52
- const __dirname = dirname(fileURLToPath(import.meta.url));
53
-
54
- /** @type {import('next').NextConfig} */
55
- const nextConfig = {
56
- turbopack: {
57
- root: resolve(__dirname, "${rel}"),
58
- },
59
-
60
- allowedDevOrigins: ['*'],
61
-
62
- images: {
63
- remotePatterns: [{ protocol: 'https', hostname: '**' }],
64
- },
65
- };
66
-
67
- export default nextConfig;
68
- `;
69
- fs.writeFileSync(path.join(devRoot, "next.config.mjs"), content);
70
- }
71
- function generateTsConfig(devRoot, projectRoot) {
72
- // Paths must be relative to tsconfig location (.cmssy/dev/)
73
- const rel = path.relative(devRoot, projectRoot);
74
- // Read project tsconfig to forward user-defined path aliases and includes
75
- const projectTsConfigPath = path.join(projectRoot, "tsconfig.json");
76
- let userPaths = {};
77
- let userIncludes = [];
78
- if (fs.existsSync(projectTsConfigPath)) {
79
- try {
80
- const projectTsConfig = JSON.parse(fs.readFileSync(projectTsConfigPath, "utf-8"));
81
- const rawPaths = projectTsConfig.compilerOptions?.paths || {};
82
- // Re-map user paths relative to .cmssy/dev/ (project tsconfig uses baseUrl: ".")
83
- for (const [alias, targets] of Object.entries(rawPaths)) {
84
- // Skip cmssy-cli/config — we handle it ourselves
85
- if (alias === "cmssy-cli/config")
86
- continue;
87
- userPaths[alias] = targets.map((t) => `${rel}/${t}`);
88
- }
89
- // Re-map user includes relative to .cmssy/dev/
90
- // Convert catch-all globs (e.g. blocks/**/*) to TS-only for better perf
91
- const rawIncludes = projectTsConfig.include || [];
92
- userIncludes = rawIncludes.flatMap((inc) => {
93
- const remapped = `${rel}/${inc}`;
94
- if (remapped.endsWith("/**/*")) {
95
- const base = remapped.slice(0, -5);
96
- return [`${base}/**/*.ts`, `${base}/**/*.tsx`];
97
- }
98
- return [remapped];
99
- });
100
- }
101
- catch {
102
- // Ignore parse errors — fall back to defaults
103
- }
104
- }
105
- const tsConfig = {
106
- compilerOptions: {
107
- target: "ES2020",
108
- lib: ["dom", "dom.iterable", "esnext"],
109
- allowJs: true,
110
- skipLibCheck: true,
111
- strict: true,
112
- noEmit: true,
113
- esModuleInterop: true,
114
- module: "esnext",
115
- moduleResolution: "bundler",
116
- resolveJsonModule: true,
117
- isolatedModules: true,
118
- jsx: "preserve",
119
- incremental: true,
120
- plugins: [{ name: "next" }],
121
- paths: {
122
- // User-defined aliases from project tsconfig (e.g. @/* for shadcn)
123
- ...userPaths,
124
- // Cmssy built-in aliases (override user if conflicting)
125
- "@blocks/*": [`${rel}/blocks/*`],
126
- "@templates/*": [`${rel}/templates/*`],
127
- "@styles/*": [`${rel}/styles/*`],
128
- "@lib/*": [`${rel}/lib/*`],
129
- "cmssy-cli/config": [`${rel}/node_modules/cmssy-cli/config`],
130
- },
131
- },
132
- include: [
133
- "next-env.d.ts",
134
- "**/*.ts",
135
- "**/*.tsx",
136
- ".next/types/**/*.ts",
137
- `${rel}/blocks/**/*.ts`,
138
- `${rel}/blocks/**/*.tsx`,
139
- `${rel}/templates/**/*.ts`,
140
- `${rel}/templates/**/*.tsx`,
141
- // User-defined includes from project tsconfig (e.g. components/**/*, lib/**/*)
142
- ...userIncludes,
143
- ],
144
- exclude: ["node_modules"],
145
- };
146
- fs.writeFileSync(path.join(devRoot, "tsconfig.json"), JSON.stringify(tsConfig, null, 2) + "\n");
147
- }
148
- function generateRootLayout(devRoot) {
149
- const content = `import type { Metadata } from "next";
150
- import "./globals.css";
151
-
152
- export const metadata: Metadata = {
153
- title: "Cmssy Dev Server",
154
- icons: {
155
- icon: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='20' fill='%23667eea'/%3E%3Ctext x='50' y='70' font-size='60' font-weight='bold' text-anchor='middle' fill='white' font-family='system-ui'%3EC%3C/text%3E%3C/svg%3E",
156
- },
157
- };
158
-
159
- export default function RootLayout({
160
- children,
161
- }: {
162
- children: React.ReactNode;
163
- }) {
164
- return (
165
- <html lang="en">
166
- <body>{children}</body>
167
- </html>
168
- );
169
- }
170
- `;
171
- fs.writeFileSync(path.join(devRoot, "app/layout.tsx"), content);
172
- }
173
- function generateGlobalsCss(devRoot, projectRoot) {
174
- const rel = path.relative(path.join(devRoot, "app"), projectRoot);
175
- // Check for project CSS files that contain Tailwind / theme
176
- const cssFiles = ["styles/main.css", "styles/globals.css", "app/globals.css"];
177
- const projectCssFile = cssFiles.find((f) => fs.existsSync(path.join(projectRoot, f)));
178
- // Import the project's main CSS (Tailwind + theme) if it exists
179
- const projectCssImport = projectCssFile
180
- ? `@import "${rel}/${projectCssFile}";\n\n`
181
- : "";
182
- const content = `${projectCssImport}*,
183
- *::before,
184
- *::after {
185
- box-sizing: border-box;
186
- }
187
-
188
- html,
189
- body {
190
- height: 100%;
191
- margin: 0;
192
- padding: 0;
193
- }
194
-
195
- body {
196
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
197
- background: #f5f5f5;
198
- line-height: 1.5;
199
- -webkit-font-smoothing: antialiased;
200
- }
201
- `;
202
- fs.writeFileSync(path.join(devRoot, "app/globals.css"), content);
203
- }
204
- function generateHomePage(devRoot) {
205
- // The home page embeds the dev UI as a client component
206
- // It uses an iframe-based architecture: left sidebar (block list),
207
- // center (preview iframe), right sidebar (editor)
208
- const content = `"use client";
209
-
210
- import { useState, useEffect, useCallback, useRef } from "react";
211
-
212
- interface Block {
213
- type: "block" | "template";
214
- name: string;
215
- displayName: string;
216
- description?: string;
217
- category?: string;
218
- tags?: string[];
219
- version: string;
220
- hasConfig?: boolean;
221
- schema?: Record<string, any>;
222
- pages?: Array<{ name: string; slug: string; blocksCount: number }>;
223
- layoutPositions?: Array<{ position: string; type: string }>;
224
- }
225
-
226
- export default function DevHome() {
227
- const [blocks, setBlocks] = useState<Block[]>([]);
228
- const [selected, setSelected] = useState<Block | null>(null);
229
- const [previewData, setPreviewData] = useState<Record<string, unknown>>({});
230
- const [configLoading, setConfigLoading] = useState(false);
231
- const configDataRef = useRef<Record<string, unknown>>({});
232
- const iframeRef = useRef<HTMLIFrameElement>(null);
233
- const iframeLoadedRef = useRef(false);
234
- const [isDirty, setIsDirty] = useState(false);
235
- const [showBlockList, setShowBlockList] = useState(true);
236
- const [showEditor, setShowEditor] = useState(true);
237
-
238
- // Load blocks list
239
- useEffect(() => {
240
- fetch("/api/blocks")
241
- .then((r) => r.json())
242
- .then(setBlocks)
243
- .catch(console.error);
244
- }, []);
245
-
246
- // Load config when block selected
247
- useEffect(() => {
248
- if (!selected || selected.type === "template") return;
249
- setConfigLoading(true);
250
- fetch(\`/api/blocks/\${selected.name}/config\`)
251
- .then((r) => r.json())
252
- .then((config) => {
253
- setSelected((prev) =>
254
- prev ? { ...prev, schema: config.schema } : null
255
- );
256
- const data = config.previewData || {};
257
- configDataRef.current = data;
258
- setPreviewData(data);
259
- setConfigLoading(false);
260
- })
261
- .catch(() => setConfigLoading(false));
262
- }, [selected?.name]);
263
-
264
- // Select block — templates redirect to full-page preview
265
- const handleSelect = useCallback((block: Block) => {
266
- if (block.type === "template") {
267
- window.location.href = \`/preview/\${block.name}\`;
268
- return;
269
- }
270
- setSelected(block);
271
- setPreviewData({});
272
- configDataRef.current = {};
273
- setIsDirty(false);
274
- iframeLoadedRef.current = false;
275
- }, []);
276
-
277
- // Send props to iframe
278
- useEffect(() => {
279
- if (!iframeRef.current?.contentWindow || !iframeLoadedRef.current) return;
280
- if (!previewData || Object.keys(previewData).length === 0) return;
281
- iframeRef.current.contentWindow.postMessage(
282
- { type: "UPDATE_PROPS", props: previewData },
283
- "*"
284
- );
285
- }, [previewData]);
286
-
287
- // Auto-save preview data
288
- useEffect(() => {
289
- if (!isDirty || !selected || Object.keys(configDataRef.current).length === 0) return;
290
- const t = setTimeout(async () => {
291
- const dataToSave = { ...configDataRef.current, ...previewData };
292
- await fetch(\`/api/preview/\${selected.name}\`, {
293
- method: "POST",
294
- headers: { "Content-Type": "application/json" },
295
- body: JSON.stringify(dataToSave),
296
- });
297
- setIsDirty(false);
298
- }, 500);
299
- return () => clearTimeout(t);
300
- }, [previewData, selected, isDirty]);
301
-
302
- // URL state
303
- useEffect(() => {
304
- if (blocks.length === 0) return;
305
- const params = new URLSearchParams(window.location.search);
306
- const name = params.get("block") || params.get("template");
307
- if (name) {
308
- const b = blocks.find((x) => x.name === name);
309
- if (b) handleSelect(b);
310
- }
311
- }, [blocks, handleSelect]);
312
-
313
- const previewUrl = selected ? \`/preview/\${selected.name}\` : null;
314
-
315
- function renderField(field: any, value: any, onChange: (val: any) => void) {
316
- if (field.type === "multiLine" || field.type === "richText") {
317
- return (
318
- <textarea
319
- value={(value as string) || ""}
320
- onChange={(e) => onChange(e.target.value)}
321
- placeholder={field.placeholder}
322
- style={{ width: "100%", padding: "8px", border: "1px solid #ddd", borderRadius: "4px", fontSize: "13px", minHeight: "60px", resize: "vertical", fontFamily: "inherit" }}
323
- />
324
- );
325
- }
326
- if (field.type === "boolean") {
327
- return (
328
- <label style={{ display: "flex", alignItems: "center", gap: "8px", cursor: "pointer" }}>
329
- <input
330
- type="checkbox"
331
- checked={!!value}
332
- onChange={(e) => onChange(e.target.checked)}
333
- />
334
- {field.label}
335
- </label>
336
- );
337
- }
338
- if (field.type === "select") {
339
- return (
340
- <select
341
- value={(value as string) || ""}
342
- onChange={(e) => onChange(e.target.value)}
343
- style={{ width: "100%", padding: "8px", border: "1px solid #ddd", borderRadius: "4px", fontSize: "13px" }}
344
- >
345
- <option value="">Select...</option>
346
- {field.options?.map((opt: any) => (
347
- <option key={opt.value} value={opt.value}>{opt.label}</option>
348
- ))}
349
- </select>
350
- );
351
- }
352
- if (field.type === "multiselect") {
353
- const selected = Array.isArray(value) ? value : [];
354
- return (
355
- <div style={{ border: "1px solid #ddd", borderRadius: "4px", padding: "8px", display: "flex", flexWrap: "wrap", gap: "6px" }}>
356
- {field.options?.map((opt: any) => (
357
- <label key={opt.value} style={{ display: "flex", alignItems: "center", gap: "4px", fontSize: "13px", cursor: "pointer" }}>
358
- <input
359
- type="checkbox"
360
- checked={selected.includes(opt.value)}
361
- onChange={(e) => {
362
- const next = e.target.checked
363
- ? [...selected, opt.value]
364
- : selected.filter((v: string) => v !== opt.value);
365
- onChange(next);
366
- }}
367
- />
368
- {opt.label}
369
- </label>
370
- ))}
371
- {!field.options?.length && <span style={{ color: "#999", fontSize: "12px" }}>No options defined</span>}
372
- </div>
373
- );
374
- }
375
- if (field.type === "date") {
376
- return (
377
- <input
378
- type="date"
379
- value={(value as string) || ""}
380
- onChange={(e) => onChange(e.target.value)}
381
- style={{ width: "100%", padding: "8px", border: "1px solid #ddd", borderRadius: "4px", fontSize: "13px" }}
382
- />
383
- );
384
- }
385
- if (field.type === "media") {
386
- return (
387
- <div>
388
- {value && (
389
- <div style={{ marginBottom: "6px", borderRadius: "4px", overflow: "hidden", border: "1px solid #ddd" }}>
390
- <img src={value as string} alt="" style={{ maxWidth: "100%", maxHeight: "120px", objectFit: "contain", display: "block" }} />
391
- </div>
392
- )}
393
- <input
394
- type="text"
395
- value={(value as string) || ""}
396
- onChange={(e) => onChange(e.target.value)}
397
- placeholder="Image URL"
398
- style={{ width: "100%", padding: "8px", border: "1px solid #ddd", borderRadius: "4px", fontSize: "13px" }}
399
- />
400
- </div>
401
- );
402
- }
403
- if (field.type === "repeater" && field.schema) {
404
- const items = (Array.isArray(value) ? value : []) as any[];
405
- return (
406
- <div style={{ border: "1px solid #ddd", borderRadius: "6px", overflow: "hidden" }}>
407
- {items.map((item: any, idx: number) => (
408
- <div key={idx} style={{ padding: "12px", borderBottom: "1px solid #eee", background: idx % 2 === 0 ? "#fafafa" : "#fff" }}>
409
- <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "8px" }}>
410
- <span style={{ fontSize: "12px", fontWeight: 600, color: "#888" }}>#{idx + 1}</span>
411
- <button
412
- type="button"
413
- onClick={() => {
414
- const newItems = [...items];
415
- newItems.splice(idx, 1);
416
- onChange(newItems);
417
- }}
418
- style={{ fontSize: "11px", color: "#e53935", background: "none", border: "none", cursor: "pointer" }}
419
- >&times; Remove</button>
420
- </div>
421
- {Object.entries(field.schema).map(([subKey, subField]: [string, any]) => (
422
- <div key={subKey} style={{ marginBottom: "8px" }}>
423
- <label style={{ display: "block", fontSize: "11px", fontWeight: 500, marginBottom: "4px", color: "#666" }}>
424
- {subField.label || subKey}
425
- </label>
426
- {renderField(subField, item[subKey], (subVal) => {
427
- const newItems = [...items];
428
- newItems[idx] = { ...newItems[idx], [subKey]: subVal };
429
- onChange(newItems);
430
- })}
431
- </div>
432
- ))}
433
- </div>
434
- ))}
435
- <button
436
- type="button"
437
- onClick={() => {
438
- const newItem: any = {};
439
- Object.entries(field.schema).forEach(([k, f]: [string, any]) => {
440
- newItem[k] = (f as any).type === "repeater" ? [] : "";
441
- });
442
- onChange([...items, newItem]);
443
- }}
444
- style={{ width: "100%", padding: "10px", fontSize: "13px", color: "#667eea", background: "none", border: "none", cursor: "pointer", fontWeight: 500 }}
445
- >+ Add item</button>
446
- </div>
447
- );
448
- }
449
- // singleLine, link, numeric, color, form, emailTemplate, emailConfiguration, pageSelector
450
- return (
451
- <input
452
- type={field.type === "numeric" ? "number" : field.type === "color" ? "color" : field.type === "link" ? "url" : "text"}
453
- value={(value as string) || ""}
454
- onChange={(e) => onChange(field.type === "numeric" ? Number(e.target.value) : e.target.value)}
455
- placeholder={field.placeholder || (field.type === "link" ? "https://..." : field.type === "form" || field.type === "emailTemplate" || field.type === "emailConfiguration" || field.type === "pageSelector" ? "Enter ID..." : "")}
456
- style={{ width: "100%", padding: "8px", border: "1px solid #ddd", borderRadius: "4px", fontSize: "13px" }}
457
- />
458
- );
459
- }
460
-
461
- return (
462
- <div style={{ display: "grid", gridTemplateColumns: \`\${showBlockList ? "280px" : "0px"} 1fr \${showEditor ? "400px" : "0px"}\`, height: "100vh", transition: "grid-template-columns 0.2s ease" }}>
463
- {/* Block List */}
464
- <div style={{ background: "#fff", borderRight: showBlockList ? "1px solid #e0e0e0" : "none", overflow: showBlockList ? "auto" : "hidden", width: showBlockList ? "auto" : 0 }}>
465
- <div style={{ padding: "16px", borderBottom: "1px solid #e0e0e0", background: "#fafafa" }}>
466
- <h1 style={{ fontSize: "18px", fontWeight: 600, margin: 0 }}>Cmssy Dev</h1>
467
- <p style={{ fontSize: "13px", color: "#666", margin: "4px 0 0" }}>
468
- {blocks.length} blocks
469
- </p>
470
- </div>
471
- <div style={{ padding: "12px" }}>
472
- {blocks.map((b) => (
473
- <div
474
- key={b.name}
475
- onClick={() => {
476
- handleSelect(b);
477
- const url = new URL(window.location.href);
478
- url.searchParams.set(b.type === "template" ? "template" : "block", b.name);
479
- window.history.replaceState({}, "", url.toString());
480
- }}
481
- style={{
482
- padding: "12px 16px",
483
- marginBottom: "4px",
484
- borderRadius: "8px",
485
- cursor: "pointer",
486
- background: selected?.name === b.name ? "#667eea" : "transparent",
487
- color: selected?.name === b.name ? "white" : "inherit",
488
- }}
489
- >
490
- <div style={{ fontSize: "14px", fontWeight: 500 }}>{b.displayName}</div>
491
- <div style={{ fontSize: "12px", opacity: 0.7 }}>
492
- {b.type} &middot; v{b.version}
493
- </div>
494
- </div>
495
- ))}
496
- </div>
497
- </div>
498
-
499
- {/* Preview */}
500
- <div style={{ background: "#fafafa", display: "flex", flexDirection: "column" }}>
501
- <div style={{ padding: "10px 16px", background: "white", borderBottom: "1px solid #e0e0e0", display: "flex", alignItems: "center", justifyContent: "space-between", gap: "12px" }}>
502
- <button
503
- type="button"
504
- onClick={() => setShowBlockList(!showBlockList)}
505
- title={showBlockList ? "Hide block list" : "Show block list"}
506
- style={{ background: showBlockList ? "#f0f0f0" : "#667eea", color: showBlockList ? "#333" : "#fff", border: "1px solid #ddd", borderRadius: "6px", padding: "6px 10px", cursor: "pointer", fontSize: "13px", fontWeight: 500, whiteSpace: "nowrap" }}
507
- >{showBlockList ? "\u2190 Blocks" : "\u2192 Blocks"}</button>
508
- <div style={{ flex: 1, fontSize: "16px", fontWeight: 600, textAlign: "center", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
509
- {selected?.displayName || "Preview"}
510
- </div>
511
- <button
512
- type="button"
513
- onClick={() => setShowEditor(!showEditor)}
514
- title={showEditor ? "Hide editor" : "Show editor"}
515
- style={{ background: showEditor ? "#f0f0f0" : "#667eea", color: showEditor ? "#333" : "#fff", border: "1px solid #ddd", borderRadius: "6px", padding: "6px 10px", cursor: "pointer", fontSize: "13px", fontWeight: 500, whiteSpace: "nowrap" }}
516
- >{showEditor ? "Editor \u2192" : "Editor \u2190"}</button>
517
- </div>
518
- <div style={{ flex: 1, padding: "24px", display: "flex", alignItems: "center", justifyContent: "center" }}>
519
- {previewUrl ? (
520
- <div style={{ width: "100%", height: "100%", background: "white", borderRadius: "12px", boxShadow: "0 2px 8px rgba(0,0,0,0.1)", overflow: "hidden" }}>
521
- <iframe
522
- ref={iframeRef}
523
- src={previewUrl}
524
- key={previewUrl}
525
- onLoad={() => { iframeLoadedRef.current = true; }}
526
- style={{ width: "100%", height: "100%", border: "none" }}
527
- />
528
- </div>
529
- ) : (
530
- <div style={{ textAlign: "center", color: "#999" }}>
531
- <p>Select a block to preview</p>
532
- </div>
533
- )}
534
- </div>
535
- </div>
536
-
537
- {/* Editor */}
538
- <div style={{ background: "#fff", borderLeft: showEditor ? "1px solid #e0e0e0" : "none", overflow: showEditor ? "auto" : "hidden", width: showEditor ? "auto" : 0 }}>
539
- <div style={{ padding: "16px", borderBottom: "1px solid #e0e0e0", background: "#fafafa" }}>
540
- <h2 style={{ fontSize: "16px", fontWeight: 600, margin: 0 }}>Editor</h2>
541
- </div>
542
- <div style={{ padding: "20px" }}>
543
- {!selected && <p style={{ color: "#999" }}>Select a block to edit</p>}
544
- {selected && configLoading && <p style={{ color: "#999" }}>Loading...</p>}
545
- {selected && !configLoading && selected.schema && (
546
- <div>
547
- {Object.entries(selected.schema).map(([key, field]: [string, any]) => (
548
- <div key={key} style={{ marginBottom: "20px" }}>
549
- <label style={{ display: "block", fontSize: "13px", fontWeight: 500, marginBottom: "6px" }}>
550
- {field.label || key}
551
- {field.required && <span style={{ color: "#e53935" }}> *</span>}
552
- </label>
553
- {renderField(field, previewData[key], (val) => {
554
- setPreviewData({ ...previewData, [key]: val });
555
- setIsDirty(true);
556
- })}
557
- {field.helpText && (
558
- <div style={{ fontSize: "12px", color: "#666", marginTop: "4px" }}>{field.helpText}</div>
559
- )}
560
- </div>
561
- ))}
562
- </div>
563
- )}
564
- </div>
565
- </div>
566
- </div>
567
- );
568
- }
569
- `;
570
- fs.writeFileSync(path.join(devRoot, "app/page.tsx"), content);
571
- }
572
- function generateBlocksApiRoute(devRoot) {
573
- const content = `import { NextResponse } from "next/server";
574
- import fs from "fs";
575
- import path from "path";
576
-
577
- // Project root is passed via env var set by cmssy dev
578
- const projectRoot = process.env.CMSSY_PROJECT_ROOT || process.cwd();
579
-
580
- export async function GET() {
581
- const blocks: any[] = [];
582
-
583
- // Scan blocks/
584
- const blocksDir = path.join(projectRoot, "blocks");
585
- if (fs.existsSync(blocksDir)) {
586
- const dirs = fs.readdirSync(blocksDir, { withFileTypes: true })
587
- .filter((d) => d.isDirectory());
588
-
589
- for (const dir of dirs) {
590
- const pkgPath = path.join(blocksDir, dir.name, "package.json");
591
- const pkg = fs.existsSync(pkgPath) ? JSON.parse(fs.readFileSync(pkgPath, "utf-8")) : {};
592
- blocks.push({
593
- type: "block",
594
- name: dir.name,
595
- displayName: pkg.cmssy?.displayName || dir.name,
596
- version: pkg.version || "1.0.0",
597
- category: pkg.cmssy?.category || "other",
598
- tags: pkg.cmssy?.tags || [],
599
- description: pkg.description || "",
600
- hasConfig: fs.existsSync(path.join(blocksDir, dir.name, "config.ts")),
601
- });
602
- }
603
- }
604
-
605
- // Scan templates/
606
- const templatesDir = path.join(projectRoot, "templates");
607
- if (fs.existsSync(templatesDir)) {
608
- const dirs = fs.readdirSync(templatesDir, { withFileTypes: true })
609
- .filter((d) => d.isDirectory());
610
-
611
- for (const dir of dirs) {
612
- const pkgPath = path.join(templatesDir, dir.name, "package.json");
613
- const pkg = fs.existsSync(pkgPath) ? JSON.parse(fs.readFileSync(pkgPath, "utf-8")) : {};
614
- blocks.push({
615
- type: "template",
616
- name: dir.name,
617
- displayName: pkg.cmssy?.displayName || dir.name,
618
- version: pkg.version || "1.0.0",
619
- category: pkg.cmssy?.category || "pages",
620
- tags: pkg.cmssy?.tags || [],
621
- description: pkg.description || "",
622
- hasConfig: fs.existsSync(path.join(templatesDir, dir.name, "config.ts")),
623
- });
624
- }
625
- }
626
-
627
- return NextResponse.json(blocks);
628
- }
629
- `;
630
- fs.mkdirSync(path.join(devRoot, "app/api/blocks"), { recursive: true });
631
- fs.writeFileSync(path.join(devRoot, "app/api/blocks/route.ts"), content);
632
- }
633
- function generateBlockConfigApiRoute(devRoot) {
634
- const dir = path.join(devRoot, "app/api/blocks/[name]/config");
635
- fs.mkdirSync(dir, { recursive: true });
636
- const content = `import { NextResponse } from "next/server";
637
- import fs from "fs";
638
- import path from "path";
639
- import { execSync } from "child_process";
640
-
641
- const projectRoot = process.env.CMSSY_PROJECT_ROOT || process.cwd();
642
-
643
- function loadBlockConfig(blockPath: string): Record<string, unknown> | null {
644
- const configPath = path.join(blockPath, "config.ts");
645
- if (!fs.existsSync(configPath)) return null;
646
-
647
- try {
648
- // Find tsx binary
649
- const tsxPaths = [
650
- path.join(projectRoot, "node_modules", ".bin", "tsx"),
651
- path.join(projectRoot, "node_modules", "cmssy-cli", "node_modules", ".bin", "tsx"),
652
- ];
653
- const tsxBinary = tsxPaths.find((p) => fs.existsSync(p)) || "npx -y tsx";
654
-
655
- // Create mock cmssy-cli/config module
656
- const cacheDir = path.join(projectRoot, ".cmssy", "cache");
657
- fs.mkdirSync(cacheDir, { recursive: true });
658
-
659
- const mockConfigPath = path.join(cacheDir, "cmssy-cli-config.mjs");
660
- fs.writeFileSync(mockConfigPath,
661
- "export const defineBlock = (config) => config;\\nexport const defineTemplate = (config) => config;"
662
- );
663
-
664
- // Replace import path in config
665
- const configContent = fs.readFileSync(configPath, "utf-8");
666
- const modified = configContent.replace(
667
- /from\\s+['"](?:@cmssy\\/cli\\/config|cmssy-cli\\/config)['"]/g,
668
- \`from '\${mockConfigPath.replace(/\\\\\\\\/g, "/")}'\`
669
- );
670
-
671
- const tempPath = path.join(cacheDir, "temp-block-config.ts");
672
- fs.writeFileSync(tempPath, modified);
673
-
674
- const evalCode = \`import cfg from '\${tempPath.replace(/\\\\\\\\/g, "/")}'; console.log(JSON.stringify(cfg.default || cfg));\`;
675
- const cmd = tsxBinary.includes("npx")
676
- ? \`\${tsxBinary} --eval "\${evalCode}"\`
677
- : \`"\${tsxBinary}" --eval "\${evalCode}"\`;
678
-
679
- const output = execSync(cmd, {
680
- encoding: "utf-8",
681
- cwd: projectRoot,
682
- stdio: ["pipe", "pipe", "pipe"],
683
- });
684
-
685
- // Clean up
686
- try { fs.unlinkSync(tempPath); } catch {}
687
- try { fs.unlinkSync(mockConfigPath); } catch {}
688
-
689
- const lines = output.trim().split("\\n");
690
- return JSON.parse(lines[lines.length - 1]);
691
- } catch {
692
- return null;
693
- }
694
- }
695
-
696
- export async function GET(
697
- _request: Request,
698
- { params }: { params: Promise<{ name: string }> }
699
- ) {
700
- const { name } = await params;
701
-
702
- // Look in blocks/ first, then templates/
703
- let blockPath = path.join(projectRoot, "blocks", name);
704
- let resourceType: "block" | "template" = "block";
705
- if (!fs.existsSync(blockPath)) {
706
- blockPath = path.join(projectRoot, "templates", name);
707
- resourceType = "template";
708
- }
709
-
710
- if (!fs.existsSync(blockPath)) {
711
- return NextResponse.json({ error: "Block not found" }, { status: 404 });
712
- }
713
-
714
- // Load preview data
715
- const previewPath = path.join(blockPath, "preview.json");
716
- const previewData = fs.existsSync(previewPath)
717
- ? JSON.parse(fs.readFileSync(previewPath, "utf-8"))
718
- : {};
719
-
720
- // Load config schema
721
- const config = loadBlockConfig(blockPath);
722
-
723
- // Check for pages.json (templates), fall back to config.ts
724
- const pagesJsonPath = path.join(blockPath, "pages.json");
725
- let pagesData = fs.existsSync(pagesJsonPath)
726
- ? JSON.parse(fs.readFileSync(pagesJsonPath, "utf-8"))
727
- : null;
728
-
729
- // If no pages.json but config has pages (template), convert on the fly
730
- if (!pagesData && config && (config.pages || config.layoutPositions)) {
731
- const layoutPositions: Record<string, any> = {};
732
- if (Array.isArray(config.layoutPositions)) {
733
- for (const lp of config.layoutPositions as any[]) {
734
- layoutPositions[lp.position] = { type: lp.type, content: lp.content || {} };
735
- }
736
- } else if (config.layoutPositions && typeof config.layoutPositions === "object") {
737
- Object.assign(layoutPositions, config.layoutPositions);
738
- }
739
-
740
- const pages = ((config.pages || []) as any[]).map((page: any, index: number) => ({
741
- name: page.name,
742
- slug: page.slug === "home" || page.slug === "/" || index === 0
743
- ? "/"
744
- : page.slug.startsWith("/") ? page.slug : "/" + page.slug,
745
- blocks: page.blocks || [],
746
- }));
747
-
748
- pagesData = { layoutPositions, pages };
749
- }
750
-
751
- return NextResponse.json({
752
- name,
753
- schema: config?.schema || {},
754
- previewData,
755
- pages: config?.pages,
756
- layoutPositions: config?.layoutPositions,
757
- pagesData,
758
- });
759
- }
760
- `;
761
- fs.writeFileSync(path.join(dir, "route.ts"), content);
762
- }
763
- function generatePreviewApiRoute(devRoot) {
764
- const dir = path.join(devRoot, "app/api/preview/[blockName]");
765
- fs.mkdirSync(dir, { recursive: true });
766
- const content = `import { NextResponse } from "next/server";
767
- import fs from "fs";
768
- import path from "path";
769
-
770
- const projectRoot = process.env.CMSSY_PROJECT_ROOT || process.cwd();
771
-
772
- export async function GET(
773
- _request: Request,
774
- { params }: { params: Promise<{ blockName: string }> }
775
- ) {
776
- const { blockName } = await params;
777
- let blockPath = path.join(projectRoot, "blocks", blockName);
778
- if (!fs.existsSync(blockPath)) {
779
- blockPath = path.join(projectRoot, "templates", blockName);
780
- }
781
-
782
- const previewPath = path.join(blockPath, "preview.json");
783
- const data = fs.existsSync(previewPath)
784
- ? JSON.parse(fs.readFileSync(previewPath, "utf-8"))
785
- : {};
786
-
787
- return NextResponse.json(data);
788
- }
789
-
790
- export async function POST(
791
- request: Request,
792
- { params }: { params: Promise<{ blockName: string }> }
793
- ) {
794
- const { blockName } = await params;
795
- const body = await request.json();
796
-
797
- let blockPath = path.join(projectRoot, "blocks", blockName);
798
- if (!fs.existsSync(blockPath)) {
799
- blockPath = path.join(projectRoot, "templates", blockName);
800
- }
801
-
802
- const previewPath = path.join(blockPath, "preview.json");
803
- fs.writeFileSync(previewPath, JSON.stringify(body, null, 2));
804
-
805
- return NextResponse.json({ success: true });
806
- }
807
- `;
808
- fs.writeFileSync(path.join(dir, "route.ts"), content);
809
- }
810
- function generateWorkspacesApiRoute(devRoot) {
811
- const content = `import { NextResponse } from "next/server";
812
-
813
- export async function GET() {
814
- // Workspace listing requires API token - return empty for now
815
- // The full implementation uses GraphQL client with cmssy configure credentials
816
- return NextResponse.json([]);
817
- }
818
- `;
819
- fs.writeFileSync(path.join(devRoot, "app/api/workspaces/route.ts"), content);
820
- }
821
- /**
822
- * Convert full block type name to simple name.
823
- * "@cmssy-marketing/blocks.hero" -> "hero"
824
- * "@vendor/blocks.pricing-table" -> "pricing-table"
825
- * "hero" -> "hero" (already simple)
826
- */
827
- function convertBlockTypeToSimple(blockType) {
828
- let simple = blockType;
829
- if (simple.includes("/")) {
830
- simple = simple.split("/").pop();
831
- }
832
- if (simple.startsWith("blocks.")) {
833
- simple = simple.substring(7);
834
- }
835
- else if (simple.startsWith("templates.")) {
836
- simple = simple.substring(10);
837
- }
838
- return simple;
839
- }
840
- /**
841
- * Load config.ts synchronously using tsx/esbuild.
842
- * Used to generate template preview pages when pages.json is missing.
843
- */
844
- function loadTemplateConfigSync(templateDir, projectRoot) {
845
- const configPath = path.join(templateDir, "config.ts");
846
- if (!fs.existsSync(configPath))
847
- return null;
848
- try {
849
- const cliPath = path.dirname(path.dirname(new URL(import.meta.url).pathname));
850
- const possibleTsxPaths = [
851
- path.join(cliPath, "..", "node_modules", ".bin", "tsx"),
852
- path.join(cliPath, "..", "..", "node_modules", ".bin", "tsx"),
853
- path.join(projectRoot, "node_modules", ".bin", "tsx"),
854
- ];
855
- let tsxBinary = possibleTsxPaths.find((p) => fs.existsSync(p));
856
- if (!tsxBinary)
857
- tsxBinary = "npx -y tsx";
858
- const cacheDir = path.join(projectRoot, ".cmssy", "cache");
859
- fs.ensureDirSync(cacheDir);
860
- const mockConfigPath = path.join(cacheDir, "cmssy-cli-config.mjs");
861
- fs.writeFileSync(mockConfigPath, "export const defineBlock = (config) => config;\nexport const defineTemplate = (config) => config;\n");
862
- const configContent = fs.readFileSync(configPath, "utf-8");
863
- const modified = configContent.replace(/from\s+['"](?:@?cmssy-?(?:\/cli)?\/config|cmssy-cli\/config)['"]/g, `from '${mockConfigPath.replace(/\\/g, "/")}'`);
864
- const tempPath = path.join(cacheDir, `temp-template-config-${Date.now()}.ts`);
865
- fs.writeFileSync(tempPath, modified);
866
- const evalCode = `import cfg from '${tempPath.replace(/\\/g, "/")}'; console.log(JSON.stringify(cfg.default || cfg));`;
867
- const cmd = tsxBinary.includes("npx")
868
- ? `${tsxBinary} --eval "${evalCode}"`
869
- : `"${tsxBinary}" --eval "${evalCode}"`;
870
- const output = execSync(cmd, {
871
- encoding: "utf-8",
872
- cwd: projectRoot,
873
- stdio: ["pipe", "pipe", "pipe"],
874
- });
875
- try {
876
- fs.removeSync(tempPath);
877
- }
878
- catch { }
879
- try {
880
- fs.removeSync(mockConfigPath);
881
- }
882
- catch { }
883
- const lines = output.trim().split("\n");
884
- return JSON.parse(lines[lines.length - 1]);
885
- }
886
- catch {
887
- return null;
888
- }
889
- }
890
- /**
891
- * Convert template config (from config.ts defineTemplate) to pages.json format.
892
- * - layoutPositions: array → object keyed by position
893
- * - page slugs: "home" → "/", others → "/{slug}"
894
- */
895
- function convertConfigToPagesData(config) {
896
- // Convert layoutPositions from array to object
897
- const layoutPositions = {};
898
- if (Array.isArray(config.layoutPositions)) {
899
- for (const lp of config.layoutPositions) {
900
- layoutPositions[lp.position] = {
901
- type: lp.type,
902
- content: lp.content || {},
903
- };
904
- }
905
- }
906
- else if (config.layoutPositions &&
907
- typeof config.layoutPositions === "object") {
908
- // Already in object format
909
- Object.assign(layoutPositions, config.layoutPositions);
910
- }
911
- // Convert page slugs
912
- const pages = (config.pages || []).map((page, index) => ({
913
- name: page.name,
914
- slug: page.slug === "home" || page.slug === "/" || index === 0
915
- ? "/"
916
- : page.slug.startsWith("/")
917
- ? page.slug
918
- : `/${page.slug}`,
919
- blocks: page.blocks || [],
920
- }));
921
- return { layoutPositions, pages };
922
- }
923
- function generatePreviewPages(devRoot, projectRoot, resources) {
924
- for (const resource of resources) {
925
- if (resource.type === "template") {
926
- generateTemplatePreviewPage(devRoot, projectRoot, resource);
927
- }
928
- else {
929
- generateBlockPreviewPage(devRoot, projectRoot, resource);
930
- }
931
- }
932
- }
933
- function generateBlockPreviewPage(devRoot, projectRoot, resource) {
934
- const pageDir = path.join(devRoot, "app/preview", resource.name);
935
- fs.mkdirSync(pageDir, { recursive: true });
936
- const blockSrcDir = path.join(projectRoot, "blocks", resource.name, "src");
937
- const hasIndex = fs.existsSync(path.join(blockSrcDir, "index.tsx")) ||
938
- fs.existsSync(path.join(blockSrcDir, "index.ts"));
939
- const hasCss = fs.existsSync(path.join(blockSrcDir, "index.css"));
940
- if (!hasIndex)
941
- return;
942
- const finalBlockImport = `@blocks/${resource.name}/src/index`;
943
- const finalCssImport = `@blocks/${resource.name}/src/index.css`;
944
- const pageContent = `"use client";
945
-
946
- import { useState, useEffect } from "react";
947
- import BlockComponent from "${finalBlockImport}";
948
- ${hasCss ? `import "${finalCssImport}";` : ""}
949
-
950
- export default function ${toPascalCase(resource.name)}Preview() {
951
- const [data, setData] = useState<Record<string, unknown>>({});
952
-
953
- useEffect(() => {
954
- fetch("/api/preview/${resource.name}")
955
- .then((r) => r.json())
956
- .then(setData)
957
- .catch(console.error);
958
- }, []);
959
-
960
- useEffect(() => {
961
- function handleMessage(e: MessageEvent) {
962
- if (e.data?.type === "UPDATE_PROPS") {
963
- setData(e.data.props);
964
- }
965
- }
966
- window.addEventListener("message", handleMessage);
967
- return () => window.removeEventListener("message", handleMessage);
968
- }, []);
969
-
970
- return (
971
- <div>
972
- <div style={{
973
- position: "fixed", top: 0, left: 0, right: 0,
974
- background: "white", borderBottom: "1px solid #e0e0e0",
975
- padding: "1rem 2rem", zIndex: 1000,
976
- display: "flex", justifyContent: "space-between", alignItems: "center",
977
- }}>
978
- <div style={{ fontSize: "1.25rem", fontWeight: 600 }}>${resource.displayName || resource.name}</div>
979
- <a href="/" style={{ color: "#667eea", textDecoration: "none", fontWeight: 500 }} target="_parent">
980
- &larr; Back to Home
981
- </a>
982
- </div>
983
- <div style={{ marginTop: "60px", minHeight: "calc(100vh - 60px)" }}>
984
- <BlockComponent content={data} />
985
- </div>
986
- </div>
987
- );
988
- }
989
- `;
990
- fs.writeFileSync(path.join(pageDir, "page.tsx"), pageContent);
991
- }
992
- function generateTemplatePreviewPage(devRoot, projectRoot, resource) {
993
- // Read pages data from pages.json or fall back to config.ts
994
- const templateDir = path.join(projectRoot, "templates", resource.name);
995
- const pagesJsonPath = path.join(templateDir, "pages.json");
996
- let pagesData;
997
- if (fs.existsSync(pagesJsonPath)) {
998
- pagesData = JSON.parse(fs.readFileSync(pagesJsonPath, "utf-8"));
999
- }
1000
- else {
1001
- // No pages.json — load from config.ts and convert format
1002
- const config = loadTemplateConfigSync(templateDir, projectRoot);
1003
- if (!config || (!config.pages && !config.layoutPositions))
1004
- return;
1005
- pagesData = convertConfigToPagesData(config);
1006
- }
1007
- const pages = pagesData.pages || [];
1008
- const layoutPositions = pagesData.layoutPositions || {};
1009
- // Collect all unique block types across all pages + layoutPositions
1010
- const blockTypesSet = new Set();
1011
- for (const page of pages) {
1012
- for (const block of page.blocks || []) {
1013
- blockTypesSet.add(convertBlockTypeToSimple(block.type));
1014
- }
1015
- }
1016
- for (const data of Object.values(layoutPositions)) {
1017
- blockTypesSet.add(convertBlockTypeToSimple(data.type));
1018
- }
1019
- // Check which blocks exist in blocks/ dir with src/index.tsx
1020
- const blockImports = [];
1021
- const cssImports = [];
1022
- const componentMapEntries = [];
1023
- for (const blockName of Array.from(blockTypesSet)) {
1024
- const blockSrcDir = path.join(projectRoot, "blocks", blockName, "src");
1025
- const hasIndex = fs.existsSync(path.join(blockSrcDir, "index.tsx")) ||
1026
- fs.existsSync(path.join(blockSrcDir, "index.ts"));
1027
- const hasCss = fs.existsSync(path.join(blockSrcDir, "index.css"));
1028
- if (!hasIndex)
1029
- continue;
1030
- const pascalName = toPascalCase(blockName);
1031
- blockImports.push(`import ${pascalName} from "@blocks/${blockName}/src/index";`);
1032
- if (hasCss) {
1033
- cssImports.push(`import "@blocks/${blockName}/src/index.css";`);
1034
- }
1035
- componentMapEntries.push(` "${blockName}": ${pascalName},`);
1036
- }
1037
- // Generate [[...slug]] catch-all route
1038
- const pageDir = path.join(devRoot, "app/preview", resource.name, "[[...slug]]");
1039
- fs.mkdirSync(pageDir, { recursive: true });
1040
- const pageContent = `"use client";
1041
-
1042
- import { useState, useEffect, useRef } from "react";
1043
- import { useParams, useRouter } from "next/navigation";
1044
- ${blockImports.join("\n")}
1045
- ${cssImports.join("\n")}
1046
-
1047
- const BLOCK_COMPONENTS: Record<string, React.ComponentType<any>> = {
1048
- ${componentMapEntries.join("\n")}
1049
- };
1050
-
1051
- interface BlockData {
1052
- type: string;
1053
- content: Record<string, any>;
1054
- }
1055
-
1056
- interface PageData {
1057
- name: string;
1058
- slug: string;
1059
- blocks: BlockData[];
1060
- }
1061
-
1062
- interface PagesJson {
1063
- layoutPositions?: Record<string, BlockData>;
1064
- pages: PageData[];
1065
- }
1066
-
1067
- function convertBlockType(type: string): string {
1068
- let simple = type;
1069
- if (simple.includes("/")) simple = simple.split("/").pop()!;
1070
- if (simple.startsWith("blocks.")) simple = simple.substring(7);
1071
- else if (simple.startsWith("templates.")) simple = simple.substring(10);
1072
- return simple;
1073
- }
1074
-
1075
- export default function ${toPascalCase(resource.name)}TemplatePreview() {
1076
- const params = useParams();
1077
- const router = useRouter();
1078
- const slugParts = (params.slug as string[]) || [];
1079
- const currentSlug = slugParts.length > 0 ? "/" + slugParts.join("/") : "/";
1080
- const tabsRef = useRef<HTMLDivElement>(null);
1081
- const activeTabRef = useRef<HTMLButtonElement>(null);
1082
-
1083
- const [pagesData, setPagesData] = useState<PagesJson | null>(null);
1084
- const [contentOverrides, setContentOverrides] = useState<Record<string, BlockData[]>>({});
1085
- const [navVisible, setNavVisible] = useState(true);
1086
- const [paletteOpen, setPaletteOpen] = useState(false);
1087
- const [paletteQuery, setPaletteQuery] = useState("");
1088
- const [navCollapsed, setNavCollapsed] = useState(false);
1089
- const paletteInputRef = useRef<HTMLInputElement>(null);
1090
-
1091
- // Fetch pages data from config API
1092
- useEffect(() => {
1093
- fetch("/api/blocks/${resource.name}/config")
1094
- .then((r) => r.json())
1095
- .then((config) => {
1096
- if (config.pagesData) {
1097
- setPagesData(config.pagesData);
1098
- }
1099
- })
1100
- .catch(console.error);
1101
- }, []);
1102
-
1103
- // Listen for live content updates from editor
1104
- useEffect(() => {
1105
- function handleMessage(e: MessageEvent) {
1106
- if (e.data?.type === "UPDATE_TEMPLATE_CONTENT") {
1107
- setContentOverrides((prev) => ({
1108
- ...prev,
1109
- [e.data.pageSlug]: e.data.blocks,
1110
- }));
1111
- }
1112
- }
1113
- window.addEventListener("message", handleMessage);
1114
- return () => window.removeEventListener("message", handleMessage);
1115
- }, []);
1116
-
1117
- // Scroll active tab into view
1118
- useEffect(() => {
1119
- if (activeTabRef.current && tabsRef.current) {
1120
- activeTabRef.current.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
1121
- }
1122
- }, [currentSlug, pagesData]);
1123
-
1124
- // Ctrl+K / Cmd+K to open palette
1125
- useEffect(() => {
1126
- function onKeyDown(e: KeyboardEvent) {
1127
- if ((e.metaKey || e.ctrlKey) && e.key === "k") {
1128
- e.preventDefault();
1129
- setPaletteOpen((v) => !v);
1130
- setPaletteQuery("");
1131
- }
1132
- if (e.key === "Escape") {
1133
- setPaletteOpen(false);
1134
- }
1135
- }
1136
- window.addEventListener("keydown", onKeyDown);
1137
- return () => window.removeEventListener("keydown", onKeyDown);
1138
- }, []);
1139
-
1140
- // Focus input when palette opens
1141
- useEffect(() => {
1142
- if (paletteOpen && paletteInputRef.current) {
1143
- paletteInputRef.current.focus();
1144
- }
1145
- }, [paletteOpen]);
1146
-
1147
- // Auto-hide nav on scroll down, show on scroll up
1148
- useEffect(() => {
1149
- let lastY = 0;
1150
- function onScroll() {
1151
- const y = window.scrollY;
1152
- if (y > 80 && y > lastY) setNavVisible(false);
1153
- else setNavVisible(true);
1154
- lastY = y;
1155
- }
1156
- window.addEventListener("scroll", onScroll, { passive: true });
1157
- return () => window.removeEventListener("scroll", onScroll);
1158
- }, []);
1159
-
1160
- if (!pagesData) {
1161
- return (
1162
- <div style={{
1163
- height: "100vh",
1164
- display: "flex",
1165
- alignItems: "center",
1166
- justifyContent: "center",
1167
- background: "#0a0a0a",
1168
- }}>
1169
- <div style={{ textAlign: "center" }}>
1170
- <div style={{
1171
- width: "24px",
1172
- height: "24px",
1173
- border: "2px solid rgba(255,255,255,0.1)",
1174
- borderTopColor: "#667eea",
1175
- borderRadius: "50%",
1176
- animation: "spin 0.8s linear infinite",
1177
- margin: "0 auto 16px",
1178
- }} />
1179
- <div style={{ color: "rgba(255,255,255,0.4)", fontSize: "13px", letterSpacing: "0.05em" }}>
1180
- Loading template...
1181
- </div>
1182
- </div>
1183
- <style>{\`@keyframes spin { to { transform: rotate(360deg) } }\`}</style>
1184
- </div>
1185
- );
1186
- }
1187
-
1188
- // Find current page
1189
- const currentPage = pagesData.pages.find((p) => p.slug === currentSlug);
1190
- if (!currentPage) {
1191
- return (
1192
- <div style={{
1193
- height: "100vh",
1194
- display: "flex",
1195
- alignItems: "center",
1196
- justifyContent: "center",
1197
- background: "#fafafa",
1198
- }}>
1199
- <div style={{ textAlign: "center" }}>
1200
- <div style={{ fontSize: "48px", marginBottom: "12px", opacity: 0.3 }}>404</div>
1201
- <div style={{ color: "#666", fontSize: "14px" }}>Page not found: {currentSlug}</div>
1202
- <button
1203
- onClick={() => router.push("/preview/${resource.name}")}
1204
- style={{
1205
- marginTop: "20px",
1206
- padding: "8px 20px",
1207
- background: "#667eea",
1208
- color: "#fff",
1209
- border: "none",
1210
- borderRadius: "6px",
1211
- fontSize: "13px",
1212
- cursor: "pointer",
1213
- }}
1214
- >Go to homepage</button>
1215
- </div>
1216
- </div>
1217
- );
1218
- }
1219
-
1220
- // Use overrides if available, otherwise use pages.json content
1221
- const pageBlocks = contentOverrides[currentSlug] || currentPage.blocks || [];
1222
-
1223
- // Render header layout position
1224
- const headerData = pagesData.layoutPositions?.header;
1225
- const headerType = headerData ? convertBlockType(headerData.type) : null;
1226
- const HeaderComponent = headerType ? BLOCK_COMPONENTS[headerType] : null;
1227
-
1228
- // Render footer layout position
1229
- const footerData = pagesData.layoutPositions?.footer;
1230
- const footerType = footerData ? convertBlockType(footerData.type) : null;
1231
- const FooterComponent = footerType ? BLOCK_COMPONENTS[footerType] : null;
1232
-
1233
- // Render sidebar_left layout position
1234
- const sidebarLeftData = pagesData.layoutPositions?.sidebar_left;
1235
- const sidebarLeftType = sidebarLeftData ? convertBlockType(sidebarLeftData.type) : null;
1236
- const SidebarLeftComponent = sidebarLeftType ? BLOCK_COMPONENTS[sidebarLeftType] : null;
1237
-
1238
- const navigateTo = (slug: string) => {
1239
- const path = slug === "/" ? "/preview/${resource.name}" : \`/preview/${resource.name}\${slug}\`;
1240
- router.push(path);
1241
- };
1242
-
1243
- return (
1244
- <>
1245
- <style>{\`
1246
- @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600&display=swap');
1247
-
1248
- .cmssy-nav {
1249
- position: fixed;
1250
- top: 0;
1251
- left: 0;
1252
- right: 0;
1253
- z-index: 9999;
1254
- transform: translateY(\${navVisible ? "0" : "-100%"});
1255
- transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1256
- }
1257
- .cmssy-nav-inner {
1258
- margin: 10px;
1259
- background: rgba(10, 10, 10, 0.92);
1260
- backdrop-filter: blur(20px) saturate(180%);
1261
- -webkit-backdrop-filter: blur(20px) saturate(180%);
1262
- border-radius: 14px;
1263
- border: 1px solid rgba(255, 255, 255, 0.08);
1264
- box-shadow:
1265
- 0 8px 32px rgba(0, 0, 0, 0.4),
1266
- 0 0 0 1px rgba(255, 255, 255, 0.05) inset;
1267
- padding: 8px 12px;
1268
- display: flex;
1269
- align-items: center;
1270
- gap: 8px;
1271
- font-family: 'DM Sans', -apple-system, sans-serif;
1272
- }
1273
- .cmssy-back-btn {
1274
- display: flex;
1275
- align-items: center;
1276
- gap: 6px;
1277
- padding: 6px 12px;
1278
- background: rgba(255, 255, 255, 0.06);
1279
- border: 1px solid rgba(255, 255, 255, 0.08);
1280
- border-radius: 8px;
1281
- color: rgba(255, 255, 255, 0.7);
1282
- font-size: 12px;
1283
- font-weight: 500;
1284
- cursor: pointer;
1285
- text-decoration: none;
1286
- white-space: nowrap;
1287
- transition: all 0.15s ease;
1288
- font-family: inherit;
1289
- letter-spacing: 0.01em;
1290
- }
1291
- .cmssy-back-btn:hover {
1292
- background: rgba(255, 255, 255, 0.12);
1293
- color: #fff;
1294
- }
1295
- .cmssy-divider {
1296
- width: 1px;
1297
- height: 24px;
1298
- background: rgba(255, 255, 255, 0.1);
1299
- flex-shrink: 0;
1300
- }
1301
- .cmssy-template-name {
1302
- font-size: 13px;
1303
- font-weight: 600;
1304
- color: #fff;
1305
- white-space: nowrap;
1306
- letter-spacing: -0.01em;
1307
- flex-shrink: 0;
1308
- }
1309
- .cmssy-tabs-wrapper {
1310
- flex: 1;
1311
- overflow-x: auto;
1312
- overflow-y: hidden;
1313
- scrollbar-width: none;
1314
- -ms-overflow-style: none;
1315
- mask-image: linear-gradient(to right, transparent, black 20px, black calc(100% - 20px), transparent);
1316
- -webkit-mask-image: linear-gradient(to right, transparent, black 20px, black calc(100% - 20px), transparent);
1317
- }
1318
- .cmssy-tabs-wrapper::-webkit-scrollbar {
1319
- display: none;
1320
- }
1321
- .cmssy-tabs {
1322
- display: flex;
1323
- gap: 2px;
1324
- padding: 0 8px;
1325
- }
1326
- .cmssy-tab {
1327
- padding: 5px 12px;
1328
- font-size: 12px;
1329
- font-weight: 500;
1330
- border: none;
1331
- border-radius: 7px;
1332
- cursor: pointer;
1333
- white-space: nowrap;
1334
- transition: all 0.15s ease;
1335
- font-family: inherit;
1336
- letter-spacing: 0.01em;
1337
- }
1338
- .cmssy-tab-inactive {
1339
- background: transparent;
1340
- color: rgba(255, 255, 255, 0.45);
1341
- }
1342
- .cmssy-tab-inactive:hover {
1343
- background: rgba(255, 255, 255, 0.08);
1344
- color: rgba(255, 255, 255, 0.8);
1345
- }
1346
- .cmssy-tab-active {
1347
- background: #667eea;
1348
- color: #fff;
1349
- box-shadow: 0 2px 8px rgba(102, 126, 234, 0.35);
1350
- }
1351
- .cmssy-page-count {
1352
- font-size: 11px;
1353
- font-weight: 500;
1354
- color: rgba(255, 255, 255, 0.3);
1355
- white-space: nowrap;
1356
- flex-shrink: 0;
1357
- padding-right: 4px;
1358
- }
1359
- .cmssy-collapse-btn {
1360
- display: flex;
1361
- align-items: center;
1362
- justify-content: center;
1363
- width: 28px;
1364
- height: 28px;
1365
- background: rgba(255, 255, 255, 0.06);
1366
- border: 1px solid rgba(255, 255, 255, 0.08);
1367
- border-radius: 7px;
1368
- color: rgba(255, 255, 255, 0.5);
1369
- cursor: pointer;
1370
- flex-shrink: 0;
1371
- transition: all 0.15s ease;
1372
- font-family: inherit;
1373
- }
1374
- .cmssy-collapse-btn:hover {
1375
- background: rgba(255, 255, 255, 0.12);
1376
- color: #fff;
1377
- }
1378
- .cmssy-nav-collapsed .cmssy-nav-inner {
1379
- padding: 6px 8px;
1380
- width: fit-content;
1381
- }
1382
- .cmssy-nav-pill {
1383
- display: flex;
1384
- align-items: center;
1385
- gap: 8px;
1386
- font-family: 'DM Sans', -apple-system, sans-serif;
1387
- }
1388
- .cmssy-nav-pill-page {
1389
- font-size: 12px;
1390
- font-weight: 500;
1391
- color: rgba(255, 255, 255, 0.7);
1392
- white-space: nowrap;
1393
- }
1394
- .cmssy-search-btn {
1395
- display: flex;
1396
- align-items: center;
1397
- gap: 6px;
1398
- padding: 5px 10px;
1399
- background: rgba(255, 255, 255, 0.06);
1400
- border: 1px solid rgba(255, 255, 255, 0.08);
1401
- border-radius: 7px;
1402
- color: rgba(255, 255, 255, 0.5);
1403
- font-size: 11px;
1404
- font-family: inherit;
1405
- cursor: pointer;
1406
- white-space: nowrap;
1407
- transition: all 0.15s ease;
1408
- flex-shrink: 0;
1409
- }
1410
- .cmssy-search-btn:hover {
1411
- background: rgba(255, 255, 255, 0.1);
1412
- color: rgba(255, 255, 255, 0.8);
1413
- }
1414
- .cmssy-search-kbd {
1415
- font-size: 10px;
1416
- padding: 1px 5px;
1417
- border-radius: 4px;
1418
- background: rgba(255, 255, 255, 0.08);
1419
- color: rgba(255, 255, 255, 0.35);
1420
- font-family: inherit;
1421
- }
1422
- .cmssy-palette-overlay {
1423
- position: fixed;
1424
- inset: 0;
1425
- z-index: 10000;
1426
- background: rgba(0, 0, 0, 0.5);
1427
- backdrop-filter: blur(4px);
1428
- display: flex;
1429
- align-items: flex-start;
1430
- justify-content: center;
1431
- padding-top: 20vh;
1432
- animation: cmssyFadeIn 0.15s ease;
1433
- }
1434
- .cmssy-palette {
1435
- width: 480px;
1436
- max-width: calc(100vw - 32px);
1437
- background: #1a1a1a;
1438
- border: 1px solid rgba(255, 255, 255, 0.1);
1439
- border-radius: 14px;
1440
- box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6);
1441
- overflow: hidden;
1442
- animation: cmssySlideUp 0.15s ease;
1443
- font-family: 'DM Sans', -apple-system, sans-serif;
1444
- }
1445
- .cmssy-palette-input-wrapper {
1446
- display: flex;
1447
- align-items: center;
1448
- padding: 14px 16px;
1449
- border-bottom: 1px solid rgba(255, 255, 255, 0.08);
1450
- gap: 10px;
1451
- }
1452
- .cmssy-palette-input {
1453
- flex: 1;
1454
- background: none;
1455
- border: none;
1456
- outline: none;
1457
- color: #fff;
1458
- font-size: 15px;
1459
- font-family: inherit;
1460
- letter-spacing: -0.01em;
1461
- }
1462
- .cmssy-palette-input::placeholder {
1463
- color: rgba(255, 255, 255, 0.3);
1464
- }
1465
- .cmssy-palette-list {
1466
- max-height: 340px;
1467
- overflow-y: auto;
1468
- padding: 6px;
1469
- scrollbar-width: thin;
1470
- scrollbar-color: rgba(255,255,255,0.1) transparent;
1471
- }
1472
- .cmssy-palette-item {
1473
- display: flex;
1474
- align-items: center;
1475
- justify-content: space-between;
1476
- padding: 10px 12px;
1477
- border-radius: 8px;
1478
- cursor: pointer;
1479
- transition: background 0.1s ease;
1480
- border: none;
1481
- background: none;
1482
- width: 100%;
1483
- text-align: left;
1484
- font-family: inherit;
1485
- }
1486
- .cmssy-palette-item:hover {
1487
- background: rgba(255, 255, 255, 0.06);
1488
- }
1489
- .cmssy-palette-item-active {
1490
- background: rgba(102, 126, 234, 0.15);
1491
- }
1492
- .cmssy-palette-item-active:hover {
1493
- background: rgba(102, 126, 234, 0.2);
1494
- }
1495
- .cmssy-palette-item-name {
1496
- font-size: 14px;
1497
- font-weight: 500;
1498
- color: #fff;
1499
- }
1500
- .cmssy-palette-item-slug {
1501
- font-size: 12px;
1502
- color: rgba(255, 255, 255, 0.3);
1503
- font-family: monospace;
1504
- }
1505
- .cmssy-palette-item-current {
1506
- font-size: 10px;
1507
- padding: 2px 8px;
1508
- border-radius: 4px;
1509
- background: #667eea;
1510
- color: #fff;
1511
- font-weight: 600;
1512
- letter-spacing: 0.03em;
1513
- }
1514
- .cmssy-palette-empty {
1515
- padding: 24px 16px;
1516
- text-align: center;
1517
- color: rgba(255, 255, 255, 0.3);
1518
- font-size: 13px;
1519
- }
1520
- @keyframes cmssyFadeIn {
1521
- from { opacity: 0; }
1522
- to { opacity: 1; }
1523
- }
1524
- @keyframes cmssySlideUp {
1525
- from { opacity: 0; transform: translateY(8px) scale(0.98); }
1526
- to { opacity: 1; transform: translateY(0) scale(1); }
1527
- }
1528
- \`}</style>
1529
-
1530
- {/* Floating Navigation Bar */}
1531
- <div className={\`cmssy-nav \${navCollapsed ? "cmssy-nav-collapsed" : ""}\`}>
1532
- <div className="cmssy-nav-inner">
1533
- {navCollapsed ? (
1534
- <div className="cmssy-nav-pill">
1535
- <a href="/" className="cmssy-back-btn">
1536
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1537
- <polyline points="15 18 9 12 15 6" />
1538
- </svg>
1539
- Dev
1540
- </a>
1541
- <div className="cmssy-divider" />
1542
- <div className="cmssy-nav-pill-page">{currentPage?.name || "—"}</div>
1543
- <button
1544
- className="cmssy-search-btn"
1545
- onClick={() => { setPaletteOpen(true); setPaletteQuery(""); }}
1546
- title="Search pages (Ctrl+K)"
1547
- >
1548
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1549
- <circle cx="11" cy="11" r="8" />
1550
- <line x1="21" y1="21" x2="16.65" y2="16.65" />
1551
- </svg>
1552
- <span className="cmssy-search-kbd">\u2318K</span>
1553
- </button>
1554
- <button
1555
- className="cmssy-collapse-btn"
1556
- onClick={() => setNavCollapsed(false)}
1557
- title="Expand navigation"
1558
- >
1559
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1560
- <polyline points="6 9 12 15 18 9" />
1561
- </svg>
1562
- </button>
1563
- </div>
1564
- ) : (
1565
- <>
1566
- <a href="/" className="cmssy-back-btn">
1567
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1568
- <polyline points="15 18 9 12 15 6" />
1569
- </svg>
1570
- Dev
1571
- </a>
1572
-
1573
- <div className="cmssy-divider" />
1574
-
1575
- <div className="cmssy-template-name">${resource.displayName || resource.name}</div>
1576
-
1577
- <div className="cmssy-divider" />
1578
-
1579
- <div className="cmssy-tabs-wrapper" ref={tabsRef}>
1580
- <div className="cmssy-tabs">
1581
- {pagesData.pages.map((page) => (
1582
- <button
1583
- key={page.slug}
1584
- ref={page.slug === currentSlug ? activeTabRef : undefined}
1585
- onClick={() => navigateTo(page.slug)}
1586
- className={\`cmssy-tab \${page.slug === currentSlug ? "cmssy-tab-active" : "cmssy-tab-inactive"}\`}
1587
- >
1588
- {page.name}
1589
- </button>
1590
- ))}
1591
- </div>
1592
- </div>
1593
-
1594
- <button
1595
- className="cmssy-search-btn"
1596
- onClick={() => { setPaletteOpen(true); setPaletteQuery(""); }}
1597
- title="Search pages (Ctrl+K)"
1598
- >
1599
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1600
- <circle cx="11" cy="11" r="8" />
1601
- <line x1="21" y1="21" x2="16.65" y2="16.65" />
1602
- </svg>
1603
- <span className="cmssy-search-kbd">\u2318K</span>
1604
- </button>
1605
-
1606
- <button
1607
- className="cmssy-collapse-btn"
1608
- onClick={() => setNavCollapsed(true)}
1609
- title="Collapse navigation"
1610
- >
1611
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1612
- <polyline points="18 15 12 9 6 15" />
1613
- </svg>
1614
- </button>
1615
- </>
1616
- )}
1617
- </div>
1618
- </div>
1619
-
1620
- {/* Command Palette */}
1621
- {paletteOpen && (
1622
- <div className="cmssy-palette-overlay" onClick={() => setPaletteOpen(false)}>
1623
- <div className="cmssy-palette" onClick={(e) => e.stopPropagation()}>
1624
- <div className="cmssy-palette-input-wrapper">
1625
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.4)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1626
- <circle cx="11" cy="11" r="8" />
1627
- <line x1="21" y1="21" x2="16.65" y2="16.65" />
1628
- </svg>
1629
- <input
1630
- ref={paletteInputRef}
1631
- type="text"
1632
- className="cmssy-palette-input"
1633
- placeholder="Search pages..."
1634
- value={paletteQuery}
1635
- onChange={(e) => setPaletteQuery(e.target.value)}
1636
- onKeyDown={(e) => {
1637
- if (e.key === "Enter") {
1638
- const q = paletteQuery.toLowerCase();
1639
- const match = pagesData.pages.find(
1640
- (p) => p.name.toLowerCase().includes(q) || p.slug.toLowerCase().includes(q)
1641
- );
1642
- if (match) {
1643
- navigateTo(match.slug);
1644
- setPaletteOpen(false);
1645
- }
1646
- }
1647
- }}
1648
- />
1649
- </div>
1650
- <div className="cmssy-palette-list">
1651
- {(() => {
1652
- const q = paletteQuery.toLowerCase();
1653
- const filtered = pagesData.pages.filter(
1654
- (p) => p.name.toLowerCase().includes(q) || p.slug.toLowerCase().includes(q)
1655
- );
1656
- if (filtered.length === 0) {
1657
- return <div className="cmssy-palette-empty">No pages match &ldquo;{paletteQuery}&rdquo;</div>;
1658
- }
1659
- return filtered.map((page) => (
1660
- <button
1661
- key={page.slug}
1662
- className={\`cmssy-palette-item \${page.slug === currentSlug ? "cmssy-palette-item-active" : ""}\`}
1663
- onClick={() => {
1664
- navigateTo(page.slug);
1665
- setPaletteOpen(false);
1666
- }}
1667
- >
1668
- <div>
1669
- <div className="cmssy-palette-item-name">{page.name}</div>
1670
- <div className="cmssy-palette-item-slug">{page.slug}</div>
1671
- </div>
1672
- {page.slug === currentSlug && (
1673
- <span className="cmssy-palette-item-current">Current</span>
1674
- )}
1675
- </button>
1676
- ));
1677
- })()}
1678
- </div>
1679
- </div>
1680
- </div>
1681
- )}
1682
-
1683
- {/* Page Content */}
1684
- <div>
1685
- {HeaderComponent && headerData && (
1686
- <HeaderComponent content={headerData.content || {}} />
1687
- )}
1688
- {SidebarLeftComponent && sidebarLeftData ? (
1689
- <div style={{ display: "grid", gridTemplateColumns: "280px 1fr", minHeight: "100vh" }}>
1690
- <SidebarLeftComponent content={sidebarLeftData.content || {}} />
1691
- <main>
1692
- {pageBlocks.map((block, idx) => {
1693
- const blockType = convertBlockType(block.type);
1694
- const Component = BLOCK_COMPONENTS[blockType];
1695
- if (!Component) {
1696
- return (
1697
- <div key={idx} style={{
1698
- padding: "2rem",
1699
- background: "#fff3cd",
1700
- textAlign: "center",
1701
- margin: "1rem",
1702
- borderRadius: "8px",
1703
- fontSize: "14px",
1704
- color: "#856404",
1705
- }}>
1706
- Missing block component: {blockType}
1707
- </div>
1708
- );
1709
- }
1710
- return <Component key={idx} content={block.content || {}} />;
1711
- })}
1712
- </main>
1713
- </div>
1714
- ) : (
1715
- pageBlocks.map((block, idx) => {
1716
- const blockType = convertBlockType(block.type);
1717
- const Component = BLOCK_COMPONENTS[blockType];
1718
- if (!Component) {
1719
- return (
1720
- <div key={idx} style={{
1721
- padding: "2rem",
1722
- background: "#fff3cd",
1723
- textAlign: "center",
1724
- margin: "1rem",
1725
- borderRadius: "8px",
1726
- fontSize: "14px",
1727
- color: "#856404",
1728
- }}>
1729
- Missing block component: {blockType}
1730
- </div>
1731
- );
1732
- }
1733
- return <Component key={idx} content={block.content || {}} />;
1734
- })
1735
- )}
1736
- {FooterComponent && footerData && (
1737
- <FooterComponent content={footerData.content || {}} />
1738
- )}
1739
- </div>
1740
- </>
1741
- );
1742
- }
1743
- `;
1744
- fs.writeFileSync(path.join(pageDir, "page.tsx"), pageContent);
1745
- }
1746
- function toPascalCase(str) {
1747
- return str
1748
- .split(/[-_\s]+/)
1749
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
1750
- .join("");
1751
- }
1752
- //# sourceMappingURL=dev-generator.js.map