@cmssy/cli 0.8.0 → 0.10.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 (69) hide show
  1. package/config.d.ts +2 -2
  2. package/config.js +1 -1
  3. package/dist/cli.js +22 -8
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/codegen.d.ts +7 -0
  6. package/dist/commands/codegen.d.ts.map +1 -0
  7. package/dist/commands/codegen.js +133 -0
  8. package/dist/commands/codegen.js.map +1 -0
  9. package/dist/commands/create.d.ts.map +1 -1
  10. package/dist/commands/create.js +11 -15
  11. package/dist/commands/create.js.map +1 -1
  12. package/dist/commands/dev.d.ts.map +1 -1
  13. package/dist/commands/dev.js +5 -2
  14. package/dist/commands/dev.js.map +1 -1
  15. package/dist/commands/init.d.ts +0 -1
  16. package/dist/commands/init.d.ts.map +1 -1
  17. package/dist/commands/init.js +350 -367
  18. package/dist/commands/init.js.map +1 -1
  19. package/dist/commands/publish.d.ts.map +1 -1
  20. package/dist/commands/publish.js +119 -9
  21. package/dist/commands/publish.js.map +1 -1
  22. package/dist/utils/block-config.d.ts +65 -2
  23. package/dist/utils/block-config.d.ts.map +1 -1
  24. package/dist/utils/block-config.js +17 -5
  25. package/dist/utils/block-config.js.map +1 -1
  26. package/dist/utils/dev-generator/api-routes/blocks.d.ts +3 -0
  27. package/dist/utils/dev-generator/api-routes/blocks.d.ts.map +1 -0
  28. package/dist/utils/dev-generator/api-routes/blocks.js +194 -0
  29. package/dist/utils/dev-generator/api-routes/blocks.js.map +1 -0
  30. package/dist/utils/dev-generator/api-routes/config.d.ts +2 -0
  31. package/dist/utils/dev-generator/api-routes/config.d.ts.map +1 -0
  32. package/dist/utils/dev-generator/api-routes/config.js +107 -0
  33. package/dist/utils/dev-generator/api-routes/config.js.map +1 -0
  34. package/dist/utils/dev-generator/api-routes/preview.d.ts +2 -0
  35. package/dist/utils/dev-generator/api-routes/preview.d.ts.map +1 -0
  36. package/dist/utils/dev-generator/api-routes/preview.js +50 -0
  37. package/dist/utils/dev-generator/api-routes/preview.js.map +1 -0
  38. package/dist/utils/dev-generator/api-routes/workspaces.d.ts +2 -0
  39. package/dist/utils/dev-generator/api-routes/workspaces.d.ts.map +1 -0
  40. package/dist/utils/dev-generator/api-routes/workspaces.js +128 -0
  41. package/dist/utils/dev-generator/api-routes/workspaces.js.map +1 -0
  42. package/dist/utils/dev-generator/helpers.d.ts +23 -0
  43. package/dist/utils/dev-generator/helpers.d.ts.map +1 -0
  44. package/dist/utils/dev-generator/helpers.js +112 -0
  45. package/dist/utils/dev-generator/helpers.js.map +1 -0
  46. package/dist/utils/dev-generator/home-page.d.ts +2 -0
  47. package/dist/utils/dev-generator/home-page.d.ts.map +1 -0
  48. package/dist/utils/dev-generator/home-page.js +643 -0
  49. package/dist/utils/dev-generator/home-page.js.map +1 -0
  50. package/dist/utils/dev-generator/index.d.ts +12 -0
  51. package/dist/utils/dev-generator/index.d.ts.map +1 -0
  52. package/dist/utils/dev-generator/index.js +53 -0
  53. package/dist/utils/dev-generator/index.js.map +1 -0
  54. package/dist/utils/dev-generator/layout.d.ts +3 -0
  55. package/dist/utils/dev-generator/layout.d.ts.map +1 -0
  56. package/dist/utils/dev-generator/layout.js +59 -0
  57. package/dist/utils/dev-generator/layout.js.map +1 -0
  58. package/dist/utils/dev-generator/next-config.d.ts +3 -0
  59. package/dist/utils/dev-generator/next-config.d.ts.map +1 -0
  60. package/dist/utils/dev-generator/next-config.js +104 -0
  61. package/dist/utils/dev-generator/next-config.js.map +1 -0
  62. package/dist/utils/dev-generator/preview-pages.d.ts +3 -0
  63. package/dist/utils/dev-generator/preview-pages.d.ts.map +1 -0
  64. package/dist/utils/dev-generator/preview-pages.js +827 -0
  65. package/dist/utils/dev-generator/preview-pages.js.map +1 -0
  66. package/dist/utils/graphql.d.ts.map +1 -1
  67. package/dist/utils/graphql.js +2 -0
  68. package/dist/utils/graphql.js.map +1 -1
  69. package/package.json +2 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"home-page.d.ts","sourceRoot":"","sources":["../../../src/utils/dev-generator/home-page.ts"],"names":[],"mappings":"AAGA,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,QA+nB/C"}
