astro-tractstack 2.0.19 → 2.0.21

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 (54) 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/edit/PanelSwitch.tsx +3 -24
  17. package/templates/src/components/edit/SettingsPanel.tsx +0 -1
  18. package/templates/src/components/edit/ToolMode.tsx +6 -14
  19. package/templates/src/components/edit/pane/AddPanePanel.tsx +58 -25
  20. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +140 -133
  21. package/templates/src/components/edit/pane/AddPanePanel_paste.tsx +111 -0
  22. package/templates/src/components/edit/pane/RestylePaneModal.tsx +231 -282
  23. package/templates/src/components/edit/pane/steps/AiDesignStep.tsx +0 -5
  24. package/templates/src/components/edit/pane/steps/DesignLibraryStep.tsx +4 -13
  25. package/templates/src/components/edit/panels/StyleBreakPanel.tsx +1 -3
  26. package/templates/src/components/edit/panels/StyleElementPanel_update.tsx +0 -6
  27. package/templates/src/components/edit/panels/StyleImagePanel_update.tsx +0 -3
  28. package/templates/src/components/edit/panels/StyleLiElementPanel_update.tsx +0 -4
  29. package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +8 -5
  30. package/templates/src/components/edit/panels/StyleLinkPanel_update.tsx +1 -2
  31. package/templates/src/components/edit/panels/StyleParentPanel.tsx +1 -3
  32. package/templates/src/components/edit/panels/StyleParentPanel_update.tsx +2 -5
  33. package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +2 -8
  34. package/templates/src/components/edit/panels/StyleWidgetPanel_update.tsx +0 -4
  35. package/templates/src/components/edit/state/SaveToLibraryModal.tsx +29 -16
  36. package/templates/src/components/edit/storyfragment/StoryFragmentConfigPanel.tsx +9 -26
  37. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +7 -16
  38. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +5 -6
  39. package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +0 -5
  40. package/templates/src/components/fields/BackgroundImageWrapper.tsx +1 -7
  41. package/templates/src/components/fields/ColorPickerCombo.tsx +8 -12
  42. package/templates/src/components/fields/ViewportComboBox.tsx +4 -6
  43. package/templates/src/stores/nodes.ts +14 -6
  44. package/templates/src/stores/storykeep.ts +3 -3
  45. package/templates/src/types/compositorTypes.ts +2 -0
  46. package/templates/src/utils/compositor/TemplatePanes.ts +0 -76
  47. package/templates/src/utils/compositor/aiPaneParser.ts +3 -1
  48. package/templates/src/utils/compositor/designLibraryHelper.ts +523 -203
  49. package/templates/src/utils/helpers.ts +5 -4
  50. package/utils/inject-files.ts +6 -32
  51. package/templates/src/components/compositor/preview/VisualBreakPreview.tsx +0 -154
  52. package/templates/src/components/edit/pane/PageGen_preview.tsx +0 -511
  53. package/templates/src/utils/compositor/processMarkdown.ts +0 -445
  54. package/templates/src/utils/compositor/templateMarkdownStyles.ts +0 -1273
@@ -1,19 +1,24 @@
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
+ StorageGridLayoutNode,
13
+ StorageBgPane,
14
+ ArtpackImageNode,
15
+ BgImageNode,
16
+ VisualBreakNode,
17
+ TemplatePane,
18
+ TemplateNode,
19
+ TemplateGridLayout,
20
+ TemplateMarkdown,
21
+ ParentClassesPayload,
17
22
  } from '@/types/compositorTypes';
