astro-tractstack 2.0.20 → 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.
@@ -9,13 +9,16 @@ import type {
9
9
  StoragePane,
10
10
  StorageNode,
11
11
  StorageMarkdown,
12
- StorageBgPane,
13
12
  StorageGridLayoutNode,
13
+ StorageBgPane,
14
14
  ArtpackImageNode,
15
15
  BgImageNode,
16
16
  VisualBreakNode,
17
17
  TemplatePane,
18
18
  TemplateNode,
19
+ TemplateGridLayout,
20
+ TemplateMarkdown,
21
+ ParentClassesPayload,
19
22
  } from '@/types/compositorTypes';
20
23
  import type {
21
24
  BrandConfig,
@@ -31,7 +34,7 @@ import {
31
34
 
32
35
  type CopyMode = 'retain' | 'lorem' | 'blank';
33
36
 
34
- export type ExtractedCopy = StorageNode[];
37
+ export type ExtractedCopy = StorageNode[][];
35
38
 
36
39
  const LOREM_SHORT = 'Lorem ipsum dolor sit amet.';
37
40
  const LOREM_LONG =
@@ -101,127 +104,33 @@ function convertLiveNodeToStorageNode(
101
104
  return storageNode;
102
105
  }
103
106
 
104
- export async function savePaneToLibrary(
105
- paneId: string,
106
- tenantId: string,
107
- config: BrandConfig,
108
- formData: {
109
- title: string;
110
- category: string;
111
- copyMode: CopyMode;
112
- }
113
- ): Promise<BrandConfigState | null> {
107
+ export function extractPaneCopy(paneNode: PaneNode): ExtractedCopy {
114
108
  const ctx = getCtx();
115
- const { title, category, copyMode } = formData;
116
- const paneNode = ctx.allNodes.get().get(paneId) as PaneNode;
117
-
118
- if (!paneNode) {
119
- console.error('savePaneToLibrary: PaneNode not found.');
120
- return null;
121
- }
122
-
123
109
  const childNodes = ctx
124
- .getChildNodeIDs(paneId)
110
+ .getChildNodeIDs(paneNode.id)
125
111
  .map((id) => ctx.allNodes.get().get(id));
126
112
 
127
- const markdownNode = childNodes.find((n) => n?.nodeType === 'Markdown') as
128
- | MarkdownPaneFragmentNode
129
- | undefined;
130
-
131
- const bgPaneNode = childNodes.find((n) => n?.nodeType === 'BgPane') as
132
- | ArtpackImageNode
133
- | BgImageNode
134
- | VisualBreakNode
135
- | undefined;
136
-
137
- const newStorageMarkdown: StorageMarkdown | undefined = markdownNode
138
- ? {
139
- nodeType: 'Markdown',
140
- type: 'markdown',
141
- defaultClasses: markdownNode.defaultClasses || {},
142
- parentClasses: markdownNode.parentClasses || [],
143
- nodes:
144
- copyMode !== 'blank'
145
- ? ctx
146
- .getChildNodeIDs(markdownNode.id)
147
- .map((childId) => {
148
- const childNode = ctx.allNodes.get().get(childId) as FlatNode;
149
- return convertLiveNodeToStorageNode(childNode, ctx, copyMode);
150
- })
151
- .filter((n): n is StorageNode => n !== null)
152
- : [],
153
- }
154
- : undefined;
155
-
156
- const newStorageBgPane: StorageBgPane | undefined = bgPaneNode
157
- ? { ...bgPaneNode }
158
- : undefined;
159
-
160
- if (newStorageBgPane) {
161
- delete (newStorageBgPane as any).id;
162
- delete (newStorageBgPane as any).parentId;
163
- }
164
-
165
- const newStoragePane: StoragePane = {
166
- nodeType: 'Pane',
167
- title: title,
168
- slug: '',
169
- bgColour: paneNode.bgColour,
170
- isDecorative: paneNode.isDecorative,
171
- heightOffsetDesktop: paneNode.heightOffsetDesktop,
172
- heightOffsetMobile: paneNode.heightOffsetMobile,
173
- heightOffsetTablet: paneNode.heightOffsetTablet,
174
- heightRatioDesktop: paneNode.heightRatioDesktop,
175
- heightRatioMobile: paneNode.heightRatioMobile,
176
- heightRatioTablet: paneNode.heightRatioTablet,
177
- ...(newStorageMarkdown ? { markdowns: [newStorageMarkdown] } : {}),
178
- ...(newStorageBgPane ? { bgPane: newStorageBgPane } : {}),
179
- };
180
-
181
- const newLibraryEntry: DesignLibraryEntry = {
182
- category: category,
183
- title: title,
184
- markdownCount: 1,
185
- template: newStoragePane,
186
- };
187
-
188
- const currentState: BrandConfigState = convertToLocalState(config);
189
- const currentLibrary =
190
- (currentState.designLibrary as DesignLibraryConfig) || [];
191
-
192
- const existingEntryIndex = currentLibrary.findIndex(
193
- (entry) => entry.title === title && entry.category === category
194
- );
195
-
196
- let newLibrary: DesignLibraryConfig;
197
- if (existingEntryIndex !== -1) {
198
- newLibrary = [...currentLibrary];
199
- newLibrary[existingEntryIndex] = newLibraryEntry;
200
- } else {
201
- newLibrary = [...currentLibrary, newLibraryEntry];
202
- }
203
-
204
- const updatedState: BrandConfigState = {
205
- ...currentState,
206
- designLibrary: newLibrary,
207
- };
208
-
209
- const backendDTO: BrandConfig = convertToBackendFormat(updatedState);
113
+ const gridLayoutNode = childNodes.find(
114
+ (n) => n?.nodeType === 'GridLayoutNode'
115
+ ) as GridLayoutNode | undefined;
210
116
 
211
- try {
212
- await saveBrandConfig(tenantId, backendDTO);
213
- return updatedState;
214
- } catch (error) {
215
- console.error('Failed to save design library:', error);
216
- return null;
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
+ });
217
133
  }
218
- }
219
-
220
- export function extractPaneCopy(paneNode: PaneNode): ExtractedCopy {
221
- const ctx = getCtx();
222
- const childNodes = ctx
223
- .getChildNodeIDs(paneNode.id)
224
- .map((id) => ctx.allNodes.get().get(id));
225
134
 
226
135
  const markdownNode = childNodes.find((n) => n?.nodeType === 'Markdown') as
227
136
  | MarkdownPaneFragmentNode
@@ -231,34 +140,37 @@ export function extractPaneCopy(paneNode: PaneNode): ExtractedCopy {
231
140
  return [];
232
141
  }
233
142
 
234
- return (
143
+ const nodes =
235
144
  ctx
236
145
  .getChildNodeIDs(markdownNode.id)
237
146
  .map((childId) => {
238
147
  const childNode = ctx.allNodes.get().get(childId) as FlatNode;
239
148
  return convertLiveNodeToStorageNode(childNode, ctx, 'retain');
240
149
  })
241
- .filter((n): n is StorageNode => n !== null) || []
242
- );
150
+ .filter((n): n is StorageNode => n !== null) || [];
151
+
152
+ return [nodes];
243
153
  }
244
154
 
245
155
  export function mergeCopyIntoTemplate(
246
156
  template: StoragePane,
247
157
  copy: ExtractedCopy
248
158
  ): StoragePane {
249
- const newTemplate = { ...template };
250
- if (newTemplate.markdowns) {
251
- newTemplate.markdowns[0].nodes = copy;
252
- } else if (copy.length > 0) {
253
- if (!newTemplate.markdowns) newTemplate.markdowns = [];
254
- newTemplate.markdowns[0] = {
255
- nodeType: 'Markdown',
256
- type: 'markdown',
257
- defaultClasses: {},
258
- parentClasses: [],
259
- nodes: copy,
260
- };
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
+ }
261
172
  }
173
+
262
174
  return newTemplate;
263
175
  }
