astro-tractstack 2.0.18 → 2.0.20

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 (58) hide show
  1. package/dist/index.js +6 -32
  2. package/package.json +1 -1
  3. package/templates/src/components/codehooks/BunnyVideoSetup.tsx +1 -4
  4. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +0 -4
  5. package/templates/src/components/codehooks/ListContentSetup.tsx +1 -8
  6. package/templates/src/components/codehooks/ProductCardSetup.tsx +0 -2
  7. package/templates/src/components/codehooks/ProductGridSetup.tsx +0 -2
  8. package/templates/src/components/compositor/Compositor.tsx +3 -6
  9. package/templates/src/components/compositor/Node.tsx +13 -32
  10. package/templates/src/components/compositor/NodeWithGuid.tsx +49 -5
  11. package/templates/src/components/compositor/nodes/Pane.tsx +4 -21
  12. package/templates/src/components/compositor/nodes/Pane_DesignLibrary.tsx +27 -7
  13. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag.tsx +3 -1
  14. package/templates/src/components/compositor/preview/OgImagePreview.tsx +0 -5
  15. package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +5 -6
  16. package/templates/src/components/compositor/preview/PanesPreviewGenerator.tsx +1 -0
  17. package/templates/src/components/edit/PanelSwitch.tsx +3 -24
  18. package/templates/src/components/edit/SettingsPanel.tsx +0 -1
  19. package/templates/src/components/edit/ToolMode.tsx +6 -14
  20. package/templates/src/components/edit/pane/AddPanePanel.tsx +45 -25
  21. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +277 -70
  22. package/templates/src/components/edit/pane/AddPanePanel_paste.tsx +111 -0
  23. package/templates/src/components/edit/pane/RestylePaneModal.tsx +7 -14
  24. package/templates/src/components/edit/pane/steps/AiDesignStep.tsx +0 -5
  25. package/templates/src/components/edit/pane/steps/DesignLibraryStep.tsx +4 -11
  26. package/templates/src/components/edit/panels/StyleBreakPanel.tsx +1 -3
  27. package/templates/src/components/edit/panels/StyleElementPanel_update.tsx +0 -6
  28. package/templates/src/components/edit/panels/StyleImagePanel.tsx +0 -1
  29. package/templates/src/components/edit/panels/StyleImagePanel_update.tsx +0 -3
  30. package/templates/src/components/edit/panels/StyleLiElementPanel_update.tsx +0 -4
  31. package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +8 -5
  32. package/templates/src/components/edit/panels/StyleLinkPanel_update.tsx +1 -2
  33. package/templates/src/components/edit/panels/StyleParentPanel.tsx +1 -3
  34. package/templates/src/components/edit/panels/StyleParentPanel_update.tsx +2 -5
  35. package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +2 -8
  36. package/templates/src/components/edit/panels/StyleWidgetPanel_update.tsx +0 -4
  37. package/templates/src/components/edit/state/SaveToLibraryModal.tsx +27 -16
  38. package/templates/src/components/edit/storyfragment/StoryFragmentConfigPanel.tsx +9 -26
  39. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +7 -16
  40. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +5 -6
  41. package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +0 -5
  42. package/templates/src/components/fields/BackgroundImageWrapper.tsx +1 -7
  43. package/templates/src/components/fields/ColorPickerCombo.tsx +8 -12
  44. package/templates/src/components/fields/ViewportComboBox.tsx +4 -6
  45. package/templates/src/constants/prompts.json +22 -1
  46. package/templates/src/stores/nodes.ts +297 -222
  47. package/templates/src/stores/storykeep.ts +3 -3
  48. package/templates/src/types/compositorTypes.ts +21 -1
  49. package/templates/src/types/tractstack.ts +1 -0
  50. package/templates/src/utils/compositor/TemplatePanes.ts +0 -76
  51. package/templates/src/utils/compositor/aiPaneParser.ts +265 -83
  52. package/templates/src/utils/compositor/designLibraryHelper.ts +252 -26
  53. package/templates/src/utils/helpers.ts +5 -4
  54. package/utils/inject-files.ts +6 -32
  55. package/templates/src/components/compositor/preview/VisualBreakPreview.tsx +0 -154
  56. package/templates/src/components/edit/pane/PageGen_preview.tsx +0 -511
  57. package/templates/src/utils/compositor/processMarkdown.ts +0 -445
  58. package/templates/src/utils/compositor/templateMarkdownStyles.ts +0 -1273
