astro-tractstack 2.2.2 → 2.2.3

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 (67) hide show
  1. package/package.json +1 -1
  2. package/templates/src/components/codehooks/BunnyVideoSetup.tsx +0 -1
  3. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +0 -1
  4. package/templates/src/components/codehooks/ListContentSetup.tsx +0 -1
  5. package/templates/src/components/codehooks/ProductCardSetup.tsx +0 -1
  6. package/templates/src/components/codehooks/ProductGridSetup.tsx +0 -1
  7. package/templates/src/components/compositor/Compositor.tsx +0 -1
  8. package/templates/src/components/compositor/Node.tsx +157 -134
  9. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag.tsx +2 -4
  10. package/templates/src/components/edit/Header.tsx +1 -2
  11. package/templates/src/components/edit/context/ContextPaneConfig_slug.tsx +1 -1
  12. package/templates/src/components/edit/context/ContextPaneConfig_title.tsx +0 -1
  13. package/templates/src/components/edit/pane/AddPanePanel_break.tsx +1 -0
  14. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +8 -12
  15. package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +9 -6
  16. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +2 -2
  17. package/templates/src/components/edit/pane/PanePanel_impression.tsx +0 -4
  18. package/templates/src/components/edit/pane/PanePanel_path.tsx +0 -1
  19. package/templates/src/components/edit/pane/PanePanel_title.tsx +1 -2
  20. package/templates/src/components/edit/pane/RestylePaneModal.tsx +1 -4
  21. package/templates/src/components/edit/pane/steps/AiCreativeDesignStep.tsx +0 -3
  22. package/templates/src/components/edit/pane/steps/AiRefineDesignStep.tsx +2 -2
  23. package/templates/src/components/edit/pane/steps/AiStandardDesignStep.tsx +173 -80
  24. package/templates/src/components/edit/pane/steps/CreativeInjectStep.tsx +0 -5
  25. package/templates/src/components/edit/pane/steps/DesignLibraryStep.tsx +2 -1
  26. package/templates/src/components/edit/panels/StyleBreakPanel.tsx +1 -4
  27. package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +0 -1
  28. package/templates/src/components/edit/panels/StyleElementPanel.tsx +1 -1
  29. package/templates/src/components/edit/panels/StyleElementPanel_remove.tsx +1 -4
  30. package/templates/src/components/edit/panels/StyleElementPanel_update.tsx +3 -3
  31. package/templates/src/components/edit/panels/StyleImagePanel.tsx +3 -3
  32. package/templates/src/components/edit/panels/StyleImagePanel_remove.tsx +1 -4
  33. package/templates/src/components/edit/panels/StyleImagePanel_update.tsx +3 -4
  34. package/templates/src/components/edit/panels/StyleLiElementPanel_remove.tsx +1 -4
  35. package/templates/src/components/edit/panels/StyleLiElementPanel_update.tsx +3 -3
  36. package/templates/src/components/edit/panels/StyleLinkPanel.tsx +1 -1
  37. package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +1 -1
  38. package/templates/src/components/edit/panels/StyleLinkPanel_remove.tsx +1 -1
  39. package/templates/src/components/edit/panels/StyleLinkPanel_update.tsx +1 -1
  40. package/templates/src/components/edit/panels/StyleParentPanel.tsx +0 -7
  41. package/templates/src/components/edit/panels/StyleParentPanel_deleteLayer.tsx +0 -2
  42. package/templates/src/components/edit/panels/StyleParentPanel_remove.tsx +0 -2
  43. package/templates/src/components/edit/panels/StyleParentPanel_update.tsx +0 -2
  44. package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +0 -3
  45. package/templates/src/components/edit/panels/StyleWidgetPanel_remove.tsx +1 -4
  46. package/templates/src/components/edit/panels/StyleWidgetPanel_update.tsx +3 -4
  47. package/templates/src/components/edit/panels/StyleWordCarouselPanel.tsx +0 -2
  48. package/templates/src/components/edit/state/StylesMemory.tsx +3 -9
  49. package/templates/src/components/edit/storyfragment/StoryFragmentConfigPanel.tsx +0 -1
  50. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_menu.tsx +0 -2
  51. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +0 -2
  52. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +0 -1
  53. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_title.tsx +0 -1
  54. package/templates/src/components/fields/ArtpackImage.tsx +0 -7
  55. package/templates/src/components/fields/BackgroundImage.tsx +0 -14
  56. package/templates/src/components/fields/BackgroundImageWrapper.tsx +0 -5
  57. package/templates/src/components/fields/ImageUpload.tsx +0 -3
  58. package/templates/src/pages/[...slug]/edit.astro +0 -1
  59. package/templates/src/pages/sandbox.astro +0 -1
  60. package/templates/src/stores/nodes.ts +278 -312
  61. package/templates/src/stores/nodesHistory.ts +59 -24
  62. package/templates/src/utils/api/setupHelpers.ts +1 -1
  63. package/templates/src/utils/compositor/aiPaneParser.ts +57 -0
  64. package/templates/src/utils/compositor/designLibraryHelper.ts +1 -3
  65. package/templates/src/utils/compositor/htmlAst.ts +109 -2
  66. package/templates/src/utils/compositor/nodesHelper.ts +1 -9
  67. package/templates/src/utils/compositor/savePipeline.ts +1 -4
@@ -7,7 +7,11 @@ import { extractClassesFromNodes } from '@/utils/compositor/nodesHelper';
7
7
  import { handleClickEventDefault } from '@/utils/compositor/handleClickEvent';
8
8
  import allowInsert from '@/utils/compositor/allowInsert';
9
9
  import { reservedSlugs } from '@/constants';
10
- import { NodesHistory, PatchOp } from '@/stores/nodesHistory';
10
+ import {
11
+ NodesHistory,
12
+ PatchOp,
13
+ VERBOSE as VERBOSE_HISTORY,
14
+ } from '@/stores/nodesHistory';
11
15
  import { moveNodeAtLocationInContext } from '@/utils/compositor/nodesHelper';
