cmssy-cli 0.20.1 → 0.24.1

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 (80) hide show
  1. package/config.d.ts +1 -1
  2. package/dist/cli.js +136 -23
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/add-source.d.ts +7 -0
  5. package/dist/commands/add-source.d.ts.map +1 -0
  6. package/dist/commands/add-source.js +238 -0
  7. package/dist/commands/add-source.js.map +1 -0
  8. package/dist/commands/build.d.ts +1 -0
  9. package/dist/commands/build.d.ts.map +1 -1
  10. package/dist/commands/build.js +56 -12
  11. package/dist/commands/build.js.map +1 -1
  12. package/dist/commands/create.d.ts.map +1 -1
  13. package/dist/commands/create.js +22 -2
  14. package/dist/commands/create.js.map +1 -1
  15. package/dist/commands/dev.d.ts.map +1 -1
  16. package/dist/commands/dev.js +652 -410
  17. package/dist/commands/dev.js.map +1 -1
  18. package/dist/commands/init.d.ts.map +1 -1
  19. package/dist/commands/init.js +3 -1
  20. package/dist/commands/init.js.map +1 -1
  21. package/dist/commands/migrate.d.ts.map +1 -1
  22. package/dist/commands/migrate.js +3 -1
  23. package/dist/commands/migrate.js.map +1 -1
  24. package/dist/commands/publish.js +74 -0
  25. package/dist/commands/publish.js.map +1 -1
  26. package/dist/dev-ui/app.js +166 -19
  27. package/dist/dev-ui/index.html +138 -0
  28. package/dist/dev-ui-react/App.tsx +164 -0
  29. package/dist/dev-ui-react/__tests__/previewData.test.ts +193 -0
  30. package/dist/dev-ui-react/components/BlocksList.tsx +232 -0
  31. package/dist/dev-ui-react/components/Editor.tsx +469 -0
  32. package/dist/dev-ui-react/components/Preview.tsx +146 -0
  33. package/dist/dev-ui-react/hooks/useBlocks.ts +80 -0
  34. package/dist/dev-ui-react/index.html +13 -0
  35. package/dist/dev-ui-react/main.tsx +8 -0
  36. package/dist/dev-ui-react/styles.css +856 -0
  37. package/dist/dev-ui-react/types.ts +45 -0
  38. package/dist/types/block-config.d.ts +100 -2
  39. package/dist/types/block-config.d.ts.map +1 -1
  40. package/dist/types/block-config.js +6 -1
  41. package/dist/types/block-config.js.map +1 -1
  42. package/dist/utils/block-config.js +3 -3
  43. package/dist/utils/block-config.js.map +1 -1
  44. package/dist/utils/blocks-meta-cache.d.ts +28 -0
  45. package/dist/utils/blocks-meta-cache.d.ts.map +1 -0
  46. package/dist/utils/blocks-meta-cache.js +72 -0
  47. package/dist/utils/blocks-meta-cache.js.map +1 -0
  48. package/dist/utils/builder.d.ts +3 -0
  49. package/dist/utils/builder.d.ts.map +1 -1
  50. package/dist/utils/builder.js +17 -14
  51. package/dist/utils/builder.js.map +1 -1
  52. package/dist/utils/field-schema.d.ts +2 -0
  53. package/dist/utils/field-schema.d.ts.map +1 -1
  54. package/dist/utils/field-schema.js +21 -4
  55. package/dist/utils/field-schema.js.map +1 -1
  56. package/dist/utils/graphql.d.ts +2 -0
  57. package/dist/utils/graphql.d.ts.map +1 -1
  58. package/dist/utils/graphql.js +22 -0
  59. package/dist/utils/graphql.js.map +1 -1
  60. package/dist/utils/scanner.d.ts +5 -3
  61. package/dist/utils/scanner.d.ts.map +1 -1
  62. package/dist/utils/scanner.js +23 -16
  63. package/dist/utils/scanner.js.map +1 -1
  64. package/dist/utils/type-generator.d.ts +7 -1
  65. package/dist/utils/type-generator.d.ts.map +1 -1
  66. package/dist/utils/type-generator.js +58 -41
  67. package/dist/utils/type-generator.js.map +1 -1
  68. package/package.json +8 -3
  69. package/dist/commands/deploy.d.ts +0 -9
  70. package/dist/commands/deploy.d.ts.map +0 -1
  71. package/dist/commands/deploy.js +0 -226
  72. package/dist/commands/deploy.js.map +0 -1
  73. package/dist/commands/push.d.ts +0 -9
  74. package/dist/commands/push.d.ts.map +0 -1
  75. package/dist/commands/push.js +0 -199
  76. package/dist/commands/push.js.map +0 -1
  77. package/dist/utils/blockforge-config.d.ts +0 -19
  78. package/dist/utils/blockforge-config.d.ts.map +0 -1
  79. package/dist/utils/blockforge-config.js +0 -19
  80. package/dist/utils/blockforge-config.js.map +0 -1
