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
@@ -157,7 +157,9 @@ export const resetStoryKeepState = () => {
157
157
  };
158
158
 
159
159
  export const settingsPanelStore = atom<SettingsPanelSignal | null>(null);
160
-
160
+ export const stylePanelTargetMemoryStore = atom<Map<string, number>>(
161
+ new Map<string, number>()
162
+ );
161
163
  export const styleElementInfoStore = map<{
162
164
  markdownParentId: string | null;
163
165
  tagName: string | null;
@@ -76,6 +76,7 @@ export type SettingsPanelSignal = {
76
76
  minimized?: boolean;
77
77
  expanded?: boolean;
78
78
  editLock?: number;
79
+ targetProperty?: 'parentClasses' | 'gridClasses';
79
80
  };
80
81
 
81
82
  export interface OgImageParams {
@@ -173,6 +174,7 @@ export type NodeType =
173
174
  | 'Pane'
174
175
  | 'StoryFragment'
175
176
  | 'BgPane'
177
+ | 'GridLayoutNode'
176
178
  | 'Markdown'
177
179
  | 'TagElement'
178
180
  | 'TractStack'
@@ -253,7 +255,12 @@ export interface TractStackNode extends BaseNode {
253
255
  }
254
256
 
255
257
  export interface PaneFragmentNode extends BaseNode {
256
- type: 'markdown' | 'visual-break' | 'background-image' | 'artpack-image';
258
+ type:
259
+ | 'markdown'
260
+ | 'visual-break'
261
+ | 'background-image'
262
+ | 'artpack-image'
263
+ | 'grid-layout';
257
264
  hiddenViewportMobile?: boolean;
258
265
  hiddenViewportTablet?: boolean;
259
266
  hiddenViewportDesktop?: boolean;
@@ -272,6 +279,22 @@ export interface MarkdownPaneFragmentNode extends PaneFragmentNode {
272
279
  >;
273
280
  parentClasses?: ParentClassesPayload;
274
281
  parentCss?: string[];
282
+ gridClasses?: DefaultClassValue;
283
+ }
284
+
285
+ export interface GridLayoutNode extends PaneFragmentNode {
286
+ nodeType: 'GridLayoutNode';
287
+ type: 'grid-layout';
288
+ parentClasses?: ParentClassesPayload;
289
+ defaultClasses?: Record<
290
+ string,
291
+ {
292
+ mobile: Record<string, string>;
293
+ tablet: Record<string, string>;
294
+ desktop: Record<string, string>;
295
+ }
296
+ >;
297
+ gridColumns: { mobile: number; tablet: number; desktop: number };
275
298
  }
276
299
 
277
300
  export interface ArtpackImageNode extends PaneFragmentNode {
@@ -401,10 +424,15 @@ export type TemplateMarkdown = MarkdownPaneFragmentNode & {
401
424
  markdownBody?: string;
402
425
  };
403
426
 
427
+ export type TemplateGridLayout = GridLayoutNode & {
428
+ nodes?: TemplateMarkdown[];
429
+ };
430
+
404
431
  export type TemplatePane = PaneNode & {
405
432
  id?: string;
406
433
  parentId?: string;
407
434
  markdown?: TemplateMarkdown;
435
+ gridLayout?: TemplateGridLayout;
408
436
  bgPane?: VisualBreakNode | ArtpackImageNode | BgImageNode;
409
437
  };
410
438
 
@@ -440,6 +468,18 @@ export type StorageMarkdown = Omit<
440
468
  nodes?: StorageNode[];
441
469
  };
442
470
 
471
+ export type StorageGridLayoutNode = {
472
+ nodeType: string;
473
+ type: string;
474
+ defaultClasses?: Record<string, any>;
475
+ parentClasses?: Record<string, any>[];
476
+ gridColumns: {
477
+ mobile: number;
478
+ tablet: number;
479
+ desktop: number;
480
+ };
481
+ };
482
+
443
483
  export type StoragePane = Omit<
444
484
  PaneNode,
445
485
  | 'id'
@@ -453,7 +493,8 @@ export type StoragePane = Omit<
453
493
  | 'codeHookPayload'
454
494
  | 'markdown'
455
495
  > & {
456
- markdown?: StorageMarkdown;
496
+ markdowns?: StorageMarkdown[];
497
+ gridLayout?: StorageGridLayoutNode;
457
498
  bgPane?: StorageBgPane;
458
499
  };
459
500
 
@@ -481,7 +522,7 @@ export type LoadData = {
481
522
  paneNodes?: PaneNode[];
482
523
  tractstackNodes?: TractStackNode[];
483
524
  childNodes?: (BaseNode | FlatNode)[];
484
- paneFragmentNodes?: PaneFragmentNode[];
525
+ paneFragmentNodes?: (PaneFragmentNode | GridLayoutNode)[];
485
526
  flatNodes?: FlatNode[];
486
527
  impressionNodes?: ImpressionNode[];
487
528
  beliefNodes?: BeliefNode[];
@@ -511,6 +552,18 @@ export interface BasePanelProps {
511
552
  onTitleChange?: (title: string) => void;
512
553
  }
513
554
 
555
+ export type ParentBasePanelProps = {
556
+ node: MarkdownPaneFragmentNode | GridLayoutNode | null;
557
+ parentNode?: FlatNode | PaneNode;
558
+ config?: BrandConfig | null;
559
+ layer?: number;
560
+ className?: string;
561
+ childId?: string;
562
+ availableCodeHooks?: string[];
563
+ onTitleChange?: (title: string) => void;
564
+ targetProperty?: 'parentClasses' | 'gridClasses';
565
+ };
566
+
514
567
  interface WidgetParameterDefinition {
515
568
  label: string;
516
569
  defaultValue: string;
@@ -3,6 +3,7 @@ import type { StoragePane } from './compositorTypes';
3
3
  export type DesignLibraryEntry = {
4
4
  category: string;
5
5
  title: string;
6
+ markdownCount: number;
6
7
  template: StoragePane;
7
8
  };
8
9
 
@@ -7,6 +7,7 @@ export const TemplateH2Node = {
7
7
  {
8
8
  copy: 'Catchy title',
9
9
  tagName: 'text',
10
+ nodeType: 'TagElement',
10
11
  },
11
12
  ],
12
13
  } as TemplateNode;
@@ -18,6 +19,7 @@ export const TemplateH3Node = {
18
19
  {
19
20
  copy: 'Catchy sub-title',
20
21
  tagName: 'text',
22
+ nodeType: 'TagElement',
21
23
  },
22
24
  ],
23
25
  } as TemplateNode;
@@ -29,6 +31,7 @@ export const TemplateH4Node = {
29
31
  {
30
32
  copy: 'Catchy sub-title',
31
33
  tagName: 'text',
34
+ nodeType: 'TagElement',
32
35
  },
33
36
  ],
34
37
  } as TemplateNode;
@@ -40,6 +43,7 @@ export const TemplatePNode = {
40
43
  {
41
44
  copy: '...',
42
45
  tagName: 'text',
46
+ nodeType: 'TagElement',
43
47
  },
44
48
  ],
45
49
  } as TemplateNode;
@@ -51,6 +55,7 @@ export const TemplateOLNode = {
51
55
  {
52
56
  copy: '...',
53
57
  tagName: 'text',
58
+ nodeType: 'TagElement',
54
59
  },
55
60
  ],
56
61
  } as TemplateNode;