264
176
 
@@ -290,18 +202,63 @@ export function convertStorageToLiveTemplate(
290
202
  storagePane: StoragePane
291
203
  ): TemplatePane {
292
204
  const paneId = ulid();
293
- const markdownId = ulid();
294
- const flatNodeList: TemplateNode[] = [];
205
+ let liveMarkdown: TemplateMarkdown | undefined = undefined;
206
+ let liveGridLayout: TemplateGridLayout | undefined = undefined;
207
+ let liveBgPane: ArtpackImageNode | BgImageNode | VisualBreakNode | undefined =
208
+ undefined;
295
209
 
296
- if (storagePane.markdowns && storagePane.markdowns[0].nodes) {
297
- for (const storageNode of storagePane.markdowns[0].nodes) {
298
- const processedNodes = processStorageNode(storageNode, markdownId);
299
- flatNodeList.push(...processedNodes);
300
- }
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
+ };
301
260
  }
302
261
 
303
- let liveBgPane: ArtpackImageNode | BgImageNode | VisualBreakNode | undefined =
304
- undefined;
305
262
  if (storagePane.bgPane) {
306
263
  const bgPaneId = ulid();
307
264
  liveBgPane = {
@@ -311,23 +268,14 @@ export function convertStorageToLiveTemplate(
311
268
  };
312
269
  }
313
270
 
314
- const { gridLayout, ...restOfStoragePane } = storagePane;
271
+ const { markdowns, gridLayout, bgPane, ...restOfStoragePane } = storagePane;
272
+
315
273
  const liveTemplatePane: TemplatePane = {
316
274
  ...restOfStoragePane,
317
275
  id: paneId,
318
276
  parentId: '',
319
- markdown: {
320
- ...((storagePane.markdowns && storagePane.markdowns[0]) || {
321
- nodeType: 'Markdown',
322
- type: 'markdown',
323
- defaultClasses: {},
324
- parentClasses: [],
325
- }),
326
- id: markdownId,
327
- markdownId: markdownId,
328
- parentId: paneId,
329
- nodes: flatNodeList,
330
- },
277
+ markdown: liveMarkdown,
278
+ gridLayout: liveGridLayout,
331
279
  bgPane: liveBgPane,
332
280
  };
333
281
 
@@ -373,142 +321,129 @@ export function convertTemplateToAIShell(template: TemplatePane): string {
373
321
  defaultClasses: {},
374
322
  };
375
323
 
376
- // 1. Process parentClasses (layout)
377
- if (template.markdown?.parentClasses) {
378
- shell.parentClasses = template.markdown.parentClasses.map((layer) => {
379
- const newLayer: { mobile?: string; tablet?: string; desktop?: string } =
380
- {};
381
- if (layer.mobile && Object.keys(layer.mobile).length > 0) {
382
- newLayer.mobile = classObjectToString(layer.mobile);
383
- }
384
- if (layer.tablet && Object.keys(layer.tablet).length > 0) {
385
- newLayer.tablet = classObjectToString(layer.tablet);
386
- }
387
- if (layer.desktop && Object.keys(layer.desktop).length > 0) {
388
- newLayer.desktop = classObjectToString(layer.desktop);
389
- }
390
- return newLayer;
391
- });
392
- }
393
-
394
- // 2. Process defaultClasses (typography, etc.)
395
- if (template.markdown?.defaultClasses) {
396
- for (const tag in template.markdown.defaultClasses) {
397
- const styles = template.markdown.defaultClasses[tag];
398
- const newTagStyles: {
399
- mobile?: string;
400
- tablet?: string;
401
- desktop?: string;
402
- } = {};
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
+ }
403
342
 
404
- if (styles.mobile && Object.keys(styles.mobile).length > 0) {
405
- newTagStyles.mobile = classObjectToString(styles.mobile);
406
- }
407
- if (styles.tablet && Object.keys(styles.tablet).length > 0) {
408
- newTagStyles.tablet = classObjectToString(styles.tablet);
409
- }
410
- if (styles.desktop && Object.keys(styles.desktop).length > 0) {
411
- newTagStyles.desktop = classObjectToString(styles.desktop);
412
- }
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
+ }
413
362
 
414
- if (Object.keys(newTagStyles).length > 0) {
415
- shell.defaultClasses[tag] = newTagStyles;
363
+ if (Object.keys(newTagStyles).length > 0) {
364
+ shell.defaultClasses[tag] = newTagStyles;
365
+ }
416
366
  }
417
367
  }
418
- }
419
-
420
- return JSON.stringify(shell, null, 2);
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
368
 
472
- function applyIdMap(node: any, map: Map<string, string>) {
473
- if (!node || typeof node !== 'object') return;
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
+ }
474
418
 
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
- }
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
+ }
484
438
 
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);
439
+ if (Object.keys(newTagStyles).length > 0) {
440
+ shell.defaultClasses[tag] = newTagStyles;
441
+ }
442
+ }
443
+ }
497
444
  }
498
- }
499
445
 
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;
446
+ return JSON.stringify(shell, null, 2);
512
447
  }
513
448
 
514
449
  function convertLivePaneToStoragePane(
@@ -640,3 +575,165 @@ function convertLivePaneToStoragePane(
640
575
 
641
576
  return storagePane;
642
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
+ }