@@ -20,6 +20,27 @@ let availableTags = [];
20
20
  async function init() {
21
21
  await loadBlocks();
22
22
  setupSSE();
23
+
24
+ // Check for block parameter in URL
25
+ const urlParams = new URLSearchParams(window.location.search);
26
+ const blockParam = urlParams.get('block');
27
+ if (blockParam) {
28
+ const block = blocks.find(b => b.name === blockParam);
29
+ if (block) {
30
+ await selectBlock(blockParam);
31
+ }
32
+ }
33
+ }
34
+
35
+ // Update URL with current block (without page reload)
36
+ function updateUrlWithBlock(blockName) {
37
+ const url = new URL(window.location);
38
+ if (blockName) {
39
+ url.searchParams.set('block', blockName);
40
+ } else {
41
+ url.searchParams.delete('block');
42
+ }
43
+ window.history.replaceState({}, '', url);
23
44
  }
24
45
 
25
46
  // Load all blocks from API
@@ -295,26 +316,70 @@ function renderBlockItem(block) {
295
316
  `;
296
317
  }
297
318
 
298
- // Select a block
319
+ // Select a block (lazy loads config)
299
320
  async function selectBlock(blockName) {
300
321
  const block = blocks.find(b => b.name === blockName);
301
322
  if (!block) return;
302
323
 
303
324
  currentBlock = block;
304
- renderBlocksList(); // Update active state
325
+ updateUrlWithBlock(blockName);
326
+ renderBlocksList();
305
327
 
306
- // Load preview data
328
+ // Show loading state in editor
329
+ document.getElementById('preview-title').textContent = block.displayName || block.name;
330
+ document.getElementById('editor-subtitle').textContent = block.name;
331
+ document.getElementById('editor-content').innerHTML = `
332
+ <div class="loading">
333
+ <div class="spinner"></div>
334
+ <span>Loading properties...</span>
335
+ </div>
336
+ `;
337
+
338
+ // Lazy load full config and preview data
307
339
  try {
308
- const response = await fetch(`/api/preview/${blockName}`);
309
- previewData = await response.json();
340
+ const response = await fetch(`/api/blocks/${blockName}/config`);
341
+ if (!response.ok) {
342
+ throw new Error('Failed to load block config');
343
+ }
344
+ const config = await response.json();
345
+
346
+ // Update block with loaded config
347
+ block.schema = config.schema;
348
+ block.category = config.category;
349
+ block.tags = config.tags;
350
+ block.displayName = config.displayName;
351
+ block.description = config.description;
352
+ previewData = config.previewData || {};
353
+
354
+ // For templates, also load pages info
355
+ if (block.type === 'template') {
356
+ try {
357
+ const pagesResponse = await fetch(`/api/templates/${blockName}/pages`);
358
+ if (pagesResponse.ok) {
359
+ const templateData = await pagesResponse.json();
360
+ block.pages = templateData.pages;
361
+ block.layoutSlots = templateData.layoutSlots;
362
+ }
363
+ } catch (err) {
364
+ console.warn('Could not load template pages:', err);
365
+ }
366
+ }
367
+
368
+ // Update filters if we got new category/tags
369
+ populateFilters();
310
370
  } catch (error) {
311
- console.error('Failed to load preview data:', error);
312
- previewData = {};
371
+ console.error('Failed to load block config:', error);
372
+ document.getElementById('editor-content').innerHTML = `
373
+ <div class="editor-empty" style="color: #e53935;">
374
+ Failed to load block config.<br>
375
+ <small>Check console for details.</small>
376
+ </div>
377
+ `;
378
+ return;
313
379
  }
314
380
 
315
381
  // Update UI
316
382
  document.getElementById('preview-title').textContent = block.displayName || block.name;
317
- document.getElementById('editor-subtitle').textContent = block.name;
318
383
 
319
384
  // Show publish button
320
385
  const publishBtn = document.getElementById('publish-btn');
@@ -334,27 +399,51 @@ function renderPreview() {
334
399
  if (!currentBlock) return;
335
400
 
336
401
  const previewContent = document.getElementById('preview-content');
337
- previewContent.innerHTML = `
338
- <div class="preview-iframe-wrapper">
339
- <iframe
340
- class="preview-iframe"
341
- src="/preview/${currentBlock.name}"
342
- id="preview-iframe"
343
- ></iframe>
344
- </div>
345
- `;
402
+ const isTemplate = currentBlock.type === 'template' && currentBlock.pages && currentBlock.pages.length > 0;
403
+
404
+ if (isTemplate) {
405
+ // Template preview - show page selector and full page preview
406
+ const firstPage = currentBlock.pages[0];
407
+ previewContent.innerHTML = `
408
+ <div class="preview-iframe-wrapper template-preview">
409
+ <iframe
410
+ class="preview-iframe"
411
+ src="/preview/template/${currentBlock.name}/${firstPage.slug}"
412
+ id="preview-iframe"
413
+ ></iframe>
414
+ </div>
415
+ `;
416
+ } else {
417
+ // Block preview - single block
418
+ previewContent.innerHTML = `
419
+ <div class="preview-iframe-wrapper">
420
+ <iframe
421
+ class="preview-iframe"
422
+ src="/preview/${currentBlock.name}"
423
+ id="preview-iframe"
424
+ ></iframe>
425
+ </div>
426
+ `;
427
+ }
346
428
  }
347
429
 
348
430
  // Render editor form
349
431
  function renderEditor() {
432
+ const editorContent = document.getElementById('editor-content');
433
+
434
+ // For templates, show pages overview
435
+ if (currentBlock && currentBlock.type === 'template' && currentBlock.pages) {
436
+ editorContent.innerHTML = renderTemplateEditor();
437
+ return;
438
+ }
439
+
350
440
  if (!currentBlock || !currentBlock.schema) {
351
- document.getElementById('editor-content').innerHTML = `
441
+ editorContent.innerHTML = `
352
442
  <div class="editor-empty">No schema defined for this block</div>
353
443
  `;
354
444
  return;
355
445
  }
