astro-tractstack 2.0.19 → 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 (55) 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 +2 -8
  22. package/templates/src/components/edit/pane/AddPanePanel_paste.tsx +111 -0
  23. package/templates/src/components/edit/pane/RestylePaneModal.tsx +6 -13
  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_update.tsx +0 -3
  29. package/templates/src/components/edit/panels/StyleLiElementPanel_update.tsx +0 -4
  30. package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +8 -5
  31. package/templates/src/components/edit/panels/StyleLinkPanel_update.tsx +1 -2
  32. package/templates/src/components/edit/panels/StyleParentPanel.tsx +1 -3
  33. package/templates/src/components/edit/panels/StyleParentPanel_update.tsx +2 -5
  34. package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +2 -8
  35. package/templates/src/components/edit/panels/StyleWidgetPanel_update.tsx +0 -4
  36. package/templates/src/components/edit/state/SaveToLibraryModal.tsx +27 -16
  37. package/templates/src/components/edit/storyfragment/StoryFragmentConfigPanel.tsx +9 -26
  38. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +7 -16
  39. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +5 -6
  40. package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +0 -5
  41. package/templates/src/components/fields/BackgroundImageWrapper.tsx +1 -7
  42. package/templates/src/components/fields/ColorPickerCombo.tsx +8 -12
  43. package/templates/src/components/fields/ViewportComboBox.tsx +4 -6
  44. package/templates/src/stores/nodes.ts +14 -6
  45. package/templates/src/stores/storykeep.ts +3 -3
  46. package/templates/src/types/compositorTypes.ts +2 -0
  47. package/templates/src/utils/compositor/TemplatePanes.ts +0 -76
  48. package/templates/src/utils/compositor/aiPaneParser.ts +3 -1
  49. package/templates/src/utils/compositor/designLibraryHelper.ts +240 -17
  50. package/templates/src/utils/helpers.ts +5 -4
  51. package/utils/inject-files.ts +6 -32
  52. package/templates/src/components/compositor/preview/VisualBreakPreview.tsx +0 -154
  53. package/templates/src/components/edit/pane/PageGen_preview.tsx +0 -511
  54. package/templates/src/utils/compositor/processMarkdown.ts +0 -445
  55. package/templates/src/utils/compositor/templateMarkdownStyles.ts +0 -1273
@@ -8,7 +8,6 @@ import ColorPickerCombo from './ColorPickerCombo';
8
8
  import { getCtx } from '@/stores/nodes';
9
9
  import { hasArtpacksStore, settingsPanelStore } from '@/stores/storykeep';
10
10
  import { cloneDeep } from '@/utils/helpers';