12
16
  import {
13
17
  rehydrateChildrenFromHtml,
@@ -90,7 +94,6 @@ export class NodesContext {
90
94
  allNodes = atom<Map<string, BaseNode>>(new Map<string, BaseNode>());
91
95
  impressionNodes = atom<Set<ImpressionNode>>(new Set<ImpressionNode>());
92
96
  parentNodes = atom<Map<string, string[]>>(new Map<string, string[]>());
93
- showSaveBypass = atom<boolean>(false);
94
97
  hasTitle = atom<boolean>(false);
95
98
  hasPanes = atom<boolean>(false);
96
99
  isTemplate = atom<boolean>(false);
@@ -257,24 +260,6 @@ export class NodesContext {
257
260
  });
258
261
  }
259
262
 
260
- //setActiveGhost(nodeId: string): void {
261
- // const currentActiveId = this.ghostTextActiveId.get();
262
- // // If this is already the active ghost, do nothing
263
- // if (currentActiveId === nodeId) return;
264
- // // If another ghost is active, clear it first
265
- // if (currentActiveId && currentActiveId !== nodeId) {
266
- // // Set to empty string to close any existing ghost
267
- // this.ghostTextActiveId.set("");
268
- // // After a short delay to allow the previous ghost to close,
269
- // // set the new active ghost
270
- // setTimeout(() => {
271
- // this.ghostTextActiveId.set(nodeId);
272
- // }, 100);
273
- // } else {
274
- // this.ghostTextActiveId.set(nodeId);
275
- // }
276
- //}
277
-
278
263
  updateHasPanesStatus() {
279
264
  const allNodes = this.allNodes.get();
280
265
  const storyFragments = Array.from(allNodes.values()).filter(
@@ -328,7 +313,7 @@ export class NodesContext {
328
313
  const storyfragmentNode = cloneDeep(
329
314
  this.allNodes.get().get(storyfragmentNodeId)
330
315
  ) as StoryFragmentNode;
331
- this.modifyNodes([{ ...storyfragmentNode, isChanged: true }]);
316
+ this.modifyNodes([{ ...storyfragmentNode }]);
332
317
  break;
333
318
  }
334
319
  case `TagElement`: {
@@ -336,7 +321,7 @@ export class NodesContext {
336
321
  const paneNode = cloneDeep(
337
322
  this.allNodes.get().get(paneNodeId)
338
323
  ) as PaneNode;
339
- this.modifyNodes([{ ...paneNode, isChanged: true }]);
324
+ this.modifyNodes([{ ...paneNode }]);
340
325
  break;
341
326
  }
342
327
  default:
@@ -517,7 +502,7 @@ export class NodesContext {
517
502
  this.parentNodes.set(newParentNodes);
518
503
 
519
504
  if (originalPaneNode) {
520
- this.modifyNodes([{ ...originalPaneNode, isChanged: true }], {
505
+ this.modifyNodes([{ ...originalPaneNode }], {
521
506
  notify: false,
522
507
  recordHistory: false,
523
508
  });
@@ -542,11 +527,15 @@ export class NodesContext {
542
527
 
543
528
  const newAnchorId = redoLogic();
544
529
 
545
- this.history.addPatch({
546
- op: PatchOp.REPLACE,
547
- undo: undoLogic,
548
- redo: redoLogic,
549
- });
530
+ if (!this.isTemplate.get()) {
531
+ if (VERBOSE_HISTORY)
532
+ console.log('[Nodes] Action: wrapRangeInAnchor', { range });
533
+ this.history.addPatch({
534
+ op: PatchOp.REPLACE,
535
+ undo: undoLogic,
536
+ redo: redoLogic,
537
+ });
538
+ }
550
539
 
551
540
  return new Promise((resolve) =>
552
541
  setTimeout(() => resolve(newAnchorId), 310)
@@ -554,31 +543,29 @@ export class NodesContext {
554
543
  }
555
544
 
556
545
  applyShellToPane(paneId: string, template: TemplatePane) {
557
- const allNodes = new Map(this.allNodes.get());
558
- const originalPane = allNodes.get(paneId);
546
+ const allNodesMap = this.allNodes.get();
547
+ const originalPane = allNodesMap.get(paneId);
559
548
  if (!originalPane) return;
560
549
 
561
- const paneNode = cloneDeep(originalPane) as PaneNode;
562
- paneNode.isChanged = true;
550
+ const nodesToUpdate: BaseNode[] = [];
563
551
 
552
+ const paneNode = cloneDeep(originalPane) as PaneNode;
564
553
  if (template.bgColour) {
565
554
  paneNode.bgColour = template.bgColour;
566
555
  }
567
556
  if (template.htmlAst) {
568
557
  paneNode.htmlAst = template.htmlAst;
569
558
  }
570
-
571
- allNodes.set(paneId, paneNode);
559
+ nodesToUpdate.push(paneNode);
572
560
 
573
561
  const childrenIds = this.getChildNodeIDs(paneId);
574
562
 
575
563
  const gridNodeRaw = childrenIds
576
- .map((id) => allNodes.get(id))
564
+ .map((id) => allNodesMap.get(id))
577
565
  .find((n) => n?.nodeType === 'GridLayoutNode');
578
566
 
579
567
  if (gridNodeRaw && template.gridLayout) {
580
568
  const gridLayoutNode = cloneDeep(gridNodeRaw) as GridLayoutNode;
581
- gridLayoutNode.isChanged = true;
582
569
 
583
570
  if (template.gridLayout.parentClasses) {
584
571
  gridLayoutNode.parentClasses = template.gridLayout.parentClasses;
@@ -586,7 +573,7 @@ export class NodesContext {
586
573
  if (template.gridLayout.defaultClasses) {
587
574
  gridLayoutNode.defaultClasses = template.gridLayout.defaultClasses;
588
575
  }
589
- allNodes.set(gridLayoutNode.id, gridLayoutNode);
576
+ nodesToUpdate.push(gridLayoutNode);
590
577
 
591
578
  if (
592
579
  template.gridLayout.nodes &&
@@ -596,27 +583,25 @@ export class NodesContext {
596
583
 
597
584
  columnIds.forEach((colId, index) => {
598
585
  const templateCol = template.gridLayout!.nodes![index];
599
- const colNodeRaw = allNodes.get(colId);
586
+ const colNodeRaw = allNodesMap.get(colId);
600
587
  if (templateCol && colNodeRaw) {
601
588
  const liveColNode = cloneDeep(
602
589
  colNodeRaw
603
590
  ) as MarkdownPaneFragmentNode;
604
591
  liveColNode.gridClasses = templateCol.gridClasses;
605
- liveColNode.isChanged = true;
606
- allNodes.set(colId, liveColNode);
592
+ nodesToUpdate.push(liveColNode);
607
593
  }
608
594
  });
609
595
  }
610
596
  } else {
611
597
  const markdownNodes = childrenIds
612
- .map((id) => allNodes.get(id))
598
+ .map((id) => allNodesMap.get(id))
613
599
  .filter(
614
600
  (n) => n?.nodeType === 'Markdown'
615
601
  ) as MarkdownPaneFragmentNode[];
616
602
 
617
603
  if (markdownNodes.length > 0 && template.markdown) {
618
604
  const primaryMarkdown = cloneDeep(markdownNodes[0]);
619
- primaryMarkdown.isChanged = true;
620
605
 
621
606
  if (template.markdown.parentClasses) {
622
607
  primaryMarkdown.parentClasses = template.markdown.parentClasses;
@@ -624,14 +609,12 @@ export class NodesContext {
624
609
  if (template.markdown.defaultClasses) {
625
610
  primaryMarkdown.defaultClasses = template.markdown.defaultClasses;
626
611
  }
627
- allNodes.set(primaryMarkdown.id, primaryMarkdown);
612
+ nodesToUpdate.push(primaryMarkdown);
628
613
  }
629
614
  }
630
615
 
631
- this.allNodes.set(allNodes);
632
- this.notifyNode(paneId);
633
- this.notifyNode('root');
634
- this.showSaveBypass.set(true);
616
+ // Force a fresh history entry for this operation
617
+ this.modifyNodes(nodesToUpdate, { merge: false });
635
618
  }
636
619
 
637
620
  async updateCreativeAsset(
@@ -681,28 +664,19 @@ export class NodesContext {
681
664
  }
682
665
 
683
666
  paneNode.htmlAst = newHtmlAst;
684
- paneNode.isChanged = true;
685
667
 
686
668
  this.modifyNodes([paneNode]);
687
669
  }
688
670
 
689
671
  updateCreativePane(paneId: string, containerId: string, htmlContent: string) {
690
- const allNodes = new Map(this.allNodes.get());
691
- const originalPane = allNodes.get(paneId);
692
-
693
- // 1. Validation and Clone (matching applyShellToPane pattern)
672
+ const originalPane = this.allNodes.get().get(paneId);
694
673
  if (!originalPane || originalPane.nodeType !== 'Pane') return;
695
674
 
696
- // Deep clone ensures we don't mutate state outside the atom update
697
675
  const paneNode = cloneDeep(originalPane) as PaneNode;
698
-
699
- // Guard: Ensure we are in HTML mode
700
676
  if (!('htmlAst' in paneNode) || !paneNode.htmlAst) return;
701
677
 
702
- // 2. Logic: Rehydrate and Patch
703
678
  const newChildren = rehydrateChildrenFromHtml(htmlContent);
704
679
 
705
- // Recursive patcher to find the container in the cloned tree
706
680
  const patchNode = (nodes: any[]): boolean => {
707
681
  for (const node of nodes) {
708
682
  if (node.id === containerId) {
@@ -716,13 +690,8 @@ export class NodesContext {
716
690
  return false;
717
691
  };
718
692
 
719
- // 3. Commit
720
693
  if (patchNode(paneNode.htmlAst.tree)) {
721
- paneNode.isChanged = true;
722
- allNodes.set(paneId, paneNode);
723
- this.allNodes.set(allNodes);
724
- this.notifyNode(paneId);
725
- this.showSaveBypass.set(true);
694
+ this.modifyNodes([paneNode], { merge: false });
726
695
  }
727
696
  }
728
697
 
@@ -905,8 +874,6 @@ export class NodesContext {
905
874
  * @param tagName - The tag name for the wrapper element (e.g., 'span').
906
875
  * @returns A Promise that resolves with the new wrapper node's ID, or null.
907
876
  */
908
- // Replacement for wrapRangeInSpan in src/stores/nodes.ts
909
-
910
877
  public async wrapRangeInSpan(
911
878
  range: SelectionRange,
912
879
  tagName: 'span'
@@ -1072,7 +1039,7 @@ export class NodesContext {
1072
1039
  this.parentNodes.set(newParentNodes);
1073
1040
 
1074
1041
  if (originalPaneNode) {
1075
- this.modifyNodes([{ ...originalPaneNode, isChanged: true }], {
1042
+ this.modifyNodes([{ ...originalPaneNode }], {
1076
1043
  notify: false,
1077
1044
  recordHistory: false,
1078
1045
  });
@@ -1116,11 +1083,15 @@ export class NodesContext {
1116
1083
 
1117
1084
  const newSpanId = redoLogic();
1118
1085
 
1119
- this.history.addPatch({
1120
- op: PatchOp.REPLACE,
1121
- undo: undoLogic,
1122
- redo: redoLogic,
1123
- });
1086
+ if (!this.isTemplate.get()) {
1087
+ if (VERBOSE_HISTORY)
1088
+ console.log('[Nodes] Action: wrapRangeInSpan', { range, tagName });
1089
+ this.history.addPatch({
1090
+ op: PatchOp.REPLACE,
1091
+ undo: undoLogic,
1092
+ redo: redoLogic,
1093
+ });
1094
+ }
1124
1095
 
1125
1096
  return new Promise((resolve) => setTimeout(() => resolve(newSpanId), 310));
1126
1097
  }
@@ -1742,37 +1713,33 @@ export class NodesContext {
1742
1713
  options?: {
1743
1714
  notify?: boolean;
1744
1715
  recordHistory?: boolean;
1716
+ merge?: boolean;
1745
1717
  }
1746
1718
  ) {
1747
1719
  const undoList: ((ctx: NodesContext) => void)[] = [];
1748
1720
  const redoList: ((ctx: NodesContext) => void)[] = [];
1749
1721
  const shouldNotify = options?.notify ?? true;
1750
- const shouldRecordHistory = options?.recordHistory ?? true;
1722
+ const shouldRecordHistory =
1723
+ (options?.recordHistory ?? true) && !this.isTemplate.get();
1724
+
1725
+ for (const incomingNode of newData) {
1726
+ // Centralized persistence flag: Always mark modified nodes as changed
1727
+ const node = { ...incomingNode, isChanged: true };
1751
1728
 
1752
- for (let i = 0; i < newData.length; i++) {
1753
- const node = newData[i];
1754
1729
  const currentNodeData = this.allNodes.get().get(node.id) as BaseNode;
1755
- if (!currentNodeData) {
1756
- continue;
1757
- }
1730
+ if (!currentNodeData) continue;
1731
+
1732
+ if (isDeepEqual(currentNodeData, node)) continue;
1758
1733
 
1759
- if (isDeepEqual(currentNodeData, node)) {
1760
- continue;
1734
+ if (VERBOSE_HISTORY) {
1735
+ console.log(`[Nodes] Modifying ${node.nodeType} (${node.id})`, node);
1761
1736
  }
1762
1737
 
1763
1738
  const newNodes = new Map(this.allNodes.get());
1764
1739
  newNodes.set(node.id, node);
1765
1740
  this.allNodes.set(newNodes);
1766
1741
 
1767
- const deepEqualWithExclusions = isDeepEqual(currentNodeData, node, [
1768
- 'isChanged',
1769
- ]);
1770
-
1771
- if (deepEqualWithExclusions) {
1772
- if (shouldNotify) this.notifyNode(node.id);
1773
- continue;
1774
- }
1775
-
1742
+ // Check if we need to dirty parents (bubbling changes up)
1776
1743
  switch (node.nodeType) {
1777
1744
  case 'GridLayoutNode':
1778
1745
  case 'TagElement':
@@ -1784,7 +1751,7 @@ export class NodesContext {
1784
1751
  if (paneNodeId) {
1785
1752
  const paneNode = this.allNodes.get().get(paneNodeId);
1786
1753
  if (paneNode && !paneNode.isChanged) {
1787
- nodesToDirty.push({ ...paneNode, isChanged: true });
1754
+ nodesToDirty.push({ ...paneNode });
1788
1755
  }
1789
1756
  }
1790
1757
 
@@ -1796,7 +1763,7 @@ export class NodesContext {
1796
1763
  !parentNode.isChanged
1797
1764
  ) {
1798
1765
  if (!nodesToDirty.some((n) => n.id === parentNode.id)) {
1799
- nodesToDirty.push({ ...parentNode, isChanged: true });
1766
+ nodesToDirty.push({ ...parentNode });
1800
1767
  }
1801
1768
  }
1802
1769
  }
@@ -1807,18 +1774,8 @@ export class NodesContext {
1807
1774
  recordHistory: false,
1808
1775
  });
1809
1776
  }
1810
-
1811
- this.notifyNode(ROOT_NODE_NAME);
1812
1777
  break;
1813
1778
  }
1814
-
1815
- case `Menu`:
1816
- case `Pane`:
1817
- case `StoryFragment`:
1818
- break;
1819
-
1820
- default:
1821
- console.warn(`must dirty check missed on `, node.nodeType);
1822
1779
  }
1823
1780
 
1824
1781
  undoList.push((ctx: NodesContext) => {
@@ -1826,8 +1783,7 @@ export class NodesContext {
1826
1783
  newNodes.set(node.id, currentNodeData);
1827
1784
  ctx.allNodes.set(newNodes);
1828
1785
  if (shouldNotify) {
1829
- const parentNode = this.nodeToNotify(node.id, node.nodeType);
1830
- if (parentNode) this.notifyNode(parentNode);
1786
+ ctx.notifyNode(node.id);
1831
1787
  }
1832
1788
  });
1833
1789
  redoList.push((ctx: NodesContext) => {
@@ -1835,28 +1791,32 @@ export class NodesContext {
1835
1791
  newNodes.set(node.id, node);
1836
1792
  ctx.allNodes.set(newNodes);
1837
1793
  if (shouldNotify) {
1838
- const parentNode = this.nodeToNotify(node.id, node.nodeType);
1839
- if (parentNode) this.notifyNode(parentNode);
1794
+ ctx.notifyNode(node.id);
1840
1795
  }
1841
1796
  });
1842
1797
 
1843
1798
  if (shouldNotify) {
1844
- if ([`Menu`, `StoryFragment`].includes(node.nodeType))
1845
- this.notifyNode(ROOT_NODE_NAME);
1846
1799
  this.notifyNode(node.id);
1800
+ const parentNodeToNotify = this.nodeToNotify(node.id, node.nodeType);
1801
+ if (parentNodeToNotify && parentNodeToNotify !== node.id) {
1802
+ this.notifyNode(parentNodeToNotify);
1803
+ } else this.notifyNode('root');
1847
1804
  }
1848
1805
  }
1849
1806
 
1850
1807
  if (undoList.length > 0 && shouldRecordHistory) {
1851
- this.history.addPatch({
1852
- op: PatchOp.REPLACE,
1853
- undo: (ctx) => {
1854
- undoList.forEach((fn) => fn(ctx));
1855
- },
1856
- redo: (ctx) => {
1857
- redoList.forEach((fn) => fn(ctx));
1808
+ this.history.addPatch(
1809
+ {
1810
+ op: PatchOp.REPLACE,
1811
+ undo: (ctx) => {
1812
+ undoList.forEach((fn) => fn(ctx));
1813
+ },
1814
+ redo: (ctx) => {
1815
+ redoList.forEach((fn) => fn(ctx));
1816
+ },
1858
1817
  },
1859
- });
1818
+ { merge: options?.merge }
1819
+ );
1860
1820
  }
1861
1821
  }
1862
1822
 
@@ -1915,7 +1875,6 @@ export class NodesContext {
1915
1875
  duplicatedPane.title = ownerNode.title;
1916
1876
  if (ownerNode && 'slug' in ownerNode && typeof ownerNode.slug === `string`)
1917
1877
  duplicatedPane.slug = ownerNode.slug;
1918
- duplicatedPane.isChanged = true;
1919
1878
 
1920
1879
  // Track all nodes that need to be added
1921
1880
  // Call the new helper to process markdown, gridLayout, and bgPane
@@ -1960,7 +1919,6 @@ export class NodesContext {
1960
1919
  const duplicatedPaneId = pane?.id || ulid();
1961
1920
  duplicatedPane.id = duplicatedPaneId;
1962
1921
  duplicatedPane.parentId = ownerNode.id;
1963
- duplicatedPane.isChanged = true;
1964
1922
 
1965
1923
  if (this.rootNodeId.get() !== 'tmp') {
1966
1924
  if (
@@ -2002,7 +1960,6 @@ export class NodesContext {
2002
1960
  location &&
2003
1961
  storyFragmentNode?.nodeType === 'StoryFragment'
2004
1962
  ) {
2005
- storyFragmentWasChanged = storyFragmentNode.isChanged || false;
2006
1963
  specificIdx = storyFragmentNode.paneIds.indexOf(insertPaneId);
2007
1964
  elIdx = specificIdx;
2008
1965
  if (elIdx === -1) {
@@ -2019,7 +1976,6 @@ export class NodesContext {
2019
1976
  );
2020
1977
  }
2021
1978
  }
2022
- storyFragmentNode.isChanged = true;
2023
1979
  }
2024
1980
 
2025
1981
  this.addNode(duplicatedPane as PaneNode);
@@ -2034,46 +1990,55 @@ export class NodesContext {
2034
1990
  // Combine the pane and all its child nodes for the history patch
2035
1991
  const nodesToHistory = [duplicatedPane as BaseNode, ...allNodes];
2036
1992
 
2037
- this.history.addPatch({
2038
- op: PatchOp.ADD,
2039
- undo: (ctx) => {
2040
- // Delete all nodes created (pane + children)
2041
- ctx.deleteNodes(nodesToHistory);
1993
+ if (!this.isTemplate.get()) {
1994
+ if (VERBOSE_HISTORY)
1995
+ console.log('[Nodes] Action: addTemplatePane', {
1996
+ id: duplicatedPane.id,
1997
+ slug: duplicatedPane.slug,
1998
+ });
1999
+ this.history.addPatch({
2000
+ op: PatchOp.ADD,
2001
+ undo: (ctx) => {
2002
+ // Delete all nodes created (pane + children)
2003
+ ctx.deleteNodes(nodesToHistory);
2042
2004
 
2043
- if (
2044
- storyFragmentNode &&
2045
- storyFragmentNode.nodeType === 'StoryFragment' &&
2046
- Array.isArray(storyFragmentNode.paneIds)
2047
- ) {
2048
- storyFragmentNode.paneIds = storyFragmentNode.paneIds.filter(
2049
- (id: string) => id !== duplicatedPane.id
2050
- );
2051
- storyFragmentNode.isChanged = storyFragmentWasChanged;
2052
- }
2053
- },
2054
- redo: (ctx) => {
2055
- if (storyFragmentNode?.nodeType === 'StoryFragment') {
2056
- if (elIdx === -1) {
2057
- storyFragmentNode.paneIds.push(duplicatedPane.id);
2058
- } else {
2059
- if (location === 'before') {
2060
- storyFragmentNode.paneIds.splice(elIdx, 0, duplicatedPane.id);
2005
+ if (
2006
+ storyFragmentNode &&
2007
+ storyFragmentNode.nodeType === 'StoryFragment' &&
2008
+ Array.isArray(storyFragmentNode.paneIds)
2009
+ ) {
2010
+ storyFragmentNode.paneIds = storyFragmentNode.paneIds.filter(
2011
+ (id: string) => id !== duplicatedPane.id
2012
+ );
2013
+ }
2014
+ },
2015
+ redo: (ctx) => {
2016
+ if (storyFragmentNode?.nodeType === 'StoryFragment') {
2017
+ if (elIdx === -1) {
2018
+ storyFragmentNode.paneIds.push(duplicatedPane.id);
2061
2019
  } else {
2062
- storyFragmentNode.paneIds.splice(elIdx + 1, 0, duplicatedPane.id);
2020
+ if (location === 'before') {
2021
+ storyFragmentNode.paneIds.splice(elIdx, 0, duplicatedPane.id);
2022
+ } else {
2023
+ storyFragmentNode.paneIds.splice(
2024
+ elIdx + 1,
2025
+ 0,
2026
+ duplicatedPane.id
2027
+ );
2028
+ }
2063
2029
  }
2064
2030
  }
2065
- storyFragmentNode.isChanged = true;
2066
- }
2067
2031
 
2068
- // Add all nodes back (pane + children)
2069
- ctx.addNodes(nodesToHistory);
2070
- ctx.linkChildToParent(
2071
- duplicatedPane.id,
2072
- duplicatedPane.parentId,
2073
- specificIdx
2074
- );
2075
- },
2076
- });
2032
+ // Add all nodes back (pane + children)
2033
+ ctx.addNodes(nodesToHistory);
2034
+ ctx.linkChildToParent(
2035
+ duplicatedPane.id,
2036
+ duplicatedPane.parentId,
2037
+ specificIdx
2038
+ );
2039
+ },
2040
+ });
2041
+ }
2077
2042
 
2078
2043
  return duplicatedPaneId;
2079
2044
  }
@@ -2117,11 +2082,18 @@ export class NodesContext {
2117
2082
  targetId
2118
2083
  );
2119
2084
  this.addNodes(flattenedNodes);
2120
- this.history.addPatch({
2121
- op: PatchOp.ADD,
2122
- undo: (ctx) => ctx.deleteNodes(flattenedNodes),
2123
- redo: (ctx) => ctx.addNodes(flattenedNodes),
2124
- });
2085
+ if (!this.isTemplate.get()) {
2086
+ if (VERBOSE_HISTORY)
2087
+ console.log('[Nodes] Action: addTemplateImpressionNode', {
2088
+ targetId,
2089
+ nodeCount: flattenedNodes.length,
2090
+ });
2091
+ this.history.addPatch({
2092
+ op: PatchOp.ADD,
2093
+ undo: (ctx) => ctx.deleteNodes(flattenedNodes),
2094
+ redo: (ctx) => ctx.addNodes(flattenedNodes),
2095
+ });
2096
+ }
2125
2097
  }
2126
2098
 
2127
2099
  addTemplateNode(
@@ -2234,7 +2206,7 @@ export class NodesContext {
2234
2206
 
2235
2207
  // 5. PERFORM REMAINING STATE MUTATIONS
2236
2208
  if (originalPaneNode) {
2237
- this.modifyNodes([{ ...originalPaneNode, isChanged: true }], {
2209
+ this.modifyNodes([{ ...originalPaneNode }], {
2238
2210
  notify: false,
2239
2211
  recordHistory: false,
2240
2212
  });
@@ -2266,54 +2238,61 @@ export class NodesContext {
2266
2238
  }
2267
2239
 
2268
2240
  // 6. RECORD THE ENTIRE ATOMIC OPERATION in a single history patch.
2269
- this.history.addPatch({
2270
- op: PatchOp.ADD,
2271
- undo: (ctx) => {
2272
- // Undo all changes: delete the element and the auto-created markdown node (if it exists)
2273
- ctx.deleteNodes(flattenedNodes);
2274
- if (autoCreatedMarkdownNode) {
2275
- ctx.deleteNodes([autoCreatedMarkdownNode]);
2276
- }
2277
- if (originalPaneNode) {
2278
- const newNodes = new Map(ctx.allNodes.get());
2279
- newNodes.set(originalPaneNode.id, originalPaneNode);
2280
- ctx.allNodes.set(newNodes);
2281
- }
2282
- if (paneNodeId) ctx.notifyNode(paneNodeId);
2283
- },
2284
- redo: (ctx) => {
2285
- // Redo all changes in the correct order
2286
- if (originalPaneNode) {
2287
- ctx.modifyNodes([{ ...originalPaneNode, isChanged: true }], {
2288
- notify: false,
2289
- recordHistory: false,
2290
- });
2291
- }
2292
- if (autoCreatedMarkdownNode) {
2293
- ctx.addNode(autoCreatedMarkdownNode);
2294
- }
2295
- ctx.addNodes(flattenedNodes);
2296
-
2297
- // Re-apply insertion logic
2298
- const parentNodesMap = ctx.parentNodes.get();
2299
- const parentChildren = parentNodesMap.get(parentId);
2300
- if (insertNodeId && location && parentChildren) {
2301
- const insertIndex = parentChildren.indexOf(insertNodeId);
2302
- if (insertIndex !== -1) {
2303
- const currentChildren = parentChildren.filter(
2304
- (id) => !newTopLevelIds.includes(id)
2305
- );
2306
- if (location === 'before') {
2307
- currentChildren.splice(insertIndex, 0, ...newTopLevelIds);
2308
- } else {
2309
- currentChildren.splice(insertIndex + 1, 0, ...newTopLevelIds);
2241
+ if (!this.isTemplate.get()) {
2242
+ if (VERBOSE_HISTORY)
2243
+ console.log('[Nodes] Action: addTemplateNode', {
2244
+ targetId,
2245
+ nodeCount: flattenedNodes.length,
2246
+ });
2247
+ this.history.addPatch({
2248
+ op: PatchOp.ADD,
2249
+ undo: (ctx) => {
2250
+ // Undo all changes: delete the element and the auto-created markdown node (if it exists)
2251
+ ctx.deleteNodes(flattenedNodes);
2252
+ if (autoCreatedMarkdownNode) {
2253
+ ctx.deleteNodes([autoCreatedMarkdownNode]);
2254
+ }
2255
+ if (originalPaneNode) {
2256
+ const newNodes = new Map(ctx.allNodes.get());
2257
+ newNodes.set(originalPaneNode.id, originalPaneNode);
2258
+ ctx.allNodes.set(newNodes);
2259
+ }
2260
+ if (paneNodeId) ctx.notifyNode(paneNodeId);
2261
+ },
2262
+ redo: (ctx) => {
2263
+ // Redo all changes in the correct order
2264
+ if (originalPaneNode) {
2265
+ ctx.modifyNodes([{ ...originalPaneNode }], {
2266
+ notify: false,
2267
+ recordHistory: false,
2268
+ });
2269
+ }
2270
+ if (autoCreatedMarkdownNode) {
2271
+ ctx.addNode(autoCreatedMarkdownNode);
2272
+ }
2273
+ ctx.addNodes(flattenedNodes);
2274
+
2275
+ // Re-apply insertion logic
2276
+ const parentNodesMap = ctx.parentNodes.get();
2277
+ const parentChildren = parentNodesMap.get(parentId);
2278
+ if (insertNodeId && location && parentChildren) {
2279
+ const insertIndex = parentChildren.indexOf(insertNodeId);
2280
+ if (insertIndex !== -1) {
2281
+ const currentChildren = parentChildren.filter(
2282
+ (id) => !newTopLevelIds.includes(id)
2283
+ );
2284
+ if (location === 'before') {
2285
+ currentChildren.splice(insertIndex, 0, ...newTopLevelIds);
2286
+ } else {
2287
+ currentChildren.splice(insertIndex + 1, 0, ...newTopLevelIds);
2288
+ }
2289
+ parentNodesMap.set(parentId, currentChildren);
2310
2290
  }
2311
- parentNodesMap.set(parentId, currentChildren);
2312
2291
  }
2313
- }
2314
- if (paneNodeId) ctx.notifyNode(paneNodeId);
2315
- },
2316
- });
2292
+ if (paneNodeId) ctx.notifyNode(paneNodeId);
2293
+ },
2294
+ });
2295
+ }
2317
2296
 
2318
2297
  // 7. SEND A SINGLE NOTIFICATION to update the UI.
2319
2298
  if (paneNodeId) {
@@ -2486,7 +2465,7 @@ export class NodesContext {
2486
2465
  this.allNodes.get().get(paneNodeId)
2487
2466
  ) as PaneNode;
2488
2467
  if (paneNode) {
2489
- this.modifyNodes([{ ...paneNode, isChanged: true }]);
2468
+ this.modifyNodes([{ ...paneNode }]);
2490
2469
  }
2491
2470
  }
2492
2471
  }
@@ -2498,23 +2477,29 @@ export class NodesContext {
2498
2477
 
2499
2478
  this.notifyNode(ROOT_NODE_NAME);
2500
2479
 
2501
- // Add to history for undo/redo
2502
- this.history.addPatch({
2503
- op: PatchOp.REMOVE,
2504
- undo: (ctx) => {
2505
- ctx.addNodes(toDelete);
2506
- if (targetNode.nodeType === 'Pane' && parentId !== null) {
2507
- const storyFragment = this.allNodes
2508
- .get()
2509
- .get(parentId) as StoryFragmentNode;
2510
- if (storyFragment) {
2511
- storyFragment.paneIds.splice(paneIdx, 0, targetNodeId);
2512
- this.linkChildToParent(targetNodeId, parentId, paneIdx);
2480
+ if (!this.isTemplate.get()) {
2481
+ if (VERBOSE_HISTORY)
2482
+ console.log('[Nodes] Action: deleteNode', {
2483
+ targetNodeId,
2484
+ deletedCount: toDelete.length,
2485
+ });
2486
+ this.history.addPatch({
2487
+ op: PatchOp.REMOVE,
2488
+ undo: (ctx) => {
2489
+ ctx.addNodes(toDelete);
2490
+ if (targetNode.nodeType === 'Pane' && parentId !== null) {
2491
+ const storyFragment = this.allNodes
2492
+ .get()
2493
+ .get(parentId) as StoryFragmentNode;
2494
+ if (storyFragment) {
2495
+ storyFragment.paneIds.splice(paneIdx, 0, targetNodeId);
2496
+ this.linkChildToParent(targetNodeId, parentId, paneIdx);
2497
+ }
2513
2498
  }
2514
- }
2515
- },
2516
- redo: (ctx) => ctx.deleteNodes(toDelete),
2517
- });
2499
+ },
2500
+ redo: (ctx) => ctx.deleteNodes(toDelete),
2501
+ });
2502
+ }
2518
2503
  }
2519
2504
 
2520
2505
  getNodesRecursively(node: BaseNode | undefined): BaseNode[] {
@@ -2611,7 +2596,7 @@ export class NodesContext {
2611
2596
  this.allNodes.get().get(storyFragmentId)
2612
2597
  ) as StoryFragmentNode;
2613
2598
  if (storyFragment) {
2614
- this.modifyNodes([{ ...storyFragment, isChanged: true }]);
2599
+ this.modifyNodes([{ ...storyFragment }]);
2615
2600
  }
2616
2601
  } else {
2617
2602
  const parentNode = this.nodeToNotify(
@@ -2621,64 +2606,72 @@ export class NodesContext {
2621
2606
  this.notifyNode(parentNode || '');
2622
2607
  }
2623
2608
 
2624
- this.history.addPatch({
2625
- op: PatchOp.REPLACE,
2626
- undo: (ctx) => {
2627
- const oldParentNodes = ctx.getChildNodeIDs(node.parentId || '');
2628
- const newParentNodes = ctx.getChildNodeIDs(
2629
- newLocationNode.parentId || ''
2630
- );
2631
- if (newParentNodes) {
2632
- newParentNodes.splice(newParentNodes.indexOf(nodeId), 1);
2633
- }
2634
- if (oldParentNodes) {
2635
- oldParentNodes.splice(originalIdx, 0, nodeId);
2636
- }
2637
- node.parentId = oldParentId;
2638
-
2639
- if (node.nodeType === 'Pane' && originalPaneIds) {
2640
- const storyFragmentId = ctx.getClosestNodeTypeFromId(
2641
- node.id,
2642
- 'StoryFragment'
2609
+ if (!this.isTemplate.get()) {
2610
+ if (VERBOSE_HISTORY)
2611
+ console.log('[Nodes] Action: moveNodeTo', {
2612
+ nodeId,
2613
+ insertNodeId,
2614
+ location,
2615
+ });
2616
+ this.history.addPatch({
2617
+ op: PatchOp.REPLACE,
2618
+ undo: (ctx) => {
2619
+ const oldParentNodes = ctx.getChildNodeIDs(node.parentId || '');
2620
+ const newParentNodes = ctx.getChildNodeIDs(
2621
+ newLocationNode.parentId || ''
2643
2622
  );
2644
- const storyFragment = cloneDeep(
2645
- ctx.allNodes.get().get(storyFragmentId)
2646
- ) as StoryFragmentNode;
2647
- if (storyFragment) {
2648
- storyFragment.paneIds = [...originalPaneIds];
2649
- this.modifyNodes([{ ...storyFragment, isChanged: true }]);
2623
+ if (newParentNodes) {
2624
+ newParentNodes.splice(newParentNodes.indexOf(nodeId), 1);
2650
2625
  }
2651
- }
2626
+ if (oldParentNodes) {
2627
+ oldParentNodes.splice(originalIdx, 0, nodeId);
2628
+ }
2629
+ node.parentId = oldParentId;
2652
2630
 
2653
- //const parentNode = ctx.nodeToNotify(node?.parentId || "", node.nodeType);
2654
- ctx.notifyNode(node.id || '');
2655
- },
2656
- redo: (ctx) => {
2657
- moveNodeAtLocationInContext(
2658
- oldParentNodes,
2659
- originalIdx,
2660
- newLocationNode,
2661
- insertNodeId,
2662
- nodeId,
2663
- location,
2664
- node,
2665
- ctx
2666
- );
2631
+ if (node.nodeType === 'Pane' && originalPaneIds) {
2632
+ const storyFragmentId = ctx.getClosestNodeTypeFromId(
2633
+ node.id,
2634
+ 'StoryFragment'
2635
+ );
2636
+ const storyFragment = cloneDeep(
2637
+ ctx.allNodes.get().get(storyFragmentId)
2638
+ ) as StoryFragmentNode;
2639
+ if (storyFragment) {
2640
+ storyFragment.paneIds = [...originalPaneIds];
2641
+ this.modifyNodes([{ ...storyFragment }]);
2642
+ }
2643
+ }
2667
2644
 
2668
- if (node.nodeType === 'Pane') {
2669
- const storyFragmentId = ctx.getClosestNodeTypeFromId(
2670
- node.id,
2671
- 'StoryFragment'
2645
+ //const parentNode = ctx.nodeToNotify(node?.parentId || "", node.nodeType);
2646
+ ctx.notifyNode(node.id || '');
2647
+ },
2648
+ redo: (ctx) => {
2649
+ moveNodeAtLocationInContext(
2650
+ oldParentNodes,
2651
+ originalIdx,
2652
+ newLocationNode,
2653
+ insertNodeId,
2654
+ nodeId,
2655
+ location,
2656
+ node,
2657
+ ctx
2672
2658
  );
2673
- const storyFragment = cloneDeep(
2674
- ctx.allNodes.get().get(storyFragmentId)
2675
- ) as StoryFragmentNode;
2676
- if (storyFragment) {
2677
- this.modifyNodes([{ ...storyFragment, isChanged: true }]);
2659
+
2660
+ if (node.nodeType === 'Pane') {
2661
+ const storyFragmentId = ctx.getClosestNodeTypeFromId(
2662
+ node.id,
2663
+ 'StoryFragment'
2664
+ );
2665
+ const storyFragment = cloneDeep(
2666
+ ctx.allNodes.get().get(storyFragmentId)
2667
+ ) as StoryFragmentNode;
2668
+ if (storyFragment) {
2669
+ this.modifyNodes([{ ...storyFragment }]);
2670
+ }
2678
2671
  }
2679
- }
2680
- },
2681
- });
2672
+ },
2673
+ });
2674
+ }
2682
2675
  }
2683
2676
 
2684
2677
  getPaneImageFileIds(paneId: string): string[] {
@@ -2772,7 +2765,6 @@ export class NodesContext {
2772
2765
  const updatedStoryFragment: StoryFragmentNode = {
2773
2766
  ...storyfragment,
2774
2767
  paneIds: newPaneIds,
2775
- isChanged: true,
2776
2768
  };
2777
2769
 
2778
2770
  // Pass the correctly typed object to modifyNodes
@@ -3202,7 +3194,7 @@ export class NodesContext {
3202
3194
  this.parentNodes.set(newParentNodes);
3203
3195
 
3204
3196
  // Mark pane as dirty
3205
- this.modifyNodes([{ ...originalPaneNode, isChanged: true }], {
3197
+ this.modifyNodes([{ ...originalPaneNode }], {
3206
3198
  notify: false,
3207
3199
  recordHistory: false,
3208
3200
  });
@@ -3228,41 +3220,15 @@ export class NodesContext {
3228
3220
  // --- 5. Execute the operation and add to history ---
3229
3221
  applyUnwrap();
3230
3222
 
3231
- this.history.addPatch({
3232
- op: PatchOp.REPLACE, // Using REPLACE as it's a complex operation
3233
- undo: () => applyRewrap(),
3234
- redo: () => applyUnwrap(),
3235
- });
3236
- }
3237
-
3238
- /**
3239
- * Executes a series of updates on a temporary context and then applies the
3240
- * results to the main context in a single operation, triggering one UI update.
3241
- * @param work - An async function that receives the temporary context and performs modifications.
3242
- */
3243
- async applyAtomicUpdate(
3244
- work: (tmpCtx: NodesContext) => Promise<void>
3245
- ): Promise<void> {
3246
- // 1. Create a temporary, "off-screen" context
3247
- const tmpCtx = new NodesContext();
3248
- // Prime the temp context with the same root ID and other relevant state
3249
- tmpCtx.rootNodeId.set(this.rootNodeId.get());
3250
- tmpCtx.allNodes.set(new Map(this.allNodes.get()));
3251
- tmpCtx.parentNodes.set(new Map(this.parentNodes.get()));
3252
-
3253
- // 2. Execute the long-running work on the temporary context
3254
- await work(tmpCtx);
3255
-
3256
- // 3. Get the results from the temporary context
3257
- const newNodes = tmpCtx.allNodes.get();
3258
- const newParentRelations = tmpCtx.parentNodes.get();
3259
-
3260
- // 4. Swap/Merge the results into the main context
3261
- this.allNodes.set(newNodes);
3262
- this.parentNodes.set(newParentRelations);
3263
-
3264
- // 5. Trigger a single notification to re-render the UI
3265
- this.notifyNode('root');
3223
+ if (!this.isTemplate.get()) {
3224
+ if (VERBOSE_HISTORY)
3225
+ console.log('[Nodes] Action: unwrapNode', { nodeId });
3226
+ this.history.addPatch({
3227
+ op: PatchOp.REPLACE, // Using REPLACE as it's a complex operation
3228
+ undo: () => applyRewrap(),
3229
+ redo: () => applyUnwrap(),
3230
+ });
3231
+ }
3266
3232
  }
3267
3233
 
3268
3234
  private deleteNodes(nodesList: BaseNode[]): BaseNode[] {