astro-tractstack 2.0.17 → 2.0.19

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 (63) hide show
  1. package/dist/index.js +18 -0
  2. package/package.json +1 -1
  3. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +1 -1
  4. package/templates/src/components/codehooks/ListContentSetup.tsx +1 -1
  5. package/templates/src/components/compositor/Compositor.tsx +1 -0
  6. package/templates/src/components/compositor/Node.tsx +41 -17
  7. package/templates/src/components/compositor/nodes/GhostInsertBlock.tsx +9 -6
  8. package/templates/src/components/compositor/nodes/GridLayout.tsx +124 -0
  9. package/templates/src/components/compositor/nodes/GridLayout_eraser.tsx +33 -0
  10. package/templates/src/components/compositor/nodes/Markdown.tsx +67 -37
  11. package/templates/src/components/compositor/nodes/Markdown_eraser.tsx +56 -0
  12. package/templates/src/components/compositor/preview/FeaturedArticlePreview.tsx +8 -2
  13. package/templates/src/components/edit/PanelSwitch.tsx +232 -75
  14. package/templates/src/components/edit/SettingsPanel.tsx +0 -1
  15. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +3 -3
  16. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +402 -167
  17. package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +2 -2
  18. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -7
  19. package/templates/src/components/edit/pane/PanePanel_impression.tsx +1 -1
  20. package/templates/src/components/edit/pane/RestylePaneModal.tsx +8 -5
  21. package/templates/src/components/edit/pane/steps/AiDesignStep.tsx +6 -6
  22. package/templates/src/components/edit/pane/steps/CopyInputStep.tsx +3 -3
  23. package/templates/src/components/edit/pane/steps/DesignLibraryStep.tsx +4 -4
  24. package/templates/src/components/edit/panels/StyleElementPanel.tsx +11 -4
  25. package/templates/src/components/edit/panels/StyleElementPanel_add.tsx +8 -8
  26. package/templates/src/components/edit/panels/StyleElementPanel_remove.tsx +14 -4
  27. package/templates/src/components/edit/panels/StyleElementPanel_update.tsx +16 -4
  28. package/templates/src/components/edit/panels/StyleImagePanel.tsx +7 -3
  29. package/templates/src/components/edit/panels/StyleImagePanel_add.tsx +9 -2
  30. package/templates/src/components/edit/panels/StyleImagePanel_remove.tsx +5 -2
  31. package/templates/src/components/edit/panels/StyleImagePanel_update.tsx +5 -2
  32. package/templates/src/components/edit/panels/StyleLiElementPanel.tsx +7 -3
  33. package/templates/src/components/edit/panels/StyleLiElementPanel_add.tsx +9 -2
  34. package/templates/src/components/edit/panels/StyleLiElementPanel_remove.tsx +5 -2
  35. package/templates/src/components/edit/panels/StyleLiElementPanel_update.tsx +5 -2
  36. package/templates/src/components/edit/panels/StyleParentPanel.tsx +530 -171
  37. package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +77 -42
  38. package/templates/src/components/edit/panels/StyleParentPanel_deleteLayer.tsx +38 -22
  39. package/templates/src/components/edit/panels/StyleParentPanel_remove.tsx +171 -66
  40. package/templates/src/components/edit/panels/StyleParentPanel_update.tsx +166 -98
  41. package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +7 -3
  42. package/templates/src/components/edit/panels/StyleWidgetPanel_add.tsx +9 -2
  43. package/templates/src/components/edit/panels/StyleWidgetPanel_remove.tsx +5 -2
  44. package/templates/src/components/edit/panels/StyleWidgetPanel_update.tsx +6 -2
  45. package/templates/src/components/edit/state/SaveModal.tsx +10 -2
  46. package/templates/src/components/edit/state/SaveToLibraryModal.tsx +6 -6
  47. package/templates/src/components/fields/PaneBreakShapeSelector.tsx +1 -1
  48. package/templates/src/components/widgets/ImpressionWrapper.tsx +4 -1
  49. package/templates/src/constants/prompts.json +23 -2
  50. package/templates/src/stores/nodes.ts +356 -212
  51. package/templates/src/stores/storykeep.ts +3 -1
  52. package/templates/src/types/compositorTypes.ts +56 -3
  53. package/templates/src/types/tractstack.ts +1 -0
  54. package/templates/src/utils/compositor/TemplateNodes.ts +8 -0
  55. package/templates/src/utils/compositor/aiPaneParser.ts +263 -83
  56. package/templates/src/utils/compositor/designLibraryHelper.ts +12 -9
  57. package/templates/src/utils/compositor/nodesHelper.ts +229 -0
  58. package/templates/src/utils/compositor/reduceNodesClassNames.ts +40 -1
  59. package/templates/src/utils/compositor/typeGuards.ts +7 -0
  60. package/templates/src/utils/etl/extractor.ts +1 -5
  61. package/templates/src/utils/etl/index.ts +1 -0
  62. package/templates/src/utils/etl/transformer.ts +70 -25
  63. package/utils/inject-files.ts +18 -0