356
446
 
357
- const editorContent = document.getElementById('editor-content');
358
447
  const fields = Object.entries(currentBlock.schema);
359
448
 
360
449
  editorContent.innerHTML = fields.map(([key, field]) =>
@@ -365,6 +454,64 @@ function renderEditor() {
365
454
  attachFieldListeners();
366
455
  }
367
456
 
457
+ // Render template editor with pages list
458
+ function renderTemplateEditor() {
459
+ const pages = currentBlock.pages || [];
460
+ const layoutSlots = currentBlock.layoutSlots || [];
461
+
462
+ const pagesHtml = pages.map((page, index) => `
463
+ <div class="template-page-item" onclick="navigateToTemplatePage('${page.slug}')">
464
+ <div class="template-page-header">
465
+ <span class="template-page-name">${escapeHtml(page.name)}</span>
466
+ <span class="template-page-blocks">${page.blocksCount} blocks</span>
467
+ </div>
468
+ <div class="template-page-slug">/${page.slug}</div>
469
+ </div>
470
+ `).join('');
471
+
472
+ const layoutHtml = layoutSlots.length > 0 ? `
473
+ <div class="template-section">
474
+ <h4 class="template-section-title">Layout Slots</h4>
475
+ ${layoutSlots.map(slot => `
476
+ <div class="template-layout-slot">
477
+ <span class="slot-type">${slot.slot}</span>
478
+ <span class="slot-block">${slot.type}</span>
479
+ </div>
480
+ `).join('')}
481
+ </div>
482
+ ` : '';
483
+
484
+ return `
485
+ <div class="template-editor">
486
+ <div class="template-info">
487
+ <div class="template-info-badge">Template</div>
488
+ <p class="template-info-desc">${escapeHtml(currentBlock.description || 'No description')}</p>
489
+ </div>
490
+
491
+ <div class="template-section">
492
+ <h4 class="template-section-title">Pages (${pages.length})</h4>
493
+ <div class="template-pages-list">
494
+ ${pagesHtml || '<div class="editor-empty">No pages defined</div>'}
495
+ </div>
496
+ </div>
497
+
498
+ ${layoutHtml}
499
+
500
+ <div class="template-hint">
501
+ <p>Click on a page to preview it, or use the tabs in the preview header.</p>
502
+ </div>
503
+ </div>
504
+ `;
505
+ }
506
+
507
+ // Navigate to template page in preview
508
+ window.navigateToTemplatePage = function(pageSlug) {
509
+ const iframe = document.getElementById('preview-iframe');
510
+ if (iframe && currentBlock) {
511
+ iframe.src = `/preview/template/${currentBlock.name}/${pageSlug}`;
512
+ }
513
+ };
514
+
368
515
  // Render a single field based on type
369
516
  function renderField(key, field, value) {
370
517
  const required = field.required ? '<span class="field-required">*</span>' : '';
@@ -302,6 +302,9 @@
302
302
  display: flex;
303
303
  flex-wrap: wrap;
304
304
  gap: 6px;
305
+ max-height: 120px;
306
+ overflow-y: auto;
307
+ padding-right: 4px;
305
308
  }
306
309
 
307
310
  .tag-chip {
@@ -1118,6 +1121,141 @@
1118
1121
  .editor-content {
1119
1122
  transition: opacity 0.2s ease;
1120
1123
  }
1124
+
1125
+ /* Template Editor Styles */
1126
+ .template-editor {
1127
+ padding: 0;
1128
+ }
1129
+
1130
+ .template-info {
1131
+ padding: 16px;
1132
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1133
+ color: white;
1134
+ border-radius: 8px;
1135
+ margin-bottom: 20px;
1136
+ }
1137
+
1138
+ .template-info-badge {
1139
+ display: inline-block;
1140
+ background: rgba(255,255,255,0.2);
1141
+ padding: 4px 10px;
1142
+ border-radius: 12px;
1143
+ font-size: 11px;
1144
+ font-weight: 600;
1145
+ text-transform: uppercase;
1146
+ letter-spacing: 0.5px;
1147
+ margin-bottom: 8px;
1148
+ }
1149
+
1150
+ .template-info-desc {
1151
+ margin: 0;
1152
+ font-size: 13px;
1153
+ opacity: 0.9;
1154
+ line-height: 1.5;
1155
+ }
1156
+
1157
+ .template-section {
1158
+ margin-bottom: 24px;
1159
+ }
1160
+
1161
+ .template-section-title {
1162
+ font-size: 12px;
1163
+ font-weight: 600;
1164
+ text-transform: uppercase;
1165
+ letter-spacing: 0.5px;
1166
+ color: #666;
1167
+ margin: 0 0 12px 0;
1168
+ padding-bottom: 8px;
1169
+ border-bottom: 1px solid #eee;
1170
+ }
1171
+
1172
+ .template-pages-list {
1173
+ display: flex;
1174
+ flex-direction: column;
1175
+ gap: 8px;
1176
+ }
1177
+
1178
+ .template-page-item {
1179
+ padding: 12px 16px;
1180
+ background: #f8f9fa;
1181
+ border: 1px solid #e9ecef;
1182
+ border-radius: 8px;
1183
+ cursor: pointer;
1184
+ transition: all 0.2s;
1185
+ }
1186
+
1187
+ .template-page-item:hover {
1188
+ background: #e9ecef;
1189
+ border-color: #667eea;
1190
+ }
1191
+
1192
+ .template-page-header {
1193
+ display: flex;
1194
+ justify-content: space-between;
1195
+ align-items: center;
1196
+ margin-bottom: 4px;
1197
+ }
1198
+
1199
+ .template-page-name {
1200
+ font-weight: 500;
1201
+ font-size: 14px;
1202
+ color: #333;
1203
+ }
1204
+
1205
+ .template-page-blocks {
1206
+ font-size: 11px;
1207
+ color: #666;
1208
+ background: #e9ecef;
1209
+ padding: 2px 8px;
1210
+ border-radius: 10px;
1211
+ }
1212
+
1213
+ .template-page-slug {
1214
+ font-size: 12px;
1215
+ color: #999;
1216
+ font-family: monospace;
1217
+ }
1218
+
1219
+ .template-layout-slot {
1220
+ display: flex;
1221
+ justify-content: space-between;
1222
+ align-items: center;
1223
+ padding: 10px 14px;
1224
+ background: #f8f9fa;
1225
+ border-radius: 6px;
1226
+ margin-bottom: 8px;
1227
+ }
1228
+
1229
+ .slot-type {
1230
+ font-size: 12px;
1231
+ font-weight: 500;
1232
+ text-transform: capitalize;
1233
+ color: #667eea;
1234
+ }
1235
+
1236
+ .slot-block {
1237
+ font-size: 12px;
1238
+ color: #666;
1239
+ font-family: monospace;
1240
+ }
1241
+
1242
+ .template-hint {
1243
+ padding: 12px;
1244
+ background: #fff3cd;
1245
+ border-radius: 6px;
1246
+ margin-top: 20px;
1247
+ }
1248
+
1249
+ .template-hint p {
1250
+ margin: 0;
1251
+ font-size: 12px;
1252
+ color: #856404;
1253
+ }
1254
+
1255
+ /* Template preview style */
1256
+ .template-preview {
1257
+ background: #1a1a2e;
1258
+ }
1121
1259
  </style>
1122
1260
  </head>
1123
1261
  <body>
@@ -0,0 +1,164 @@
1
+ import { useState, useEffect, useCallback, useRef } from 'react';
2
+ import { BlocksList } from './components/BlocksList';
3
+ import { Preview } from './components/Preview';
4
+ import { Editor } from './components/Editor';
5
+ import { useBlocks, useBlockConfig } from './hooks/useBlocks';
6
+ import { Block } from './types';
7
+ import './styles.css';
8
+
9
+ export function App() {
10
+ const { blocks, loading: blocksLoading } = useBlocks();
11
+ const [selectedBlock, setSelectedBlock] = useState<Block | null>(null);
12
+ const [previewData, setPreviewData] = useState<Record<string, unknown>>({});
13
+ const [currentPage, setCurrentPage] = useState<string | undefined>();
14
+ const [isDirty, setIsDirty] = useState(false);
15
+ // Keep reference to complete config data for merging during save
16
+ const configDataRef = useRef<Record<string, unknown>>({});
17
+
18
+ const { config, loading: configLoading } = useBlockConfig(
19
+ selectedBlock?.name || null,
20
+ selectedBlock?.type || null
21
+ );
22
+
23
+ // Update selected block with loaded config
24
+ useEffect(() => {
25
+ if (selectedBlock && config) {
26
+ const updatedBlock: Block = {
27
+ ...selectedBlock,
28
+ schema: config.schema as Block['schema'],
29
+ pages: config.pages,
30
+ layoutSlots: config.layoutSlots,
31
+ };
32
+ setSelectedBlock(updatedBlock);
33
+
34
+ // Store complete config data in ref for merging during save
35
+ const configData = config.previewData || {};
36
+ configDataRef.current = configData;
37
+
38
+ // Set previewData from config (complete data)
39
+ setPreviewData(configData);
40
+
41
+ // Set first page for templates
42
+ if (config.pages && config.pages.length > 0) {
43
+ setCurrentPage(config.pages[0].slug);
44
+ }
45
+ }
46
+ }, [config, selectedBlock?.name]);
47
+
48
+ // Handle block selection
49
+ const handleSelectBlock = useCallback((block: Block) => {
50
+ setSelectedBlock(block);
51
+ setPreviewData({});
52
+ setCurrentPage(undefined);
53
+ setIsDirty(false);
54
+ configDataRef.current = {};
55
+
56
+ // Update URL based on type
57
+ const url = new URL(window.location.href);
58
+ url.searchParams.delete('block');
59
+ url.searchParams.delete('template');
60
+ if (block.type === 'template') {
61
+ url.searchParams.set('template', block.name);
62
+ } else {
63
+ url.searchParams.set('block', block.name);
64
+ }
65
+ window.history.replaceState({}, '', url.toString());
66
+ }, []);
67
+
68
+ // Handle preview data change - just update state, effect handles saving
69
+ const handlePreviewDataChange = useCallback(
70
+ (newData: Record<string, unknown>) => {
71
+ setPreviewData(newData);
72
+ setIsDirty(true);
73
+ },
74
+ []
75
+ );
76
+
77
+ // Debounced save effect - saves when data changes and is dirty
78
+ useEffect(() => {
79
+ // Don't save if loading, not dirty, no block selected, or config not loaded yet
80
+ // configDataRef must have data to prevent losing fields
81
+ if (configLoading || !isDirty || !selectedBlock || Object.keys(configDataRef.current).length === 0) {
82
+ return;
83
+ }
84
+
85
+ const controller = new AbortController();
86
+ const timeoutId = setTimeout(async () => {
87
+ try {
88
+ // Merge user edits with complete config data to never lose fields
89
+ const dataToSave = { ...configDataRef.current, ...previewData };
90
+
91
+ await fetch(`/api/preview/${selectedBlock.name}`, {
92
+ method: 'POST',
93
+ headers: { 'Content-Type': 'application/json' },
94
+ body: JSON.stringify(dataToSave),
95
+ signal: controller.signal,
96
+ });
97
+ setIsDirty(false);
98
+ } catch (error) {
99
+ if (error instanceof Error && error.name !== 'AbortError') {
100
+ console.error('Failed to save preview data:', error);
101
+ }
102
+ }
103
+ }, 500);
104
+
105
+ // Cleanup: cancel timeout and abort fetch on unmount or dependency change
106
+ return () => {
107
+ clearTimeout(timeoutId);
108
+ controller.abort();
109
+ };
110
+ }, [previewData, selectedBlock, configLoading, isDirty]);
111
+
112
+ // Handle template page navigation
113
+ const handleNavigateToPage = useCallback((pageSlug: string) => {
114
+ setCurrentPage(pageSlug);
115
+ }, []);
116
+
117
+ // Load block/template from URL on mount (runs once when blocks are loaded)
118
+ const urlLoadedRef = useRef(false);
119
+ useEffect(() => {
120
+ if (blocks.length === 0 || urlLoadedRef.current) return;
121
+
122
+ const params = new URLSearchParams(window.location.search);
123
+ const blockName = params.get('block');
124
+ const templateName = params.get('template');
125
+
126
+ if (templateName) {
127
+ const template = blocks.find((b) => b.name === templateName && b.type === 'template');
128
+ if (template) {
129
+ urlLoadedRef.current = true;
130
+ handleSelectBlock(template);
131
+ }
132
+ } else if (blockName) {
133
+ const block = blocks.find((b) => b.name === blockName);
134
+ if (block) {
135
+ urlLoadedRef.current = true;
136
+ handleSelectBlock(block);
137
+ }
138
+ }
139
+ }, [blocks, handleSelectBlock]);
140
+
141
+ return (
142
+ <div className="container">
143
+ <BlocksList
144
+ blocks={blocks}
145
+ selectedBlock={selectedBlock}
146
+ onSelectBlock={handleSelectBlock}
147
+ loading={blocksLoading}
148
+ />
149
+ <Preview
150
+ block={selectedBlock}
151
+ previewData={previewData}
152
+ currentPage={currentPage}
153
+ loading={configLoading}
154
+ />
155
+ <Editor
156
+ block={selectedBlock}
157
+ loading={configLoading}
158
+ previewData={previewData}
159
+ onPreviewDataChange={handlePreviewDataChange}
160
+ onNavigateToPage={handleNavigateToPage}
161
+ />
162
+ </div>
163
+ );
164
+ }