11
- import type { BrandConfig } from '@/types/tractstack';
12
11
  import type {
13
12
  BgImageNode,
14
13
  ArtpackImageNode,
@@ -18,7 +17,6 @@ import { isArtpackImageNode } from '@/utils/compositor/typeGuards';
18
17
 
19
18
  export interface BackgroundImageWrapperProps {
20
19
  paneId: string;
21
- config?: BrandConfig;
22
20
  }
23
21
 
24
22
  const CheckIcon = () => (
@@ -51,10 +49,7 @@ const ChevronDownIcon = () => (
51
49
  </svg>
52
50
  );
53
51
 
54
- const BackgroundImageWrapper = ({
55
- paneId,
56
- config,
57
- }: BackgroundImageWrapperProps) => {
52
+ const BackgroundImageWrapper = ({ paneId }: BackgroundImageWrapperProps) => {
58
53
  const ctx = getCtx();
59
54
  const allNodes = useStore(ctx.allNodes);
60
55
  const $artpacks = useStore(hasArtpacksStore);
@@ -165,7 +160,6 @@ const BackgroundImageWrapper = ({
165
160
  title="Pane Background Color"
166
161
  defaultColor={(allNodes.get(paneId) as PaneNode)?.bgColour || ''}
167
162
  onColorChange={handleColorChange}
168
- config={config!}
169
163
  allowNull={true}
170
164
  />
171
165
  {!bgNode && (
@@ -12,12 +12,11 @@ import {
12
12
  findClosestTailwindColor,
13
13
  getComputedColor,
14
14
  } from '@/utils/compositor/tailwindColors';
15
+ import { brandConfigStore } from '@/stores/storykeep';
15
16
  import { debounce, useDropdownDirection } from '@/utils/helpers';
16
- import type { BrandConfig } from '@/types/tractstack';
17
17
 
18
18
  export interface ColorPickerProps {
19
19
  title: string;
20
- config: BrandConfig;
21
20
  defaultColor: string;
22
21
  onColorChange: (color: string) => void;
23
22
  skipTailwind?: boolean;
@@ -28,14 +27,14 @@ const ColorPickerCombo = ({
28
27
  title,
29
28
  defaultColor,
30
29
  onColorChange,
31
- config,
32
30
  skipTailwind = false,
33
31
  allowNull = false,
34
32
  }: ColorPickerProps) => {
33
+ const brandColors = brandConfigStore.get()?.BRAND_COLOURS || '';
35
34
  const [hexColor, setHexColor] = useState(defaultColor);
36
35
  const initialTailwindColor = skipTailwind
37
36
  ? ''
38
- : hexToTailwind(defaultColor, config.BRAND_COLOURS) || '';
37
+ : hexToTailwind(defaultColor, brandColors) || '';
39
38
  const [selectedTailwindColor, setSelectedTailwindColor] =
40
39
  useState(initialTailwindColor);
41
40
  const [query, setQuery] = useState('');
@@ -79,10 +78,7 @@ const ColorPickerCombo = ({
79
78
  setHexColor(computedColor);
80
79
 
81
80
  if (!skipTailwind) {
82
- const exactTailwindColor = hexToTailwind(
83
- computedColor,
84
- config.BRAND_COLOURS
85
- );
81
+ const exactTailwindColor = hexToTailwind(computedColor, brandColors);
86
82
  if (exactTailwindColor) {
87
83
  setSelectedTailwindColor(exactTailwindColor);
88
84
  setQuery('');
@@ -101,7 +97,7 @@ const ColorPickerCombo = ({
101
97
 
102
98
  onColorChange(computedColor);
103
99
  }, 16),
104
- [onColorChange, skipTailwind, config.BRAND_COLOURS]
100
+ [onColorChange, skipTailwind, brandColors]
105
101
  );
106
102
 
107
103
  // Handle Tailwind color selection
@@ -114,12 +110,12 @@ const ColorPickerCombo = ({
114
110
  setQuery(''); // Clear query after selection
115
111
 
116
112
  const newHexColor = getComputedColor(
117
- tailwindToHex(`bg-${newTailwindColor}`, config.BRAND_COLOURS || null)
113
+ tailwindToHex(`bg-${newTailwindColor}`, brandColors || null)
118
114
  );
119
115
  setHexColor(newHexColor);
120
116
  onColorChange(newHexColor);
121
117
  },
122
- [onColorChange, config, skipTailwind]
118
+ [onColorChange, brandColors, skipTailwind]
123
119
  );
124
120
 
125
121
  // New function to handle color removal
@@ -259,7 +255,7 @@ const ColorPickerCombo = ({
259
255
  style={{
260
256
  backgroundColor: tailwindToHex(
261
257
  `bg-${color}`,
262
- config.BRAND_COLOURS || null
258
+ brandColors
263
259
  ),
264
260
  }}
265
261
  />
@@ -13,9 +13,8 @@ import DevicePhoneMobileIcon from '@heroicons/react/24/outline/DevicePhoneMobile
13
13
  import DeviceTabletIcon from '@heroicons/react/24/outline/DeviceTabletIcon';
14
14
  import ComputerDesktopIcon from '@heroicons/react/24/outline/ComputerDesktopIcon';
15
15
  import { classNames } from '@/utils/helpers';
16
- import { settingsPanelStore } from '@/stores/storykeep';
16
+ import { brandConfigStore, settingsPanelStore } from '@/stores/storykeep';
17
17
  import { tailwindToHex, colorValues } from '@/utils/compositor/tailwindColors';
18
- import type { BrandConfig } from '@/types/tractstack';
19
18
 
20
19
  interface ViewportComboBoxProps {
21
20
  value: string;
@@ -29,7 +28,6 @@ interface ViewportComboBoxProps {
29
28
  allowNegative?: boolean;
30
29
  isNegative?: boolean;
31
30
  isInferred?: boolean;
32
- config: BrandConfig;
33
31
  }
34
32
 
35
33
  const ViewportComboBox = ({
@@ -40,8 +38,8 @@ const ViewportComboBox = ({
40
38
  allowNegative = false,
41
39
  isNegative = false,
42
40
  isInferred = false,
43
- config,
44
41
  }: ViewportComboBoxProps) => {
42
+ const brandColors = brandConfigStore.get()?.BRAND_COLOURS || '';
45
43
  const [internalValue, setInternalValue] = useState(value);
46
44
  const [query, setQuery] = useState('');
47
45
  const [isNowNegative, setIsNowNegative] = useState(isNegative);
@@ -174,7 +172,7 @@ const ViewportComboBox = ({
174
172
  style={{
175
173
  backgroundColor: tailwindToHex(
176
174
  internalValue,
177
- config.BRAND_COLOURS || null
175
+ brandColors
178
176
  ),
179
177
  }}
180
178
  />
@@ -218,7 +216,7 @@ const ViewportComboBox = ({
218
216
  style={{
219
217
  backgroundColor: tailwindToHex(
220
218
  item,
221
- config.BRAND_COLOURS || null
219
+ brandColors
222
220
  ),
223
221
  }}
224
222
  />
@@ -103,7 +103,6 @@ export class NodesContext {
103
103
  toolAddModeStore = map<{ value: ToolAddMode }>({
104
104
  value: 'p',
105
105
  });
106
- showGuids = atom<boolean>(false);
107
106
 
108
107
  /**
109
108
  * Sets an edit lock on a specific node to prevent re-renders during editing
@@ -1784,10 +1783,15 @@ export class NodesContext {
1784
1783
  ownerId
1785
1784
  );
1786
1785
 
1787
- // Remove bgPane from the pane object if it exists, as it's now a separate node
1788
1786
  if (duplicatedPane.bgPane) {
1789
1787
  delete duplicatedPane.bgPane;
1790
1788
  }
1789
+ if (duplicatedPane.markdown) {
1790
+ delete duplicatedPane.markdown;
1791
+ }
1792
+ if (duplicatedPane.gridLayout) {
1793
+ delete duplicatedPane.gridLayout;
1794
+ }
1791
1795
 
1792
1796
  this.addNode(duplicatedPane as PaneNode);
1793
1797
  this.addNodes(allNodes);
@@ -1837,11 +1841,15 @@ export class NodesContext {
1837
1841
  duplicatedPaneId
1838
1842
  );
1839
1843
 
1840
- // Remove bgPane from the pane object if it exists, as it's now a separate node
1841
- // This preserves the original logic
1842
1844
  if (duplicatedPane.bgPane) {
1843
1845
  delete duplicatedPane.bgPane;
1844
1846
  }
1847
+ if (duplicatedPane.markdown) {
1848
+ delete duplicatedPane.markdown;
1849
+ }
1850
+ if (duplicatedPane.gridLayout) {
1851
+ delete duplicatedPane.gridLayout;
1852
+ }
1845
1853
 
1846
1854
  const storyFragmentNode = ownerNode as StoryFragmentNode;
1847
1855
  let specificIdx = -1;
@@ -2009,7 +2017,7 @@ export class NodesContext {
2009
2017
 
2010
2018
  let autoCreatedMarkdownNode: MarkdownPaneFragmentNode | null = null;
2011
2019
 
2012
- console.log(`--- [TRAP - TEMPLATE BEFORE] ---`, cloneDeep(node));
2020
+ //console.log(`--- [TRAP - TEMPLATE BEFORE] ---`, cloneDeep(node));
2013
2021
  // 3. HANDLE EMPTY PANE BY AUTO-CREATING A MARKDOWN NODE
2014
2022
  if (targetNode.nodeType === 'Pane') {
2015
2023
  // Create a minimal markdown node to act as the container
@@ -2087,7 +2095,7 @@ export class NodesContext {
2087
2095
  );
2088
2096
  }
2089
2097
 
2090
- console.log(`--- [TRAP - FLATTENED AFTER] ---`, cloneDeep(flattenedNodes));
2098
+ //console.log(`--- [TRAP - FLATTENED AFTER] ---`, cloneDeep(flattenedNodes));
2091
2099
 
2092
2100
  // 5. PERFORM REMAINING STATE MUTATIONS
2093
2101
  if (originalPaneNode) {
@@ -6,6 +6,7 @@ import type {
6
6
  FullContentMapItem,
7
7
  Theme,
8
8
  ArtpacksStore,
9
+ BrandConfig,
9
10
  } from '@/types/tractstack';
10
11
  import type { SettingsPanelSignal, ViewportKey } from '@/types/compositorTypes';
11
12
  import type {
@@ -21,9 +22,6 @@ export const fullContentMapStore = atom<FullContentMapItem[]>([]);
21
22
  export const hasArtpacksStore = map<ArtpacksStore>({});
22
23
  export const urlParamsStore = atom<Record<string, string | boolean>>({});
23
24
  export const canonicalURLStore = atom<string>('');
24
- export const brandColourStore = atom<string>(
25
- '10120d,fcfcfc,f58333,c8df8c,293f58,a7b1b7,393d34,e3e3e3'
26
- );
27
25
  export const preferredThemeStore = atom<Theme>('light');
28
26
 
29
27
  export const hasAssemblyAIStore = atom<boolean>(false);
@@ -156,6 +154,8 @@ export const resetStoryKeepState = () => {
156
154
  canRedoStore.set(false);
157
155
  };
158
156
 
157
+ export const brandConfigStore = atom<BrandConfig | null>(null);
158
+
159
159
  export const settingsPanelStore = atom<SettingsPanelSignal | null>(null);
160
160
  export const stylePanelTargetMemoryStore = atom<Map<string, number>>(
161
161
  new Map<string, number>()
@@ -43,6 +43,7 @@ export enum PaneAddMode {
43
43
  BREAK = 'BREAK',
44
44
  REUSE = 'REUSE',
45
45
  CODEHOOK = 'CODEHOOK',
46
+ PASTE = 'PASTE',
46
47
  }
47
48
 
48
49
  export enum PaneConfigMode {
@@ -478,6 +479,7 @@ export type StorageGridLayoutNode = {
478
479
  tablet: number;
479
480
  desktop: number;
480
481
  };
482
+ nodes?: StorageMarkdown[];
481
483
  };
482
484
 
483
485
  export type StoragePane = Omit<
@@ -1,13 +1,4 @@
1
- import {
2
- //TemplateAsideNode,
3
- TemplateH2Node,
4
- TemplateH3Node,
5
- TemplatePNode,
6
- } from './TemplateNodes';
7
- import { getTemplateSimpleMarkdown } from './TemplateMarkdowns';
8
- import { getColor, tailwindToHex } from './tailwindColors';
9
1
  import type { TemplatePane } from '@/types/compositorTypes';
10
- import type { Theme } from '@/types/tractstack';
11
2
 
12
3
  export const getTemplateVisualBreakPane = (variant: string) => {
13
4
  // colour will be set on insert based on adjacent nodes
@@ -31,70 +22,3 @@ export const getTemplateVisualBreakPane = (variant: string) => {
31
22
  },
32
23
  } as TemplatePane;
33
24
  };
34
-
35
- export const getTemplateSimplePane = (
36
- theme: Theme,
37
- brand: string,
38
- useOdd: boolean = false
39
- ) => {
40
- return {
41
- nodeType: 'Pane',
42
- title: '',
43
- slug: '',
44
- bgColour: tailwindToHex(
45
- getColor(
46
- {
47
- light: !useOdd ? 'brand-2' : 'white',
48
- 'light-bw': !useOdd ? 'white' : 'brand-2',
49
- 'light-bold': !useOdd ? 'brand-2' : 'white',
50
- dark: !useOdd ? 'black' : 'brand-1',
51
- 'dark-bw': !useOdd ? 'black' : 'brand-1',
52
- 'dark-bold': !useOdd ? 'brand-1' : 'black',
53
- },
54
- theme
55
- ),
56
- brand
57
- ),
58
- markdown: {
59
- ...getTemplateSimpleMarkdown(theme),
60
- nodes: [
61
- { ...TemplateH2Node, copy: 'H2 node in simple pane' },
62
- { ...TemplatePNode, copy: 'P node in simple pane' },
63
- { ...TemplateH3Node, copy: 'H3 node in simple pane' },
64
- //{ ...TemplateAsideNode, copy: "Aside node in simple pane" },
65
- ],
66
- },
67
- } as TemplatePane;
68
- };
69
-
70
- export const getTemplateMarkdownPane = (
71
- theme: Theme,
72
- variant: string,
73
- brand: string,
74
- useOdd: boolean = false
75
- ) => {
76
- console.log(`variant: ${variant}`);
77
- return {
78
- nodeType: 'Pane',
79
- title: '',
80
- slug: '',
81
- bgColour: tailwindToHex(
82
- getColor(
83
- {
84
- light: !useOdd ? 'brand-2' : 'white',
85
- 'light-bw': !useOdd ? 'white' : 'brand-2',
86
- 'light-bold': !useOdd ? 'brand-2' : 'white',
87
- dark: !useOdd ? 'black' : 'brand-1',
88
- 'dark-bw': !useOdd ? 'black' : 'brand-1',
89
- 'dark-bold': !useOdd ? 'brand-1' : 'black',
90
- },
91
- theme
92
- ),
93
- brand
94
- ),
95
- markdown: {
96
- ...getTemplateSimpleMarkdown(theme),
97
- markdownBody: `## add a catchy title here\n\nyour story continues... and continues... and continues... and continues... and continues... and continues... with nice layout and typography.\n\n[Try it now!](try) &nbsp; [Learn more](learn)\n`,
98
- },
99
- } as TemplatePane;
100
- };
@@ -642,7 +642,7 @@ export const parseAiPane = (
642
642
  ): Omit<TemplatePane, 'nodes'> & {
643
643
  nodes?: (TemplateMarkdown | GridLayoutNode)[];
644
644
  } => {
645
- console.log('--- ENTERING parseAiPane ---', { shellJson, copyHtml, layout });
645
+ //console.log('--- ENTERING parseAiPane ---', { shellJson, copyHtml, layout });
646
646
 
647
647
  const shell: ShellJson = JSON.parse(shellJson);
648
648
  const paneId = ulid();
@@ -697,6 +697,7 @@ export const parseAiPane = (
697
697
  isDecorative: false,
698
698
  gridLayout: gridLayoutNode,
699
699
  };
700
+ /*
700
701
  console.log({
701
702
  shellDefaults: shellDefaults,
702
703
  transformedParentClasses: transformedParentClasses,
@@ -704,6 +705,7 @@ export const parseAiPane = (
704
705
  gridLayoutNode: gridLayoutNode,
705
706
  templatePane: templatePane,
706
707
  });
708
+ */
707
709
  return templatePane;
708
710
  }
709
711
 
@@ -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
@@ -208,10 +210,10 @@ export async function savePaneToLibrary(
208
210
 
209
211
  try {
210
212
  await saveBrandConfig(tenantId, backendDTO);
211
- return true;
213
+ return updatedState;
212
214
  } catch (error) {
213
215
  console.error('Failed to save design library:', error);
214
- return false;
216
+ return null;
215
217
  }
216
218
  }
217
219
 
@@ -417,3 +419,224 @@ export function convertTemplateToAIShell(template: TemplatePane): string {
417
419
 
418
420
  return JSON.stringify(shell, null, 2);
419
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
  }