astro-tractstack 2.1.2 → 2.2.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 (131) hide show
  1. package/README.md +54 -266
  2. package/bin/create-tractstack.js +9 -6
  3. package/dist/index.js +109 -71
  4. package/package.json +4 -2
  5. package/templates/css/custom.css +5 -0
  6. package/templates/custom/minimal/CodeHook.astro +1 -0
  7. package/templates/custom/with-examples/CodeHook.astro +1 -0
  8. package/templates/icons/code.svg +18 -0
  9. package/templates/icons/li.svg +4 -0
  10. package/templates/icons/link.svg +22 -0
  11. package/templates/icons/p.svg +3 -0
  12. package/templates/src/client/app.js +80 -1
  13. package/templates/src/components/Footer.astro +1 -1
  14. package/templates/src/components/codehooks/BunnyVideoSetup.tsx +6 -6
  15. package/templates/src/components/codehooks/EpinetDurationSelector.tsx +3 -3
  16. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +1 -1
  17. package/templates/src/components/codehooks/ListContentSetup.tsx +2 -2
  18. package/templates/src/components/codehooks/ProductCardSetup.tsx +1 -1
  19. package/templates/src/components/codehooks/ProductGridSetup.tsx +2 -2
  20. package/templates/src/components/codehooks/SandboxRegisterForm.tsx +3 -3
  21. package/templates/src/components/compositor/Compositor.tsx +25 -9
  22. package/templates/src/components/compositor/Node.tsx +168 -496
  23. package/templates/src/components/compositor/PanelVisibilityWrapper.tsx +1 -0
  24. package/templates/src/components/compositor/elements/SignUp.tsx +1 -1
  25. package/templates/src/components/compositor/elements/YouTubeWrapper.tsx +2 -0
  26. package/templates/src/components/compositor/nodes/CreativePane.tsx +262 -0
  27. package/templates/src/components/compositor/nodes/GhostInsertBlock.tsx +4 -6
  28. package/templates/src/components/compositor/nodes/GridLayout.tsx +4 -2
  29. package/templates/src/components/compositor/nodes/Markdown.tsx +18 -3
  30. package/templates/src/components/compositor/nodes/Pane.tsx +11 -5
  31. package/templates/src/components/compositor/nodes/RenderChildren.tsx +1 -1
  32. package/templates/src/components/compositor/nodes/tagElements/NodeAnchorComponent.tsx +5 -5
  33. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag.tsx +90 -42
  34. package/templates/src/components/compositor/nodes/tagElements/NodeImg.tsx +2 -0
  35. package/templates/src/components/compositor/nodes/tagElements/NodeText.tsx +27 -1
  36. package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +10 -8
  37. package/templates/src/components/compositor/tools/NodeOverlay.tsx +224 -0
  38. package/templates/src/components/compositor/tools/PaneOverlay.tsx +122 -0
  39. package/templates/src/components/edit/Header.tsx +68 -9
  40. package/templates/src/components/edit/PanelSwitch.tsx +42 -4
  41. package/templates/src/components/edit/SettingsPanel.tsx +2 -3
  42. package/templates/src/components/edit/ToolMode.tsx +1 -31
  43. package/templates/src/components/edit/pane/AddPanePanel_break.tsx +2 -2
  44. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +1 -1
  45. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +193 -659
  46. package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +15 -82
  47. package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +95 -45
  48. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +137 -49
  49. package/templates/src/components/edit/pane/RestylePaneModal.tsx +1 -1
  50. package/templates/src/components/edit/pane/steps/AiCreativeDesignStep.tsx +375 -0
  51. package/templates/src/components/edit/pane/steps/AiDesignStep.tsx +1 -23
  52. package/templates/src/components/edit/pane/steps/AiLibraryCopyStep.tsx +327 -0
  53. package/templates/src/components/edit/pane/steps/AiRefineDesignStep.tsx +267 -0
  54. package/templates/src/components/edit/pane/steps/AiStandardDesignStep.tsx +371 -0
  55. package/templates/src/components/edit/pane/steps/CopyInputStep.tsx +201 -76
  56. package/templates/src/components/edit/pane/steps/CreativeInjectStep.tsx +141 -0
  57. package/templates/src/components/edit/panels/CreativeImagePanel.tsx +435 -0
  58. package/templates/src/components/edit/panels/CreativeLinkPanel.tsx +110 -0
  59. package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +1 -1
  60. package/templates/src/components/edit/panels/StyleParentPanel.tsx +118 -126
  61. package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +3 -2
  62. package/templates/src/components/edit/panels/StyleParentPanel_deleteLayer.tsx +1 -0
  63. package/templates/src/components/edit/panels/StyleParentPanel_remove.tsx +3 -1
  64. package/templates/src/components/edit/panels/StyleParentPanel_update.tsx +3 -1
  65. package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +1 -1
  66. package/templates/src/components/edit/state/SaveModal.tsx +19 -787
  67. package/templates/src/components/edit/state/SaveToLibraryModal.tsx +2 -2
  68. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_menu.tsx +1 -1
  69. package/templates/src/components/edit/widgets/BunnyWidget.tsx +5 -5
  70. package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +1 -1
  71. package/templates/src/components/edit/widgets/SignupWidget.tsx +1 -1
  72. package/templates/src/components/fields/ActionBuilderTimeSelector.tsx +1 -1
  73. package/templates/src/components/fields/ArtpackImage.tsx +11 -3
  74. package/templates/src/components/fields/BackgroundImage.tsx +8 -0
  75. package/templates/src/components/fields/BackgroundImageWrapper.tsx +15 -9
  76. package/templates/src/components/fields/ImageUpload.tsx +6 -0
  77. package/templates/src/components/form/ActionBuilderField.tsx +15 -5
  78. package/templates/src/components/form/ActionBuilderSlugSelector.tsx +1 -1
  79. package/templates/src/components/form/ColorPicker.tsx +1 -1
  80. package/templates/src/components/form/EnumSelect.tsx +1 -1
  81. package/templates/src/components/form/NumberInput.tsx +1 -1
  82. package/templates/src/components/form/StringArrayInput.tsx +1 -1
  83. package/templates/src/components/form/StringInput.tsx +1 -1
  84. package/templates/src/components/form/UnsavedChangesBar.tsx +1 -1
  85. package/templates/src/components/form/advanced/APIConfigSection.tsx +2 -2
  86. package/templates/src/components/form/advanced/AuthConfigSection.tsx +2 -2
  87. package/templates/src/components/profile/ProfileCreate.tsx +1 -1
  88. package/templates/src/components/profile/ProfileEdit.tsx +1 -1
  89. package/templates/src/components/storykeep/Dashboard_Advanced.tsx +2 -2
  90. package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +1 -1
  91. package/templates/src/components/storykeep/controls/content/ContentSummary.tsx +2 -2
  92. package/templates/src/components/storykeep/controls/content/KnownResourceTable.tsx +1 -1
  93. package/templates/src/components/storykeep/controls/content/ManageContent.tsx +6 -6
  94. package/templates/src/components/storykeep/controls/content/MenuForm.tsx +1 -1
  95. package/templates/src/components/storykeep/controls/content/PaneTable.tsx +358 -0
  96. package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +1 -1
  97. package/templates/src/constants/prompts.json +18 -10
  98. package/templates/src/constants.ts +3 -0
  99. package/templates/src/hooks/usePaneFragments.ts +60 -0
  100. package/templates/src/lib/session.ts +71 -16
  101. package/templates/src/pages/[...slug].astro +5 -46
  102. package/templates/src/pages/api/css.ts +149 -0
  103. package/templates/src/pages/context/[...contextSlug].astro +1 -0
  104. package/templates/src/pages/maint.astro +1 -1
  105. package/templates/src/pages/storykeep/login.astro +2 -2
  106. package/templates/src/stores/nodes.ts +162 -49
  107. package/templates/src/stores/orphanAnalysis.ts +6 -30
  108. package/templates/src/stores/previews.ts +7 -0
  109. package/templates/src/stores/storykeep.ts +0 -8
  110. package/templates/src/types/compositorTypes.ts +53 -10
  111. package/templates/src/utils/compositor/aiGeneration.ts +93 -0
  112. package/templates/src/utils/compositor/allowInsert.ts +2 -0
  113. package/templates/src/utils/compositor/htmlAst.ts +704 -0
  114. package/templates/src/utils/compositor/nodesHelper.ts +281 -102
  115. package/templates/src/utils/compositor/savePipeline.ts +893 -0
  116. package/templates/src/utils/etl/index.ts +3 -0
  117. package/templates/src/utils/etl/transformer.ts +10 -0
  118. package/templates/src/utils/helpers.ts +101 -0
  119. package/utils/inject-files.ts +100 -62
  120. package/templates/icons/text.svg +0 -6
  121. package/templates/src/components/compositor/NodeWithGuid.tsx +0 -69
  122. package/templates/src/components/compositor/nodes/GridLayout_eraser.tsx +0 -33
  123. package/templates/src/components/compositor/nodes/Markdown_eraser.tsx +0 -56
  124. package/templates/src/components/compositor/nodes/Pane_DesignLibrary.tsx +0 -269
  125. package/templates/src/components/compositor/nodes/Pane_eraser.tsx +0 -186
  126. package/templates/src/components/compositor/nodes/Pane_layout.tsx +0 -79
  127. package/templates/src/components/compositor/nodes/tagElements/NodeA_eraser.tsx +0 -26
  128. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_eraser.tsx +0 -61
  129. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_insert.tsx +0 -120
  130. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_settings.tsx +0 -62
  131. package/templates/src/components/compositor/nodes/tagElements/NodeButton_eraser.tsx +0 -26