@@ -62,6 +67,7 @@ export const TemplateULNode = {
62
67
  {
63
68
  copy: '...',
64
69
  tagName: 'text',
70
+ nodeType: 'TagElement',
65
71
  },
66
72
  ],
67
73
  } as TemplateNode;
@@ -73,6 +79,7 @@ export const TemplateLINode = {
73
79
  {
74
80
  copy: '...',
75
81
  tagName: 'text',
82
+ nodeType: 'TagElement',
76
83
  },
77
84
  ],
78
85
  } as TemplateNode;
@@ -84,6 +91,7 @@ export const TemplateLINode = {
84
91
  // {
85
92
  // copy: "aside node",
86
93
  // tagName: "text",
94
+ //nodeType: 'TagElement',
87
95
  // },
88
96
  // ],
89
97
  //} as TemplateNode;
@@ -7,14 +7,23 @@ import type {
7
7
  DefaultClasses,
8
8
  ResponsiveClasses,
9
9
  ButtonPayload,
10
+ GridLayoutNode,
10
11
  } from '@/types/compositorTypes';
11
- import { tailwindClasses } from '@/utils/compositor/tailwindClasses';
12
12
  import { isDeepEqual } from '@/utils/helpers';
13
+ import { tailwindClasses } from '@/utils/compositor/tailwindClasses';
13
14
 
14
15
  type LLMShellLayer = {
15
- mobile?: Record<string, string>;
16
- tablet?: Record<string, string>;
17
- desktop?: Record<string, string>;
16
+ mobile?: string;
17
+ tablet?: string;
18
+ desktop?: string;
19
+ };
20
+
21
+ type LLMColumnLayer = {
22
+ gridClasses: {
23
+ mobile?: string;
24
+ tablet?: string;
25
+ desktop?: string;
26
+ };
18
27
  };
19
28
 
20
29
  type LLMDefaultClasses = {
@@ -29,6 +38,7 @@ type ShellJson = {
29
38
  bgColour: string;
30
39
  parentClasses: LLMShellLayer[];
31
40
  defaultClasses: LLMDefaultClasses;
41
+ columns?: LLMColumnLayer[];
32
42
  };
33
43
 
34
44
  type ParsedNode = {
@@ -36,12 +46,6 @@ type ParsedNode = {
36
46
  responsiveClasses: ResponsiveClasses;
37
47
  };
38
48
 
39
- type ParentClassLayer = {
40
- mobile: Record<string, string>;
41
- tablet: Record<string, string>;
42
- desktop: Record<string, string>;
43
- };
44
-
45
49
  type DefaultClassValue = {
46
50
  mobile: Record<string, string>;
47
51
  tablet: Record<string, string>;
@@ -54,7 +58,6 @@ type ClassLookupValue = {
54
58
  viewport: 'mobile' | 'tablet' | 'desktop';
55
59
  };
56
60
 
57
- let KEY_NORMALIZATION_LOOKUP: Map<string, string> | null = null;
58
61
  let RESPONSIVE_CLASS_LOOKUP: Map<string, ClassLookupValue> | null = null;
59
62
  let BUTTON_CLASS_LOOKUP: Map<string, { key: string; value: string }> | null =
60
63
  null;
@@ -74,37 +77,6 @@ const ALLOWED_TAGS = new Set([
74
77
  'a',
75
78
  ]);
76
79
 
77
- function buildKeyNormalizationLookup(): Map<string, string> {
78
- if (KEY_NORMALIZATION_LOOKUP) {
79
- return KEY_NORMALIZATION_LOOKUP;
80
- }
81
-
82
- const keyMap = new Map<string, string>();
83
- for (const key in tailwindClasses) {
84
- keyMap.set(key.toLowerCase(), key);
85
- }
86
- KEY_NORMALIZATION_LOOKUP = keyMap;
87
- return keyMap;
88
- }
89
-
90
- function normalizeKeys(
91
- styleObj: Record<string, string> | undefined
92
- ): Record<string, string> {
93
- if (!styleObj) return {};
94
-
95
- const keyMap = buildKeyNormalizationLookup();
96
- const normalized: Record<string, string> = {};
97
-
98
- for (const key in styleObj) {
99
- if (Object.prototype.hasOwnProperty.call(styleObj, key)) {
100
- const lowerKey = key.toLowerCase();
101
- const correctKey = keyMap.get(lowerKey);
102
- normalized[correctKey || key] = styleObj[key];
103
- }
104
- }
105
- return normalized;
106
- }
107
-
108
80
  function buildResponsiveClassLookup(): Map<string, ClassLookupValue> {
109
81
  if (RESPONSIVE_CLASS_LOOKUP) {
110
82
  return RESPONSIVE_CLASS_LOOKUP;
@@ -406,6 +378,121 @@ function findMostCommonClasses(nodes: ParsedNode[]): ResponsiveClasses {
406
378
  return classMap.get(mostCommonKey) || {};
407
379
  }
408
380
 
381
+ /**
382
+ * Calculates the difference between a node's full style and the default theme for its tag.
383
+ * @returns An object with only the override styles, or undefined if there are no overrides.
384
+ */
385
+ function calculateOverrides(
386
+ fullStyle: ResponsiveClasses,
387
+ defaultStyle: DefaultClassValue
388
+ ): ResponsiveClasses | undefined {
389
+ const overrides: ResponsiveClasses = {};
390
+ const viewports: Array<keyof ResponsiveClasses> = [
391
+ 'mobile',
392
+ 'tablet',
393
+ 'desktop',
394
+ ];
395
+
396
+ for (const viewport of viewports) {
397
+ const fullVPStyle = fullStyle[viewport];
398
+ const defaultVPStyle = defaultStyle[viewport];
399
+
400
+ if (!fullVPStyle) {
401
+ continue;
402
+ }
403
+
404
+ for (const key in fullVPStyle) {
405
+ if (
406
+ Object.prototype.hasOwnProperty.call(fullVPStyle, key) &&
407
+ fullVPStyle[key] !== defaultVPStyle?.[key]
408
+ ) {
409
+ if (!overrides[viewport]) {
410
+ overrides[viewport] = {};
411
+ }
412
+ overrides[viewport]![key] = fullVPStyle[key];
413
+ }
414
+ }
415
+ }
416
+
417
+ if (Object.keys(overrides).length > 0 && !isDeepEqual(overrides, {})) {
418
+ return overrides;
419
+ }
420
+
421
+ return undefined;
422
+ }
423
+
424
+ /**
425
+ * Reconciles classes for a set of final TemplateNodes against a base theme.
426
+ * It discovers the true theme from the content, merges it into the base theme,
427
+ * and then mutates the nodes to only contain true override classes.
428
+ * @param nodes The array of TemplateNode objects to mutate.
429
+ * @param baseDefaults The base theme object to mutate.
430
+ */
431
+ function reconcileClasses(
432
+ nodes: TemplateNode[],
433
+ baseDefaults: DefaultClasses
434
+ ): void {
435
+ const nodesByTag: Record<string, TemplateNode[]> = {};
436
+ const tempParsedNodes: ParsedNode[] = [];
437
+
438
+ nodes.forEach((node) => {
439
+ // Create a temporary ParsedNode structure for analysis
440
+ const tempParsedNode: ParsedNode = {
441
+ flatNode: node,
442
+ responsiveClasses: node.overrideClasses || {},
443
+ };
444
+ tempParsedNodes.push(tempParsedNode);
445
+
446
+ const tagName = node.tagName;
447
+ if (!['p', 'h2', 'h3', 'h4', 'h5'].includes(tagName)) {
448
+ return;
449
+ }
450
+ if (!nodesByTag[tagName]) {
451
+ nodesByTag[tagName] = [];
452
+ }
453
+ nodesByTag[tagName].push(node);
454
+ });
455
+
456
+ const finalDefaults = JSON.parse(JSON.stringify(baseDefaults));
457
+
458
+ for (const tagName in nodesByTag) {
459
+ const nodesForTag = nodesByTag[tagName];
460
+ const tempParsedForTag = nodesForTag.map((n) => ({
461
+ flatNode: n,
462
+ responsiveClasses: n.overrideClasses || {},
463
+ }));
464
+
465
+ const commonStyleForTag = findMostCommonClasses(tempParsedForTag);
466
+ const mergedDefault = mergeResponsive(
467
+ finalDefaults[tagName],
468
+ commonStyleForTag
469
+ );
470
+ finalDefaults[tagName] = ensureRequiredViewports(mergedDefault);
471
+ }
472
+
473
+ nodes.forEach((node) => {
474
+ const tagName = node.tagName;
475
+ const defaultStyleForTag = finalDefaults[tagName];
476
+ const fullStyle = node.overrideClasses || {};
477
+
478
+ if (defaultStyleForTag) {
479
+ const overrides = calculateOverrides(fullStyle, defaultStyleForTag);
480
+ node.overrideClasses = overrides;
481
+ } else if (Object.keys(fullStyle).length > 0 && node.tagName !== 'span') {
482
+ if (!isDeepEqual(fullStyle, {})) {
483
+ node.overrideClasses = fullStyle;
484
+ } else {
485
+ node.overrideClasses = undefined;
486
+ }
487
+ } else if (node.tagName !== 'span') {
488
+ node.overrideClasses = undefined;
489
+ }
490
+ });
491
+
492
+ Object.keys(baseDefaults).forEach((key) => delete baseDefaults[key]);
493
+ Object.assign(baseDefaults, finalDefaults);
494
+ }
495
+
409
496
  function ensureRequiredViewports(
410
497
  responsive: ResponsiveClasses | undefined
411
498
  ): DefaultClassValue {
@@ -502,8 +589,6 @@ export function parseAiCopyHtml(
502
589
  const allParsedNodes: ParsedNode[] = [];
503
590
  walkDom(doc.body, markdownId, allParsedNodes, markdownId);
504
591
 
505
- // When parsing copy in isolation, all classes are treated as potential overrides.
506
- // The consumer is responsible for merging these with a set of defaults if needed.
507
592
  return allParsedNodes.map((pNode) => {
508
593
  if (
509
594
  Object.keys(pNode.responsiveClasses).length > 0 &&
@@ -515,53 +600,148 @@ export function parseAiCopyHtml(
515
600
  });
516
601
  }
517
602
 
603
+ function transformClassesFromShellLayer(
604
+ layer: LLMShellLayer | LLMColumnLayer['gridClasses']
605
+ ): DefaultClassValue {
606
+ const mobileClasses = sanitizeResponsiveClasses(layer.mobile);
607
+
608
+ const tabletString = layer.tablet
609
+ ? layer.tablet
610
+ .split(/\s+/)
611
+ .filter(Boolean)
612
+ .map((c) => `md:${c}`)
613
+ .join(' ')
614
+ : undefined;
615
+ const tabletClasses = sanitizeResponsiveClasses(tabletString);
616
+
617
+ const desktopString = layer.desktop
618
+ ? layer.desktop
619
+ .split(/\s+/)
620
+ .filter(Boolean)
621
+ .map((c) => `xl:${c}`)
622
+ .join(' ')
623
+ : undefined;
624
+ const desktopClasses = sanitizeResponsiveClasses(desktopString);
625
+
626
+ let merged = mergeResponsive(mobileClasses, tabletClasses);
627
+ merged = mergeResponsive(merged, desktopClasses);
628
+
629
+ return ensureRequiredViewports(merged);
630
+ }
631
+
632
+ function transformParentClassesFromShell(
633
+ llmParentClasses: LLMShellLayer[]
634
+ ): ParentClassesPayload {
635
+ return llmParentClasses.map((layer) => transformClassesFromShellLayer(layer));
636
+ }
637
+
518
638
  export const parseAiPane = (
519
639
  shellJson: string,
520
- copyHtml: string,
640
+ copyHtml: string | string[],
521
641
  layout: string
522
- ): TemplatePane => {
523
- const shell: ShellJson = JSON.parse(shellJson);
642
+ ): Omit<TemplatePane, 'nodes'> & {
643
+ nodes?: (TemplateMarkdown | GridLayoutNode)[];
644
+ } => {
645
+ console.log('--- ENTERING parseAiPane ---', { shellJson, copyHtml, layout });
524
646
 
647
+ const shell: ShellJson = JSON.parse(shellJson);
525
648
  const paneId = ulid();
526
- const markdownId = ulid();
527
-
528
- const transformedParentClasses: ParentClassesPayload = (
529
- shell.parentClasses || []
530
- ).map(
531
- (layer): ParentClassLayer => ({
532
- mobile: normalizeKeys(layer.mobile),
533
- tablet: normalizeKeys(layer.tablet),
534
- desktop: normalizeKeys(layer.desktop),
535
- })
536
- );
537
649
 
538
- const shellDefaults = parseDefaultClassesFromShell(shell.defaultClasses);
650
+ // --- GRID LAYOUT PATH ---
651
+ if (shell.columns && Array.isArray(copyHtml)) {
652
+ const gridLayoutId = ulid();
653
+ const shellDefaults = parseDefaultClassesFromShell(shell.defaultClasses);
654
+ const transformedParentClasses = transformParentClassesFromShell(
655
+ shell.parentClasses || []
656
+ );
539
657
 
540
- const markdownNode: TemplateMarkdown = {
541
- id: markdownId,
542
- nodeType: 'Markdown',
543
- parentId: paneId,
544
- type: 'markdown',
545
- markdownId: ulid(),
546
- parentClasses: transformedParentClasses,
547
- defaultClasses: shellDefaults,
548
- };
658
+ const childMarkdownNodes = shell.columns.map((column, index) => {
659
+ const markdownId = ulid();
660
+ const gridClasses = transformClassesFromShellLayer(column.gridClasses);
661
+ const templateNodes = parseAiCopyHtml(copyHtml[index] || '', markdownId);
662
+
663
+ const markdownNode: TemplateMarkdown = {
664
+ id: markdownId,
665
+ nodeType: 'Markdown',
666
+ parentId: gridLayoutId,
667
+ type: 'markdown',
668
+ markdownId: ulid(),
669
+ gridClasses,
670
+ nodes: templateNodes,
671
+ };
672
+ return markdownNode;
673
+ });
674
+
675
+ const gridLayoutNode: GridLayoutNode & { nodes: TemplateMarkdown[] } = {
676
+ id: gridLayoutId,
677
+ nodeType: 'GridLayoutNode',
678
+ parentId: paneId,
679
+ type: 'grid-layout',
680
+ parentClasses: transformedParentClasses,
681
+ defaultClasses: shellDefaults,
682
+ gridColumns: {
683
+ mobile: 1,
684
+ tablet: 2,
685
+ desktop: 2,
686
+ },
687
+ nodes: childMarkdownNodes,
688
+ };
689
+
690
+ const templatePane = {
691
+ id: paneId,
692
+ nodeType: 'Pane' as const,
693
+ parentId: '',
694
+ title: 'AI Pane',
695
+ slug: `ai-${paneId.slice(-4)}`,
696
+ bgColour: shell.bgColour,
697
+ isDecorative: false,
698
+ gridLayout: gridLayoutNode,
699
+ };
700
+ console.log({
701
+ shellDefaults: shellDefaults,
702
+ transformedParentClasses: transformedParentClasses,
703
+ childMarkdownNodes: childMarkdownNodes,
704
+ gridLayoutNode: gridLayoutNode,
705
+ templatePane: templatePane,
706
+ });
707
+ return templatePane;
708
+ }
549
709
 
550
- const templateNodes = parseAiCopyHtml(copyHtml, markdownId);
551
-
552
- const templatePane: TemplatePane = {
553
- id: paneId,
554
- nodeType: 'Pane',
555
- parentId: '',
556
- title: 'AI Pane',
557
- slug: `ai-${paneId.slice(-4)}`,
558
- bgColour: shell.bgColour,
559
- isDecorative: false,
560
- markdown: {
561
- ...markdownNode,
710
+ // --- SINGLE-COLUMN LAYOUT PATH ---
711
+ if (typeof copyHtml === 'string') {
712
+ const markdownId = ulid();
713
+ const shellDefaults = parseDefaultClassesFromShell(shell.defaultClasses);
714
+ const transformedParentClasses = transformParentClassesFromShell(
715
+ shell.parentClasses || []
716
+ );
717
+ const templateNodes = parseAiCopyHtml(copyHtml, markdownId);
718
+
719
+ reconcileClasses(templateNodes, shellDefaults);
720
+
721
+ const markdownNode: TemplateMarkdown = {
722
+ id: markdownId,
723
+ nodeType: 'Markdown',
724
+ parentId: paneId,
725
+ type: 'markdown',
726
+ markdownId: ulid(),
727
+ parentClasses: transformedParentClasses,
728
+ defaultClasses: shellDefaults,
562
729
  nodes: templateNodes,
563
- },
564
- };
730
+ };
731
+
732
+ const templatePane = {
733
+ id: paneId,
734
+ nodeType: 'Pane' as const,
735
+ parentId: '',
736
+ title: 'AI Pane',
737
+ slug: `ai-${paneId.slice(-4)}`,
738
+ bgColour: shell.bgColour,
739
+ isDecorative: false,
740
+ markdown: markdownNode,
741
+ };
742
+ return templatePane;
743
+ }
565
744
 
566
- return templatePane;
745
+ // Fallback for invalid input
746
+ throw new Error('Invalid input for parseAiPane');
567
747
  };
@@ -172,13 +172,14 @@ export async function savePaneToLibrary(
172
172
  heightRatioDesktop: paneNode.heightRatioDesktop,
173
173
  heightRatioMobile: paneNode.heightRatioMobile,
174
174
  heightRatioTablet: paneNode.heightRatioTablet,
175
- markdown: newStorageMarkdown,
176
- bgPane: newStorageBgPane,
175
+ ...(newStorageMarkdown ? { markdowns: [newStorageMarkdown] } : {}),
176
+ ...(newStorageBgPane ? { bgPane: newStorageBgPane } : {}),
177
177
  };
178
178
 
179
179
  const newLibraryEntry: DesignLibraryEntry = {
180
180
  category: category,
181
181
  title: title,
182
+ markdownCount: 1,
182
183
  template: newStoragePane,
183
184
  };
184
185
 
@@ -244,10 +245,11 @@ export function mergeCopyIntoTemplate(
244
245
  copy: ExtractedCopy
245
246
  ): StoragePane {
246
247
  const newTemplate = { ...template };
247
- if (newTemplate.markdown) {
248
- newTemplate.markdown.nodes = copy;
248
+ if (newTemplate.markdowns) {
249
+ newTemplate.markdowns[0].nodes = copy;
249
250
  } else if (copy.length > 0) {
250
- newTemplate.markdown = {
251
+ if (!newTemplate.markdowns) newTemplate.markdowns = [];
252
+ newTemplate.markdowns[0] = {
251
253
  nodeType: 'Markdown',
252
254
  type: 'markdown',
253
255
  defaultClasses: {},
@@ -289,8 +291,8 @@ export function convertStorageToLiveTemplate(
289
291
  const markdownId = ulid();
290
292
  const flatNodeList: TemplateNode[] = [];
291
293
 
292
- if (storagePane.markdown && storagePane.markdown.nodes) {
293
- for (const storageNode of storagePane.markdown.nodes) {
294
+ if (storagePane.markdowns && storagePane.markdowns[0].nodes) {
295
+ for (const storageNode of storagePane.markdowns[0].nodes) {
294
296
  const processedNodes = processStorageNode(storageNode, markdownId);
295
297
  flatNodeList.push(...processedNodes);
296
298
  }
@@ -307,12 +309,13 @@ export function convertStorageToLiveTemplate(
307
309
  };
308
310
  }
309
311
 
312
+ const { gridLayout, ...restOfStoragePane } = storagePane;
310
313
  const liveTemplatePane: TemplatePane = {
311
- ...storagePane,
314
+ ...restOfStoragePane,
312
315
  id: paneId,
313
316
  parentId: '',
314
317
  markdown: {
315
- ...(storagePane.markdown || {
318
+ ...((storagePane.markdowns && storagePane.markdowns[0]) || {
316
319
  nodeType: 'Markdown',
317
320
  type: 'markdown',
318
321
  defaultClasses: {},