@@ -1,19 +1,21 @@
1
1
  import { ulid } from 'ulid';
2
2
  import { getCtx, type NodesContext } from '@/stores/nodes';
3
3
  import { tailwindClasses } from '@/utils/compositor/tailwindClasses';
4
- import {
5
- type PaneNode,
6
- type FlatNode,
7
- type MarkdownPaneFragmentNode,
8
- type StoragePane,
9
- type StorageNode,
10
- type StorageMarkdown,
11
- type StorageBgPane,
12
- type ArtpackImageNode,
13
- type BgImageNode,
14
- type VisualBreakNode,
15
- type TemplatePane,
16
- type TemplateNode,
4
+ import type {
5
+ PaneNode,
6
+ FlatNode,
7
+ MarkdownPaneFragmentNode,
8
+ GridLayoutNode,
9
+ StoragePane,
10
+ StorageNode,
11
+ StorageMarkdown,
12
+ StorageBgPane,
13
+ StorageGridLayoutNode,
14
+ ArtpackImageNode,
15
+ BgImageNode,
16
+ VisualBreakNode,
17
+ TemplatePane,
18
+ TemplateNode,
17
19
  } from '@/types/compositorTypes';
18
20
  import type {
19
21
  BrandConfig,
@@ -108,14 +110,14 @@ export async function savePaneToLibrary(
108
110
  category: string;
109
111
  copyMode: CopyMode;
110
112
  }
111
- ): Promise<boolean> {
113
+ ): Promise<BrandConfigState | null> {
112
114
  const ctx = getCtx();
113
115
  const { title, category, copyMode } = formData;
114
116
  const paneNode = ctx.allNodes.get().get(paneId) as PaneNode;
115
117
 
116
118
  if (!paneNode) {
117
119
  console.error('savePaneToLibrary: PaneNode not found.');
118
- return false;
120
+ return null;
119
121
  }
120
122
 
121
123
  const childNodes = ctx
@@ -172,13 +174,14 @@ export async function savePaneToLibrary(
172
174
  heightRatioDesktop: paneNode.heightRatioDesktop,
173
175
  heightRatioMobile: paneNode.heightRatioMobile,
174
176
  heightRatioTablet: paneNode.heightRatioTablet,
175
- markdown: newStorageMarkdown,
176
- bgPane: newStorageBgPane,
177
+ ...(newStorageMarkdown ? { markdowns: [newStorageMarkdown] } : {}),
178
+ ...(newStorageBgPane ? { bgPane: newStorageBgPane } : {}),
177
179
  };
178
180
 
179
181
  const newLibraryEntry: DesignLibraryEntry = {
180
182
  category: category,
181
183
  title: title,
184
+ markdownCount: 1,
182
185
  template: newStoragePane,
183
186
  };
184
187
 
@@ -207,10 +210,10 @@ export async function savePaneToLibrary(
207
210
 
208
211
  try {
209
212
  await saveBrandConfig(tenantId, backendDTO);
210
- return true;
213
+ return updatedState;
211
214
  } catch (error) {
212
215
  console.error('Failed to save design library:', error);
213
- return false;
216
+ return null;
214
217
  }
215
218
  }
216
219
 
@@ -244,10 +247,11 @@ export function mergeCopyIntoTemplate(
244
247
  copy: ExtractedCopy
245
248
  ): StoragePane {
246
249
  const newTemplate = { ...template };
247
- if (newTemplate.markdown) {
248
- newTemplate.markdown.nodes = copy;
250
+ if (newTemplate.markdowns) {
251
+ newTemplate.markdowns[0].nodes = copy;
249
252
  } else if (copy.length > 0) {
250
- newTemplate.markdown = {
253
+ if (!newTemplate.markdowns) newTemplate.markdowns = [];
254
+ newTemplate.markdowns[0] = {
251
255
  nodeType: 'Markdown',
252
256
  type: 'markdown',
253
257
  defaultClasses: {},
@@ -289,8 +293,8 @@ export function convertStorageToLiveTemplate(
289
293
  const markdownId = ulid();
290
294
  const flatNodeList: TemplateNode[] = [];
291
295
 
292
- if (storagePane.markdown && storagePane.markdown.nodes) {
293
- for (const storageNode of storagePane.markdown.nodes) {
296
+ if (storagePane.markdowns && storagePane.markdowns[0].nodes) {
297
+ for (const storageNode of storagePane.markdowns[0].nodes) {
294
298
  const processedNodes = processStorageNode(storageNode, markdownId);
295
299
  flatNodeList.push(...processedNodes);
296
300
  }
@@ -307,12 +311,13 @@ export function convertStorageToLiveTemplate(
307
311
  };
308
312
  }
309
313
 
314
+ const { gridLayout, ...restOfStoragePane } = storagePane;
310
315
  const liveTemplatePane: TemplatePane = {
311
- ...storagePane,
316
+ ...restOfStoragePane,
312
317
  id: paneId,
313
318
  parentId: '',
314
319
  markdown: {
315
- ...(storagePane.markdown || {
320
+ ...((storagePane.markdowns && storagePane.markdowns[0]) || {
316
321
  nodeType: 'Markdown',
317
322
  type: 'markdown',
318
323
  defaultClasses: {},
@@ -414,3 +419,224 @@ export function convertTemplateToAIShell(template: TemplatePane): string {
414
419
 
415
420
  return JSON.stringify(shell, null, 2);
416
421
  }
422
+
423
+ export async function copyPaneToClipboard(paneId: string): Promise<boolean> {
424
+ const ctx = getCtx();
425
+ const paneNode = ctx.allNodes.get().get(paneId) as PaneNode;
426
+
427
+ const storagePane = convertLivePaneToStoragePane(paneId, ctx, {
428
+ title: paneNode?.title || 'Pasted Pane',
429
+ copyMode: 'retain',
430
+ });
431
+
432
+ if (!storagePane) {
433
+ return false;
434
+ }
435
+
436
+ try {
437
+ const jsonPayload = JSON.stringify(storagePane, null, 2);
438
+ await navigator.clipboard.writeText(jsonPayload);
439
+ return true;
440
+ } catch (error) {
441
+ console.error('Failed to copy pane to clipboard:', error);
442
+ return false;
443
+ }
444
+ }
445
+
446
+ function buildIdMap(node: any, map: Map<string, string>) {
447
+ if (!node || typeof node !== 'object') return;
448
+
449
+ if (node.id && !map.has(node.id)) {
450
+ map.set(node.id, ulid());
451
+ }
452
+ // Markdown nodes have a second unique identifier
453
+ if (node.markdownId && !map.has(node.markdownId)) {
454
+ map.set(node.markdownId, ulid());
455
+ }
456
+
457
+ // Recursively traverse all possible child arrays/objects
458
+ if (node.markdowns) {
459
+ node.markdowns.forEach((n: any) => buildIdMap(n, map));
460
+ }
461
+ if (node.gridLayout) {
462
+ buildIdMap(node.gridLayout, map);
463
+ }
464
+ if (node.nodes) {
465
+ node.nodes.forEach((n: any) => buildIdMap(n, map));
466
+ }
467
+ if (node.bgPane) {
468
+ buildIdMap(node.bgPane, map);
469
+ }
470
+ }
471
+
472
+ function applyIdMap(node: any, map: Map<string, string>) {
473
+ if (!node || typeof node !== 'object') return;
474
+
475
+ if (node.id && map.has(node.id)) {
476
+ node.id = map.get(node.id);
477
+ }
478
+ if (node.parentId && map.has(node.parentId)) {
479
+ node.parentId = map.get(node.parentId);
480
+ }
481
+ if (node.markdownId && map.has(node.markdownId)) {
482
+ node.markdownId = map.get(node.markdownId);
483
+ }
484
+
485
+ // Recursively traverse all possible child arrays/objects
486
+ if (node.markdowns) {
487
+ node.markdowns.forEach((n: any) => applyIdMap(n, map));
488
+ }
489
+ if (node.gridLayout) {
490
+ applyIdMap(node.gridLayout, map);
491
+ }
492
+ if (node.nodes) {
493
+ node.nodes.forEach((n: any) => applyIdMap(n, map));
494
+ }
495
+ if (node.bgPane) {
496
+ applyIdMap(node.bgPane, map);
497
+ }
498
+ }
499
+
500
+ export function remapPaneIds(pane: StoragePane): StoragePane {
501
+ const idMap = new Map<string, string>();
502
+ // The input object may have come from JSON.parse, so we treat it as 'any' internally
503
+ const clone = JSON.parse(JSON.stringify(pane as any));
504
+
505
+ // First pass: Traverse the entire structure to build a complete map of old IDs to new IDs.
506
+ buildIdMap(clone, idMap);
507
+
508
+ // Second pass: Traverse again to apply the new IDs, ensuring parent-child relationships are correct.
509
+ applyIdMap(clone, idMap);
510
+
511
+ return clone as StoragePane;
512
+ }
513
+
514
+ function convertLivePaneToStoragePane(
515
+ paneId: string,
516
+ ctx: NodesContext,
517
+ options: {
518
+ title: string;
519
+ copyMode: CopyMode;
520
+ }
521
+ ): StoragePane | null {
522
+ const paneNode = ctx.allNodes.get().get(paneId) as PaneNode;
523
+ if (!paneNode) {
524
+ console.error('convertLivePaneToStoragePane: PaneNode not found.');
525
+ return null;
526
+ }
527
+
528
+ const { title, copyMode } = options;
529
+ const childNodes = ctx
530
+ .getChildNodeIDs(paneId)
531
+ .map((id) => ctx.allNodes.get().get(id));
532
+
533
+ const markdownNode = childNodes.find((n) => n?.nodeType === 'Markdown') as
534
+ | MarkdownPaneFragmentNode
535
+ | undefined;
536
+
537
+ const gridLayoutNode = childNodes.find(
538
+ (n) => n?.nodeType === 'GridLayoutNode'
539
+ ) as GridLayoutNode | undefined;
540
+
541
+ const bgPaneNode = childNodes.find((n) => n?.nodeType === 'BgPane') as
542
+ | ArtpackImageNode
543
+ | BgImageNode
544
+ | VisualBreakNode
545
+ | undefined;
546
+
547
+ let storageMarkdown: StorageMarkdown | undefined;
548
+ let storageGridLayout: StorageGridLayoutNode | undefined;
549
+
550
+ if (markdownNode) {
551
+ storageMarkdown = {
552
+ nodeType: 'Markdown',
553
+ type: 'markdown',
554
+ defaultClasses: markdownNode.defaultClasses || {},
555
+ parentClasses: markdownNode.parentClasses || [],
556
+ nodes:
557
+ copyMode !== 'blank'
558
+ ? ctx
559
+ .getChildNodeIDs(markdownNode.id)
560
+ .map((childId) => {
561
+ const childNode = ctx.allNodes.get().get(childId) as FlatNode;
562
+ return convertLiveNodeToStorageNode(childNode, ctx, copyMode);
563
+ })
564
+ .filter((n): n is StorageNode => n !== null)
565
+ : [],
566
+ };
567
+ } else if (gridLayoutNode) {
568
+ const { id, parentId, isChanged, ...restOfGrid } = gridLayoutNode;
569
+ storageGridLayout = {
570
+ ...restOfGrid,
571
+ nodes: ctx
572
+ .getChildNodeIDs(gridLayoutNode.id)
573
+ .map((columnId) => {
574
+ const columnNode = ctx.allNodes
575
+ .get()
576
+ .get(columnId) as MarkdownPaneFragmentNode;
577
+ if (!columnNode) return null;
578
+
579
+ const {
580
+ id,
581
+ parentId,
582
+ isChanged,
583
+ markdownId,
584
+ parentCss,
585
+ ...restOfColumn
586
+ } = columnNode;
587
+
588
+ const storageColumn: StorageMarkdown = {
589
+ ...restOfColumn,
590
+ nodeType: 'Markdown',
591
+ type: 'markdown',
592
+ nodes:
593
+ copyMode !== 'blank'
594
+ ? ctx
595
+ .getChildNodeIDs(columnNode.id)
596
+ .map((childId) => {
597
+ const childNode = ctx.allNodes
598
+ .get()
599
+ .get(childId) as FlatNode;
600
+ return convertLiveNodeToStorageNode(
601
+ childNode,
602
+ ctx,
603
+ copyMode
604
+ );
605
+ })
606
+ .filter((n): n is StorageNode => n !== null)
607
+ : [],
608
+ };
609
+ return storageColumn;
610
+ })
611
+ .filter((n): n is StorageMarkdown => n !== null),
612
+ };
613
+ }
614
+
615
+ const storageBgPane: StorageBgPane | undefined = bgPaneNode
616
+ ? { ...bgPaneNode }
617
+ : undefined;
618
+
619
+ if (storageBgPane) {
620
+ delete (storageBgPane as any).id;
621
+ delete (storageBgPane as any).parentId;
622
+ }
623
+
624
+ const storagePane: StoragePane = {
625
+ nodeType: 'Pane',
626
+ title: title,
627
+ slug: '',
628
+ bgColour: paneNode.bgColour,
629
+ isDecorative: paneNode.isDecorative,
630
+ heightOffsetDesktop: paneNode.heightOffsetDesktop,
631
+ heightOffsetMobile: paneNode.heightOffsetMobile,
632
+ heightOffsetTablet: paneNode.heightOffsetTablet,
633
+ heightRatioDesktop: paneNode.heightRatioDesktop,
634
+ heightRatioMobile: paneNode.heightRatioMobile,
635
+ heightRatioTablet: paneNode.heightRatioTablet,
636
+ ...(storageMarkdown ? { markdowns: [storageMarkdown] } : {}),
637
+ ...(storageGridLayout ? { gridLayout: storageGridLayout } : {}),
638
+ ...(storageBgPane ? { bgPane: storageBgPane } : {}),
639
+ };
640
+
641
+ return storagePane;
642
+ }
@@ -291,14 +291,15 @@ export function titleToSlug(title: string, maxLength: number = 50): string {
291
291
  }
292
292
 
293
293
  export function findUniqueSlug(slug: string, existingSlugs: string[]): string {
294
- if (!existingSlugs.includes(slug)) {
295
- return slug;
294
+ const tempSlug = slug || `story`;
295
+ if (!existingSlugs.includes(tempSlug)) {
296
+ return tempSlug;
296
297
  }
297
298
  let counter = 1;
298
- let newSlug = `${slug}-${counter}`;
299
+ let newSlug = `${tempSlug}-${counter}`;
299
300
  while (existingSlugs.includes(newSlug)) {
300
301
  counter++;
301
- newSlug = `${slug}-${counter}`;
302
+ newSlug = `${tempSlug}-${counter}`;
302
303
  }
303
304
  return newSlug;
304
305
  }
@@ -453,6 +453,12 @@ export async function injectTemplateFiles(
453
453
  src: resolve('../templates/src/components/edit/pane/AddPanePanel.tsx'),
454
454
  dest: 'src/components/edit/pane/AddPanePanel.tsx',
455
455
  },
456
+ {
457
+ src: resolve(
458
+ '../templates/src/components/edit/pane/AddPanePanel_paste.tsx'
459
+ ),
460
+ dest: 'src/components/edit/pane/AddPanePanel_paste.tsx',
461
+ },
456
462
  {
457
463
  src: resolve(
458
464
  '../templates/src/components/edit/pane/AddPanePanel_break.tsx'
@@ -549,22 +555,6 @@ export async function injectTemplateFiles(
549
555
  ),
550
556
  dest: 'src/components/edit/context/ContextPaneConfig_slug.tsx',
551
557
  },
552
- {
553
- src: resolve('../templates/src/components/edit/pane/PageGenSelector.tsx'),
554
- dest: 'src/components/edit/pane/PageGenSelector.tsx',
555
- },
556
- {
557
- src: resolve('../templates/src/components/edit/pane/PageGenSpecial.tsx'),
558
- dest: 'src/components/edit/pane/PageGenSpecial.tsx',
559
- },
560
- {
561
- src: resolve('../templates/src/components/edit/pane/PageGen.tsx'),
562
- dest: 'src/components/edit/pane/PageGen.tsx',
563
- },
564
- {
565
- src: resolve('../templates/src/components/edit/pane/PageGen_preview.tsx'),
566
- dest: 'src/components/edit/pane/PageGen_preview.tsx',
567
- },
568
558
  // Compositor previews
569
559
  {
570
560
  src: resolve(
@@ -584,12 +574,6 @@ export async function injectTemplateFiles(
584
574
  ),
585
575
  dest: 'src/components/compositor/preview/OgImagePreview.tsx',
586
576
  },
587
- {
588
- src: resolve(
589
- '../templates/src/components/compositor/preview/VisualBreakPreview.tsx'
590
- ),
591
- dest: 'src/components/compositor/preview/VisualBreakPreview.tsx',
592
- },
593
577
  {
594
578
  src: resolve(
595
579
  '../templates/src/components/compositor/preview/ListContentPreview.tsx'
@@ -653,16 +637,6 @@ export async function injectTemplateFiles(
653
637
  src: resolve('../templates/src/utils/compositor/aiPaneParser.ts'),
654
638
  dest: 'src/utils/compositor/aiPaneParser.ts',
655
639
  },
656
- {
657
- src: resolve('../templates/src/utils/compositor/processMarkdown.ts'),
658
- dest: 'src/utils/compositor/processMarkdown.ts',
659
- },
660
- {
661
- src: resolve(
662
- '../templates/src/utils/compositor/templateMarkdownStyles.ts'
663
- ),
664
- dest: 'src/utils/compositor/templateMarkdownStyles.ts',
665
- },
666
640
  {
667
641
  src: resolve(
668
642
  '../templates/src/utils/compositor/nodesMarkdownGenerator.ts'
@@ -1,154 +0,0 @@
1
- import { useEffect, useState } from 'react';
2
- import { NodesContext } from '@/stores/nodes';
3
- import { createEmptyStorykeep } from '@/utils/compositor/nodesHelper';
4
- import { getTemplateVisualBreakPane } from '@/utils/compositor/TemplatePanes';
5
- import {
6
- PanesPreviewGenerator,
7
- type PanePreviewRequest,
8
- type PaneFragmentResult,
9
- } from '@/components/compositor/preview/PanesPreviewGenerator';
10
- import {
11
- PaneSnapshotGenerator,
12
- type SnapshotData,
13
- } from '@/components/compositor/preview/PaneSnapshotGenerator';
14
-
15
- interface VisualBreakPreviewProps {
16
- bgColour: string;
17
- fillColour: string;
18
- variant?: string; // Optional variant name for the visual break
19
- height?: number; // Optional height for the container
20
- }
21
-
22
- // The state is managed as a single object since this component only ever handles one preview at a time.
23
- // This is slightly simpler than managing an array with a single item.
24
- type PreviewState = {
25
- htmlFragment?: string;
26
- snapshot?: SnapshotData;
27
- error?: string;
28
- };
29
-
30
- /**
31
- * Renders a preview of a single visual break variant.
32
- *
33
- * This component uses a modern two-step process for efficiency:
34
- * 1. It uses PanesPreviewGenerator to fetch an HTML fragment of the break.
35
- * 2. It then uses PaneSnapshotGenerator to convert that HTML into an image snapshot.
36
- */
37
- export const VisualBreakPreview = ({
38
- bgColour,
39
- fillColour,
40
- variant = 'cutwide2', // Default to cutwide2 as it's a commonly used break
41
- height = 120, // Default height that works well for most breaks
42
- }: VisualBreakPreviewProps) => {
43
- const [previewState, setPreviewState] = useState<PreviewState | null>(null);
44
- const [fragmentRequest, setFragmentRequest] = useState<PanePreviewRequest[]>(
45
- []
46
- );
47
-
48
- useEffect(() => {
49
- // Reset state whenever the props change to trigger a full regeneration
50
- setPreviewState(null);
51
-
52
- // STEP 1: Create a temporary NodesContext for the preview.
53
- const ctx = new NodesContext();
54
- ctx.addNode(createEmptyStorykeep('tmp')); // Add root node
55
-
56
- // Get the template for the specified variant and apply the dynamic colours
57
- const template = getTemplateVisualBreakPane(variant);
58
- if (template) {
59
- if (template.bgColour) template.bgColour = bgColour;
60
- if (template.bgPane && template.bgPane.type === 'visual-break') {
61
- if (template.bgPane.breakDesktop) {
62
- template.bgPane.breakDesktop.svgFill = fillColour;
63
- }
64
- if (template.bgPane.breakTablet) {
65
- template.bgPane.breakTablet.svgFill = fillColour;
66
- }
67
- if (template.bgPane.breakMobile) {
68
- template.bgPane.breakMobile.svgFill = fillColour;
69
- }
70
- }
71
- ctx.addTemplatePane('tmp', template); // Add the template to the context
72
- }
73
-
74
- // Prepare a request for the PanesPreviewGenerator to get the HTML.
75
- setFragmentRequest([{ id: 'visual-break-preview', ctx }]);
76
- }, [variant, bgColour, fillColour]);
77
-
78
- // Handler for when the HTML fragment has been generated
79
- const handleFragmentComplete = (results: PaneFragmentResult[]) => {
80
- const result = results[0];
81
- if (result?.htmlString) {
82
- setPreviewState({ htmlFragment: result.htmlString });
83
- } else {
84
- setPreviewState({
85
- error: result?.error || 'Failed to generate HTML fragment.',
86
- });
87
- }
88
- setFragmentRequest([]); // Clear the request to prevent re-fetching
89
- };
90
-
91
- // Handler for when the image snapshot has been generated from the HTML.
92
- // The 'id' parameter is unused here as we only manage one snapshot at a time.
93
- const handleSnapshotComplete = (data: SnapshotData) => {
94
- setPreviewState((prev) => (prev ? { ...prev, snapshot: data } : null));
95
- };
96
-
97
- // Display a pulsing placeholder while the process is running
98
- if (!previewState) {
99
- return <div className="my-4 h-12 animate-pulse bg-gray-200" />;
100
- }
101
-
102
- // Display an error message if something went wrong
103
- if (previewState.error) {
104
- return (
105
- <div className="flex items-center justify-center rounded-md border border-red-200 bg-red-50 p-4 text-sm text-red-700">
106
- Preview could not be generated: {previewState.error}
107
- </div>
108
- );
109
- }
110
-
111
- return (
112
- <div
113
- className="relative w-full overflow-hidden"
114
- style={!previewState.snapshot ? { height: `${height}px` } : undefined}
115
- >
116
- {/* STEP 2: Render the generator to fetch the HTML fragment. This component renders nothing itself. */}
117
- {fragmentRequest.length > 0 && (
118
- <PanesPreviewGenerator
119
- requests={fragmentRequest}
120
- onComplete={handleFragmentComplete}
121
- onError={(err) => setPreviewState({ error: err })}
122
- />
123
- )}
124
-
125
- {/* STEP 3: Once HTML is available, render the snapshot generator to create the image. This component also renders nothing. */}
126
- {previewState.htmlFragment && !previewState.snapshot && (
127
- <PaneSnapshotGenerator
128
- id="visual-break-snapshot"
129
- htmlString={previewState.htmlFragment}
130
- outputWidth={800} // Matches the original output width
131
- onComplete={(_id, data) => handleSnapshotComplete(data)}
132
- onError={(_id, err) =>
133
- setPreviewState((prev) =>
134
- prev ? { ...prev, error: err } : { error: err }
135
- )
136
- }
137
- />
138
- )}
139
-
140
- {/* STEP 4: Once the snapshot is complete, display the final image. */}
141
- {previewState.snapshot && (
142
- <div className="w-full">
143
- <img
144
- src={previewState.snapshot.imageData}
145
- alt={`Visual break ${variant}`}
146
- className="w-full"
147
- />
148
- </div>
149
- )}
150
- </div>
151
- );
152
- };
153
-
154
- export default VisualBreakPreview;