@@ -0,0 +1,643 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ export function generateHomePage(devRoot) {
4
+ // The home page embeds the dev UI as a client component
5
+ // It uses an iframe-based architecture: left sidebar (block list),
6
+ // center (preview iframe), right sidebar (editor)
7
+ const content = `"use client";
8
+
9
+ import { useState, useEffect, useCallback, useRef } from "react";
10
+
11
+ interface Block {
12
+ type: "block" | "template";
13
+ name: string;
14
+ displayName: string;
15
+ description?: string;
16
+ category?: string;
17
+ tags?: string[];
18
+ version: string;
19
+ hasConfig?: boolean;
20
+ schema?: Record<string, any>;
21
+ pages?: Array<{ name: string; slug: string; blocksCount: number }>;
22
+ layoutPositions?: Array<{ position: string; type: string }>;
23
+ }
24
+
25
+ interface WorkspaceInfo {
26
+ connected: boolean;
27
+ reason?: string;
28
+ error?: string;
29
+ workspace?: { id: string; slug: string; name: string; myRole?: { name: string } };
30
+ workspaces?: any[];
31
+ publishedBlocks?: Array<{ blockType: string; name: string; version: string }>;
32
+ }
33
+
34
+ type SyncStatus = "local-only" | "published" | "outdated";
35
+
36
+ function getSyncStatus(blockName: string, localVersion: string, wsInfo: WorkspaceInfo | null): SyncStatus {
37
+ if (!wsInfo?.connected || !wsInfo.publishedBlocks) return "local-only";
38
+ const remote = wsInfo.publishedBlocks.find((b) => b.blockType === blockName || b.name === blockName);
39
+ if (!remote) return "local-only";
40
+ if (remote.version === localVersion) return "published";
41
+ return "outdated";
42
+ }
43
+
44
+ export default function DevHome() {
45
+ const [blocks, setBlocks] = useState<Block[]>([]);
46
+ const [selected, setSelected] = useState<Block | null>(null);
47
+ const [previewData, setPreviewData] = useState<Record<string, unknown>>({});
48
+ const [configLoading, setConfigLoading] = useState(false);
49
+ const configDataRef = useRef<Record<string, unknown>>({});
50
+ const iframeRef = useRef<HTMLIFrameElement>(null);
51
+ const iframeLoadedRef = useRef(false);
52
+ const [isDirty, setIsDirty] = useState(false);
53
+ const [showBlockList, setShowBlockList] = useState(true);
54
+ const [showEditor, setShowEditor] = useState(true);
55
+ const [wsInfo, setWsInfo] = useState<WorkspaceInfo | null>(null);
56
+ const [wsLoading, setWsLoading] = useState(true);
57
+ const [showSettings, setShowSettings] = useState(false);
58
+ const [settingsData, setSettingsData] = useState<{
59
+ apiUrl: string; hasToken: boolean; maskedToken: string | null;
60
+ workspaceId: string | null; newToken: string; newApiUrl: string; newWorkspaceId: string;
61
+ saving: boolean; testing: boolean; testResult: string | null;
62
+ project: any;
63
+ }>({
64
+ apiUrl: "", hasToken: false, maskedToken: null, workspaceId: null,
65
+ newToken: "", newApiUrl: "", newWorkspaceId: "",
66
+ saving: false, testing: false, testResult: null, project: null,
67
+ });
68
+
69
+ // Load config when settings opened
70
+ useEffect(() => {
71
+ if (!showSettings) return;
72
+ fetch("/api/config")
73
+ .then((r) => r.json())
74
+ .then((data) => {
75
+ setSettingsData((prev) => ({
76
+ ...prev,
77
+ apiUrl: data.env.apiUrl,
78
+ hasToken: data.env.hasToken,
79
+ maskedToken: data.env.maskedToken,
80
+ workspaceId: data.env.workspaceId,
81
+ newApiUrl: data.env.apiUrl,
82
+ newWorkspaceId: data.env.workspaceId || "",
83
+ project: data.project,
84
+ }));
85
+ })
86
+ .catch(console.error);
87
+ }, [showSettings]);
88
+
89
+ // Load workspace info
90
+ useEffect(() => {
91
+ fetch("/api/workspaces")
92
+ .then((r) => r.json())
93
+ .then((data) => { setWsInfo(data); setWsLoading(false); })
94
+ .catch(() => setWsLoading(false));
95
+ }, []);
96
+
97
+ // Load blocks list
98
+ useEffect(() => {
99
+ fetch("/api/blocks")
100
+ .then((r) => r.json())
101
+ .then(setBlocks)
102
+ .catch(console.error);
103
+ }, []);
104
+
105
+ // Load config when block selected
106
+ useEffect(() => {
107
+ if (!selected || selected.type === "template") return;
108
+ setConfigLoading(true);
109
+ fetch(\`/api/blocks/\${selected.name}/config\`)
110
+ .then((r) => r.json())
111
+ .then((config) => {
112
+ setSelected((prev) =>
113
+ prev ? { ...prev, schema: config.schema } : null
114
+ );
115
+ const data = config.previewData || {};
116
+ configDataRef.current = data;
117
+ setPreviewData(data);
118
+ setConfigLoading(false);
119
+ })
120
+ .catch(() => setConfigLoading(false));
121
+ }, [selected?.name]);
122
+
123
+ // Select block — templates redirect to full-page preview
124
+ const handleSelect = useCallback((block: Block) => {
125
+ if (block.type === "template") {
126
+ window.location.href = \`/preview/\${block.name}\`;
127
+ return;
128
+ }
129
+ setSelected(block);
130
+ setPreviewData({});
131
+ configDataRef.current = {};
132
+ setIsDirty(false);
133
+ iframeLoadedRef.current = false;
134
+ }, []);
135
+
136
+ // Send props to iframe
137
+ useEffect(() => {
138
+ if (!iframeRef.current?.contentWindow || !iframeLoadedRef.current) return;
139
+ if (!previewData || Object.keys(previewData).length === 0) return;
140
+ iframeRef.current.contentWindow.postMessage(
141
+ { type: "UPDATE_PROPS", props: previewData },
142
+ "*"
143
+ );
144
+ }, [previewData]);
145
+
146
+ // Auto-save preview data
147
+ useEffect(() => {
148
+ if (!isDirty || !selected || Object.keys(configDataRef.current).length === 0) return;
149
+ const t = setTimeout(async () => {
150
+ const dataToSave = { ...configDataRef.current, ...previewData };
151
+ await fetch(\`/api/preview/\${selected.name}\`, {
152
+ method: "POST",
153
+ headers: { "Content-Type": "application/json" },
154
+ body: JSON.stringify(dataToSave),
155
+ });
156
+ setIsDirty(false);
157
+ }, 500);
158
+ return () => clearTimeout(t);
159
+ }, [previewData, selected, isDirty]);
160
+
161
+ // URL state
162
+ useEffect(() => {
163
+ if (blocks.length === 0) return;
164
+ const params = new URLSearchParams(window.location.search);
165
+ const name = params.get("block") || params.get("template");
166
+ if (name) {
167
+ const b = blocks.find((x) => x.name === name);
168
+ if (b) handleSelect(b);
169
+ }
170
+ }, [blocks, handleSelect]);
171
+
172
+ const previewUrl = selected ? \`/preview/\${selected.name}\` : null;
173
+
174
+ function renderField(field: any, value: any, onChange: (val: any) => void) {
175
+ if (field.type === "multiLine" || field.type === "richText") {
176
+ return (
177
+ <textarea
178
+ value={(value as string) || ""}
179
+ onChange={(e) => onChange(e.target.value)}
180
+ placeholder={field.placeholder}
181
+ style={{ width: "100%", padding: "8px", border: "1px solid #ddd", borderRadius: "4px", fontSize: "13px", minHeight: "60px", resize: "vertical", fontFamily: "inherit" }}
182
+ />
183
+ );
184
+ }
185
+ if (field.type === "boolean") {
186
+ return (
187
+ <label style={{ display: "flex", alignItems: "center", gap: "8px", cursor: "pointer" }}>
188
+ <input
189
+ type="checkbox"
190
+ checked={!!value}
191
+ onChange={(e) => onChange(e.target.checked)}
192
+ />
193
+ {field.label}
194
+ </label>
195
+ );
196
+ }
197
+ if (field.type === "select") {
198
+ return (
199
+ <select
200
+ value={(value as string) || ""}
201
+ onChange={(e) => onChange(e.target.value)}
202
+ style={{ width: "100%", padding: "8px", border: "1px solid #ddd", borderRadius: "4px", fontSize: "13px" }}
203
+ >
204
+ <option value="">Select...</option>
205
+ {field.options?.map((opt: any) => (
206
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
207
+ ))}
208
+ </select>
209
+ );
210
+ }
211
+ if (field.type === "multiselect") {
212
+ const selected = Array.isArray(value) ? value : [];
213
+ return (
214
+ <div style={{ border: "1px solid #ddd", borderRadius: "4px", padding: "8px", display: "flex", flexWrap: "wrap", gap: "6px" }}>
215
+ {field.options?.map((opt: any) => (
216
+ <label key={opt.value} style={{ display: "flex", alignItems: "center", gap: "4px", fontSize: "13px", cursor: "pointer" }}>
217
+ <input
218
+ type="checkbox"
219
+ checked={selected.includes(opt.value)}
220
+ onChange={(e) => {
221
+ const next = e.target.checked
222
+ ? [...selected, opt.value]
223
+ : selected.filter((v: string) => v !== opt.value);
224
+ onChange(next);
225
+ }}
226
+ />
227
+ {opt.label}
228
+ </label>
229
+ ))}
230
+ {!field.options?.length && <span style={{ color: "#999", fontSize: "12px" }}>No options defined</span>}
231
+ </div>
232
+ );
233
+ }
234
+ if (field.type === "date") {
235
+ return (
236
+ <input
237
+ type="date"
238
+ value={(value as string) || ""}
239
+ onChange={(e) => onChange(e.target.value)}
240
+ style={{ width: "100%", padding: "8px", border: "1px solid #ddd", borderRadius: "4px", fontSize: "13px" }}
241
+ />
242
+ );
243
+ }
244
+ if (field.type === "media") {
245
+ return (
246
+ <div>
247
+ {value && (
248
+ <div style={{ marginBottom: "6px", borderRadius: "4px", overflow: "hidden", border: "1px solid #ddd" }}>
249
+ <img src={value as string} alt="" style={{ maxWidth: "100%", maxHeight: "120px", objectFit: "contain", display: "block" }} />
250
+ </div>
251
+ )}
252
+ <input
253
+ type="text"
254
+ value={(value as string) || ""}
255
+ onChange={(e) => onChange(e.target.value)}
256
+ placeholder="Image URL"
257
+ style={{ width: "100%", padding: "8px", border: "1px solid #ddd", borderRadius: "4px", fontSize: "13px" }}
258
+ />
259
+ </div>
260
+ );
261
+ }
262
+ if (field.type === "repeater" && field.schema) {
263
+ const items = (Array.isArray(value) ? value : []) as any[];
264
+ return (
265
+ <div style={{ border: "1px solid #ddd", borderRadius: "6px", overflow: "hidden" }}>
266
+ {items.map((item: any, idx: number) => (
267
+ <div key={idx} style={{ padding: "12px", borderBottom: "1px solid #eee", background: idx % 2 === 0 ? "#fafafa" : "#fff" }}>
268
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "8px" }}>
269
+ <span style={{ fontSize: "12px", fontWeight: 600, color: "#888" }}>#{idx + 1}</span>
270
+ <button
271
+ type="button"
272
+ onClick={() => {
273
+ const newItems = [...items];
274
+ newItems.splice(idx, 1);
275
+ onChange(newItems);
276
+ }}
277
+ style={{ fontSize: "11px", color: "#e53935", background: "none", border: "none", cursor: "pointer" }}
278
+ >&times; Remove</button>
279
+ </div>
280
+ {Object.entries(field.schema).map(([subKey, subField]: [string, any]) => (
281
+ <div key={subKey} style={{ marginBottom: "8px" }}>
282
+ <label style={{ display: "block", fontSize: "11px", fontWeight: 500, marginBottom: "4px", color: "#666" }}>
283
+ {subField.label || subKey}
284
+ </label>
285
+ {renderField(subField, item[subKey], (subVal) => {
286
+ const newItems = [...items];
287
+ newItems[idx] = { ...newItems[idx], [subKey]: subVal };
288
+ onChange(newItems);
289
+ })}
290
+ </div>
291
+ ))}
292
+ </div>
293
+ ))}
294
+ <button
295
+ type="button"
296
+ onClick={() => {
297
+ const newItem: any = {};
298
+ Object.entries(field.schema).forEach(([k, f]: [string, any]) => {
299
+ newItem[k] = (f as any).type === "repeater" ? [] : "";
300
+ });
301
+ onChange([...items, newItem]);
302
+ }}
303
+ style={{ width: "100%", padding: "10px", fontSize: "13px", color: "#667eea", background: "none", border: "none", cursor: "pointer", fontWeight: 500 }}
304
+ >+ Add item</button>
305
+ </div>
306
+ );
307
+ }
308
+ // singleLine, link, numeric, color, form, emailTemplate, emailConfiguration, pageSelector
309
+ return (
310
+ <input
311
+ type={field.type === "numeric" ? "number" : field.type === "color" ? "color" : field.type === "link" ? "url" : "text"}
312
+ value={(value as string) || ""}
313
+ onChange={(e) => onChange(field.type === "numeric" ? Number(e.target.value) : e.target.value)}
314
+ placeholder={field.placeholder || (field.type === "link" ? "https://..." : field.type === "form" || field.type === "emailTemplate" || field.type === "emailConfiguration" || field.type === "pageSelector" ? "Enter ID..." : "")}
315
+ style={{ width: "100%", padding: "8px", border: "1px solid #ddd", borderRadius: "4px", fontSize: "13px" }}
316
+ />
317
+ );
318
+ }
319
+
320
+ return (
321
+ <div style={{ display: "grid", gridTemplateColumns: \`\${showBlockList ? "280px" : "0px"} 1fr \${showEditor ? "400px" : "0px"}\`, height: "100vh", transition: "grid-template-columns 0.2s ease" }}>
322
+ {/* Block List */}
323
+ <div style={{ background: "#fff", borderRight: showBlockList ? "1px solid #e0e0e0" : "none", overflow: showBlockList ? "auto" : "hidden", width: showBlockList ? "auto" : 0 }}>
324
+ <div style={{ padding: "16px", borderBottom: "1px solid #e0e0e0", background: "#fafafa" }}>
325
+ <h1 style={{ fontSize: "18px", fontWeight: 600, margin: 0 }}>Cmssy Dev</h1>
326
+ <p style={{ fontSize: "13px", color: "#666", margin: "4px 0 0" }}>
327
+ {blocks.length} blocks
328
+ </p>
329
+ </div>
330
+ {/* Workspace Connection */}
331
+ <div style={{ padding: "12px 16px", borderBottom: "1px solid #e0e0e0", fontSize: "12px" }}>
332
+ {wsLoading ? (
333
+ <div style={{ color: "#999" }}>Connecting...</div>
334
+ ) : wsInfo?.connected ? (
335
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
336
+ <div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
337
+ <div style={{ width: "8px", height: "8px", borderRadius: "50%", background: "#22c55e", flexShrink: 0 }} />
338
+ <span style={{ fontWeight: 500 }}>{wsInfo.workspace?.name}</span>
339
+ </div>
340
+ <button
341
+ type="button"
342
+ onClick={() => {
343
+ setWsLoading(true);
344
+ fetch("/api/workspaces")
345
+ .then((r) => r.json())
346
+ .then((data) => { setWsInfo(data); setWsLoading(false); })
347
+ .catch(() => setWsLoading(false));
348
+ }}
349
+ style={{ background: "none", border: "none", cursor: "pointer", color: "#667eea", fontSize: "11px", padding: "2px 4px" }}
350
+ title="Refresh"
351
+ >\u21BB</button>
352
+ </div>
353
+ ) : (
354
+ <div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
355
+ <div style={{ width: "8px", height: "8px", borderRadius: "50%", background: "#ef4444", flexShrink: 0 }} />
356
+ <span style={{ color: "#999" }}>
357
+ {wsInfo?.reason === "no_token" ? "Not configured" : wsInfo?.reason === "auth_error" ? "Invalid token" : "Disconnected"}
358
+ </span>
359
+ </div>
360
+ )}
361
+ </div>
362
+ <div style={{ padding: "12px" }}>
363
+ {blocks.map((b) => (
364
+ <div
365
+ key={b.name}
366
+ onClick={() => {
367
+ handleSelect(b);
368
+ const url = new URL(window.location.href);
369
+ url.searchParams.set(b.type === "template" ? "template" : "block", b.name);
370
+ window.history.replaceState({}, "", url.toString());
371
+ }}
372
+ style={{
373
+ padding: "12px 16px",
374
+ marginBottom: "4px",
375
+ borderRadius: "8px",
376
+ cursor: "pointer",
377
+ background: selected?.name === b.name ? "#667eea" : "transparent",
378
+ color: selected?.name === b.name ? "white" : "inherit",
379
+ }}
380
+ >
381
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
382
+ <div style={{ fontSize: "14px", fontWeight: 500 }}>{b.displayName}</div>
383
+ {wsInfo?.connected && (() => {
384
+ const status = getSyncStatus(b.name, b.version, wsInfo);
385
+ const colors: Record<SyncStatus, { bg: string; text: string; label: string }> = {
386
+ "published": { bg: selected?.name === b.name ? "rgba(255,255,255,0.2)" : "#dcfce7", text: selected?.name === b.name ? "#fff" : "#166534", label: "\u2713" },
387
+ "outdated": { bg: selected?.name === b.name ? "rgba(255,255,255,0.2)" : "#fef3c7", text: selected?.name === b.name ? "#fff" : "#92400e", label: "\u2191" },
388
+ "local-only": { bg: "transparent", text: "transparent", label: "" },
389
+ };
390
+ const c = colors[status];
391
+ if (status === "local-only") return null;
392
+ return (
393
+ <span title={status} style={{ fontSize: "10px", padding: "1px 6px", borderRadius: "4px", background: c.bg, color: c.text, fontWeight: 600 }}>{c.label}</span>
394
+ );
395
+ })()}
396
+ </div>
397
+ <div style={{ fontSize: "12px", opacity: 0.7 }}>
398
+ {b.type} &middot; v{b.version}
399
+ </div>
400
+ </div>
401
+ ))}
402
+ </div>
403
+ </div>
404
+
405
+ {/* Preview */}
406
+ <div style={{ background: "#fafafa", display: "flex", flexDirection: "column" }}>
407
+ <div style={{ padding: "10px 16px", background: "white", borderBottom: "1px solid #e0e0e0", display: "flex", alignItems: "center", justifyContent: "space-between", gap: "12px" }}>
408
+ <button
409
+ type="button"
410
+ onClick={() => setShowBlockList(!showBlockList)}
411
+ title={showBlockList ? "Hide block list" : "Show block list"}
412
+ 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" }}
413
+ >{showBlockList ? "\u2190 Blocks" : "\u2192 Blocks"}</button>
414
+ <div style={{ flex: 1, fontSize: "16px", fontWeight: 600, textAlign: "center", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
415
+ {selected?.displayName || "Preview"}
416
+ </div>
417
+ <button
418
+ type="button"
419
+ onClick={() => setShowEditor(!showEditor)}
420
+ title={showEditor ? "Hide editor" : "Show editor"}
421
+ 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" }}
422
+ >{showEditor ? "Editor \u2192" : "Editor \u2190"}</button>
423
+ <button
424
+ type="button"
425
+ onClick={() => setShowSettings(!showSettings)}
426
+ title="Settings"
427
+ style={{ background: showSettings ? "#667eea" : "#f0f0f0", color: showSettings ? "#fff" : "#333", border: "1px solid #ddd", borderRadius: "6px", padding: "6px 10px", cursor: "pointer", fontSize: "15px", lineHeight: 1 }}
428
+ >\u2699</button>
429
+ </div>
430
+ <div style={{ flex: 1, padding: "24px", display: "flex", alignItems: "center", justifyContent: "center" }}>
431
+ {previewUrl ? (
432
+ <div style={{ width: "100%", height: "100%", background: "white", borderRadius: "12px", boxShadow: "0 2px 8px rgba(0,0,0,0.1)", overflow: "hidden" }}>
433
+ <iframe
434
+ ref={iframeRef}
435
+ src={previewUrl}
436
+ key={previewUrl}
437
+ onLoad={() => { iframeLoadedRef.current = true; }}
438
+ style={{ width: "100%", height: "100%", border: "none" }}
439
+ />
440
+ </div>
441
+ ) : (
442
+ <div style={{ textAlign: "center", color: "#999" }}>
443
+ <p>Select a block to preview</p>
444
+ </div>
445
+ )}
446
+ </div>
447
+ </div>
448
+
449
+ {/* Editor */}
450
+ <div style={{ background: "#fff", borderLeft: showEditor ? "1px solid #e0e0e0" : "none", overflow: showEditor ? "auto" : "hidden", width: showEditor ? "auto" : 0 }}>
451
+ <div style={{ padding: "16px", borderBottom: "1px solid #e0e0e0", background: "#fafafa" }}>
452
+ <h2 style={{ fontSize: "16px", fontWeight: 600, margin: 0 }}>Editor</h2>
453
+ </div>
454
+ <div style={{ padding: "20px" }}>
455
+ {!selected && <p style={{ color: "#999" }}>Select a block to edit</p>}
456
+ {selected && configLoading && <p style={{ color: "#999" }}>Loading...</p>}
457
+ {selected && !configLoading && selected.schema && (
458
+ <div>
459
+ {Object.entries(selected.schema).map(([key, field]: [string, any]) => (
460
+ <div key={key} style={{ marginBottom: "20px" }}>
461
+ <label style={{ display: "block", fontSize: "13px", fontWeight: 500, marginBottom: "6px" }}>
462
+ {field.label || key}
463
+ {field.required && <span style={{ color: "#e53935" }}> *</span>}
464
+ </label>
465
+ {renderField(field, previewData[key], (val) => {
466
+ setPreviewData({ ...previewData, [key]: val });
467
+ setIsDirty(true);
468
+ })}
469
+ {field.helpText && (
470
+ <div style={{ fontSize: "12px", color: "#666", marginTop: "4px" }}>{field.helpText}</div>
471
+ )}
472
+ </div>
473
+ ))}
474
+ </div>
475
+ )}
476
+ </div>
477
+ </div>
478
+
479
+ {/* Settings Panel (slide-over) */}
480
+ {showSettings && (
481
+ <div style={{
482
+ position: "fixed", top: 0, right: 0, bottom: 0, width: "420px",
483
+ background: "#fff", boxShadow: "-4px 0 24px rgba(0,0,0,0.12)",
484
+ zIndex: 1000, display: "flex", flexDirection: "column",
485
+ animation: "slideIn 0.2s ease",
486
+ }}>
487
+ <style>{\`@keyframes slideIn { from { transform: translateX(100%); } to { transform: translateX(0); } }\`}</style>
488
+ <div style={{ padding: "16px 20px", borderBottom: "1px solid #e0e0e0", display: "flex", justifyContent: "space-between", alignItems: "center", background: "#fafafa" }}>
489
+ <h2 style={{ fontSize: "16px", fontWeight: 600, margin: 0 }}>Settings</h2>
490
+ <button type="button" onClick={() => setShowSettings(false)}
491
+ style={{ background: "none", border: "none", fontSize: "20px", cursor: "pointer", color: "#666", padding: "4px" }}
492
+ >\u2715</button>
493
+ </div>
494
+ <div style={{ flex: 1, overflow: "auto", padding: "20px" }}>
495
+ {/* Connection */}
496
+ <div style={{ marginBottom: "24px" }}>
497
+ <h3 style={{ fontSize: "13px", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em", color: "#666", marginBottom: "12px" }}>Connection</h3>
498
+ <div style={{ marginBottom: "12px" }}>
499
+ <label style={{ display: "block", fontSize: "13px", fontWeight: 500, marginBottom: "4px" }}>API URL</label>
500
+ <input
501
+ type="text"
502
+ value={settingsData.newApiUrl}
503
+ onChange={(e) => setSettingsData({ ...settingsData, newApiUrl: e.target.value })}
504
+ style={{ width: "100%", padding: "8px", border: "1px solid #ddd", borderRadius: "6px", fontSize: "13px", boxSizing: "border-box" }}
505
+ />
506
+ </div>
507
+ <div style={{ marginBottom: "12px" }}>
508
+ <label style={{ display: "block", fontSize: "13px", fontWeight: 500, marginBottom: "4px" }}>
509
+ API Token {settingsData.hasToken && <span style={{ color: "#22c55e", fontSize: "11px" }}>({settingsData.maskedToken})</span>}
510
+ </label>
511
+ <input
512
+ type="password"
513
+ value={settingsData.newToken}
514
+ onChange={(e) => setSettingsData({ ...settingsData, newToken: e.target.value })}
515
+ placeholder={settingsData.hasToken ? "Leave empty to keep current" : "bf_..."}
516
+ style={{ width: "100%", padding: "8px", border: "1px solid #ddd", borderRadius: "6px", fontSize: "13px", boxSizing: "border-box" }}
517
+ />
518
+ </div>
519
+ <div style={{ display: "flex", gap: "8px" }}>
520
+ <button
521
+ type="button"
522
+ disabled={settingsData.testing}
523
+ onClick={async () => {
524
+ setSettingsData((s) => ({ ...s, testing: true, testResult: null }));
525
+ try {
526
+ // Save first if token changed
527
+ if (settingsData.newToken) {
528
+ await fetch("/api/config", {
529
+ method: "PUT",
530
+ headers: { "Content-Type": "application/json" },
531
+ body: JSON.stringify({ apiToken: settingsData.newToken, apiUrl: settingsData.newApiUrl }),
532
+ });
533
+ }
534
+ const res = await fetch("/api/workspaces");
535
+ const data = await res.json();
536
+ if (data.connected) {
537
+ setSettingsData((s) => ({ ...s, testing: false, testResult: "success" }));
538
+ setWsInfo(data);
539
+ setWsLoading(false);
540
+ } else {
541
+ setSettingsData((s) => ({ ...s, testing: false, testResult: data.error || data.reason || "failed" }));
542
+ }
543
+ } catch {
544
+ setSettingsData((s) => ({ ...s, testing: false, testResult: "Network error" }));
545
+ }
546
+ }}
547
+ style={{
548
+ padding: "8px 16px", border: "1px solid #ddd", borderRadius: "6px",
549
+ background: "#f8f8f8", cursor: "pointer", fontSize: "13px", fontWeight: 500,
550
+ }}
551
+ >{settingsData.testing ? "Testing..." : "Test Connection"}</button>
552
+ {settingsData.testResult && (
553
+ <span style={{
554
+ display: "flex", alignItems: "center", fontSize: "12px",
555
+ color: settingsData.testResult === "success" ? "#22c55e" : "#ef4444",
556
+ }}>
557
+ {settingsData.testResult === "success" ? "\u2713 Connected" : "\u2717 " + settingsData.testResult}
558
+ </span>
559
+ )}
560
+ </div>
561
+ </div>
562
+
563
+ {/* Workspace */}
564
+ <div style={{ marginBottom: "24px" }}>
565
+ <h3 style={{ fontSize: "13px", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em", color: "#666", marginBottom: "12px" }}>Workspace</h3>
566
+ {wsInfo?.connected && wsInfo.workspaces && wsInfo.workspaces.length > 0 ? (
567
+ <select
568
+ value={settingsData.newWorkspaceId}
569
+ onChange={(e) => setSettingsData({ ...settingsData, newWorkspaceId: e.target.value })}
570
+ style={{ width: "100%", padding: "8px", border: "1px solid #ddd", borderRadius: "6px", fontSize: "13px" }}
571
+ >
572
+ <option value="">Select workspace...</option>
573
+ {wsInfo.workspaces.map((w: any) => (
574
+ <option key={w.id} value={w.id}>{w.name} ({w.slug})</option>
575
+ ))}
576
+ </select>
577
+ ) : (
578
+ <p style={{ fontSize: "13px", color: "#999" }}>Connect to see available workspaces</p>
579
+ )}
580
+ </div>
581
+
582
+ {/* Project Info */}
583
+ {settingsData.project && (
584
+ <div style={{ marginBottom: "24px" }}>
585
+ <h3 style={{ fontSize: "13px", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em", color: "#666", marginBottom: "12px" }}>Project</h3>
586
+ <div style={{ background: "#f8f8f8", borderRadius: "6px", padding: "12px", fontSize: "12px", fontFamily: "monospace" }}>
587
+ {Object.entries(settingsData.project).map(([k, v]) => (
588
+ <div key={k} style={{ marginBottom: "4px" }}>
589
+ <span style={{ color: "#666" }}>{k}:</span>{" "}
590
+ <span>{typeof v === "object" ? JSON.stringify(v) : String(v)}</span>
591
+ </div>
592
+ ))}
593
+ </div>
594
+ </div>
595
+ )}
596
+
597
+ {/* Save */}
598
+ <button
599
+ type="button"
600
+ disabled={settingsData.saving}
601
+ onClick={async () => {
602
+ setSettingsData((s) => ({ ...s, saving: true }));
603
+ const body: any = { apiUrl: settingsData.newApiUrl };
604
+ if (settingsData.newToken) body.apiToken = settingsData.newToken;
605
+ if (settingsData.newWorkspaceId) body.workspaceId = settingsData.newWorkspaceId;
606
+ try {
607
+ await fetch("/api/config", {
608
+ method: "PUT",
609
+ headers: { "Content-Type": "application/json" },
610
+ body: JSON.stringify(body),
611
+ });
612
+ setSettingsData((s) => ({ ...s, saving: false, newToken: "" }));
613
+ // Refresh workspace info
614
+ const res = await fetch("/api/workspaces");
615
+ const data = await res.json();
616
+ setWsInfo(data);
617
+ // Reload config display
618
+ const cfgRes = await fetch("/api/config");
619
+ const cfgData = await cfgRes.json();
620
+ setSettingsData((s) => ({
621
+ ...s, hasToken: cfgData.env.hasToken, maskedToken: cfgData.env.maskedToken,
622
+ workspaceId: cfgData.env.workspaceId,
623
+ }));
624
+ } catch {
625
+ setSettingsData((s) => ({ ...s, saving: false }));
626
+ }
627
+ }}
628
+ style={{
629
+ width: "100%", padding: "10px", background: "#667eea", color: "#fff",
630
+ border: "none", borderRadius: "6px", fontSize: "14px", fontWeight: 500,
631
+ cursor: "pointer",
632
+ }}
633
+ >{settingsData.saving ? "Saving..." : "Save Settings"}</button>
634
+ </div>
635
+ </div>
636
+ )}
637
+ </div>
638
+ );
639
+ }
640
+ `;
641
+ fs.writeFileSync(path.join(devRoot, "app/page.tsx"), content);
642
+ }
643
+ //# sourceMappingURL=home-page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"home-page.js","sourceRoot":"","sources":["../../../src/utils/dev-generator/home-page.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,UAAU,CAAC;AAC1B,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,wDAAwD;IACxD,mEAAmE;IACnE,kDAAkD;IAClD,MAAM,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAynBjB,CAAC;IACA,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,CAAC;AAChE,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { ScannedResource } from "../scanner.js";
2
+ /**
3
+ * Generate the .cmssy/dev/ Next.js app structure for cmssy dev.
4
+ * This creates a minimal Next.js app that imports blocks directly,
5
+ * enabling "use client" boundaries, next/image, and SSR in dev preview.
6
+ */
7
+ export declare function generateDevApp(projectRoot: string, resources: ScannedResource[]): string;
8
+ /**
9
+ * Regenerate only the preview pages (called when new blocks are detected).
10
+ */
11
+ export declare function regeneratePreviewPages(projectRoot: string, resources: ScannedResource[]): void;
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/dev-generator/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAerD;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,eAAe,EAAE,GAC3B,MAAM,CAyBR;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,eAAe,EAAE,GAC3B,IAAI,CAWN"}