18
23
  import type {
19
24
  BrandConfig,
@@ -29,7 +34,7 @@ import {
29
34
 
30
35
  type CopyMode = 'retain' | 'lorem' | 'blank';
31
36
 
32
- export type ExtractedCopy = StorageNode[];
37
+ export type ExtractedCopy = StorageNode[][];
33
38
 
34
39
  const LOREM_SHORT = 'Lorem ipsum dolor sit amet.';
35
40
  const LOREM_LONG =
@@ -99,128 +104,34 @@ function convertLiveNodeToStorageNode(
99
104
  return storageNode;
100
105
  }
101
106
 
102
- export async function savePaneToLibrary(
103
- paneId: string,
104
- tenantId: string,
105
- config: BrandConfig,
106
- formData: {
107
- title: string;
108
- category: string;
109
- copyMode: CopyMode;
110
- }
111
- ): Promise<boolean> {
112
- const ctx = getCtx();
113
- const { title, category, copyMode } = formData;
114
- const paneNode = ctx.allNodes.get().get(paneId) as PaneNode;
115
-
116
- if (!paneNode) {
117
- console.error('savePaneToLibrary: PaneNode not found.');
118
- return false;
119
- }
120
-
121
- const childNodes = ctx
122
- .getChildNodeIDs(paneId)
123
- .map((id) => ctx.allNodes.get().get(id));
124
-
125
- const markdownNode = childNodes.find((n) => n?.nodeType === 'Markdown') as
126
- | MarkdownPaneFragmentNode
127
- | undefined;
128
-
129
- const bgPaneNode = childNodes.find((n) => n?.nodeType === 'BgPane') as
130
- | ArtpackImageNode
131
- | BgImageNode
132
- | VisualBreakNode
133
- | undefined;
134
-
135
- const newStorageMarkdown: StorageMarkdown | undefined = markdownNode
136
- ? {
137
- nodeType: 'Markdown',
138
- type: 'markdown',
139
- defaultClasses: markdownNode.defaultClasses || {},
140
- parentClasses: markdownNode.parentClasses || [],
141
- nodes:
142
- copyMode !== 'blank'
143
- ? ctx
144
- .getChildNodeIDs(markdownNode.id)
145
- .map((childId) => {
146
- const childNode = ctx.allNodes.get().get(childId) as FlatNode;
147
- return convertLiveNodeToStorageNode(childNode, ctx, copyMode);
148
- })
149
- .filter((n): n is StorageNode => n !== null)
150
- : [],
151
- }
152
- : undefined;
153
-
154
- const newStorageBgPane: StorageBgPane | undefined = bgPaneNode
155
- ? { ...bgPaneNode }
156
- : undefined;
157
-
158
- if (newStorageBgPane) {
159
- delete (newStorageBgPane as any).id;
160
- delete (newStorageBgPane as any).parentId;
161
- }
162
-
163
- const newStoragePane: StoragePane = {
164
- nodeType: 'Pane',
165
- title: title,
166
- slug: '',
167
- bgColour: paneNode.bgColour,
168
- isDecorative: paneNode.isDecorative,
169
- heightOffsetDesktop: paneNode.heightOffsetDesktop,
170
- heightOffsetMobile: paneNode.heightOffsetMobile,
171
- heightOffsetTablet: paneNode.heightOffsetTablet,
172
- heightRatioDesktop: paneNode.heightRatioDesktop,
173
- heightRatioMobile: paneNode.heightRatioMobile,
174
- heightRatioTablet: paneNode.heightRatioTablet,
175
- ...(newStorageMarkdown ? { markdowns: [newStorageMarkdown] } : {}),
176
- ...(newStorageBgPane ? { bgPane: newStorageBgPane } : {}),
177
- };
178
-
179
- const newLibraryEntry: DesignLibraryEntry = {
180
- category: category,
181
- title: title,
182
- markdownCount: 1,
183
- template: newStoragePane,
184
- };
185
-
186
- const currentState: BrandConfigState = convertToLocalState(config);
187
- const currentLibrary =
188
- (currentState.designLibrary as DesignLibraryConfig) || [];
189
-
190
- const existingEntryIndex = currentLibrary.findIndex(
191
- (entry) => entry.title === title && entry.category === category
192
- );
193
-
194
- let newLibrary: DesignLibraryConfig;
195
- if (existingEntryIndex !== -1) {
196
- newLibrary = [...currentLibrary];
197
- newLibrary[existingEntryIndex] = newLibraryEntry;
198
- } else {
199
- newLibrary = [...currentLibrary, newLibraryEntry];
200
- }
201
-
202
- const updatedState: BrandConfigState = {
203
- ...currentState,
204
- designLibrary: newLibrary,
205
- };
206
-
207
- const backendDTO: BrandConfig = convertToBackendFormat(updatedState);
208
-
209
- try {
210
- await saveBrandConfig(tenantId, backendDTO);
211
- return true;
212
- } catch (error) {
213
- console.error('Failed to save design library:', error);
214
- return false;
215
- }
216
- }
217
-
218
107
  export function extractPaneCopy(paneNode: PaneNode): ExtractedCopy {
219
108
  const ctx = getCtx();
220
109
  const childNodes = ctx
221
110
  .getChildNodeIDs(paneNode.id)
222
111
  .map((id) => ctx.allNodes.get().get(id));
223
112
 
113
+ const gridLayoutNode = childNodes.find(
114
+ (n) => n?.nodeType === 'GridLayoutNode'
115
+ ) as GridLayoutNode | undefined;
116
+
117
+ if (gridLayoutNode) {
118
+ const columns = ctx
119
+ .getChildNodeIDs(gridLayoutNode.id)
120
+ .map((id) => ctx.allNodes.get().get(id) as MarkdownPaneFragmentNode);
121
+
122
+ return columns.map((col) => {
123
+ return (
124
+ ctx
125
+ .getChildNodeIDs(col.id)
126
+ .map((childId) => {
127
+ const childNode = ctx.allNodes.get().get(childId) as FlatNode;
128
+ return convertLiveNodeToStorageNode(childNode, ctx, 'retain');
129
+ })
130
+ .filter((n): n is StorageNode => n !== null) || []
131
+ );
132
+ });
133
+ }
134
+
224
135
  const markdownNode = childNodes.find((n) => n?.nodeType === 'Markdown') as
225
136
  | MarkdownPaneFragmentNode
226
137
  | undefined;
@@ -229,34 +140,37 @@ export function extractPaneCopy(paneNode: PaneNode): ExtractedCopy {
229
140
  return [];
230
141
  }
231
142
 
232
- return (
143
+ const nodes =
233
144
  ctx
234
145
  .getChildNodeIDs(markdownNode.id)
235
146
  .map((childId) => {
236
147
  const childNode = ctx.allNodes.get().get(childId) as FlatNode;
237
148
  return convertLiveNodeToStorageNode(childNode, ctx, 'retain');
238
149
  })
239
- .filter((n): n is StorageNode => n !== null) || []
240
- );
150
+ .filter((n): n is StorageNode => n !== null) || [];
151
+
152
+ return [nodes];
241
153
  }
242
154
 
243
155
  export function mergeCopyIntoTemplate(
244
156
  template: StoragePane,
245
157
  copy: ExtractedCopy
246
158
  ): StoragePane {
247
- const newTemplate = { ...template };
248
- if (newTemplate.markdowns) {
249
- newTemplate.markdowns[0].nodes = copy;
250
- } else if (copy.length > 0) {
251
- if (!newTemplate.markdowns) newTemplate.markdowns = [];
252
- newTemplate.markdowns[0] = {
253
- nodeType: 'Markdown',
254
- type: 'markdown',
255
- defaultClasses: {},
256
- parentClasses: [],
257
- nodes: copy,
258
- };
159
+ const newTemplate = JSON.parse(JSON.stringify(template)) as StoragePane;
160
+
161
+ if (newTemplate.gridLayout && newTemplate.gridLayout.nodes) {
162
+ newTemplate.gridLayout.nodes.forEach((column, index) => {
163
+ if (copy[index] && copy[index].length > 0) {
164
+ column.nodes = copy[index];
165
+ }
166
+ });
167
+ } else if (newTemplate.markdowns && newTemplate.markdowns[0]) {
168
+ const flatCopy = copy.flat();
169
+ if (flatCopy.length > 0) {
170
+ newTemplate.markdowns[0].nodes = flatCopy;
171
+ }
259
172
  }
173
+
260
174
  return newTemplate;
261
175
  }
262
176
 
@@ -288,18 +202,63 @@ export function convertStorageToLiveTemplate(
288
202
  storagePane: StoragePane
289
203
  ): TemplatePane {
290
204
  const paneId = ulid();
291
- const markdownId = ulid();
292
- const flatNodeList: TemplateNode[] = [];
205
+ let liveMarkdown: TemplateMarkdown | undefined = undefined;
206
+ let liveGridLayout: TemplateGridLayout | undefined = undefined;
207
+ let liveBgPane: ArtpackImageNode | BgImageNode | VisualBreakNode | undefined =
208
+ undefined;
293
209
 
294
- if (storagePane.markdowns && storagePane.markdowns[0].nodes) {
295
- for (const storageNode of storagePane.markdowns[0].nodes) {
296
- const processedNodes = processStorageNode(storageNode, markdownId);
297
- flatNodeList.push(...processedNodes);
298
- }
210
+ if (storagePane.gridLayout) {
211
+ const gridId = ulid();
212
+ const storageGrid = storagePane.gridLayout;
213
+
214
+ liveGridLayout = {
215
+ id: gridId,
216
+ parentId: paneId,
217
+ nodeType: 'GridLayoutNode',
218
+ type: 'grid-layout',
219
+ gridColumns: storageGrid.gridColumns,
220
+ parentClasses: storageGrid.parentClasses as ParentClassesPayload,
221
+ defaultClasses: storageGrid.defaultClasses,
222
+ nodes:
223
+ storageGrid.nodes?.map((storageColumn) => {
224
+ const columnId = ulid();
225
+ const columnNodes =
226
+ storageColumn.nodes?.flatMap((storageNode) =>
227
+ processStorageNode(storageNode, columnId)
228
+ ) || [];
229
+ return {
230
+ id: columnId,
231
+ parentId: gridId,
232
+ nodeType: 'Markdown',
233
+ type: 'markdown',
234
+ markdownId: columnId,
235
+ defaultClasses: storageColumn.defaultClasses,
236
+ parentClasses: storageColumn.parentClasses,
237
+ gridClasses: storageColumn.gridClasses,
238
+ nodes: columnNodes,
239
+ };
240
+ }) || [],
241
+ };
242
+ } else if (storagePane.markdowns && storagePane.markdowns[0]) {
243
+ const markdownId = ulid();
244
+ const storageMarkdown = storagePane.markdowns[0];
245
+ const flatNodeList =
246
+ storageMarkdown.nodes?.flatMap((storageNode) =>
247
+ processStorageNode(storageNode, markdownId)
248
+ ) || [];
249
+
250
+ liveMarkdown = {
251
+ id: markdownId,
252
+ parentId: paneId,
253
+ nodeType: 'Markdown',
254
+ type: 'markdown',
255
+ markdownId: markdownId,
256
+ defaultClasses: storageMarkdown.defaultClasses,
257
+ parentClasses: storageMarkdown.parentClasses,
258
+ nodes: flatNodeList,
259
+ };
299
260
  }
300
261
 
301
- let liveBgPane: ArtpackImageNode | BgImageNode | VisualBreakNode | undefined =
302
- undefined;
303
262
  if (storagePane.bgPane) {
304
263
  const bgPaneId = ulid();
305
264
  liveBgPane = {
@@ -309,23 +268,14 @@ export function convertStorageToLiveTemplate(
309
268
  };
310
269
  }
311
270
 
312
- const { gridLayout, ...restOfStoragePane } = storagePane;
271
+ const { markdowns, gridLayout, bgPane, ...restOfStoragePane } = storagePane;
272
+
313
273
  const liveTemplatePane: TemplatePane = {
314
274
  ...restOfStoragePane,
315
275
  id: paneId,
316
276
  parentId: '',
317
- markdown: {
318
- ...((storagePane.markdowns && storagePane.markdowns[0]) || {
319
- nodeType: 'Markdown',
320
- type: 'markdown',
321
- defaultClasses: {},
322
- parentClasses: [],
323
- }),
324
- id: markdownId,
325
- markdownId: markdownId,
326
- parentId: paneId,
327
- nodes: flatNodeList,
328
- },
277
+ markdown: liveMarkdown,
278
+ gridLayout: liveGridLayout,
329
279
  bgPane: liveBgPane,
330
280
  };
331
281
 
@@ -371,49 +321,419 @@ export function convertTemplateToAIShell(template: TemplatePane): string {
371
321
  defaultClasses: {},
372
322
  };
373
323
 
374
- // 1. Process parentClasses (layout)
375
- if (template.markdown?.parentClasses) {
376
- shell.parentClasses = template.markdown.parentClasses.map((layer) => {
377
- const newLayer: { mobile?: string; tablet?: string; desktop?: string } =
378
- {};
379
- if (layer.mobile && Object.keys(layer.mobile).length > 0) {
380
- newLayer.mobile = classObjectToString(layer.mobile);
381
- }
382
- if (layer.tablet && Object.keys(layer.tablet).length > 0) {
383
- newLayer.tablet = classObjectToString(layer.tablet);
384
- }
385
- if (layer.desktop && Object.keys(layer.desktop).length > 0) {
386
- newLayer.desktop = classObjectToString(layer.desktop);
387
- }
388
- return newLayer;
389
- });
390
- }
324
+ if (template.gridLayout) {
325
+ // 1. Process parentClasses (layout)
326
+ if (template.gridLayout.parentClasses) {
327
+ shell.parentClasses = template.gridLayout.parentClasses.map((layer) => {
328
+ const newLayer: { mobile?: string; tablet?: string; desktop?: string } =
329
+ {};
330
+ if (layer.mobile && Object.keys(layer.mobile).length > 0) {
331
+ newLayer.mobile = classObjectToString(layer.mobile);
332
+ }
333
+ if (layer.tablet && Object.keys(layer.tablet).length > 0) {
334
+ newLayer.tablet = classObjectToString(layer.tablet);
335
+ }
336
+ if (layer.desktop && Object.keys(layer.desktop).length > 0) {
337
+ newLayer.desktop = classObjectToString(layer.desktop);
338
+ }
339
+ return newLayer;
340
+ });
341
+ }
391
342
 
392
- // 2. Process defaultClasses (typography, etc.)
393
- if (template.markdown?.defaultClasses) {
394
- for (const tag in template.markdown.defaultClasses) {
395
- const styles = template.markdown.defaultClasses[tag];
396
- const newTagStyles: {
397
- mobile?: string;
398
- tablet?: string;
399
- desktop?: string;
400
- } = {};
343
+ // 2. Process defaultClasses (typography, etc.)
344
+ if (template.gridLayout.defaultClasses) {
345
+ for (const tag in template.gridLayout.defaultClasses) {
346
+ const styles = template.gridLayout.defaultClasses[tag];
347
+ const newTagStyles: {
348
+ mobile?: string;
349
+ tablet?: string;
350
+ desktop?: string;
351
+ } = {};
352
+
353
+ if (styles.mobile && Object.keys(styles.mobile).length > 0) {
354
+ newTagStyles.mobile = classObjectToString(styles.mobile);
355
+ }
356
+ if (styles.tablet && Object.keys(styles.tablet).length > 0) {
357
+ newTagStyles.tablet = classObjectToString(styles.tablet);
358
+ }
359
+ if (styles.desktop && Object.keys(styles.desktop).length > 0) {
360
+ newTagStyles.desktop = classObjectToString(styles.desktop);
361
+ }
401
362
 
402
- if (styles.mobile && Object.keys(styles.mobile).length > 0) {
403
- newTagStyles.mobile = classObjectToString(styles.mobile);
404
- }
405
- if (styles.tablet && Object.keys(styles.tablet).length > 0) {
406
- newTagStyles.tablet = classObjectToString(styles.tablet);
407
- }
408
- if (styles.desktop && Object.keys(styles.desktop).length > 0) {
409
- newTagStyles.desktop = classObjectToString(styles.desktop);
363
+ if (Object.keys(newTagStyles).length > 0) {
364
+ shell.defaultClasses[tag] = newTagStyles;
365
+ }
410
366
  }
367
+ }
368
+
369
+ // 3. Process columns
370
+ if (template.gridLayout.nodes) {
371
+ shell.columns = template.gridLayout.nodes.map((column) => {
372
+ const newColumn: {
373
+ gridClasses: { mobile?: string; tablet?: string; desktop?: string };
374
+ } = {
375
+ gridClasses: {},
376
+ };
377
+
378
+ if (column.gridClasses) {
379
+ const styles = column.gridClasses;
380
+ const newGridClasses: {
381
+ mobile?: string;
382
+ tablet?: string;
383
+ desktop?: string;
384
+ } = {};
385
+
386
+ if (styles.mobile && Object.keys(styles.mobile).length > 0) {
387
+ newGridClasses.mobile = classObjectToString(styles.mobile);
388
+ }
389
+ if (styles.tablet && Object.keys(styles.tablet).length > 0) {
390
+ newGridClasses.tablet = classObjectToString(styles.tablet);
391
+ }
392
+ if (styles.desktop && Object.keys(styles.desktop).length > 0) {
393
+ newGridClasses.desktop = classObjectToString(styles.desktop);
394
+ }
395
+ newColumn.gridClasses = newGridClasses;
396
+ }
397
+ return newColumn;
398
+ });
399
+ }
400
+ } else if (template.markdown) {
401
+ // 1. Process parentClasses (layout)
402
+ if (template.markdown.parentClasses) {
403
+ shell.parentClasses = template.markdown.parentClasses.map((layer) => {
404
+ const newLayer: { mobile?: string; tablet?: string; desktop?: string } =
405
+ {};
406
+ if (layer.mobile && Object.keys(layer.mobile).length > 0) {
407
+ newLayer.mobile = classObjectToString(layer.mobile);
408
+ }
409
+ if (layer.tablet && Object.keys(layer.tablet).length > 0) {
410
+ newLayer.tablet = classObjectToString(layer.tablet);
411
+ }
412
+ if (layer.desktop && Object.keys(layer.desktop).length > 0) {
413
+ newLayer.desktop = classObjectToString(layer.desktop);
414
+ }
415
+ return newLayer;
416
+ });
417
+ }
418
+
419
+ // 2. Process defaultClasses (typography, etc.)
420
+ if (template.markdown.defaultClasses) {
421
+ for (const tag in template.markdown.defaultClasses) {
422
+ const styles = template.markdown.defaultClasses[tag];
423
+ const newTagStyles: {
424
+ mobile?: string;
425
+ tablet?: string;
426
+ desktop?: string;
427
+ } = {};
428
+
429
+ if (styles.mobile && Object.keys(styles.mobile).length > 0) {
430
+ newTagStyles.mobile = classObjectToString(styles.mobile);
431
+ }
432
+ if (styles.tablet && Object.keys(styles.tablet).length > 0) {
433
+ newTagStyles.tablet = classObjectToString(styles.tablet);
434
+ }
435
+ if (styles.desktop && Object.keys(styles.desktop).length > 0) {
436
+ newTagStyles.desktop = classObjectToString(styles.desktop);
437
+ }
411
438
 
412
- if (Object.keys(newTagStyles).length > 0) {
413
- shell.defaultClasses[tag] = newTagStyles;
439
+ if (Object.keys(newTagStyles).length > 0) {
440
+ shell.defaultClasses[tag] = newTagStyles;
441
+ }
414
442
  }
415
443
  }
416
444
  }
417
445
 
418
446
  return JSON.stringify(shell, null, 2);
419
447
  }
448
+
449
+ function convertLivePaneToStoragePane(
450
+ paneId: string,
451
+ ctx: NodesContext,
452
+ options: {
453
+ title: string;
454
+ copyMode: CopyMode;
455
+ }
456
+ ): StoragePane | null {
457
+ const paneNode = ctx.allNodes.get().get(paneId) as PaneNode;
458
+ if (!paneNode) {
459
+ console.error('convertLivePaneToStoragePane: PaneNode not found.');
460
+ return null;
461
+ }
462
+
463
+ const { title, copyMode } = options;
464
+ const childNodes = ctx
465
+ .getChildNodeIDs(paneId)
466
+ .map((id) => ctx.allNodes.get().get(id));
467
+
468
+ const markdownNode = childNodes.find((n) => n?.nodeType === 'Markdown') as
469
+ | MarkdownPaneFragmentNode
470
+ | undefined;
471
+
472
+ const gridLayoutNode = childNodes.find(
473
+ (n) => n?.nodeType === 'GridLayoutNode'
474
+ ) as GridLayoutNode | undefined;
475
+
476
+ const bgPaneNode = childNodes.find((n) => n?.nodeType === 'BgPane') as
477
+ | ArtpackImageNode
478
+ | BgImageNode
479
+ | VisualBreakNode
480
+ | undefined;
481
+
482
+ let storageMarkdown: StorageMarkdown | undefined;
483
+ let storageGridLayout: StorageGridLayoutNode | undefined;
484
+
485
+ if (markdownNode) {
486
+ storageMarkdown = {
487
+ nodeType: 'Markdown',
488
+ type: 'markdown',
489
+ defaultClasses: markdownNode.defaultClasses || {},
490
+ parentClasses: markdownNode.parentClasses || [],
491
+ nodes:
492
+ copyMode !== 'blank'
493
+ ? ctx
494
+ .getChildNodeIDs(markdownNode.id)
495
+ .map((childId) => {
496
+ const childNode = ctx.allNodes.get().get(childId) as FlatNode;
497
+ return convertLiveNodeToStorageNode(childNode, ctx, copyMode);
498
+ })
499
+ .filter((n): n is StorageNode => n !== null)
500
+ : [],
501
+ };
502
+ } else if (gridLayoutNode) {
503
+ const { id, parentId, isChanged, ...restOfGrid } = gridLayoutNode;
504
+ storageGridLayout = {
505
+ ...restOfGrid,
506
+ nodes: ctx
507
+ .getChildNodeIDs(gridLayoutNode.id)
508
+ .map((columnId) => {
509
+ const columnNode = ctx.allNodes
510
+ .get()
511
+ .get(columnId) as MarkdownPaneFragmentNode;
512
+ if (!columnNode) return null;
513
+
514
+ const {
515
+ id,
516
+ parentId,
517
+ isChanged,
518
+ markdownId,
519
+ parentCss,
520
+ ...restOfColumn
521
+ } = columnNode;
522
+
523
+ const storageColumn: StorageMarkdown = {
524
+ ...restOfColumn,
525
+ nodeType: 'Markdown',
526
+ type: 'markdown',
527
+ nodes:
528
+ copyMode !== 'blank'
529
+ ? ctx
530
+ .getChildNodeIDs(columnNode.id)
531
+ .map((childId) => {
532
+ const childNode = ctx.allNodes
533
+ .get()
534
+ .get(childId) as FlatNode;
535
+ return convertLiveNodeToStorageNode(
536
+ childNode,
537
+ ctx,
538
+ copyMode
539
+ );
540
+ })
541
+ .filter((n): n is StorageNode => n !== null)
542
+ : [],
543
+ };
544
+ return storageColumn;
545
+ })
546
+ .filter((n): n is StorageMarkdown => n !== null),
547
+ };
548
+ }
549
+
550
+ const storageBgPane: StorageBgPane | undefined = bgPaneNode
551
+ ? { ...bgPaneNode }
552
+ : undefined;
553
+
554
+ if (storageBgPane) {
555
+ delete (storageBgPane as any).id;
556
+ delete (storageBgPane as any).parentId;
557
+ }
558
+
559
+ const storagePane: StoragePane = {
560
+ nodeType: 'Pane',
561
+ title: title,
562
+ slug: '',
563
+ bgColour: paneNode.bgColour,
564
+ isDecorative: paneNode.isDecorative,
565
+ heightOffsetDesktop: paneNode.heightOffsetDesktop,
566
+ heightOffsetMobile: paneNode.heightOffsetMobile,
567
+ heightOffsetTablet: paneNode.heightOffsetTablet,
568
+ heightRatioDesktop: paneNode.heightRatioDesktop,
569
+ heightRatioMobile: paneNode.heightRatioMobile,
570
+ heightRatioTablet: paneNode.heightRatioTablet,
571
+ ...(storageMarkdown ? { markdowns: [storageMarkdown] } : {}),
572
+ ...(storageGridLayout ? { gridLayout: storageGridLayout } : {}),
573
+ ...(storageBgPane ? { bgPane: storageBgPane } : {}),
574
+ };
575
+
576
+ return storagePane;
577
+ }
578
+
579
+ export async function savePaneToLibrary(
580
+ paneId: string,
581
+ tenantId: string,
582
+ config: BrandConfig,
583
+ formData: {
584
+ title: string;
585
+ category: string;
586
+ copyMode: CopyMode;
587
+ }
588
+ ): Promise<BrandConfigState | null> {
589
+ const ctx = getCtx();
590
+ const { title, category, copyMode } = formData;
591
+
592
+ const newStoragePane = convertLivePaneToStoragePane(paneId, ctx, {
593
+ title,
594
+ copyMode,
595
+ });
596
+
597
+ if (!newStoragePane) {
598
+ console.error(
599
+ 'savePaneToLibrary: Failed to convert pane to storage format.'
600
+ );
601
+ return null;
602
+ }
603
+
604
+ let actualMarkdownCount = 0;
605
+ if (newStoragePane.gridLayout && newStoragePane.gridLayout.nodes) {
606
+ actualMarkdownCount = newStoragePane.gridLayout.nodes.length;
607
+ } else if (newStoragePane.markdowns) {
608
+ actualMarkdownCount = newStoragePane.markdowns.length;
609
+ }
610
+
611
+ const newLibraryEntry: DesignLibraryEntry = {
612
+ category: category,
613
+ title: title,
614
+ markdownCount: actualMarkdownCount,
615
+ template: newStoragePane,
616
+ };
617
+
618
+ const currentState: BrandConfigState = convertToLocalState(config);
619
+ const currentLibrary =
620
+ (currentState.designLibrary as DesignLibraryConfig) || [];
621
+
622
+ const existingEntryIndex = currentLibrary.findIndex(
623
+ (entry) => entry.title === title && entry.category === category
624
+ );
625
+
626
+ let newLibrary: DesignLibraryConfig;
627
+ if (existingEntryIndex !== -1) {
628
+ newLibrary = [...currentLibrary];
629
+ newLibrary[existingEntryIndex] = newLibraryEntry;
630
+ } else {
631
+ newLibrary = [...currentLibrary, newLibraryEntry];
632
+ }
633
+
634
+ const updatedState: BrandConfigState = {
635
+ ...currentState,
636
+ designLibrary: newLibrary,
637
+ };
638
+
639
+ const backendDTO: BrandConfig = convertToBackendFormat(updatedState);
640
+
641
+ try {
642
+ await saveBrandConfig(tenantId, backendDTO);
643
+ return updatedState;
644
+ } catch (error) {
645
+ console.error('Failed to save design library:', error);
646
+ return null;
647
+ }
648
+ }
649
+
650
+ export async function copyPaneToClipboard(paneId: string): Promise<boolean> {
651
+ const ctx = getCtx();
652
+ const paneNode = ctx.allNodes.get().get(paneId) as PaneNode;
653
+
654
+ const storagePane = convertLivePaneToStoragePane(paneId, ctx, {
655
+ title: paneNode?.title || 'Pasted Pane',
656
+ copyMode: 'retain',
657
+ });
658
+
659
+ if (!storagePane) {
660
+ return false;
661
+ }
662
+
663
+ try {
664
+ const jsonPayload = JSON.stringify(storagePane, null, 2);
665
+ await navigator.clipboard.writeText(jsonPayload);
666
+ return true;
667
+ } catch (error) {
668
+ console.error('Failed to copy pane to clipboard:', error);
669
+ return false;
670
+ }
671
+ }
672
+
673
+ function buildIdMap(node: any, map: Map<string, string>) {
674
+ if (!node || typeof node !== 'object') return;
675
+
676
+ if (node.id && !map.has(node.id)) {
677
+ map.set(node.id, ulid());
678
+ }
679
+ // Markdown nodes have a second unique identifier
680
+ if (node.markdownId && !map.has(node.markdownId)) {
681
+ map.set(node.markdownId, ulid());
682
+ }
683
+
684
+ // Recursively traverse all possible child arrays/objects
685
+ if (node.markdowns) {
686
+ node.markdowns.forEach((n: any) => buildIdMap(n, map));
687
+ }
688
+ if (node.gridLayout) {
689
+ buildIdMap(node.gridLayout, map);
690
+ }
691
+ if (node.nodes) {
692
+ node.nodes.forEach((n: any) => buildIdMap(n, map));
693
+ }
694
+ if (node.bgPane) {
695
+ buildIdMap(node.bgPane, map);
696
+ }
697
+ }
698
+
699
+ function applyIdMap(node: any, map: Map<string, string>) {
700
+ if (!node || typeof node !== 'object') return;
701
+
702
+ if (node.id && map.has(node.id)) {
703
+ node.id = map.get(node.id);
704
+ }
705
+ if (node.parentId && map.has(node.parentId)) {
706
+ node.parentId = map.get(node.parentId);
707
+ }
708
+ if (node.markdownId && map.has(node.markdownId)) {
709
+ node.markdownId = map.get(node.markdownId);
710
+ }
711
+
712
+ // Recursively traverse all possible child arrays/objects
713
+ if (node.markdowns) {
714
+ node.markdowns.forEach((n: any) => applyIdMap(n, map));
715
+ }
716
+ if (node.gridLayout) {
717
+ applyIdMap(node.gridLayout, map);
718
+ }
719
+ if (node.nodes) {
720
+ node.nodes.forEach((n: any) => applyIdMap(n, map));
721
+ }
722
+ if (node.bgPane) {
723
+ applyIdMap(node.bgPane, map);
724
+ }
725
+ }
726
+
727
+ export function remapPaneIds(pane: StoragePane): StoragePane {
728
+ const idMap = new Map<string, string>();
729
+ // The input object may have come from JSON.parse, so we treat it as 'any' internally
730
+ const clone = JSON.parse(JSON.stringify(pane as any));
731
+
732
+ // First pass: Traverse the entire structure to build a complete map of old IDs to new IDs.
733
+ buildIdMap(clone, idMap);
734
+
735
+ // Second pass: Traverse again to apply the new IDs, ensuring parent-child relationships are correct.
736
+ applyIdMap(clone, idMap);
737
+
738
+ return clone as StoragePane;
739
+ }