@@ -3,25 +3,10 @@ import { Dialog } from '@ark-ui/react/dialog';
3
3
  import { Portal } from '@ark-ui/react/portal';
4
4
  import { navigate } from 'astro:transitions/client';
5
5
  import { getCtx } from '@/stores/nodes';
6
- import {
7
- transformLivePaneForSave,
8
- transformStoryFragmentForSave,
9
- } from '@/utils/etl/index';
10
- import {
11
- getPendingImageOperation,
12
- clearPendingImageOperation,
13
- pendingHomePageSlugStore,
14
- } from '@/stores/storykeep';
6
+ import { pendingHomePageSlugStore } from '@/stores/storykeep';
15
7
  import { startLoadingAnimation } from '@/utils/helpers';
16
- import { processClassesForViewports } from '@/utils/compositor/reduceNodesClassNames';
17
- import type {
18
- FlatNode,
19
- BaseNode,
20
- PaneNode,
21
- StoryFragmentNode,
22
- MarkdownPaneFragmentNode,
23
- GridLayoutNode,
24
- } from '@/types/compositorTypes';
8
+ import { executeSavePipeline } from '@/utils/compositor/savePipeline';
9
+ import type { PaneNode, StoryFragmentNode } from '@/types/compositorTypes';
25
10
 