@@ -15,6 +15,7 @@ import {
15
15
  hasTagName,
16
16
  isDefined,
17
17
  isValidTag,
18
+ isGridLayoutNode,
18
19
  toTag,
19
20
  } from '@/utils/compositor/typeGuards';
20
21
  import { startLoadingAnimation } from '@/utils/helpers';
@@ -29,6 +30,7 @@ import type {
29
30
  BaseNode,
30
31
  FlatNode,
31
32
  ImpressionNode,
33
+ GridLayoutNode,
32
34
  MarkdownPaneFragmentNode,
33
35
  MenuNode,
34
36
  NodeType,
@@ -36,6 +38,7 @@ import type {
36
38
  PaneNode,
37
39
  StoryFragmentNode,
38
40
  Tag,
41
+ TemplateGridLayout,
39
42
  TemplateMarkdown,
40
43
  TemplateNode,
41
44
  TemplatePane,
@@ -1390,6 +1393,30 @@ export class NodesContext {
1390
1393
  if (!node) return '';
1391
1394
 
1392
1395
  switch (node.nodeType) {
1396
+ case 'GridLayoutNode': {
1397
+ const gridNode = node as GridLayoutNode;
1398
+ if (gridNode.parentClasses) {
1399
+ const [all, mobile, tablet, desktop] = processClassesForViewports(
1400
+ gridNode.parentClasses[depth],
1401
+ {}, // No override classes for GridLayout parent case
1402
+ 1
1403
+ );
1404
+
1405
+ if (isPreview) return desktop[0];
1406
+ switch (viewport) {
1407
+ case 'desktop':
1408
+ return desktop[0];
1409
+ case 'tablet':
1410
+ return tablet[0];
1411
+ case 'mobile':
1412
+ return mobile[0];
1413
+ default:
1414
+ return all[0];
1415
+ }
1416
+ }
1417
+ break;
1418
+ }
1419
+
1393
1420
  case 'Markdown':
1394
1421
  {
1395
1422
  const markdownFragment = node as MarkdownPaneFragmentNode;
@@ -1419,103 +1446,126 @@ export class NodesContext {
1419
1446
  }
1420
1447
  break;
1421
1448
 
1422
- case 'TagElement':
1423
- {
1424
- const getButtonClasses = (node: FlatNode) => {
1425
- return {
1426
- mobile: strippedStyles(node.buttonPayload?.buttonClasses || {}),
1427
- tablet: {},
1428
- desktop: {},
1429
- };
1449
+ case 'TagElement': {
1450
+ const getButtonClasses = (node: FlatNode) => {
1451
+ return {
1452
+ mobile: strippedStyles(node.buttonPayload?.buttonClasses || {}),
1453
+ tablet: {},
1454
+ desktop: {},
1430
1455
  };
1456
+ };
1431
1457
 
1432
- const getHoverClasses = (node: FlatNode) => {
1433
- return {
1434
- mobile: strippedStyles(
1435
- node.buttonPayload?.buttonHoverClasses || {}
1436
- ),
1437
- tablet: {},
1438
- desktop: {},
1439
- };
1458
+ const getHoverClasses = (node: FlatNode) => {
1459
+ return {
1460
+ mobile: strippedStyles(
1461
+ node.buttonPayload?.buttonHoverClasses || {}
1462
+ ),
1463
+ tablet: {},
1464
+ desktop: {},
1440
1465
  };
1466
+ };
1441
1467
 
1442
- if (hasButtonPayload(node)) {
1443
- const [classesPayload] = processClassesForViewports(
1444
- getButtonClasses(node),
1445
- {},
1446
- 1
1447
- );
1448
- const [classesHoverPayload] = processClassesForViewports(
1449
- getHoverClasses(node),
1450
- {},
1451
- 1
1452
- );
1453
- return `${classesPayload?.length ? classesPayload[0] : ``} ${
1454
- classesHoverPayload?.length
1455
- ? addHoverPrefix(classesHoverPayload[0])
1456
- : ``
1457
- }`;
1458
- }
1468
+ if (hasButtonPayload(node)) {
1469
+ const [classesPayload] = processClassesForViewports(
1470
+ getButtonClasses(node),
1471
+ {},
1472
+ 1
1473
+ );
1474
+ const [classesHoverPayload] = processClassesForViewports(
1475
+ getHoverClasses(node),
1476
+ {},
1477
+ 1
1478
+ );
1479
+ return `${classesPayload?.length ? classesPayload[0] : ``} ${
1480
+ classesHoverPayload?.length
1481
+ ? addHoverPrefix(classesHoverPayload[0])
1482
+ : ``
1483
+ }`;
1484
+ }
1459
1485
 
1460
- if ('tagName' in node && node.tagName === 'span') {
1461
- const spanNode = node as FlatNode;
1462
- const [all, mobile, tablet, desktop] = processClassesForViewports(
1463
- { mobile: {}, tablet: {}, desktop: {} },
1464
- spanNode.overrideClasses || {},
1465
- 1
1466
- );
1467
- const outlineClass =
1468
- this.toolModeValStore.get().value === 'styles'
1469
- ? ' outline outline-1 outline-dotted outline-gray-400/60'
1470
- : '';
1486
+ if ('tagName' in node && node.tagName === 'span') {
1487
+ const spanNode = node as FlatNode;
1488
+ const [all, mobile, tablet, desktop] = processClassesForViewports(
1489
+ { mobile: {}, tablet: {}, desktop: {} },
1490
+ spanNode.overrideClasses || {},
1491
+ 1
1492
+ );
1493
+ const outlineClass =
1494
+ this.toolModeValStore.get().value === 'styles'
1495
+ ? ' outline outline-1 outline-dotted outline-gray-400/60'
1496
+ : '';
1497
+
1498
+ const getClassString = (classes: string[]): string =>
1499
+ classes && classes.length > 0 ? classes[0] : '';
1500
+
1501
+ if (isPreview) return getClassString(desktop) + outlineClass;
1502
+ switch (viewport) {
1503
+ case 'desktop':
1504
+ return getClassString(desktop) + outlineClass;
1505
+ case 'tablet':
1506
+ return getClassString(tablet) + outlineClass;
1507
+ case 'mobile':
1508
+ return getClassString(mobile) + outlineClass;
1509
+ default:
1510
+ return getClassString(all) + outlineClass;
1511
+ }
1512
+ }
1471
1513
 
1472
- const getClassString = (classes: string[]): string =>
1473
- classes && classes.length > 0 ? classes[0] : '';
1514
+ // Begin Default Class Lookup Logic
1515
+ const markdownParentId = this.getClosestNodeTypeFromId(
1516
+ nodeId,
1517
+ 'Markdown'
1518
+ );
1519
+ if (!markdownParentId) break;
1474
1520
 
1475
- if (isPreview) return getClassString(desktop) + outlineClass;
1476
- switch (viewport) {
1477
- case 'desktop':
1478
- return getClassString(desktop) + outlineClass;
1479
- case 'tablet':
1480
- return getClassString(tablet) + outlineClass;
1481
- case 'mobile':
1482
- return getClassString(mobile) + outlineClass;
1483
- default:
1484
- return getClassString(all) + outlineClass;
1485
- }
1521
+ const markdownParentNode = this.allNodes
1522
+ .get()
1523
+ .get(markdownParentId) as MarkdownPaneFragmentNode;
1524
+ if (!markdownParentNode) break;
1525
+
1526
+ const tagNameStr = (node as FlatNode).tagName as string;
1527
+
1528
+ // By default, assume the markdown node is the source of styles.
1529
+ let styleSourceNode: MarkdownPaneFragmentNode | GridLayoutNode =
1530
+ markdownParentNode;
1531
+ let styles = styleSourceNode.defaultClasses?.[tagNameStr];
1532
+
1533
+ // If the markdown node has no styles for this tag, check for a GridLayout grandparent.
1534
+ // This handles the case where the MarkdownNode is a column.
1535
+ if (!styles || Object.keys(styles.mobile).length === 0) {
1536
+ const grandparent = markdownParentNode.parentId
1537
+ ? this.allNodes.get().get(markdownParentNode.parentId)
1538
+ : null;
1539
+
1540
+ if (grandparent && isGridLayoutNode(grandparent)) {
1541
+ styleSourceNode = grandparent;
1542
+ styles = styleSourceNode.defaultClasses?.[tagNameStr];
1486
1543
  }
1544
+ }
1487
1545
 
1488
- const closestPaneId = this.getClosestNodeTypeFromId(
1489
- nodeId,
1490
- 'Markdown'
1491
- );
1492
- const paneNode = this.allNodes
1493
- .get()
1494
- .get(closestPaneId) as MarkdownPaneFragmentNode;
1495
- if (paneNode && 'tagName' in node) {
1496
- const tagNameStr = node.tagName as string;
1497
- const styles = paneNode.defaultClasses![tagNameStr];
1498
- if (styles && styles.mobile) {
1499
- const [all, mobile, tablet, desktop] = processClassesForViewports(
1500
- styles,
1501
- (node as FlatNode)?.overrideClasses || {},
1502
- 1
1503
- );
1504
- if (isPreview) return desktop[0];
1505
- switch (viewport) {
1506
- case 'desktop':
1507
- return desktop[0];
1508
- case 'tablet':
1509
- return tablet[0];
1510
- case 'mobile':
1511
- return mobile[0];
1512
- default:
1513
- return all[0];
1514
- }
1515
- }
1516
- }
1546
+ const baseStyles =
1547
+ styles && styles.mobile
1548
+ ? styles
1549
+ : { mobile: {}, tablet: {}, desktop: {} };
1550
+
1551
+ const [all, mobile, tablet, desktop] = processClassesForViewports(
1552
+ baseStyles,
1553
+ (node as FlatNode)?.overrideClasses || {},
1554
+ 1
1555
+ );
1556
+
1557
+ if (isPreview) return desktop[0];
1558
+ switch (viewport) {
1559
+ case 'desktop':
1560
+ return desktop[0];
1561
+ case 'tablet':
1562
+ return tablet[0];
1563
+ case 'mobile':
1564
+ return mobile[0];
1565
+ default:
1566
+ return all[0];
1517
1567
  }
1518
- break;
1568
+ }
1519
1569
 
1520
1570
  case 'StoryFragment': {
1521
1571
  const storyFragment = node as StoryFragmentNode;
@@ -1584,19 +1634,44 @@ export class NodesContext {
1584
1634
  }
1585
1635
 
1586
1636
  switch (node.nodeType) {
1587
- case `TagElement`:
1588
- case `BgPane`:
1589
- case `Markdown`: {
1637
+ case 'GridLayoutNode':
1638
+ case 'TagElement':
1639
+ case 'BgPane':
1640
+ case 'Markdown': {
1641
+ const nodesToDirty: BaseNode[] = [];
1590
1642
  const paneNodeId = this.getClosestNodeTypeFromId(node.id, 'Pane');
1591
- const paneNode = cloneDeep(
1592
- this.allNodes.get().get(paneNodeId)
1593
- ) as PaneNode;
1594
- this.modifyNodes([{ ...paneNode, isChanged: true }], {
1595
- notify: false,
1596
- });
1643
+
1644
+ if (paneNodeId) {
1645
+ const paneNode = this.allNodes.get().get(paneNodeId);
1646
+ if (paneNode && !paneNode.isChanged) {
1647
+ nodesToDirty.push({ ...paneNode, isChanged: true });
1648
+ }
1649
+ }
1650
+
1651
+ if (node.parentId) {
1652
+ const parentNode = this.allNodes.get().get(node.parentId);
1653
+ if (
1654
+ parentNode &&
1655
+ parentNode.nodeType === 'GridLayoutNode' &&
1656
+ !parentNode.isChanged
1657
+ ) {
1658
+ if (!nodesToDirty.some((n) => n.id === parentNode.id)) {
1659
+ nodesToDirty.push({ ...parentNode, isChanged: true });
1660
+ }
1661
+ }
1662
+ }
1663
+
1664
+ if (nodesToDirty.length > 0) {
1665
+ this.modifyNodes(nodesToDirty, {
1666
+ notify: false,
1667
+ recordHistory: false,
1668
+ });
1669
+ }
1670
+
1597
1671
  this.notifyNode(ROOT_NODE_NAME);
1598
1672
  break;
1599
1673
  }
1674
+
1600
1675
  case `Menu`:
1601
1676
  case `Pane`:
1602
1677
  case `StoryFragment`:
@@ -1703,39 +1778,15 @@ export class NodesContext {
1703
1778
  duplicatedPane.isChanged = true;
1704
1779
 
1705
1780
  // Track all nodes that need to be added
1706
- let allNodes: BaseNode[] = [];
1707
-
1708
- // must generate nodes from markdown
1709
- if (duplicatedPane.markdown) {
1710
- duplicatedPane.markdown = cloneDeep(pane.markdown) as TemplateMarkdown;
1711
- duplicatedPane.markdown.id = pane?.markdown?.id || ulid();
1712
- duplicatedPane.markdown.markdownId = pane?.markdown?.markdownId || ulid();
1713
- duplicatedPane.markdown.parentId = ownerId;
1714
-
1715
- let markdownNodes: TemplateNode[] = [];
1716
- if (duplicatedPane.markdown.markdownBody) {
1717
- const markdownGen = new MarkdownGenerator(this);
1718
- markdownNodes = markdownGen.markdownToFlatNodes(
1719
- duplicatedPane.markdown.markdownBody,
1720
- duplicatedPane.markdown.id
1721
- ) as TemplateNode[];
1722
- allNodes = [...allNodes, duplicatedPane.markdown, ...markdownNodes];
1723
- }
1781
+ // Call the new helper to process markdown, gridLayout, and bgPane
1782
+ const allNodes: BaseNode[] = this._processPaneTemplate(
1783
+ duplicatedPane,
1784
+ ownerId
1785
+ );
1724
1786
 
1725
- // Markdown already as nodes
1726
- else if (
1727
- typeof duplicatedPane.markdown !== `undefined` &&
1728
- typeof duplicatedPane.markdown.id === `string`
1729
- ) {
1730
- duplicatedPane?.markdown.nodes?.forEach((node) => {
1731
- const childrenNodes = this.setupTemplateNodeRecursively(
1732
- node,
1733
- duplicatedPane?.markdown?.id || ''
1734
- );
1735
- markdownNodes.push(...childrenNodes);
1736
- });
1737
- allNodes = [...allNodes, duplicatedPane.markdown, ...markdownNodes];
1738
- }
1787
+ // Remove bgPane from the pane object if it exists, as it's now a separate node
1788
+ if (duplicatedPane.bgPane) {
1789
+ delete duplicatedPane.bgPane;
1739
1790
  }
1740
1791
 
1741
1792
  this.addNode(duplicatedPane as PaneNode);
@@ -1780,85 +1831,15 @@ export class NodesContext {
1780
1831
  }
1781
1832
  }
1782
1833
 
1783
- let allNodes: BaseNode[] = [];
1784
-
1785
- if (duplicatedPane.markdown) {
1786
- duplicatedPane.markdown = cloneDeep(pane.markdown) as TemplateMarkdown;
1787
- duplicatedPane.markdown.id = pane?.markdown?.id || ulid();
1788
- duplicatedPane.markdown.markdownId = pane?.markdown?.markdownId || ulid();
1789
- duplicatedPane.markdown.parentId = duplicatedPaneId;
1790
-
1791
- let markdownNodes: TemplateNode[] = [];
1792
- if (duplicatedPane.markdown.markdownBody) {
1793
- const markdownGen = new MarkdownGenerator(this);
1794
- markdownNodes = markdownGen.markdownToFlatNodes(
1795
- duplicatedPane.markdown.markdownBody,
1796
- duplicatedPane.markdown.id
1797
- ) as TemplateNode[];
1798
- allNodes = [...allNodes, duplicatedPane.markdown, ...markdownNodes];
1799
- } else if (
1800
- typeof duplicatedPane.markdown !== `undefined` &&
1801
- typeof duplicatedPane.markdown.id === `string`
1802
- ) {
1803
- // Create a map to track the original node ID to its duplicated node ID
1804
- const oldToNewIdMap = new Map<string, string>();
1805
- // First pass: Clone nodes and generate new IDs
1806
- const nodesClone =
1807
- duplicatedPane?.markdown?.nodes?.map((originalNode) => {
1808
- const newNode = cloneDeep(originalNode);
1809
- newNode.id = ulid();
1810
- oldToNewIdMap.set(originalNode.id, newNode.id);
1811
- return newNode;
1812
- }) || [];
1813
- // Second pass: Update parent IDs using the mapping
1814
- nodesClone.forEach((node) => {
1815
- // Special case for direct children of markdown
1816
- if (node.parentId === pane?.markdown?.id) {
1817
- node.parentId = duplicatedPane?.markdown?.id || '';
1818
- } else {
1819
- // For all other nodes, use the mapping to find the new parent ID
1820
- const newParentId = oldToNewIdMap.get(node.parentId || '');
1821
- if (newParentId) {
1822
- node.parentId = newParentId;
1823
- }
1824
- }
1825
- markdownNodes.push(node);
1826
- });
1827
- allNodes = [...allNodes, duplicatedPane.markdown, ...markdownNodes];
1828
- }
1829
- }
1834
+ // Call the new helper to process markdown, gridLayout, and bgPane
1835
+ const allNodes: BaseNode[] = this._processPaneTemplate(
1836
+ duplicatedPane,
1837
+ duplicatedPaneId
1838
+ );
1830
1839
 
1840
+ // Remove bgPane from the pane object if it exists, as it's now a separate node
1841
+ // This preserves the original logic
1831
1842
  if (duplicatedPane.bgPane) {
1832
- const bgPaneId = ulid();
1833
-
1834
- if (duplicatedPane.bgPane.type === 'visual-break') {
1835
- const visualBreakPane = duplicatedPane.bgPane as VisualBreakNode;
1836
- const bgPaneNode: VisualBreakNode = {
1837
- id: bgPaneId,
1838
- nodeType: 'BgPane',
1839
- parentId: duplicatedPaneId,
1840
- type: 'visual-break',
1841
- breakDesktop: visualBreakPane.breakDesktop,
1842
- breakTablet: visualBreakPane.breakTablet,
1843
- breakMobile: visualBreakPane.breakMobile,
1844
- };
1845
- allNodes.push(bgPaneNode);
1846
- } else if (duplicatedPane.bgPane.type === 'artpack-image') {
1847
- const artpackBgPane = duplicatedPane.bgPane as ArtpackImageNode;
1848
- const bgPaneNode: ArtpackImageNode = {
1849
- id: bgPaneId,
1850
- nodeType: 'BgPane',
1851
- parentId: duplicatedPaneId,
1852
- type: 'artpack-image',
1853
- collection: artpackBgPane.collection,
1854
- image: artpackBgPane.image,
1855
- src: artpackBgPane.src,
1856
- srcSet: artpackBgPane.srcSet,
1857
- alt: artpackBgPane.alt || `Artpack image`,
1858
- objectFit: artpackBgPane.objectFit || 'cover',
1859
- };
1860
- allNodes.push(bgPaneNode);
1861
- }
1862
1843
  delete duplicatedPane.bgPane;
1863
1844
  }
1864
1845
 
@@ -1901,10 +1882,14 @@ export class NodesContext {
1901
1882
  this.addNodes(allNodes);
1902
1883
  this.notifyNode(ownerId);
1903
1884
 
1885
+ // Combine the pane and all its child nodes for the history patch
1886
+ const nodesToHistory = [duplicatedPane as BaseNode, ...allNodes];
1887
+
1904
1888
  this.history.addPatch({
1905
1889
  op: PatchOp.ADD,
1906
1890
  undo: (ctx) => {
1907
- ctx.deleteNodes(allNodes);
1891
+ // Delete all nodes created (pane + children)
1892
+ ctx.deleteNodes(nodesToHistory);
1908
1893
 
1909
1894
  if (
1910
1895
  storyFragmentNode &&
@@ -1916,8 +1901,6 @@ export class NodesContext {
1916
1901
  );
1917
1902
  storyFragmentNode.isChanged = storyFragmentWasChanged;
1918
1903
  }
1919
-
1920
- ctx.deleteNodes([duplicatedPane]);
1921
1904
  },
1922
1905
  redo: (ctx) => {
1923
1906
  if (storyFragmentNode?.nodeType === 'StoryFragment') {
@@ -1933,13 +1916,13 @@ export class NodesContext {
1933
1916
  storyFragmentNode.isChanged = true;
1934
1917
  }
1935
1918
 
1936
- ctx.addNodes([duplicatedPane]);
1919
+ // Add all nodes back (pane + children)
1920
+ ctx.addNodes(nodesToHistory);
1937
1921
  ctx.linkChildToParent(
1938
1922
  duplicatedPane.id,
1939
1923
  duplicatedPane.parentId,
1940
1924
  specificIdx
1941
1925
  );
1942
- ctx.addNodes(allNodes);
1943
1926
  },
1944
1927
  });
1945
1928
 
@@ -1970,7 +1953,9 @@ export class NodesContext {
1970
1953
  });
1971
1954
  break;
1972
1955
  }
1973
- this.toolModeValStore.set({ value: 'styles' });
1956
+ if ([`p`, `h2`, `h3`, `h4`, `li`].includes(tagName))
1957
+ this.toolModeValStore.set({ value: 'text' });
1958
+ else this.toolModeValStore.set({ value: 'styles' });
1974
1959
  this.notifyNode('root');
1975
1960
  }
1976
1961
 
@@ -2024,6 +2009,7 @@ export class NodesContext {
2024
2009
 
2025
2010
  let autoCreatedMarkdownNode: MarkdownPaneFragmentNode | null = null;
2026
2011
 
2012
+ console.log(`--- [TRAP - TEMPLATE BEFORE] ---`, cloneDeep(node));
2027
2013
  // 3. HANDLE EMPTY PANE BY AUTO-CREATING A MARKDOWN NODE
2028
2014
  if (targetNode.nodeType === 'Pane') {
2029
2015
  // Create a minimal markdown node to act as the container
@@ -2101,6 +2087,8 @@ export class NodesContext {
2101
2087
  );
2102
2088
  }
2103
2089
 
2090
+ console.log(`--- [TRAP - FLATTENED AFTER] ---`, cloneDeep(flattenedNodes));
2091
+
2104
2092
  // 5. PERFORM REMAINING STATE MUTATIONS
2105
2093
  if (originalPaneNode) {
2106
2094
  this.modifyNodes([{ ...originalPaneNode, isChanged: true }], {
@@ -2198,7 +2186,9 @@ export class NodesContext {
2198
2186
 
2199
2187
  node.id = ulid();
2200
2188
  node.parentId = parentId;
2201
- result.push(node);
2189
+ const thisNode = cloneDeep(node);
2190
+ delete thisNode.nodes;
2191
+ result.push(thisNode);
2202
2192
  if ('nodes' in node && node.nodes) {
2203
2193
  for (let i = 0; i < node.nodes.length; ++i) {
2204
2194
  result = result.concat(
@@ -3147,6 +3137,160 @@ export class NodesContext {
3147
3137
 
3148
3138
  return deletedNodes;
3149
3139
  }
3140
+
3141
+ /**
3142
+ * Processes a TemplatePane's content (markdown, grid, or bgPane) and
3143
+ * returns a flat list of all nodes to be added to the store.
3144
+ * This is a de-duplicated helper used by addTemplatePane and addContextTemplatePane.
3145
+ * @param paneTemplate - The TemplatePane object to process.
3146
+ * @param newPaneId - The ID of the parent Pane node.
3147
+ * @returns An array of BaseNode objects to be added to allNodes.
3148
+ */
3149
+ private _processPaneTemplate(
3150
+ paneTemplate: TemplatePane,
3151
+ newPaneId: string
3152
+ ): BaseNode[] {
3153
+ let allNodes: BaseNode[] = [];
3154
+
3155
+ // 1. Process Markdown Content
3156
+ if (paneTemplate.markdown) {
3157
+ const duplicatedMarkdown = cloneDeep(
3158
+ paneTemplate.markdown
3159
+ ) as TemplateMarkdown;
3160
+ duplicatedMarkdown.id = paneTemplate.markdown.id || ulid();
3161
+ duplicatedMarkdown.markdownId =
3162
+ paneTemplate.markdown.markdownId || ulid();
3163
+ duplicatedMarkdown.parentId = newPaneId;
3164
+
3165
+ let markdownNodes: TemplateNode[] = [];
3166
+ if (duplicatedMarkdown.markdownBody) {
3167
+ const markdownGen = new MarkdownGenerator(this);
3168
+ markdownNodes = markdownGen.markdownToFlatNodes(
3169
+ duplicatedMarkdown.markdownBody,
3170
+ duplicatedMarkdown.id
3171
+ ) as TemplateNode[];
3172
+ allNodes = [...allNodes, duplicatedMarkdown, ...markdownNodes];
3173
+ } else if (
3174
+ typeof duplicatedMarkdown !== `undefined` &&
3175
+ typeof duplicatedMarkdown.id === `string`
3176
+ ) {
3177
+ // Create a map to track the original node ID to its duplicated node ID
3178
+ const oldToNewIdMap = new Map<string, string>();
3179
+ // First pass: Clone nodes and generate new IDs
3180
+ const nodesClone =
3181
+ duplicatedMarkdown.nodes?.map((originalNode) => {
3182
+ const newNode = cloneDeep(originalNode);
3183
+ newNode.id = ulid();
3184
+ oldToNewIdMap.set(originalNode.id, newNode.id);
3185
+ return newNode;
3186
+ }) || [];
3187
+ // Second pass: Update parent IDs using the mapping
3188
+ nodesClone.forEach((node) => {
3189
+ // Special case for direct children of markdown
3190
+ if (node.parentId === paneTemplate.markdown?.id) {
3191
+ node.parentId = duplicatedMarkdown.id;
3192
+ } else {
3193
+ // For all other nodes, use the mapping to find the new parent ID
3194
+ const newParentId = oldToNewIdMap.get(node.parentId || '');
3195
+ if (newParentId) {
3196
+ node.parentId = newParentId;
3197
+ }
3198
+ }
3199
+ markdownNodes.push(node);
3200
+ });
3201
+ allNodes = [...allNodes, duplicatedMarkdown, ...markdownNodes];
3202
+ }
3203
+
3204
+ // 2. Process GridLayout Content
3205
+ } else if (paneTemplate.gridLayout) {
3206
+ const duplicatedGrid = cloneDeep(
3207
+ paneTemplate.gridLayout
3208
+ ) as TemplateGridLayout;
3209
+ duplicatedGrid.id = paneTemplate.gridLayout.id || ulid();
3210
+ duplicatedGrid.parentId = newPaneId;
3211
+ allNodes.push(duplicatedGrid as GridLayoutNode);
3212
+
3213
+ // Map for all nodes within the grid
3214
+ const oldToNewIdMap = new Map<string, string>();
3215
+
3216
+ // First pass: Collect all column nodes and their descendant nodes
3217
+ const allOriginalNodes: TemplateNode[] = [];
3218
+ const columnNodes: TemplateMarkdown[] = [];
3219
+
3220
+ duplicatedGrid.nodes?.forEach((originalColumn) => {
3221
+ const newColumn = cloneDeep(originalColumn);
3222
+ newColumn.id = ulid();
3223
+ newColumn.markdownId = ulid();
3224
+ oldToNewIdMap.set(originalColumn.id, newColumn.id);
3225
+ columnNodes.push(newColumn);
3226
+
3227
+ originalColumn.nodes?.forEach((colNode) => {
3228
+ allOriginalNodes.push(colNode);
3229
+ });
3230
+ });
3231
+
3232
+ // Second pass: Clone all descendant nodes
3233
+ const allClonedDescendants = allOriginalNodes.map((originalNode) => {
3234
+ const newNode = cloneDeep(originalNode);
3235
+ newNode.id = ulid();
3236
+ oldToNewIdMap.set(originalNode.id, newNode.id);
3237
+ return newNode;
3238
+ });
3239
+
3240
+ // Third pass: Re-map parent IDs for columns
3241
+ columnNodes.forEach((col) => {
3242
+ col.parentId = duplicatedGrid.id;
3243
+ allNodes.push(col as MarkdownPaneFragmentNode);
3244
+ });
3245
+
3246
+ // Fourth pass: Re-map parent IDs for all descendants
3247
+ allClonedDescendants.forEach((node) => {
3248
+ const newParentId = oldToNewIdMap.get(node.parentId || '');
3249
+ if (newParentId) {
3250
+ node.parentId = newParentId;
3251
+ }
3252
+ allNodes.push(node);
3253
+ });
3254
+ }
3255
+
3256
+ // 3. Process Background Pane
3257
+ if (paneTemplate.bgPane) {
3258
+ const bgPaneId = ulid();
3259
+
3260
+ if (paneTemplate.bgPane.type === 'visual-break') {
3261
+ const visualBreakPane = paneTemplate.bgPane as VisualBreakNode;
3262
+ const bgPaneNode: VisualBreakNode = {
3263
+ id: bgPaneId,
3264
+ nodeType: 'BgPane',
3265
+ parentId: newPaneId,
3266
+ type: 'visual-break',
3267
+ breakDesktop: visualBreakPane.breakDesktop,
3268
+ breakTablet: visualBreakPane.breakTablet,
3269
+ breakMobile: visualBreakPane.breakMobile,
3270
+ };
3271
+ allNodes.push(bgPaneNode);
3272
+ } else if (paneTemplate.bgPane.type === 'artpack-image') {
3273
+ const artpackBgPane = paneTemplate.bgPane as ArtpackImageNode;
3274
+ const bgPaneNode: ArtpackImageNode = {
3275
+ id: bgPaneId,
3276
+ nodeType: 'BgPane',
3277
+ parentId: newPaneId,
3278
+ type: 'artpack-image',
3279
+ collection: artpackBgPane.collection,
3280
+ image: artpackBgPane.image,
3281
+ src: artpackBgPane.src,
3282
+ srcSet: artpackBgPane.srcSet,
3283
+ alt: artpackBgPane.alt || `Artpack image`,
3284
+ objectFit: artpackBgPane.objectFit || 'cover',
3285
+ };
3286
+ allNodes.push(bgPaneNode);
3287
+ }
3288
+ // This helper only processes nodes, it doesn't modify the paneTemplate.
3289
+ // The deletion of `duplicatedPane.bgPane` will remain in `addTemplatePane`.
3290
+ }
3291
+
3292
+ return allNodes;
3293
+ }
3150
3294
  }
3151
3295
 
3152
3296
  export const globalCtx: NodesContext = new NodesContext();