@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,1785 +0,0 @@
1
- "use client";
2
-
3
- import { useState, useEffect, useCallback, useRef } from "react";
4
-
5
- interface Block {
6
- type: "block" | "template";
7
- name: string;
8
- displayName: string;
9
- description?: string;
10
- category?: string;
11
- tags?: string[];
12
- version: string;
13
- hasConfig?: boolean;
14
- schema?: Record<string, any>;
15
- pages?: Array<{ name: string; slug: string; blocksCount: number }>;
16
- layoutPositions?: Array<{ position: string; type: string }>;
17
- }
18
-
19
- interface WorkspaceInfo {
20
- connected: boolean;
21
- reason?: string;
22
- error?: string;
23
- workspace?: {
24
- id: string;
25
- slug: string;
26
- name: string;
27
- myRole?: { name: string };
28
- };
29
- workspaces?: any[];
30
- publishedBlocks?: Array<{ blockType: string; name: string; version: string }>;
31
- }
32
-
33
- type SyncStatus = "local-only" | "published" | "outdated";
34
-
35
- function getSyncStatus(
36
- blockName: string,
37
- localVersion: string,
38
- wsInfo: WorkspaceInfo | null,
39
- ): SyncStatus {
40
- if (!wsInfo?.connected || !wsInfo.publishedBlocks) return "local-only";
41
- const remote = wsInfo.publishedBlocks.find(
42
- (b) => b.blockType === blockName || b.name === blockName,
43
- );
44
- if (!remote) return "local-only";
45
- if (remote.version === localVersion) return "published";
46
- return "outdated";
47
- }
48
-
49
- const VIEWPORT_STORAGE_KEY = "cmssy-dev-viewport";
50
-
51
- const VIEWPORT_PRESETS = [
52
- { label: "Desktop", width: 1440 },
53
- { label: "Laptop", width: 1024 },
54
- { label: "Tablet", width: 768 },
55
- { label: "Mobile", width: 375 },
56
- ] as const;
57
-
58
- export default function DevHome() {
59
- const [blocks, setBlocks] = useState<Block[]>([]);
60
- const [selected, setSelected] = useState<Block | null>(null);
61
- const [previewData, setPreviewData] = useState<Record<string, unknown>>({});
62
- const [configLoading, setConfigLoading] = useState(false);
63
- const configDataRef = useRef<Record<string, unknown>>({});
64
- const [variants, setVariants] = useState<string[]>([]);
65
- const [currentVariant, setCurrentVariant] = useState<string | null>(null);
66
- const iframeRef = useRef<HTMLIFrameElement>(null);
67
- const iframeLoadedRef = useRef(false);
68
- const [isDirty, setIsDirty] = useState(false);
69
- const [showBlockList, setShowBlockList] = useState(true);
70
- const [showEditor, setShowEditor] = useState(true);
71
- const [editorTab, setEditorTab] = useState<"content" | "context">("content");
72
- const [mockContext, setMockContext] = useState<Record<string, any>>({});
73
- const [contextPresets, setContextPresets] = useState<string[]>([]);
74
- const [viewport, setViewport] = useState<number | null>(null);
75
-
76
- // Load saved viewport from localStorage after mount (avoid SSR mismatch)
77
- useEffect(() => {
78
- const saved = localStorage.getItem(VIEWPORT_STORAGE_KEY);
79
- if (!saved) return;
80
- const parsed = parseInt(saved, 10);
81
- if (Number.isFinite(parsed) && parsed > 0) {
82
- setViewport(parsed);
83
- } else {
84
- localStorage.removeItem(VIEWPORT_STORAGE_KEY);
85
- }
86
- }, []);
87
-
88
- // Persist viewport changes
89
- useEffect(() => {
90
- if (viewport === null) {
91
- localStorage.removeItem(VIEWPORT_STORAGE_KEY);
92
- } else {
93
- localStorage.setItem(VIEWPORT_STORAGE_KEY, String(viewport));
94
- }
95
- }, [viewport]);
96
-
97
- const [wsInfo, setWsInfo] = useState<WorkspaceInfo | null>(null);
98
- const [wsLoading, setWsLoading] = useState(true);
99
- const [showSettings, setShowSettings] = useState(false);
100
- const [settingsData, setSettingsData] = useState<{
101
- apiUrl: string;
102
- hasToken: boolean;
103
- maskedToken: string | null;
104
- workspaceId: string | null;
105
- newToken: string;
106
- newApiUrl: string;
107
- newWorkspaceId: string;
108
- saving: boolean;
109
- testing: boolean;
110
- testResult: string | null;
111
- project: any;
112
- }>({
113
- apiUrl: "",
114
- hasToken: false,
115
- maskedToken: null,
116
- workspaceId: null,
117
- newToken: "",
118
- newApiUrl: "",
119
- newWorkspaceId: "",
120
- saving: false,
121
- testing: false,
122
- testResult: null,
123
- project: null,
124
- });
125
-
126
- // Load config when settings opened
127
- useEffect(() => {
128
- if (!showSettings) return;
129
- fetch("/api/config")
130
- .then((r) => r.json())
131
- .then((data) => {
132
- setSettingsData((prev) => ({
133
- ...prev,
134
- apiUrl: data.env.apiUrl,
135
- hasToken: data.env.hasToken,
136
- maskedToken: data.env.maskedToken,
137
- workspaceId: data.env.workspaceId,
138
- newApiUrl: data.env.apiUrl,
139
- newWorkspaceId: data.env.workspaceId || "",
140
- project: data.project,
141
- }));
142
- })
143
- .catch(console.error);
144
- }, [showSettings]);
145
-
146
- // Load workspace info
147
- useEffect(() => {
148
- fetch("/api/workspaces")
149
- .then((r) => r.json())
150
- .then((data) => {
151
- setWsInfo(data);
152
- setWsLoading(false);
153
- })
154
- .catch(() => setWsLoading(false));
155
- }, []);
156
-
157
- // Load mock context + presets
158
- useEffect(() => {
159
- fetch("/api/context")
160
- .then((r) => r.json())
161
- .then(setMockContext)
162
- .catch(() => {});
163
- fetch("/api/context?presets=true")
164
- .then((r) => r.json())
165
- .then((data) => setContextPresets(data.presets || []))
166
- .catch(() => {});
167
- }, []);
168
-
169
- // Debounced context persistence
170
- const contextInitRef = useRef(false);
171
- useEffect(() => {
172
- // Skip initial render (context loaded from API)
173
- if (!contextInitRef.current) {
174
- contextInitRef.current = true;
175
- return;
176
- }
177
- const t = setTimeout(() => {
178
- fetch("/api/context", {
179
- method: "PUT",
180
- headers: { "Content-Type": "application/json" },
181
- body: JSON.stringify(mockContext),
182
- }).catch(() => {});
183
- }, 500);
184
- return () => clearTimeout(t);
185
- }, [mockContext]);
186
-
187
- // Load blocks list
188
- useEffect(() => {
189
- fetch("/api/blocks")
190
- .then((r) => r.json())
191
- .then(setBlocks)
192
- .catch(console.error);
193
- }, []);
194
-
195
- // Load config + preview data when block selected or variant changes
196
- useEffect(() => {
197
- if (!selected || selected.type === "template") return;
198
- setConfigLoading(true);
199
-
200
- const variantParam = currentVariant
201
- ? `?variant=${encodeURIComponent(currentVariant)}`
202
- : "";
203
-
204
- Promise.all([
205
- fetch(`/api/blocks/${selected.name}/config`).then((r) => r.json()),
206
- fetch(`/api/preview/${selected.name}${variantParam}`).then((r) =>
207
- r.json(),
208
- ),
209
- ])
210
- .then(([config, preview]) => {
211
- setSelected((prev) =>
212
- prev ? { ...prev, schema: config.schema } : null,
213
- );
214
- const data = preview.data || config.previewData || {};
215
- configDataRef.current = data;
216
- setPreviewData(data);
217
- setVariants(preview.variants || []);
218
- setConfigLoading(false);
219
- })
220
- .catch(() => setConfigLoading(false));
221
- }, [selected?.name, currentVariant]);
222
-
223
- // Select block — templates redirect to full-page preview
224
- const handleSelect = useCallback((block: Block) => {
225
- if (block.type === "template") {
226
- window.location.href = `/preview/${block.name}`;
227
- return;
228
- }
229
- setSelected(block);
230
- setPreviewData({});
231
- configDataRef.current = {};
232
- setIsDirty(false);
233
- setCurrentVariant(null);
234
- setVariants([]);
235
- iframeLoadedRef.current = false;
236
- }, []);
237
-
238
- // Send props + context to iframe
239
- useEffect(() => {
240
- if (!iframeRef.current?.contentWindow || !iframeLoadedRef.current) return;
241
- if (!previewData || Object.keys(previewData).length === 0) return;
242
- iframeRef.current.contentWindow.postMessage(
243
- { type: "UPDATE_PROPS", props: previewData, context: mockContext },
244
- window.location.origin,
245
- );
246
- }, [previewData, mockContext]);
247
-
248
- // Auto-save preview data
249
- useEffect(() => {
250
- if (
251
- !isDirty ||
252
- !selected ||
253
- Object.keys(configDataRef.current).length === 0
254
- )
255
- return;
256
- const t = setTimeout(async () => {
257
- const dataToSave = { ...configDataRef.current, ...previewData };
258
- const variantParam = currentVariant
259
- ? `?variant=${encodeURIComponent(currentVariant)}`
260
- : "";
261
- await fetch(`/api/preview/${selected.name}${variantParam}`, {
262
- method: "POST",
263
- headers: { "Content-Type": "application/json" },
264
- body: JSON.stringify(dataToSave),
265
- });
266
- setIsDirty(false);
267
- }, 500);
268
- return () => clearTimeout(t);
269
- }, [previewData, selected, isDirty]);
270
-
271
- // URL state
272
- useEffect(() => {
273
- if (blocks.length === 0) return;
274
- const params = new URLSearchParams(window.location.search);
275
- const name = params.get("block") || params.get("template");
276
- if (name) {
277
- const b = blocks.find((x) => x.name === name);
278
- if (b) handleSelect(b);
279
- }
280
- }, [blocks, handleSelect]);
281
-
282
- const previewUrl = selected ? `/preview/${selected.name}` : null;
283
-
284
- function renderField(field: any, value: any, onChange: (val: any) => void) {
285
- if (field.type === "multiLine" || field.type === "richText") {
286
- return (
287
- <textarea
288
- value={(value as string) || ""}
289
- onChange={(e) => onChange(e.target.value)}
290
- placeholder={field.placeholder}
291
- style={{
292
- width: "100%",
293
- padding: "8px",
294
- border: "1px solid #ddd",
295
- borderRadius: "4px",
296
- fontSize: "13px",
297
- minHeight: "60px",
298
- resize: "vertical",
299
- fontFamily: "inherit",
300
- }}
301
- />
302
- );
303
- }
304
- if (field.type === "boolean") {
305
- return (
306
- <label
307
- style={{
308
- display: "flex",
309
- alignItems: "center",
310
- gap: "8px",
311
- cursor: "pointer",
312
- }}
313
- >
314
- <input
315
- type="checkbox"
316
- checked={!!value}
317
- onChange={(e) => onChange(e.target.checked)}
318
- />
319
- {field.label}
320
- </label>
321
- );
322
- }
323
- if (field.type === "select") {
324
- return (
325
- <select
326
- value={(value as string) || ""}
327
- onChange={(e) => onChange(e.target.value)}
328
- style={{
329
- width: "100%",
330
- padding: "8px",
331
- border: "1px solid #ddd",
332
- borderRadius: "4px",
333
- fontSize: "13px",
334
- }}
335
- >
336
- <option value="">Select...</option>
337
- {field.options?.map((opt: any) => (
338
- <option key={opt.value} value={opt.value}>
339
- {opt.label}
340
- </option>
341
- ))}
342
- </select>
343
- );
344
- }
345
- if (field.type === "multiselect") {
346
- const selected = Array.isArray(value) ? value : [];
347
- return (
348
- <div
349
- style={{
350
- border: "1px solid #ddd",
351
- borderRadius: "4px",
352
- padding: "8px",
353
- display: "flex",
354
- flexWrap: "wrap",
355
- gap: "6px",
356
- }}
357
- >
358
- {field.options?.map((opt: any) => (
359
- <label
360
- key={opt.value}
361
- style={{
362
- display: "flex",
363
- alignItems: "center",
364
- gap: "4px",
365
- fontSize: "13px",
366
- cursor: "pointer",
367
- }}
368
- >
369
- <input
370
- type="checkbox"
371
- checked={selected.includes(opt.value)}
372
- onChange={(e) => {
373
- const next = e.target.checked
374
- ? [...selected, opt.value]
375
- : selected.filter((v: string) => v !== opt.value);
376
- onChange(next);
377
- }}
378
- />
379
- {opt.label}
380
- </label>
381
- ))}
382
- {!field.options?.length && (
383
- <span style={{ color: "#999", fontSize: "12px" }}>
384
- No options defined
385
- </span>
386
- )}
387
- </div>
388
- );
389
- }
390
- if (field.type === "date") {
391
- return (
392
- <input
393
- type="date"
394
- value={(value as string) || ""}
395
- onChange={(e) => onChange(e.target.value)}
396
- style={{
397
- width: "100%",
398
- padding: "8px",
399
- border: "1px solid #ddd",
400
- borderRadius: "4px",
401
- fontSize: "13px",
402
- }}
403
- />
404
- );
405
- }
406
- if (field.type === "media") {
407
- return (
408
- <div>
409
- {value && (
410
- <div
411
- style={{
412
- marginBottom: "6px",
413
- borderRadius: "4px",
414
- overflow: "hidden",
415
- border: "1px solid #ddd",
416
- }}
417
- >
418
- <img
419
- src={value as string}
420
- alt=""
421
- style={{
422
- maxWidth: "100%",
423
- maxHeight: "120px",
424
- objectFit: "contain",
425
- display: "block",
426
- }}
427
- />
428
- </div>
429
- )}
430
- <input
431
- type="text"
432
- value={(value as string) || ""}
433
- onChange={(e) => onChange(e.target.value)}
434
- placeholder="Image URL"
435
- style={{
436
- width: "100%",
437
- padding: "8px",
438
- border: "1px solid #ddd",
439
- borderRadius: "4px",
440
- fontSize: "13px",
441
- }}
442
- />
443
- </div>
444
- );
445
- }
446
- if (field.type === "repeater" && field.schema) {
447
- const items = (Array.isArray(value) ? value : []) as any[];
448
- return (
449
- <div
450
- style={{
451
- border: "1px solid #ddd",
452
- borderRadius: "6px",
453
- overflow: "hidden",
454
- }}
455
- >
456
- {items.map((item: any, idx: number) => (
457
- <div
458
- key={idx}
459
- style={{
460
- padding: "12px",
461
- borderBottom: "1px solid #eee",
462
- background: idx % 2 === 0 ? "#fafafa" : "#fff",
463
- }}
464
- >
465
- <div
466
- style={{
467
- display: "flex",
468
- justifyContent: "space-between",
469
- alignItems: "center",
470
- marginBottom: "8px",
471
- }}
472
- >
473
- <span
474
- style={{ fontSize: "12px", fontWeight: 600, color: "#888" }}
475
- >
476
- #{idx + 1}
477
- </span>
478
- <button
479
- type="button"
480
- onClick={() => {
481
- const newItems = [...items];
482
- newItems.splice(idx, 1);
483
- onChange(newItems);
484
- }}
485
- style={{
486
- fontSize: "11px",
487
- color: "#e53935",
488
- background: "none",
489
- border: "none",
490
- cursor: "pointer",
491
- }}
492
- >
493
- &times; Remove
494
- </button>
495
- </div>
496
- {Object.entries(field.schema).map(
497
- ([subKey, subField]: [string, any]) => (
498
- <div key={subKey} style={{ marginBottom: "8px" }}>
499
- <label
500
- style={{
501
- display: "block",
502
- fontSize: "11px",
503
- fontWeight: 500,
504
- marginBottom: "4px",
505
- color: "#666",
506
- }}
507
- >
508
- {subField.label || subKey}
509
- </label>
510
- {renderField(subField, item[subKey], (subVal) => {
511
- const newItems = [...items];
512
- newItems[idx] = { ...newItems[idx], [subKey]: subVal };
513
- onChange(newItems);
514
- })}
515
- </div>
516
- ),
517
- )}
518
- </div>
519
- ))}
520
- <button
521
- type="button"
522
- onClick={() => {
523
- const newItem: any = {};
524
- Object.entries(field.schema).forEach(([k, f]: [string, any]) => {
525
- newItem[k] = (f as any).type === "repeater" ? [] : "";
526
- });
527
- onChange([...items, newItem]);
528
- }}
529
- style={{
530
- width: "100%",
531
- padding: "10px",
532
- fontSize: "13px",
533
- color: "#667eea",
534
- background: "none",
535
- border: "none",
536
- cursor: "pointer",
537
- fontWeight: 500,
538
- }}
539
- >
540
- + Add item
541
- </button>
542
- </div>
543
- );
544
- }
545
- // singleLine, link, numeric, color, form, pageSelector
546
- return (
547
- <input
548
- type={
549
- field.type === "numeric"
550
- ? "number"
551
- : field.type === "color"
552
- ? "color"
553
- : field.type === "link"
554
- ? "url"
555
- : "text"
556
- }
557
- value={(value as string) || ""}
558
- onChange={(e) =>
559
- onChange(
560
- field.type === "numeric" ? Number(e.target.value) : e.target.value,
561
- )
562
- }
563
- placeholder={
564
- field.placeholder ||
565
- (field.type === "link"
566
- ? "https://..."
567
- : field.type === "form" || field.type === "pageSelector"
568
- ? "Enter ID..."
569
- : "")
570
- }
571
- style={{
572
- width: "100%",
573
- padding: "8px",
574
- border: "1px solid #ddd",
575
- borderRadius: "4px",
576
- fontSize: "13px",
577
- }}
578
- />
579
- );
580
- }
581
-
582
- return (
583
- <div
584
- style={{
585
- display: "grid",
586
- gridTemplateColumns: `${showBlockList ? "280px" : "0px"} 1fr ${showEditor ? "400px" : "0px"}`,
587
- height: "100vh",
588
- transition: "grid-template-columns 0.2s ease",
589
- }}
590
- >
591
- {/* Block List */}
592
- <div
593
- style={{
594
- background: "#fff",
595
- borderRight: showBlockList ? "1px solid #e0e0e0" : "none",
596
- overflow: showBlockList ? "auto" : "hidden",
597
- width: showBlockList ? "auto" : 0,
598
- }}
599
- >
600
- <div
601
- style={{
602
- padding: "16px",
603
- borderBottom: "1px solid #e0e0e0",
604
- background: "#fafafa",
605
- }}
606
- >
607
- <h1 style={{ fontSize: "18px", fontWeight: 600, margin: 0 }}>
608
- Cmssy Dev
609
- </h1>
610
- <p style={{ fontSize: "13px", color: "#666", margin: "4px 0 0" }}>
611
- {blocks.length} blocks
612
- </p>
613
- </div>
614
- {/* Workspace Connection */}
615
- <div
616
- style={{
617
- padding: "12px 16px",
618
- borderBottom: "1px solid #e0e0e0",
619
- fontSize: "12px",
620
- }}
621
- >
622
- {wsLoading ? (
623
- <div style={{ color: "#999" }}>Connecting...</div>
624
- ) : wsInfo?.connected ? (
625
- <div
626
- style={{
627
- display: "flex",
628
- alignItems: "center",
629
- justifyContent: "space-between",
630
- }}
631
- >
632
- <div
633
- style={{ display: "flex", alignItems: "center", gap: "6px" }}
634
- >
635
- <div
636
- style={{
637
- width: "8px",
638
- height: "8px",
639
- borderRadius: "50%",
640
- background: "#22c55e",
641
- flexShrink: 0,
642
- }}
643
- />
644
- <span style={{ fontWeight: 500 }}>
645
- {wsInfo.workspace?.name}
646
- </span>
647
- </div>
648
- <button
649
- type="button"
650
- onClick={() => {
651
- setWsLoading(true);
652
- fetch("/api/workspaces")
653
- .then((r) => r.json())
654
- .then((data) => {
655
- setWsInfo(data);
656
- setWsLoading(false);
657
- })
658
- .catch(() => setWsLoading(false));
659
- }}
660
- style={{
661
- background: "none",
662
- border: "none",
663
- cursor: "pointer",
664
- color: "#667eea",
665
- fontSize: "11px",
666
- padding: "2px 4px",
667
- }}
668
- title="Refresh"
669
- >
670
- {"\u21BB"}
671
- </button>
672
- </div>
673
- ) : (
674
- <div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
675
- <div
676
- style={{
677
- width: "8px",
678
- height: "8px",
679
- borderRadius: "50%",
680
- background: "#ef4444",
681
- flexShrink: 0,
682
- }}
683
- />
684
- <span style={{ color: "#999" }}>
685
- {wsInfo?.reason === "no_token"
686
- ? "Not configured"
687
- : wsInfo?.reason === "auth_error"
688
- ? "Invalid token"
689
- : "Disconnected"}
690
- </span>
691
- </div>
692
- )}
693
- </div>
694
- <div style={{ padding: "12px" }}>
695
- {blocks.map((b) => (
696
- <div
697
- key={b.name}
698
- onClick={() => {
699
- handleSelect(b);
700
- const url = new URL(window.location.href);
701
- url.searchParams.set(
702
- b.type === "template" ? "template" : "block",
703
- b.name,
704
- );
705
- window.history.replaceState({}, "", url.toString());
706
- }}
707
- style={{
708
- padding: "12px 16px",
709
- marginBottom: "4px",
710
- borderRadius: "8px",
711
- cursor: "pointer",
712
- background:
713
- selected?.name === b.name ? "#667eea" : "transparent",
714
- color: selected?.name === b.name ? "white" : "inherit",
715
- }}
716
- >
717
- <div
718
- style={{
719
- display: "flex",
720
- alignItems: "center",
721
- justifyContent: "space-between",
722
- }}
723
- >
724
- <div style={{ fontSize: "14px", fontWeight: 500 }}>
725
- {b.displayName}
726
- </div>
727
- {wsInfo?.connected &&
728
- (() => {
729
- const status = getSyncStatus(b.name, b.version, wsInfo);
730
- const colors: Record<
731
- SyncStatus,
732
- { bg: string; text: string; label: string }
733
- > = {
734
- published: {
735
- bg:
736
- selected?.name === b.name
737
- ? "rgba(255,255,255,0.2)"
738
- : "#dcfce7",
739
- text: selected?.name === b.name ? "#fff" : "#166534",
740
- label: "\u2713",
741
- },
742
- outdated: {
743
- bg:
744
- selected?.name === b.name
745
- ? "rgba(255,255,255,0.2)"
746
- : "#fef3c7",
747
- text: selected?.name === b.name ? "#fff" : "#92400e",
748
- label: "\u2191",
749
- },
750
- "local-only": {
751
- bg: "transparent",
752
- text: "transparent",
753
- label: "",
754
- },
755
- };
756
- const c = colors[status];
757
- if (status === "local-only") return null;
758
- return (
759
- <span
760
- title={status}
761
- style={{
762
- fontSize: "10px",
763
- padding: "1px 6px",
764
- borderRadius: "4px",
765
- background: c.bg,
766
- color: c.text,
767
- fontWeight: 600,
768
- }}
769
- >
770
- {c.label}
771
- </span>
772
- );
773
- })()}
774
- </div>
775
- <div style={{ fontSize: "12px", opacity: 0.7 }}>
776
- {b.type} &middot; v{b.version}
777
- </div>
778
- </div>
779
- ))}
780
- </div>
781
- </div>
782
-
783
- {/* Preview */}
784
- <div
785
- style={{
786
- background: "#fafafa",
787
- display: "flex",
788
- flexDirection: "column",
789
- }}
790
- >
791
- <div
792
- style={{
793
- padding: "10px 16px",
794
- background: "white",
795
- borderBottom: "1px solid #e0e0e0",
796
- display: "flex",
797
- alignItems: "center",
798
- justifyContent: "space-between",
799
- gap: "12px",
800
- }}
801
- >
802
- <button
803
- type="button"
804
- onClick={() => setShowBlockList(!showBlockList)}
805
- title={showBlockList ? "Hide block list" : "Show block list"}
806
- style={{
807
- background: showBlockList ? "#f0f0f0" : "#667eea",
808
- color: showBlockList ? "#333" : "#fff",
809
- border: "1px solid #ddd",
810
- borderRadius: "6px",
811
- padding: "6px 10px",
812
- cursor: "pointer",
813
- fontSize: "13px",
814
- fontWeight: 500,
815
- whiteSpace: "nowrap",
816
- }}
817
- >
818
- {showBlockList ? "\u2190 Blocks" : "\u2192 Blocks"}
819
- </button>
820
- <div
821
- style={{
822
- flex: 1,
823
- fontSize: "16px",
824
- fontWeight: 600,
825
- textAlign: "center",
826
- overflow: "hidden",
827
- textOverflow: "ellipsis",
828
- whiteSpace: "nowrap",
829
- }}
830
- >
831
- {selected?.displayName || "Preview"}
832
- </div>
833
- <button
834
- type="button"
835
- onClick={() => setShowEditor(!showEditor)}
836
- title={showEditor ? "Hide editor" : "Show editor"}
837
- style={{
838
- background: showEditor ? "#f0f0f0" : "#667eea",
839
- color: showEditor ? "#333" : "#fff",
840
- border: "1px solid #ddd",
841
- borderRadius: "6px",
842
- padding: "6px 10px",
843
- cursor: "pointer",
844
- fontSize: "13px",
845
- fontWeight: 500,
846
- whiteSpace: "nowrap",
847
- }}
848
- >
849
- {showEditor ? "Editor \u2192" : "Editor \u2190"}
850
- </button>
851
- <button
852
- type="button"
853
- onClick={() => setShowSettings(!showSettings)}
854
- title="Settings"
855
- style={{
856
- background: showSettings ? "#667eea" : "#f0f0f0",
857
- color: showSettings ? "#fff" : "#333",
858
- border: "1px solid #ddd",
859
- borderRadius: "6px",
860
- padding: "6px 10px",
861
- cursor: "pointer",
862
- fontSize: "15px",
863
- lineHeight: 1,
864
- }}
865
- >
866
- {"\u2699"}
867
- </button>
868
- </div>
869
- {/* Responsive viewport toolbar */}
870
- {previewUrl && (
871
- <div
872
- style={{
873
- padding: "6px 16px",
874
- background: "#f8f8f8",
875
- borderBottom: "1px solid #e0e0e0",
876
- display: "flex",
877
- flexWrap: "wrap",
878
- alignItems: "center",
879
- gap: "4px",
880
- rowGap: "6px",
881
- fontSize: "12px",
882
- }}
883
- >
884
- <button
885
- type="button"
886
- aria-pressed={viewport === null}
887
- onClick={() => setViewport(null)}
888
- style={{
889
- padding: "4px 10px",
890
- border: "1px solid #ddd",
891
- borderRadius: "4px",
892
- background: viewport === null ? "#667eea" : "#fff",
893
- color: viewport === null ? "#fff" : "#333",
894
- cursor: "pointer",
895
- fontSize: "12px",
896
- fontWeight: 500,
897
- }}
898
- >
899
- Full
900
- </button>
901
- {VIEWPORT_PRESETS.map((preset) => (
902
- <button
903
- key={preset.width}
904
- type="button"
905
- aria-pressed={viewport === preset.width}
906
- onClick={() => setViewport(preset.width)}
907
- style={{
908
- padding: "4px 10px",
909
- border: "1px solid #ddd",
910
- borderRadius: "4px",
911
- background: viewport === preset.width ? "#667eea" : "#fff",
912
- color: viewport === preset.width ? "#fff" : "#333",
913
- cursor: "pointer",
914
- fontSize: "12px",
915
- fontWeight: 500,
916
- }}
917
- >
918
- {preset.label} {preset.width}
919
- </button>
920
- ))}
921
- <div
922
- style={{
923
- display: "flex",
924
- alignItems: "center",
925
- gap: "4px",
926
- marginLeft: "8px",
927
- }}
928
- >
929
- <input
930
- type="number"
931
- value={viewport ?? ""}
932
- aria-label="Viewport width in pixels"
933
- onChange={(e) => {
934
- const parsed = parseInt(e.target.value, 10);
935
- const v =
936
- Number.isFinite(parsed) && parsed > 0 ? parsed : null;
937
- setViewport(v);
938
- }}
939
- placeholder="Custom"
940
- style={{
941
- width: "72px",
942
- padding: "4px 6px",
943
- border: "1px solid #ddd",
944
- borderRadius: "4px",
945
- fontSize: "12px",
946
- textAlign: "center",
947
- }}
948
- />
949
- <span style={{ color: "#999" }}>px</span>
950
- </div>
951
- </div>
952
- )}
953
- <div
954
- style={{
955
- flex: 1,
956
- padding: "24px",
957
- display: "flex",
958
- alignItems: "center",
959
- justifyContent: "center",
960
- overflow: "auto",
961
- }}
962
- >
963
- {previewUrl ? (
964
- <div
965
- style={{
966
- width:
967
- viewport !== null && viewport > 0 ? `${viewport}px` : "100%",
968
- maxWidth: "100%",
969
- height: "100%",
970
- background: "white",
971
- borderRadius: "12px",
972
- boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
973
- overflow: "hidden",
974
- transition: "width 0.2s ease",
975
- }}
976
- >
977
- <iframe
978
- ref={iframeRef}
979
- src={previewUrl}
980
- key={previewUrl}
981
- onLoad={() => {
982
- iframeLoadedRef.current = true;
983
- }}
984
- style={{ width: "100%", height: "100%", border: "none" }}
985
- />
986
- </div>
987
- ) : (
988
- <div style={{ textAlign: "center", color: "#999" }}>
989
- <p>Select a block to preview</p>
990
- </div>
991
- )}
992
- </div>
993
- </div>
994
-
995
- {/* Editor */}
996
- <div
997
- style={{
998
- background: "#fff",
999
- borderLeft: showEditor ? "1px solid #e0e0e0" : "none",
1000
- overflow: showEditor ? "auto" : "hidden",
1001
- width: showEditor ? "auto" : 0,
1002
- }}
1003
- >
1004
- <div
1005
- style={{
1006
- padding: "16px",
1007
- borderBottom: "1px solid #e0e0e0",
1008
- background: "#fafafa",
1009
- }}
1010
- >
1011
- <div style={{ display: "flex", gap: "0" }}>
1012
- {(["content", "context"] as const).map((tab) => (
1013
- <button
1014
- key={tab}
1015
- type="button"
1016
- onClick={() => setEditorTab(tab)}
1017
- style={{
1018
- padding: "6px 16px",
1019
- border: "none",
1020
- background: editorTab === tab ? "#fff" : "transparent",
1021
- borderBottom:
1022
- editorTab === tab
1023
- ? "2px solid #667eea"
1024
- : "2px solid transparent",
1025
- fontSize: "14px",
1026
- fontWeight: editorTab === tab ? 600 : 400,
1027
- cursor: "pointer",
1028
- color: editorTab === tab ? "#333" : "#888",
1029
- textTransform: "capitalize",
1030
- }}
1031
- >
1032
- {tab}
1033
- </button>
1034
- ))}
1035
- </div>
1036
- </div>
1037
- {/* Variant picker */}
1038
- {selected && (
1039
- <div
1040
- style={{
1041
- padding: "8px 16px",
1042
- borderBottom: "1px solid #e0e0e0",
1043
- display: "flex",
1044
- flexWrap: "wrap",
1045
- alignItems: "center",
1046
- gap: "4px",
1047
- fontSize: "12px",
1048
- }}
1049
- >
1050
- <button
1051
- type="button"
1052
- onClick={() => setCurrentVariant(null)}
1053
- style={{
1054
- padding: "3px 8px",
1055
- border: "1px solid #ddd",
1056
- borderRadius: "4px",
1057
- background: currentVariant === null ? "#667eea" : "#fff",
1058
- color: currentVariant === null ? "#fff" : "#333",
1059
- cursor: "pointer",
1060
- fontSize: "11px",
1061
- fontWeight: 500,
1062
- }}
1063
- >
1064
- Default
1065
- </button>
1066
- {variants.map((v) => (
1067
- <button
1068
- key={v}
1069
- type="button"
1070
- onClick={() => setCurrentVariant(v)}
1071
- style={{
1072
- padding: "3px 8px",
1073
- border: "1px solid #ddd",
1074
- borderRadius: "4px",
1075
- background: currentVariant === v ? "#667eea" : "#fff",
1076
- color: currentVariant === v ? "#fff" : "#333",
1077
- cursor: "pointer",
1078
- fontSize: "11px",
1079
- fontWeight: 500,
1080
- }}
1081
- >
1082
- {v}
1083
- </button>
1084
- ))}
1085
- <button
1086
- type="button"
1087
- onClick={async () => {
1088
- const name = prompt("Variant name (e.g., long-text):");
1089
- if (!name) return;
1090
- const dataToSave = { ...configDataRef.current, ...previewData };
1091
- try {
1092
- const res = await fetch(
1093
- `/api/preview/${selected.name}?action=save-variant`,
1094
- {
1095
- method: "POST",
1096
- headers: { "Content-Type": "application/json" },
1097
- body: JSON.stringify({
1098
- variantName: name,
1099
- data: dataToSave,
1100
- }),
1101
- },
1102
- );
1103
- if (!res.ok) {
1104
- const err = await res.json().catch(() => ({}));
1105
- alert(err.error || "Failed to save variant");
1106
- return;
1107
- }
1108
- setVariants((prev) =>
1109
- prev.includes(name) ? prev : [...prev, name],
1110
- );
1111
- setCurrentVariant(name);
1112
- } catch {
1113
- alert("Failed to save variant");
1114
- }
1115
- }}
1116
- style={{
1117
- padding: "3px 8px",
1118
- border: "1px dashed #ccc",
1119
- borderRadius: "4px",
1120
- background: "transparent",
1121
- color: "#667eea",
1122
- cursor: "pointer",
1123
- fontSize: "11px",
1124
- fontWeight: 500,
1125
- }}
1126
- >
1127
- + Save as...
1128
- </button>
1129
- </div>
1130
- )}
1131
- {/* Content tab */}
1132
- {editorTab === "content" && (
1133
- <div style={{ padding: "20px" }}>
1134
- {!selected && (
1135
- <p style={{ color: "#999" }}>Select a block to edit</p>
1136
- )}
1137
- {selected && configLoading && (
1138
- <p style={{ color: "#999" }}>Loading...</p>
1139
- )}
1140
- {selected && !configLoading && selected.schema && (
1141
- <div>
1142
- {Object.entries(selected.schema).map(
1143
- ([key, field]: [string, any]) => (
1144
- <div key={key} style={{ marginBottom: "20px" }}>
1145
- <label
1146
- style={{
1147
- display: "block",
1148
- fontSize: "13px",
1149
- fontWeight: 500,
1150
- marginBottom: "6px",
1151
- }}
1152
- >
1153
- {field.label || key}
1154
- {field.required && (
1155
- <span style={{ color: "#e53935" }}> *</span>
1156
- )}
1157
- </label>
1158
- {renderField(field, previewData[key], (val) => {
1159
- setPreviewData({ ...previewData, [key]: val });
1160
- setIsDirty(true);
1161
- })}
1162
- {field.helpText && (
1163
- <div
1164
- style={{
1165
- fontSize: "12px",
1166
- color: "#666",
1167
- marginTop: "4px",
1168
- }}
1169
- >
1170
- {field.helpText}
1171
- </div>
1172
- )}
1173
- </div>
1174
- ),
1175
- )}
1176
- </div>
1177
- )}
1178
- </div>
1179
- )}
1180
-
1181
- {/* Context tab */}
1182
- {editorTab === "context" && (
1183
- <div style={{ padding: "20px" }}>
1184
- {/* Presets */}
1185
- <div style={{ marginBottom: "20px" }}>
1186
- <label
1187
- style={{
1188
- display: "block",
1189
- fontSize: "13px",
1190
- fontWeight: 600,
1191
- marginBottom: "8px",
1192
- }}
1193
- >
1194
- Presets
1195
- </label>
1196
- <div style={{ display: "flex", flexWrap: "wrap", gap: "4px" }}>
1197
- {contextPresets.map((preset) => (
1198
- <button
1199
- key={preset}
1200
- type="button"
1201
- onClick={async () => {
1202
- const res = await fetch(
1203
- `/api/context?preset=${encodeURIComponent(preset)}`,
1204
- );
1205
- const data = await res.json();
1206
- setMockContext(data);
1207
- }}
1208
- style={{
1209
- padding: "4px 10px",
1210
- border: "1px solid #ddd",
1211
- borderRadius: "4px",
1212
- background: "#fff",
1213
- cursor: "pointer",
1214
- fontSize: "12px",
1215
- }}
1216
- >
1217
- {preset}
1218
- </button>
1219
- ))}
1220
- </div>
1221
- </div>
1222
-
1223
- {/* Locale */}
1224
- <div style={{ marginBottom: "20px" }}>
1225
- <label
1226
- style={{
1227
- display: "block",
1228
- fontSize: "13px",
1229
- fontWeight: 600,
1230
- marginBottom: "8px",
1231
- }}
1232
- >
1233
- Locale
1234
- </label>
1235
- <div style={{ marginBottom: "8px" }}>
1236
- <label
1237
- style={{
1238
- display: "block",
1239
- fontSize: "12px",
1240
- color: "#666",
1241
- marginBottom: "4px",
1242
- }}
1243
- >
1244
- Current language
1245
- </label>
1246
- <input
1247
- type="text"
1248
- value={mockContext.locale?.current || "en"}
1249
- onChange={(e) => {
1250
- const updated = {
1251
- ...mockContext,
1252
- locale: {
1253
- ...mockContext.locale,
1254
- current: e.target.value,
1255
- },
1256
- };
1257
- setMockContext(updated);
1258
- }}
1259
- style={{
1260
- width: "100%",
1261
- padding: "6px 8px",
1262
- border: "1px solid #ddd",
1263
- borderRadius: "4px",
1264
- fontSize: "13px",
1265
- boxSizing: "border-box",
1266
- }}
1267
- />
1268
- </div>
1269
- <div style={{ marginBottom: "8px" }}>
1270
- <label
1271
- style={{
1272
- display: "block",
1273
- fontSize: "12px",
1274
- color: "#666",
1275
- marginBottom: "4px",
1276
- }}
1277
- >
1278
- Enabled languages (comma-separated)
1279
- </label>
1280
- <input
1281
- type="text"
1282
- value={(mockContext.locale?.enabled || ["en"]).join(", ")}
1283
- onChange={(e) => {
1284
- const langs = e.target.value
1285
- .split(",")
1286
- .map((s: string) => s.trim())
1287
- .filter(Boolean);
1288
- const updated = {
1289
- ...mockContext,
1290
- locale: { ...mockContext.locale, enabled: langs },
1291
- };
1292
- setMockContext(updated);
1293
- }}
1294
- style={{
1295
- width: "100%",
1296
- padding: "6px 8px",
1297
- border: "1px solid #ddd",
1298
- borderRadius: "4px",
1299
- fontSize: "13px",
1300
- boxSizing: "border-box",
1301
- }}
1302
- />
1303
- </div>
1304
- </div>
1305
-
1306
- {/* Auth */}
1307
- <div style={{ marginBottom: "20px" }}>
1308
- <label
1309
- style={{
1310
- display: "flex",
1311
- alignItems: "center",
1312
- gap: "8px",
1313
- fontSize: "13px",
1314
- fontWeight: 600,
1315
- marginBottom: "8px",
1316
- cursor: "pointer",
1317
- }}
1318
- >
1319
- <input
1320
- type="checkbox"
1321
- checked={!!mockContext.auth?.isAuthenticated}
1322
- onChange={(e) => {
1323
- const updated = e.target.checked
1324
- ? {
1325
- ...mockContext,
1326
- auth: {
1327
- isAuthenticated: true,
1328
- member: mockContext.auth?.member || {
1329
- id: "dev-member-1",
1330
- email: "user@example.com",
1331
- profile: {
1332
- firstName: "Jane",
1333
- lastName: "Doe",
1334
- displayName: "Jane Doe",
1335
- avatarUrl: "",
1336
- },
1337
- role: "member",
1338
- verified: true,
1339
- },
1340
- },
1341
- }
1342
- : { ...mockContext, auth: null };
1343
- setMockContext(updated);
1344
- }}
1345
- />
1346
- Authenticated
1347
- </label>
1348
- {mockContext.auth?.isAuthenticated && (
1349
- <div style={{ paddingLeft: "4px" }}>
1350
- {(["email", "role"] as const).map((field) => (
1351
- <div key={field} style={{ marginBottom: "6px" }}>
1352
- <label
1353
- style={{
1354
- display: "block",
1355
- fontSize: "12px",
1356
- color: "#666",
1357
- marginBottom: "2px",
1358
- textTransform: "capitalize",
1359
- }}
1360
- >
1361
- {field}
1362
- </label>
1363
- <input
1364
- type="text"
1365
- value={
1366
- field === "email"
1367
- ? mockContext.auth?.member?.email || ""
1368
- : mockContext.auth?.member?.role || ""
1369
- }
1370
- onChange={(e) => {
1371
- const updated = {
1372
- ...mockContext,
1373
- auth: {
1374
- ...mockContext.auth,
1375
- member: {
1376
- ...mockContext.auth?.member,
1377
- [field]: e.target.value,
1378
- },
1379
- },
1380
- };
1381
- setMockContext(updated);
1382
- }}
1383
- style={{
1384
- width: "100%",
1385
- padding: "4px 8px",
1386
- border: "1px solid #ddd",
1387
- borderRadius: "4px",
1388
- fontSize: "12px",
1389
- boxSizing: "border-box",
1390
- }}
1391
- />
1392
- </div>
1393
- ))}
1394
- </div>
1395
- )}
1396
- </div>
1397
-
1398
- {/* Raw JSON editor */}
1399
- <div style={{ marginBottom: "20px" }}>
1400
- <label
1401
- style={{
1402
- display: "block",
1403
- fontSize: "13px",
1404
- fontWeight: 600,
1405
- marginBottom: "8px",
1406
- }}
1407
- >
1408
- Full context (JSON)
1409
- </label>
1410
- <textarea
1411
- value={JSON.stringify(mockContext, null, 2)}
1412
- onChange={(e) => {
1413
- try {
1414
- const parsed = JSON.parse(e.target.value);
1415
- setMockContext(parsed);
1416
- } catch {
1417
- // Invalid JSON, don't update
1418
- }
1419
- }}
1420
- style={{
1421
- width: "100%",
1422
- minHeight: "200px",
1423
- padding: "8px",
1424
- border: "1px solid #ddd",
1425
- borderRadius: "4px",
1426
- fontSize: "12px",
1427
- fontFamily: "monospace",
1428
- resize: "vertical",
1429
- boxSizing: "border-box",
1430
- }}
1431
- />
1432
- </div>
1433
- </div>
1434
- )}
1435
- </div>
1436
-
1437
- {/* Settings Panel (slide-over) */}
1438
- {showSettings && (
1439
- <div
1440
- style={{
1441
- position: "fixed",
1442
- top: 0,
1443
- right: 0,
1444
- bottom: 0,
1445
- width: "420px",
1446
- background: "#fff",
1447
- boxShadow: "-4px 0 24px rgba(0,0,0,0.12)",
1448
- zIndex: 1000,
1449
- display: "flex",
1450
- flexDirection: "column",
1451
- animation: "slideIn 0.2s ease",
1452
- }}
1453
- >
1454
- <style>{`@keyframes slideIn { from { transform: translateX(100%); } to { transform: translateX(0); } }`}</style>
1455
- <div
1456
- style={{
1457
- padding: "16px 20px",
1458
- borderBottom: "1px solid #e0e0e0",
1459
- display: "flex",
1460
- justifyContent: "space-between",
1461
- alignItems: "center",
1462
- background: "#fafafa",
1463
- }}
1464
- >
1465
- <h2 style={{ fontSize: "16px", fontWeight: 600, margin: 0 }}>
1466
- Settings
1467
- </h2>
1468
- <button
1469
- type="button"
1470
- onClick={() => setShowSettings(false)}
1471
- style={{
1472
- background: "none",
1473
- border: "none",
1474
- fontSize: "20px",
1475
- cursor: "pointer",
1476
- color: "#666",
1477
- padding: "4px",
1478
- }}
1479
- >
1480
- {"\u2715"}
1481
- </button>
1482
- </div>
1483
- <div style={{ flex: 1, overflow: "auto", padding: "20px" }}>
1484
- {/* Connection */}
1485
- <div style={{ marginBottom: "24px" }}>
1486
- <h3
1487
- style={{
1488
- fontSize: "13px",
1489
- fontWeight: 600,
1490
- textTransform: "uppercase",
1491
- letterSpacing: "0.05em",
1492
- color: "#666",
1493
- marginBottom: "12px",
1494
- }}
1495
- >
1496
- Connection
1497
- </h3>
1498
- <div style={{ marginBottom: "12px" }}>
1499
- <label
1500
- style={{
1501
- display: "block",
1502
- fontSize: "13px",
1503
- fontWeight: 500,
1504
- marginBottom: "4px",
1505
- }}
1506
- >
1507
- API URL
1508
- </label>
1509
- <input
1510
- type="text"
1511
- value={settingsData.newApiUrl}
1512
- onChange={(e) =>
1513
- setSettingsData({
1514
- ...settingsData,
1515
- newApiUrl: e.target.value,
1516
- })
1517
- }
1518
- style={{
1519
- width: "100%",
1520
- padding: "8px",
1521
- border: "1px solid #ddd",
1522
- borderRadius: "6px",
1523
- fontSize: "13px",
1524
- boxSizing: "border-box",
1525
- }}
1526
- />
1527
- </div>
1528
- <div style={{ marginBottom: "12px" }}>
1529
- <label
1530
- style={{
1531
- display: "block",
1532
- fontSize: "13px",
1533
- fontWeight: 500,
1534
- marginBottom: "4px",
1535
- }}
1536
- >
1537
- API Token{" "}
1538
- {settingsData.hasToken && (
1539
- <span style={{ color: "#22c55e", fontSize: "11px" }}>
1540
- ({settingsData.maskedToken})
1541
- </span>
1542
- )}
1543
- </label>
1544
- <input
1545
- type="password"
1546
- value={settingsData.newToken}
1547
- onChange={(e) =>
1548
- setSettingsData({
1549
- ...settingsData,
1550
- newToken: e.target.value,
1551
- })
1552
- }
1553
- placeholder={
1554
- settingsData.hasToken
1555
- ? "Leave empty to keep current"
1556
- : "bf_..."
1557
- }
1558
- style={{
1559
- width: "100%",
1560
- padding: "8px",
1561
- border: "1px solid #ddd",
1562
- borderRadius: "6px",
1563
- fontSize: "13px",
1564
- boxSizing: "border-box",
1565
- }}
1566
- />
1567
- </div>
1568
- <div style={{ display: "flex", gap: "8px" }}>
1569
- <button
1570
- type="button"
1571
- disabled={settingsData.testing}
1572
- onClick={async () => {
1573
- setSettingsData((s) => ({
1574
- ...s,
1575
- testing: true,
1576
- testResult: null,
1577
- }));
1578
- try {
1579
- if (settingsData.newToken) {
1580
- await fetch("/api/config", {
1581
- method: "PUT",
1582
- headers: { "Content-Type": "application/json" },
1583
- body: JSON.stringify({
1584
- apiToken: settingsData.newToken,
1585
- apiUrl: settingsData.newApiUrl,
1586
- }),
1587
- });
1588
- }
1589
- const res = await fetch("/api/workspaces");
1590
- const data = await res.json();
1591
- if (data.connected) {
1592
- setSettingsData((s) => ({
1593
- ...s,
1594
- testing: false,
1595
- testResult: "success",
1596
- }));
1597
- setWsInfo(data);
1598
- setWsLoading(false);
1599
- } else {
1600
- setSettingsData((s) => ({
1601
- ...s,
1602
- testing: false,
1603
- testResult: data.error || data.reason || "failed",
1604
- }));
1605
- }
1606
- } catch {
1607
- setSettingsData((s) => ({
1608
- ...s,
1609
- testing: false,
1610
- testResult: "Network error",
1611
- }));
1612
- }
1613
- }}
1614
- style={{
1615
- padding: "8px 16px",
1616
- border: "1px solid #ddd",
1617
- borderRadius: "6px",
1618
- background: "#f8f8f8",
1619
- cursor: "pointer",
1620
- fontSize: "13px",
1621
- fontWeight: 500,
1622
- }}
1623
- >
1624
- {settingsData.testing ? "Testing..." : "Test Connection"}
1625
- </button>
1626
- {settingsData.testResult && (
1627
- <span
1628
- style={{
1629
- display: "flex",
1630
- alignItems: "center",
1631
- fontSize: "12px",
1632
- color:
1633
- settingsData.testResult === "success"
1634
- ? "#22c55e"
1635
- : "#ef4444",
1636
- }}
1637
- >
1638
- {settingsData.testResult === "success"
1639
- ? "\u2713 Connected"
1640
- : "\u2717 " + settingsData.testResult}
1641
- </span>
1642
- )}
1643
- </div>
1644
- </div>
1645
-
1646
- {/* Workspace */}
1647
- <div style={{ marginBottom: "24px" }}>
1648
- <h3
1649
- style={{
1650
- fontSize: "13px",
1651
- fontWeight: 600,
1652
- textTransform: "uppercase",
1653
- letterSpacing: "0.05em",
1654
- color: "#666",
1655
- marginBottom: "12px",
1656
- }}
1657
- >
1658
- Workspace
1659
- </h3>
1660
- {wsInfo?.connected &&
1661
- wsInfo.workspaces &&
1662
- wsInfo.workspaces.length > 0 ? (
1663
- <select
1664
- value={settingsData.newWorkspaceId}
1665
- onChange={(e) =>
1666
- setSettingsData({
1667
- ...settingsData,
1668
- newWorkspaceId: e.target.value,
1669
- })
1670
- }
1671
- style={{
1672
- width: "100%",
1673
- padding: "8px",
1674
- border: "1px solid #ddd",
1675
- borderRadius: "6px",
1676
- fontSize: "13px",
1677
- }}
1678
- >
1679
- <option value="">Select workspace...</option>
1680
- {wsInfo.workspaces.map((w: any) => (
1681
- <option key={w.id} value={w.id}>
1682
- {w.name} ({w.slug})
1683
- </option>
1684
- ))}
1685
- </select>
1686
- ) : (
1687
- <p style={{ fontSize: "13px", color: "#999" }}>
1688
- Connect to see available workspaces
1689
- </p>
1690
- )}
1691
- </div>
1692
-
1693
- {/* Project Info */}
1694
- {settingsData.project && (
1695
- <div style={{ marginBottom: "24px" }}>
1696
- <h3
1697
- style={{
1698
- fontSize: "13px",
1699
- fontWeight: 600,
1700
- textTransform: "uppercase",
1701
- letterSpacing: "0.05em",
1702
- color: "#666",
1703
- marginBottom: "12px",
1704
- }}
1705
- >
1706
- Project
1707
- </h3>
1708
- <div
1709
- style={{
1710
- background: "#f8f8f8",
1711
- borderRadius: "6px",
1712
- padding: "12px",
1713
- fontSize: "12px",
1714
- fontFamily: "monospace",
1715
- }}
1716
- >
1717
- {Object.entries(settingsData.project).map(([k, v]) => (
1718
- <div key={k} style={{ marginBottom: "4px" }}>
1719
- <span style={{ color: "#666" }}>{k}:</span>{" "}
1720
- <span>
1721
- {typeof v === "object" ? JSON.stringify(v) : String(v)}
1722
- </span>
1723
- </div>
1724
- ))}
1725
- </div>
1726
- </div>
1727
- )}
1728
-
1729
- {/* Save */}
1730
- <button
1731
- type="button"
1732
- disabled={settingsData.saving}
1733
- onClick={async () => {
1734
- setSettingsData((s) => ({ ...s, saving: true }));
1735
- const body: any = { apiUrl: settingsData.newApiUrl };
1736
- if (settingsData.newToken)
1737
- body.apiToken = settingsData.newToken;
1738
- if (settingsData.newWorkspaceId)
1739
- body.workspaceId = settingsData.newWorkspaceId;
1740
- try {
1741
- await fetch("/api/config", {
1742
- method: "PUT",
1743
- headers: { "Content-Type": "application/json" },
1744
- body: JSON.stringify(body),
1745
- });
1746
- setSettingsData((s) => ({
1747
- ...s,
1748
- saving: false,
1749
- newToken: "",
1750
- }));
1751
- const res = await fetch("/api/workspaces");
1752
- const data = await res.json();
1753
- setWsInfo(data);
1754
- const cfgRes = await fetch("/api/config");
1755
- const cfgData = await cfgRes.json();
1756
- setSettingsData((s) => ({
1757
- ...s,
1758
- hasToken: cfgData.env.hasToken,
1759
- maskedToken: cfgData.env.maskedToken,
1760
- workspaceId: cfgData.env.workspaceId,
1761
- }));
1762
- } catch {
1763
- setSettingsData((s) => ({ ...s, saving: false }));
1764
- }
1765
- }}
1766
- style={{
1767
- width: "100%",
1768
- padding: "10px",
1769
- background: "#667eea",
1770
- color: "#fff",
1771
- border: "none",
1772
- borderRadius: "6px",
1773
- fontSize: "14px",
1774
- fontWeight: 500,
1775
- cursor: "pointer",
1776
- }}
1777
- >
1778
- {settingsData.saving ? "Saving..." : "Save Settings"}
1779
- </button>
1780
- </div>
1781
- </div>
1782
- )}
1783
- </div>
1784
- );
1785
- }