26
11
  type SaveStage =
27
12
  | 'PREPARING'
@@ -52,13 +37,6 @@ interface SaveModalProps {
52
37
  hydrate?: boolean;
53
38
  }
54
39
 
55
- const PROGRESS_PHASES = {
56
- PREPARATION: 5,
57
- UPLOADS: 60,
58
- PROCESSING: 25,
59
- FINALIZATION: 10,
60
- };
61
-
62
40
  const INDETERMINATE_STAGES: SaveStage[] = [
63
41
  'COOKING_NODES',
64
42
  'SAVING_PANES',
@@ -168,773 +146,27 @@ export default function SaveModal({
168
146
  const runSaveProcess = async () => {
169
147
  isSaving.current = true;
170
148
  const ctx = getCtx();
171
- const allDirtyNodes = ctx.getDirtyNodes();
172
149
 
173
150
  try {
174
- setStage('PREPARING');
175
- setProgress(PROGRESS_PHASES.PREPARATION);
176
- addDebugMessage(
177
- `Starting save process... (${
178
- isContext ? 'Context' : 'StoryFragment'
179
- } mode, ${isCreateMode ? 'CREATE' : 'UPDATE'})`
180
- );
181
-
182
- let dirtyPanes = allDirtyNodes.filter(
183
- (node) => node.nodeType === 'Pane'
184
- );
185
- let dirtyStoryFragments = allDirtyNodes.filter(
186
- (node) => node.nodeType === 'StoryFragment'
187
- );
188
-
189
- if (isContext) {
190
- dirtyStoryFragments = [];
191
- addDebugMessage('Context mode: Ignoring StoryFragment nodes');
192
- }
193
-
194
- const nodesWithPendingFiles = allDirtyNodes.filter(
195
- (node): node is BaseNode & { base64Data?: string } =>
196
- 'base64Data' in node && !!node.base64Data
197
- );
198
- const storyFragmentsWithPendingImages = dirtyStoryFragments.filter(
199
- (fragment) => {
200
- const pendingOp = getPendingImageOperation(fragment.id);
201
- return pendingOp && pendingOp.type === 'upload';
202
- }
203
- );
204
-
205
- const totalFileBytes = nodesWithPendingFiles.reduce(
206
- (sum, node) => sum + (node.base64Data?.length || 0),
207
- 0
208
- );
209
- const totalOgBytes = storyFragmentsWithPendingImages.reduce(
210
- (sum, fragment) => {
211
- const pendingOp = getPendingImageOperation(fragment.id);
212
- return sum + (pendingOp?.data?.length || 0);
151
+ await executeSavePipeline(
152
+ ctx,
153
+ {
154
+ slug,
155
+ isContext,
156
+ isCreateMode,
157
+ hydrate,
158
+ tenantId,
159
+ backendUrl: goBackend,
160
+ pendingHomePageSlug,
213
161
  },
214
- 0
215
- );
216
- const totalUploadBytes = totalFileBytes + totalOgBytes;
217
- let completedUploadBytes = 0;
218
-
219
- const relevantNodeCount =
220
- dirtyPanes.length + dirtyStoryFragments.length;
221
- addDebugMessage(
222
- `Found ${relevantNodeCount} relevant dirty nodes to save (${dirtyPanes.length} Panes, ${dirtyStoryFragments.length} StoryFragments)`
223
- );
224
- addDebugMessage(
225
- `Found ${storyFragmentsWithPendingImages.length} story fragments with pending OG image operations`
226
- );
227
- addDebugMessage(
228
- `Found ${nodesWithPendingFiles.length} nodes with pending file uploads`
229
- );
230
-
231
- if (
232
- relevantNodeCount === 0 &&
233
- nodesWithPendingFiles.length === 0 &&
234
- storyFragmentsWithPendingImages.length === 0 &&
235
- !pendingHomePageSlug
236
- ) {
237
- addDebugMessage('No changes to save');
238
- setStage('COMPLETED');
239
- setProgress(100);
240
- return;
241
- }
242
-
243
- const uploadedOGPaths: Record<string, string> = {};
244
-
245
- if (nodesWithPendingFiles.length > 0) {
246
- setStage('SAVING_PENDING_FILES');
247
- for (let i = 0; i < nodesWithPendingFiles.length; i++) {
248
- const fileNode = nodesWithPendingFiles[i];
249
- const fileBytes = fileNode.base64Data?.length || 0;
250
- const endpoint = `${goBackend}/api/v1/nodes/files/create`;
251
-
252
- setStageProgress({
253
- currentStep: i + 1,
254
- totalSteps: nodesWithPendingFiles.length,
255
- currentFileName: `${fileNode.id}.jpg`,
256
- isUploading: true,
257
- });
258
- addDebugMessage(
259
- `Processing file ${i + 1}/${nodesWithPendingFiles.length}: ${
260
- fileNode.id
261
- } -> POST ${endpoint}`
262
- );
263
-
264
- try {
265
- const response = await fetch(endpoint, {
266
- method: 'POST',
267
- headers: {
268
- 'Content-Type': 'application/json',
269
- 'X-Tenant-ID': tenantId,
270
- },
271
- credentials: 'include',
272
- body: JSON.stringify({ base64Data: fileNode.base64Data }),
273
- });
274
-
275
- if (!response.ok) {
276
- throw new Error(`HTTP error! status: ${response.status}`);
277
- }
278
-
279
- const result = await response.json();
280
- const updatedNode = { ...fileNode, isChanged: true };
281
-
282
- if ('base64Data' in updatedNode) {
283
- delete updatedNode.base64Data;
284
- }
285
-
286
- if ('fileId' in updatedNode) {
287
- updatedNode.fileId = result.fileId;
288
- }
289
- if ('src' in updatedNode) {
290
- updatedNode.src = result.src;
291
- }
292
- if ('srcSet' in updatedNode && result.srcSet) {
293
- updatedNode.srcSet = result.srcSet;
294
- }
295
-
296
- ctx.modifyNodes([updatedNode]);
297
-
298
- addDebugMessage(
299
- `File ${fileNode.id} uploaded successfully - got fileId: ${result.fileId}`
300
- );
301
- } catch (error) {
302
- const errorMsg =
303
- error instanceof Error ? error.message : 'Unknown error';
304
- addDebugMessage(`File ${fileNode.id} upload failed: ${errorMsg}`);
305
- throw new Error(
306
- `Failed to upload file ${fileNode.id}: ${errorMsg}`
307
- );
308
- }
309
-
310
- completedUploadBytes += fileBytes;
311
- const uploadProgress =
312
- totalUploadBytes > 0
313
- ? (completedUploadBytes / totalUploadBytes) *
314
- PROGRESS_PHASES.UPLOADS
315
- : 0;
316
- setProgress(PROGRESS_PHASES.PREPARATION + uploadProgress);
317
- setStageProgress((prev) => ({ ...prev, isUploading: false }));
162
+ {
163
+ setStage,
164
+ setProgress,
165
+ setStageProgress,
166
+ logDebug: addDebugMessage,
167
+ setIsIndeterminateStage,
318
168
  }
319
- }
320
-
321
- if (storyFragmentsWithPendingImages.length > 0) {
322
- setStage('PROCESSING_OG_IMAGES');
323
- for (let i = 0; i < storyFragmentsWithPendingImages.length; i++) {
324
- const fragment = storyFragmentsWithPendingImages[i];
325
- const pendingOp = getPendingImageOperation(fragment.id);
326
- const imageBytes = pendingOp?.data?.length || 0;
327
-
328
- if (pendingOp && pendingOp.type === 'upload' && pendingOp.data) {
329
- const ogUploadEndpoint = `${goBackend}/api/v1/nodes/images/og`;
330
- addDebugMessage(
331
- `Processing OG image ${i + 1}/${
332
- storyFragmentsWithPendingImages.length
333
- }: ${fragment.id} -> POST ${ogUploadEndpoint}`
334
- );
335
-
336
- setStageProgress({
337
- currentStep: i + 1,
338
- totalSteps: storyFragmentsWithPendingImages.length,
339
- currentFileName: pendingOp?.filename || `${fragment.id}-og.png`,
340
- isUploading: true,
341
- });
342
-
343
- const uploadPayload = {
344
- data: pendingOp.data,
345
- filename:
346
- pendingOp.filename || `${fragment.id}-${Date.now()}.png`,
347
- };
348
-
349
- try {
350
- const response = await fetch(ogUploadEndpoint, {
351
- method: 'POST',
352
- headers: {
353
- 'Content-Type': 'application/json',
354
- 'X-Tenant-ID': tenantId,
355
- },
356
- credentials: 'include',
357
- body: JSON.stringify(uploadPayload),
358
- });
359
-
360
- if (!response.ok) {
361
- throw new Error(`HTTP error! status: ${response.status}`);
362
- }
363
-
364
- const result = await response.json();
365
-
366
- uploadedOGPaths[fragment.id] = result.path;
367
- addDebugMessage(
368
- `OG image uploaded successfully: ${result.path}`
369
- );
370
- } catch (error) {
371
- const errorMsg =
372
- error instanceof Error ? error.message : 'Unknown error';
373
- addDebugMessage(
374
- `OG image upload failed for ${fragment.id}: ${errorMsg}`
375
- );
376
- throw new Error(
377
- `Failed to upload OG image for ${fragment.id}: ${errorMsg}`
378
- );
379
- }
380
- completedUploadBytes += imageBytes;
381
- const uploadProgress =
382
- totalUploadBytes > 0
383
- ? (completedUploadBytes / totalUploadBytes) *
384
- PROGRESS_PHASES.UPLOADS
385
- : 0;
386
- setProgress(PROGRESS_PHASES.PREPARATION + uploadProgress);
387
- setStageProgress((prev) => ({ ...prev, isUploading: false }));
388
- }
389
- }
390
- }
391
-
392
- if (totalUploadBytes > 0) {
393
- setProgress(PROGRESS_PHASES.PREPARATION + PROGRESS_PHASES.UPLOADS);
394
- }
395
-
396
- const totalProcessingSteps =
397
- dirtyPanes.length + dirtyStoryFragments.length;
398
- let completedProcessingSteps = 0;
399
-
400
- // --- NEW COOKING STAGE ---
401
- if (allDirtyNodes.length > 0) {
402
- setStage('COOKING_NODES');
403
- setIsIndeterminateStage(true);
404
- addDebugMessage('Cooking nodes for whitelist extraction...');
405
-
406
- const cookingUpdates: BaseNode[] = [];
407
-
408
- allDirtyNodes.forEach((liveNode) => {
409
- try {
410
- let updatedNode: BaseNode | null = null;
411
-
412
- // Pattern 1: TagElements -> elementCss
413
- if (liveNode.nodeType === 'TagElement') {
414
- const flatNode = liveNode as FlatNode;
415
- const computedCSS = ctx.getNodeClasses(flatNode.id, 'auto', 0);
416
- if (flatNode.elementCss !== computedCSS) {
417
- updatedNode = {
418
- ...liveNode,
419
- elementCss: computedCSS,
420
- } as FlatNode;
421
- }
422
- }
423
- // Pattern 2: Markdown Nodes -> parentCss & gridCss
424
- else if (liveNode.nodeType === 'Markdown') {
425
- const markdownNode = liveNode as MarkdownPaneFragmentNode;
426
- let needsUpdate = false;
427
- const nextNode = { ...markdownNode };
428
-
429
- // parentCss
430
- if (markdownNode.parentClasses) {
431
- const computedParentCss = markdownNode.parentClasses.map(
432
- (_: any, index: number) =>
433
- ctx.getNodeClasses(liveNode.id, 'auto', index)
434
- );
435
- if (
436
- JSON.stringify(markdownNode.parentCss) !==
437
- JSON.stringify(computedParentCss)
438
- ) {
439
- nextNode.parentCss = computedParentCss;
440
- needsUpdate = true;
441
- }
442
- }
443
-
444
- // gridCss
445
- if (markdownNode.gridClasses) {
446
- const [allClasses] = processClassesForViewports(
447
- markdownNode.gridClasses,
448
- {},
449
- 1
450
- );
451
- if (allClasses && allClasses.length > 0) {
452
- const computedGridCss = allClasses[0];
453
- if (markdownNode.gridCss !== computedGridCss) {
454
- nextNode.gridCss = computedGridCss;
455
- needsUpdate = true;
456
- }
457
- }
458
- }
459
-
460
- if (needsUpdate) updatedNode = nextNode;
461
- }
462
- // Pattern 3: GridLayout Nodes -> parentCss & gridCss
463
- else if (liveNode.nodeType === 'GridLayoutNode') {
464
- const gridNode = liveNode as GridLayoutNode;
465
- let needsUpdate = false;
466
- const nextNode = { ...gridNode };
467
-
468
- // parentCss
469
- if (gridNode.parentClasses) {
470
- const computedParentCss = gridNode.parentClasses.map(
471
- (_: any, index: number) =>
472
- ctx.getNodeClasses(liveNode.id, 'auto', index)
473
- );
474
- if (
475
- JSON.stringify(gridNode.parentCss) !==
476
- JSON.stringify(computedParentCss)
477
- ) {
478
- nextNode.parentCss = computedParentCss.join(` `);
479
- needsUpdate = true;
480
- }
481
- }
482
-
483
- // gridCss
484
- if (gridNode.gridColumns) {
485
- const { mobile, tablet, desktop } = gridNode.gridColumns;
486
- let computedGridCss = `grid grid-cols-${mobile}`;
487
- if (tablet !== mobile) {
488
- computedGridCss += ` md:grid-cols-${tablet}`;
489
- }
490
- if (desktop !== tablet) {
491
- computedGridCss += ` xl:grid-cols-${desktop}`;
492
- }
493
-
494
- if (gridNode.gridCss !== computedGridCss) {
495
- nextNode.gridCss = computedGridCss;
496
- needsUpdate = true;
497
- }
498
- }
499
-
500
- if (needsUpdate) updatedNode = nextNode;
501
- }
502
-
503
- if (updatedNode) {
504
- cookingUpdates.push(updatedNode);
505
- }
506
- } catch (e) {
507
- console.warn(`Failed to cook node ${liveNode.id}`, e);
508
- }
509
- });
510
-
511
- if (cookingUpdates.length > 0) {
512
- ctx.modifyNodes(cookingUpdates, {
513
- notify: false,
514
- recordHistory: false,
515
- });
516
- addDebugMessage(`Cooked ${cookingUpdates.length} nodes.`);
517
- }
518
- setIsIndeterminateStage(false);
519
- }
520
-
521
- // --- PROCESSING STYLES ---
522
- // Moved before SAVING_PANES to ensure the whitelist is generated from the cooked, exhaustive inventory
523
- setStage('PROCESSING_STYLES');
524
- setIsIndeterminateStage(true);
525
- const baseFinalizationProgress =
526
- PROGRESS_PHASES.PREPARATION +
527
- PROGRESS_PHASES.UPLOADS +
528
- PROGRESS_PHASES.PROCESSING;
529
- setProgress(
530
- baseFinalizationProgress + PROGRESS_PHASES.FINALIZATION / 2
531
169
  );
532
- addDebugMessage(`Processing styles...`);
533
-
534
- try {
535
- const { dirtyPaneIds, classes: dirtyClasses } =
536
- ctx.getDirtyNodesClassData();
537
-
538
- const astroEndpoint = `/api/tailwind`;
539
- const astroPayload = { dirtyPaneIds, dirtyClasses };
540
- const astroResponse = await fetch(astroEndpoint, {
541
- method: 'POST',
542
- headers: {
543
- 'Content-Type': 'application/json',
544
- 'X-Tenant-ID': tenantId,
545
- },
546
- credentials: 'include',
547
- body: JSON.stringify(astroPayload),
548
- });
549
-
550
- if (!astroResponse.ok) {
551
- throw new Error(
552
- `CSS generation failed! status: ${astroResponse.status}`
553
- );
554
- }
555
-
556
- const astroResult = await astroResponse.json();
557
-
558
- if (!astroResult.success || !astroResult.generatedCss) {
559
- throw new Error('CSS generation failed: no CSS returned');
560
- }
561
-
562
- addDebugMessage(
563
- `CSS generated: ${astroResult.generatedCss.length} bytes for ${dirtyClasses.length} classes`
564
- );
565
-
566
- const goEndpoint = `${goBackend}/api/v1/tailwind/update`;
567
- const goPayload = { frontendCss: astroResult.generatedCss };
568
- const goResponse = await fetch(goEndpoint, {
569
- method: 'POST',
570
- headers: {
571
- 'Content-Type': 'application/json',
572
- 'X-Tenant-ID': tenantId,
573
- },
574
- credentials: 'include',
575
- body: JSON.stringify(goPayload),
576
- });
577
-
578
- if (!goResponse.ok) {
579
- throw new Error(`CSS save failed! status: ${goResponse.status}`);
580
- }
581
-
582
- const goResult = await goResponse.json();
583
- addDebugMessage(
584
- `CSS saved successfully: stylesVer ${goResult.stylesVer}`
585
- );
586
- } catch (error) {
587
- const errorMsg =
588
- error instanceof Error ? error.message : 'Unknown error';
589
- addDebugMessage(`Styles processing failed: ${errorMsg}`);
590
- throw new Error(`Failed to process styles: ${errorMsg}`);
591
- } finally {
592
- setIsIndeterminateStage(false);
593
- }
594
-
595
- // --- SAVING PANES ---
596
- // Runs after styles to ensure DB gets minimal, correct payload
597
- if (dirtyPanes.length > 0) {
598
- setStage('SAVING_PANES');
599
- setIsIndeterminateStage(true);
600
- setStageProgress({
601
- currentStep: 0,
602
- totalSteps: dirtyPanes.length,
603
- });
604
-
605
- const bulkPayload = dirtyPanes.map((paneNode) =>
606
- transformLivePaneForSave(ctx, paneNode.id, isContext)
607
- );
608
-
609
- // Update context with minimal strings (idempotent, restoring runtime state)
610
- bulkPayload.forEach((payload) => {
611
- payload.optionsPayload.nodes.forEach((transformedNode) => {
612
- const liveNode = ctx.allNodes.get().get(transformedNode.id);
613
- if (!liveNode) return;
614
-
615
- let needsUpdate = false;
616
- let updatedNode: BaseNode = { ...liveNode };
617
-
618
- if (
619
- transformedNode.nodeType === 'TagElement' &&
620
- transformedNode.elementCss
621
- ) {
622
- const flatNode = liveNode as FlatNode;
623
- if (flatNode.elementCss !== transformedNode.elementCss) {
624
- (updatedNode as FlatNode).elementCss =
625
- transformedNode.elementCss;
626
- needsUpdate = true;
627
- }
628
- }
629
-
630
- if (
631
- transformedNode.nodeType === 'Markdown' &&
632
- transformedNode.parentCss
633
- ) {
634
- const markdownNode = liveNode as MarkdownPaneFragmentNode;
635
- const currentParentCss = markdownNode.parentCss;
636
- const newParentCss = transformedNode.parentCss as string[];
637
-
638
- const isDifferent =
639
- !currentParentCss ||
640
- currentParentCss.length !== newParentCss.length ||
641
- currentParentCss.some(
642
- (css, index) => css !== newParentCss[index]
643
- );
644
-
645
- if (isDifferent) {
646
- (updatedNode as MarkdownPaneFragmentNode).parentCss =
647
- newParentCss;
648
- needsUpdate = true;
649
- }
650
- }
651
-
652
- if (needsUpdate) {
653
- ctx.allNodes.get().set(transformedNode.id, updatedNode);
654
- }
655
- });
656
- });
657
-
658
- const endpoint = `${goBackend}/api/v1/nodes/panes/bulk`;
659
- addDebugMessage(
660
- `Processing ${dirtyPanes.length} panes via -> POST ${endpoint}`
661
- );
662
-
663
- try {
664
- const response = await fetch(endpoint, {
665
- method: 'POST',
666
- headers: {
667
- 'Content-Type': 'application/json',
668
- 'X-Tenant-ID': tenantId,
669
- },
670
- credentials: 'include',
671
- body: JSON.stringify({ panes: bulkPayload }),
672
- });
673
-
674
- if (!response.ok) {
675
- const errorText = await response.text();
676
- throw new Error(
677
- `HTTP error! status: ${response.status} - ${errorText}`
678
- );
679
- }
680
-
681
- await response.json();
682
- addDebugMessage(
683
- `${dirtyPanes.length} panes saved successfully via bulk endpoint.`
684
- );
685
- } catch (bulkError) {
686
- const errorMsg =
687
- bulkError instanceof Error
688
- ? bulkError.message
689
- : 'Unknown bulk save error';
690
- addDebugMessage(`Bulk pane save failed: ${errorMsg}`);
691
- throw new Error(`Failed to save panes in bulk: ${errorMsg}`);
692
- } finally {
693
- setIsIndeterminateStage(false);
694
- }
695
-
696
- setStageProgress({
697
- currentStep: dirtyPanes.length,
698
- totalSteps: dirtyPanes.length,
699
- });
700
- completedProcessingSteps += dirtyPanes.length;
701
- const processingProgress =
702
- (completedProcessingSteps / totalProcessingSteps) *
703
- PROGRESS_PHASES.PROCESSING;
704
- setProgress(
705
- PROGRESS_PHASES.PREPARATION +
706
- PROGRESS_PHASES.UPLOADS +
707
- processingProgress
708
- );
709
- }
710
-
711
- if (!isContext && dirtyStoryFragments.length > 0) {
712
- setStage('SAVING_STORY_FRAGMENTS');
713
- setStageProgress({
714
- currentStep: 0,
715
- totalSteps: dirtyStoryFragments.length,
716
- });
717
- for (let i = 0; i < dirtyStoryFragments.length; i++) {
718
- const fragment = dirtyStoryFragments[i];
719
-
720
- try {
721
- const payload = await transformStoryFragmentForSave(
722
- ctx,
723
- fragment.id,
724
- tenantId
725
- );
726
-
727
- if (uploadedOGPaths[fragment.id]) {
728
- payload.socialImagePath = uploadedOGPaths[fragment.id];
729
- }
730
-
731
- const endpoint = isCreateMode
732
- ? `${goBackend}/api/v1/nodes/storyfragments/create`
733
- : `${goBackend}/api/v1/nodes/storyfragments/${payload.id}/complete`;
734
- const method = isCreateMode ? 'POST' : 'PUT';
735
-
736
- addDebugMessage(
737
- `Processing story fragment ${i + 1}/${
738
- dirtyStoryFragments.length
739
- }: ${fragment.id} -> ${method} ${endpoint}`
740
- );
741
-
742
- const response = await fetch(endpoint, {
743
- method,
744
- headers: {
745
- 'Content-Type': 'application/json',
746
- 'X-Tenant-ID': tenantId,
747
- },
748
- credentials: 'include',
749
- body: JSON.stringify(payload),
750
- });
751
-
752
- if (!response.ok) {
753
- throw new Error(`HTTP error! status: ${response.status}`);
754
- }
755
-
756
- await response.json();
757
- addDebugMessage(
758
- `StoryFragment ${fragment.id} saved successfully`
759
- );
760
-
761
- if (uploadedOGPaths[fragment.id]) {
762
- clearPendingImageOperation(fragment.id);
763
- addDebugMessage(
764
- `Cleared pending image operation for ${fragment.id}`
765
- );
766
- }
767
- } catch (etlError) {
768
- const errorMsg =
769
- etlError instanceof Error ? etlError.message : 'Unknown error';
770
- addDebugMessage(
771
- `StoryFragment ${fragment.id} ETL failed: ${errorMsg}`
772
- );
773
- throw new Error(
774
- `Failed to save story fragment ${fragment.id}: ${errorMsg}`
775
- );
776
- }
777
-
778
- setStageProgress((prev) => ({ ...prev, currentStep: i + 1 }));
779
- completedProcessingSteps++;
780
- const processingProgress =
781
- (completedProcessingSteps / totalProcessingSteps) *
782
- PROGRESS_PHASES.PROCESSING;
783
- setProgress(
784
- PROGRESS_PHASES.PREPARATION +
785
- PROGRESS_PHASES.UPLOADS +
786
- processingProgress
787
- );
788
- }
789
- }
790
-
791
- if (dirtyPanes.length > 0) {
792
- setStage('LINKING_FILES');
793
- setIsIndeterminateStage(true);
794
- // ... Linking files logic continues ...
795
- // Note: Linking files remains after saving panes because it relies on panes existing in DB
796
- setProgress(baseFinalizationProgress);
797
- addDebugMessage('Starting file-pane relationship linking...');
798
-
799
- const relationships = [];
800
- for (const paneNode of dirtyPanes) {
801
- const fileIds = ctx.getPaneImageFileIds(paneNode.id);
802
- relationships.push({
803
- paneId: paneNode.id,
804
- fileIds: fileIds,
805
- });
806
- }
807
-
808
- if (relationships.some((rel) => rel.fileIds.length > 0)) {
809
- try {
810
- const bulkEndpoint = `${goBackend}/api/v1/nodes/panes/files/bulk`;
811
- addDebugMessage(
812
- `Linking relationships: ${JSON.stringify(relationships)}`
813
- );
814
-
815
- const response = await fetch(bulkEndpoint, {
816
- method: 'POST',
817
- headers: {
818
- 'Content-Type': 'application/json',
819
- 'X-Tenant-ID': tenantId,
820
- },
821
- credentials: 'include',
822
- body: JSON.stringify({ relationships }),
823
- });
824
-
825
- if (!response.ok) {
826
- throw new Error(`HTTP error! status: ${response.status}`);
827
- }
828
-
829
- const result = await response.json();
830
- addDebugMessage(
831
- `File-pane relationships linked successfully: ${result.message}`
832
- );
833
- } catch (error) {
834
- const errorMsg =
835
- error instanceof Error ? error.message : 'Unknown error';
836
- addDebugMessage(
837
- `Failed to link file-pane relationships: ${errorMsg}`
838
- );
839
- throw new Error(
840
- `Failed to link file-pane relationships: ${errorMsg}`
841
- );
842
- }
843
- } else {
844
- addDebugMessage('No file relationships to link');
845
- }
846
- setIsIndeterminateStage(false);
847
- }
848
-
849
- if (pendingHomePageSlug) {
850
- setStage('UPDATING_HOME_PAGE');
851
- setIsIndeterminateStage(true);
852
- setProgress(
853
- baseFinalizationProgress + (PROGRESS_PHASES.FINALIZATION - 2)
854
- );
855
- addDebugMessage(`Updating home page to: ${pendingHomePageSlug}`);
856
-
857
- try {
858
- const response = await fetch(`${goBackend}/api/v1/config/brand`, {
859
- method: 'GET',
860
- headers: {
861
- 'Content-Type': 'application/json',
862
- 'X-Tenant-ID': tenantId,
863
- },
864
- credentials: 'include',
865
- });
866
-
867
- if (!response.ok) {
868
- throw new Error(
869
- `Failed to get current brand config: ${response.status}`
870
- );
871
- }
872
-
873
- const currentBrandConfig = await response.json();
874
-
875
- const updatedBrandConfig = {
876
- ...currentBrandConfig,
877
- HOME_SLUG: pendingHomePageSlug,
878
- };
879
-
880
- const updateResponse = await fetch(
881
- `${goBackend}/api/v1/config/brand`,
882
- {
883
- method: 'PUT',
884
- headers: {
885
- 'Content-Type': 'application/json',
886
- 'X-Tenant-ID': tenantId,
887
- },
888
- credentials: 'include',
889
- body: JSON.stringify(updatedBrandConfig),
890
- }
891
- );
892
-
893
- if (!updateResponse.ok) {
894
- throw new Error(
895
- `Failed to update home page: ${updateResponse.status}`
896
- );
897
- }
898
-
899
- pendingHomePageSlugStore.set(null);
900
- addDebugMessage('Home page updated successfully');
901
- } catch (error) {
902
- const errorMsg =
903
- error instanceof Error ? error.message : 'Unknown error';
904
- addDebugMessage(`Home page update failed: ${errorMsg}`);
905
- throw new Error(`Failed to update home page: ${errorMsg}`);
906
- } finally {
907
- setIsIndeterminateStage(false);
908
- }
909
- }
910
-
911
- if (hydrate) {
912
- addDebugMessage('Finalizing setup (Kill Switch)...');
913
- try {
914
- const response = await fetch(`${goBackend}/api/v1/setup/complete`, {
915
- method: 'POST',
916
- headers: {
917
- 'Content-Type': 'application/json',
918
- 'X-Tenant-ID': tenantId,
919
- },
920
- credentials: 'include',
921
- body: JSON.stringify({}),
922
- });
923
-
924
- if (!response.ok) {
925
- throw new Error(`Kill Switch failed: ${response.status}`);
926
- }
927
- addDebugMessage('Hydration token cleared.');
928
- } catch (e) {
929
- console.error('Kill switch error:', e);
930
- // We don't throw here to ensure the user still gets to the dashboard
931
- addDebugMessage('Warning: Failed to clear hydration token.');
932
- }
933
- }
934
-
935
- setStage('COMPLETED');
936
- setProgress(100);
937
- addDebugMessage('Save process completed successfully!');
938
170
  } catch (err) {
939
171
  setStage('ERROR');
940
172
  const errorMessage =