astro-tractstack 2.0.13 → 2.0.15
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.
- package/dist/index.js +40 -0
- package/package.json +1 -1
- package/templates/src/client/view.js +5 -0
- package/templates/src/components/compositor/Compositor.tsx +3 -2
- package/templates/src/components/compositor/Node.tsx +25 -8
- package/templates/src/components/compositor/nodes/Pane_DesignLibrary.tsx +105 -0
- package/templates/src/components/edit/ToolMode.tsx +7 -0
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +459 -561
- package/templates/src/components/edit/pane/AiPaneGenerator.tsx +19 -82
- package/templates/src/components/edit/pane/RestylePaneModal.tsx +573 -0
- package/templates/src/components/edit/pane/steps/AiDesignStep.tsx +140 -0
- package/templates/src/components/edit/pane/steps/CopyInputStep.tsx +105 -0
- package/templates/src/components/edit/pane/steps/DesignLibraryStep.tsx +395 -0
- package/templates/src/components/edit/state/SaveToLibraryModal.tsx +205 -0
- package/templates/src/constants/prompts.json +3 -1
- package/templates/src/stores/selection.ts +4 -0
- package/templates/src/types/compositorTypes.ts +51 -1
- package/templates/src/types/tractstack.ts +36 -31
- package/templates/src/utils/aai/getTitleSlug.ts +1 -1
- package/templates/src/utils/api/brandConfig.ts +8 -2
- package/templates/src/utils/api/brandHelpers.ts +4 -0
- package/templates/src/utils/compositor/aiPaneParser.ts +32 -84
- package/templates/src/utils/compositor/designLibraryHelper.ts +416 -0
- package/templates/src/utils/compositor/processMarkdown.ts +1 -1
- package/utils/inject-files.ts +40 -0
- package/templates/src/components/edit/pane/PageGen.tsx +0 -485
- package/templates/src/components/edit/pane/PageGenSelector.tsx +0 -245
- package/templates/src/components/edit/pane/PageGenSpecial.tsx +0 -339
|
@@ -78,7 +78,6 @@ function buildKeyNormalizationLookup(): Map<string, string> {
|
|
|
78
78
|
|
|
79
79
|
const keyMap = new Map<string, string>();
|
|
80
80
|
for (const key in tailwindClasses) {
|
|
81
|
-
// Store lowercase key -> correctly cased key
|
|
82
81
|
keyMap.set(key.toLowerCase(), key);
|
|
83
82
|
}
|
|
84
83
|
KEY_NORMALIZATION_LOOKUP = keyMap;
|
|
@@ -97,7 +96,6 @@ function normalizeKeys(
|
|
|
97
96
|
if (Object.prototype.hasOwnProperty.call(styleObj, key)) {
|
|
98
97
|
const lowerKey = key.toLowerCase();
|
|
99
98
|
const correctKey = keyMap.get(lowerKey);
|
|
100
|
-
// Use the correctly cased key if found, otherwise keep original (handles potential non-Tailwind keys)
|
|
101
99
|
normalized[correctKey || key] = styleObj[key];
|
|
102
100
|
}
|
|
103
101
|
}
|
|
@@ -258,22 +256,16 @@ function walkDom(
|
|
|
258
256
|
) {
|
|
259
257
|
if (domNode.nodeType === Node.TEXT_NODE) {
|
|
260
258
|
const copy = domNode.textContent || '';
|
|
261
|
-
// Preserve leading/trailing spaces unless the *entire* content is just whitespace.
|
|
262
|
-
// Trim internal excessive whitespace as a basic sanitation step.
|
|
263
259
|
const trimmedCopy = copy.replace(/\s+/g, ' ').trim();
|
|
264
260
|
|
|
265
261
|
if (trimmedCopy.length > 0) {
|
|
266
|
-
// Use the original copy to preserve meaningful spaces, but cleaned up.
|
|
267
262
|
let finalCopy = copy.replace(/\s+/g, ' ');
|
|
268
|
-
// Preserve single leading space if original had one AND previous sibling exists
|
|
269
263
|
if (copy.startsWith(' ') && domNode.previousSibling) {
|
|
270
264
|
finalCopy = ' ' + finalCopy.trimStart();
|
|
271
265
|
}
|
|
272
|
-
// Preserve single trailing space if original had one AND next sibling exists
|
|
273
266
|
if (copy.endsWith(' ') && domNode.nextSibling) {
|
|
274
267
|
finalCopy = finalCopy.trimEnd() + ' ';
|
|
275
268
|
}
|
|
276
|
-
// Special case: if it was ONLY space, respect if it was intended between elements
|
|
277
269
|
if (
|
|
278
270
|
trimmedCopy.length === 0 &&
|
|
279
271
|
copy.length > 0 &&
|
|
@@ -283,15 +275,13 @@ function walkDom(
|
|
|
283
275
|
finalCopy = ' ';
|
|
284
276
|
}
|
|
285
277
|
|
|
286
|
-
// Only create node if there's actual content or a meaningful space
|
|
287
278
|
if (finalCopy.trim().length > 0 || finalCopy === ' ') {
|
|
288
279
|
const textNode: TemplateNode = {
|
|
289
280
|
id: ulid(),
|
|
290
281
|
nodeType: 'TagElement',
|
|
291
282
|
parentId: parentId,
|
|
292
283
|
tagName: 'text',
|
|
293
|
-
copy: finalCopy,
|
|
294
|
-
overrideClasses: {},
|
|
284
|
+
copy: finalCopy,
|
|
295
285
|
};
|
|
296
286
|
parsedNodes.push({
|
|
297
287
|
flatNode: textNode,
|
|
@@ -326,7 +316,6 @@ function walkDom(
|
|
|
326
316
|
nodeType: 'TagElement',
|
|
327
317
|
parentId: parentId,
|
|
328
318
|
tagName: 'p',
|
|
329
|
-
overrideClasses: {},
|
|
330
319
|
};
|
|
331
320
|
parsedNodes.push({
|
|
332
321
|
flatNode: pNode,
|
|
@@ -341,8 +330,7 @@ function walkDom(
|
|
|
341
330
|
id: ulid(),
|
|
342
331
|
nodeType: 'TagElement',
|
|
343
332
|
parentId: finalParentId,
|
|
344
|
-
tagName: 'a',
|
|
345
|
-
overrideClasses: {},
|
|
333
|
+
tagName: 'a', // Buttons are converted to anchor tags for our system
|
|
346
334
|
href: '#',
|
|
347
335
|
buttonPayload: {
|
|
348
336
|
...buttonPayload,
|
|
@@ -368,7 +356,6 @@ function walkDom(
|
|
|
368
356
|
nodeType: 'TagElement',
|
|
369
357
|
parentId: parentId,
|
|
370
358
|
tagName: tagName,
|
|
371
|
-
overrideClasses: {},
|
|
372
359
|
};
|
|
373
360
|
|
|
374
361
|
if (tagName === 'span') {
|
|
@@ -493,14 +480,41 @@ function parseDefaultClassesFromShell(
|
|
|
493
480
|
return sanitizedDefaults;
|
|
494
481
|
}
|
|
495
482
|
|
|
483
|
+
/**
|
|
484
|
+
* Parses a raw HTML string from the AI into a structured array of TemplateNodes.
|
|
485
|
+
* @param copyHtml The raw HTML string.
|
|
486
|
+
* @param markdownId The parent ID for the top-level nodes.
|
|
487
|
+
* @returns An array of TemplateNodes representing the structured content.
|
|
488
|
+
*/
|
|
489
|
+
export function parseAiCopyHtml(
|
|
490
|
+
copyHtml: string,
|
|
491
|
+
markdownId: string
|
|
492
|
+
): TemplateNode[] {
|
|
493
|
+
const parser = new DOMParser();
|
|
494
|
+
const doc = parser.parseFromString(copyHtml, 'text/html');
|
|
495
|
+
|
|
496
|
+
const allParsedNodes: ParsedNode[] = [];
|
|
497
|
+
walkDom(doc.body, markdownId, allParsedNodes, markdownId);
|
|
498
|
+
|
|
499
|
+
// When parsing copy in isolation, all classes are treated as potential overrides.
|
|
500
|
+
// The consumer is responsible for merging these with a set of defaults if needed.
|
|
501
|
+
return allParsedNodes.map((pNode) => {
|
|
502
|
+
if (
|
|
503
|
+
Object.keys(pNode.responsiveClasses).length > 0 &&
|
|
504
|
+
pNode.flatNode.tagName !== 'span'
|
|
505
|
+
) {
|
|
506
|
+
pNode.flatNode.overrideClasses = pNode.responsiveClasses;
|
|
507
|
+
}
|
|
508
|
+
return pNode.flatNode;
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
496
512
|
export const parseAiPane = (
|
|
497
513
|
shellJson: string,
|
|
498
514
|
copyHtml: string,
|
|
499
515
|
layout: string
|
|
500
516
|
): TemplatePane => {
|
|
501
517
|
const shell: ShellJson = JSON.parse(shellJson);
|
|
502
|
-
const parser = new DOMParser();
|
|
503
|
-
const doc = parser.parseFromString(copyHtml, 'text/html');
|
|
504
518
|
|
|
505
519
|
const paneId = ulid();
|
|
506
520
|
const markdownId = ulid();
|
|
@@ -527,73 +541,7 @@ export const parseAiPane = (
|
|
|
527
541
|
defaultClasses: shellDefaults,
|
|
528
542
|
};
|
|
529
543
|
|
|
530
|
-
const
|
|
531
|
-
walkDom(doc.body, markdownId, allParsedNodes, markdownId);
|
|
532
|
-
|
|
533
|
-
const templateNodes: TemplateNode[] = [];
|
|
534
|
-
const nodesByTag = new Map<string, ParsedNode[]>();
|
|
535
|
-
|
|
536
|
-
allParsedNodes.forEach((parsedNode) => {
|
|
537
|
-
templateNodes.push(parsedNode.flatNode);
|
|
538
|
-
const tagName = parsedNode.flatNode.tagName;
|
|
539
|
-
|
|
540
|
-
if (
|
|
541
|
-
tagName &&
|
|
542
|
-
tagName !== 'span' &&
|
|
543
|
-
tagName !== 'text' &&
|
|
544
|
-
tagName !== 'em' &&
|
|
545
|
-
tagName !== 'strong' &&
|
|
546
|
-
tagName !== 'a'
|
|
547
|
-
) {
|
|
548
|
-
if (!nodesByTag.has(tagName)) {
|
|
549
|
-
nodesByTag.set(tagName, []);
|
|
550
|
-
}
|
|
551
|
-
nodesByTag.get(tagName)!.push(parsedNode);
|
|
552
|
-
}
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
nodesByTag.forEach((nodes, tagName) => {
|
|
556
|
-
const commonResponsiveFromCopy = findMostCommonClasses(nodes);
|
|
557
|
-
const requiredCommonFromCopy = ensureRequiredViewports(
|
|
558
|
-
commonResponsiveFromCopy
|
|
559
|
-
);
|
|
560
|
-
|
|
561
|
-
const existingShellDefault = markdownNode.defaultClasses![tagName];
|
|
562
|
-
const mergedDefault = ensureRequiredViewports(
|
|
563
|
-
mergeResponsive(existingShellDefault, commonResponsiveFromCopy)
|
|
564
|
-
);
|
|
565
|
-
|
|
566
|
-
markdownNode.defaultClasses![tagName] = mergedDefault;
|
|
567
|
-
|
|
568
|
-
nodes.forEach((parsedNode) => {
|
|
569
|
-
const requiredNodeResponsive = ensureRequiredViewports(
|
|
570
|
-
parsedNode.responsiveClasses
|
|
571
|
-
);
|
|
572
|
-
|
|
573
|
-
if (!isDeepEqual(requiredNodeResponsive, requiredCommonFromCopy)) {
|
|
574
|
-
if (!parsedNode.flatNode.overrideClasses) {
|
|
575
|
-
parsedNode.flatNode.overrideClasses = {};
|
|
576
|
-
}
|
|
577
|
-
parsedNode.flatNode.overrideClasses = parsedNode.responsiveClasses;
|
|
578
|
-
}
|
|
579
|
-
});
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
if (layout.includes('Image')) {
|
|
583
|
-
const imgNode: TemplateNode = {
|
|
584
|
-
id: ulid(),
|
|
585
|
-
nodeType: 'TagElement',
|
|
586
|
-
parentId: markdownId,
|
|
587
|
-
tagName: 'img',
|
|
588
|
-
src: '/static.jpg',
|
|
589
|
-
overrideClasses: {},
|
|
590
|
-
};
|
|
591
|
-
if (layout === 'Text + Image Left') {
|
|
592
|
-
templateNodes.unshift(imgNode);
|
|
593
|
-
} else {
|
|
594
|
-
templateNodes.push(imgNode);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
544
|
+
const templateNodes = parseAiCopyHtml(copyHtml, markdownId);
|
|
597
545
|
|
|
598
546
|
const templatePane: TemplatePane = {
|
|
599
547
|
id: paneId,
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import { ulid } from 'ulid';
|
|
2
|
+
import { getCtx, type NodesContext } from '@/stores/nodes';
|
|
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,
|
|
17
|
+
} from '@/types/compositorTypes';
|
|
18
|
+
import type {
|
|
19
|
+
BrandConfig,
|
|
20
|
+
BrandConfigState,
|
|
21
|
+
DesignLibraryConfig,
|
|
22
|
+
DesignLibraryEntry,
|
|
23
|
+
} from '@/types/tractstack';
|
|
24
|
+
import { saveBrandConfig } from '@/utils/api/brandConfig';
|
|
25
|
+
import {
|
|
26
|
+
convertToLocalState,
|
|
27
|
+
convertToBackendFormat,
|
|
28
|
+
} from '@/utils/api/brandHelpers';
|
|
29
|
+
|
|
30
|
+
type CopyMode = 'retain' | 'lorem' | 'blank';
|
|
31
|
+
|
|
32
|
+
export type ExtractedCopy = StorageNode[];
|
|
33
|
+
|
|
34
|
+
const LOREM_SHORT = 'Lorem ipsum dolor sit amet.';
|
|
35
|
+
const LOREM_LONG =
|
|
36
|
+
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
|
|
37
|
+
|
|
38
|
+
function convertLiveNodeToStorageNode(
|
|
39
|
+
node: FlatNode,
|
|
40
|
+
ctx: NodesContext,
|
|
41
|
+
copyMode: CopyMode
|
|
42
|
+
): StorageNode | null {
|
|
43
|
+
if (copyMode === 'lorem') {
|
|
44
|
+
if (!node.tagName || !['h2', 'h3', 'h4', 'p'].includes(node.tagName)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const storageNode: StorageNode = {
|
|
50
|
+
nodeType: node.nodeType,
|
|
51
|
+
tagName: node.tagName,
|
|
52
|
+
tagNameCustom: node.tagNameCustom,
|
|
53
|
+
overrideClasses: copyMode === 'retain' ? node.overrideClasses : undefined,
|
|
54
|
+
href: copyMode === 'retain' ? node.href : undefined,
|
|
55
|
+
src: copyMode === 'retain' ? node.src : undefined,
|
|
56
|
+
alt: copyMode === 'retain' ? node.alt : undefined,
|
|
57
|
+
fileId: copyMode === 'retain' ? node.fileId : undefined,
|
|
58
|
+
buttonPayload: copyMode === 'retain' ? node.buttonPayload : undefined,
|
|
59
|
+
codeHookParams: copyMode === 'retain' ? node.codeHookParams : undefined,
|
|
60
|
+
elementCss: copyMode === 'retain' ? node.elementCss : undefined,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const childIds = ctx.getChildNodeIDs(node.id);
|
|
64
|
+
|
|
65
|
+
if (childIds.length > 0) {
|
|
66
|
+
const childNodes = childIds
|
|
67
|
+
.map((id) => {
|
|
68
|
+
const childNode = ctx.allNodes.get().get(id) as FlatNode;
|
|
69
|
+
if (!childNode) return null;
|
|
70
|
+
|
|
71
|
+
if (childNode.tagName === 'text' && childNode.copy) {
|
|
72
|
+
if (copyMode === 'lorem') {
|
|
73
|
+
const isHeadingParent =
|
|
74
|
+
node.tagName && node.tagName.startsWith('h');
|
|
75
|
+
return {
|
|
76
|
+
nodeType: 'TagElement',
|
|
77
|
+
tagName: 'text',
|
|
78
|
+
copy: isHeadingParent ? LOREM_SHORT : LOREM_LONG,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (copyMode === 'retain') {
|
|
82
|
+
return {
|
|
83
|
+
nodeType: 'TagElement',
|
|
84
|
+
tagName: 'text',
|
|
85
|
+
copy: childNode.copy,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return convertLiveNodeToStorageNode(childNode, ctx, copyMode);
|
|
91
|
+
})
|
|
92
|
+
.filter((n): n is StorageNode => n !== null);
|
|
93
|
+
|
|
94
|
+
if (childNodes.length > 0) {
|
|
95
|
+
storageNode.nodes = childNodes;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return storageNode;
|
|
100
|
+
}
|
|
101
|
+
|
|
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
|
+
markdown: newStorageMarkdown,
|
|
176
|
+
bgPane: newStorageBgPane,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const newLibraryEntry: DesignLibraryEntry = {
|
|
180
|
+
category: category,
|
|
181
|
+
title: title,
|
|
182
|
+
template: newStoragePane,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const currentState: BrandConfigState = convertToLocalState(config);
|
|
186
|
+
const currentLibrary =
|
|
187
|
+
(currentState.designLibrary as DesignLibraryConfig) || [];
|
|
188
|
+
|
|
189
|
+
const existingEntryIndex = currentLibrary.findIndex(
|
|
190
|
+
(entry) => entry.title === title && entry.category === category
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
let newLibrary: DesignLibraryConfig;
|
|
194
|
+
if (existingEntryIndex !== -1) {
|
|
195
|
+
newLibrary = [...currentLibrary];
|
|
196
|
+
newLibrary[existingEntryIndex] = newLibraryEntry;
|
|
197
|
+
} else {
|
|
198
|
+
newLibrary = [...currentLibrary, newLibraryEntry];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const updatedState: BrandConfigState = {
|
|
202
|
+
...currentState,
|
|
203
|
+
designLibrary: newLibrary,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const backendDTO: BrandConfig = convertToBackendFormat(updatedState);
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
await saveBrandConfig(tenantId, backendDTO);
|
|
210
|
+
return true;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.error('Failed to save design library:', error);
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function extractPaneCopy(paneNode: PaneNode): ExtractedCopy {
|
|
218
|
+
const ctx = getCtx();
|
|
219
|
+
const childNodes = ctx
|
|
220
|
+
.getChildNodeIDs(paneNode.id)
|
|
221
|
+
.map((id) => ctx.allNodes.get().get(id));
|
|
222
|
+
|
|
223
|
+
const markdownNode = childNodes.find((n) => n?.nodeType === 'Markdown') as
|
|
224
|
+
| MarkdownPaneFragmentNode
|
|
225
|
+
| undefined;
|
|
226
|
+
|
|
227
|
+
if (!markdownNode) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
ctx
|
|
233
|
+
.getChildNodeIDs(markdownNode.id)
|
|
234
|
+
.map((childId) => {
|
|
235
|
+
const childNode = ctx.allNodes.get().get(childId) as FlatNode;
|
|
236
|
+
return convertLiveNodeToStorageNode(childNode, ctx, 'retain');
|
|
237
|
+
})
|
|
238
|
+
.filter((n): n is StorageNode => n !== null) || []
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function mergeCopyIntoTemplate(
|
|
243
|
+
template: StoragePane,
|
|
244
|
+
copy: ExtractedCopy
|
|
245
|
+
): StoragePane {
|
|
246
|
+
const newTemplate = { ...template };
|
|
247
|
+
if (newTemplate.markdown) {
|
|
248
|
+
newTemplate.markdown.nodes = copy;
|
|
249
|
+
} else if (copy.length > 0) {
|
|
250
|
+
newTemplate.markdown = {
|
|
251
|
+
nodeType: 'Markdown',
|
|
252
|
+
type: 'markdown',
|
|
253
|
+
defaultClasses: {},
|
|
254
|
+
parentClasses: [],
|
|
255
|
+
nodes: copy,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return newTemplate;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function processStorageNode(
|
|
262
|
+
node: StorageNode,
|
|
263
|
+
parentId: string
|
|
264
|
+
): TemplateNode[] {
|
|
265
|
+
const newId = ulid();
|
|
266
|
+
const { nodes, ...rest } = node;
|
|
267
|
+
const liveNode: TemplateNode = {
|
|
268
|
+
...rest,
|
|
269
|
+
id: newId,
|
|
270
|
+
parentId: parentId,
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const flatList: TemplateNode[] = [liveNode];
|
|
274
|
+
|
|
275
|
+
if (nodes) {
|
|
276
|
+
for (const child of nodes) {
|
|
277
|
+
const processedChildren = processStorageNode(child, newId);
|
|
278
|
+
flatList.push(...processedChildren);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return flatList;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function convertStorageToLiveTemplate(
|
|
286
|
+
storagePane: StoragePane
|
|
287
|
+
): TemplatePane {
|
|
288
|
+
const paneId = ulid();
|
|
289
|
+
const markdownId = ulid();
|
|
290
|
+
const flatNodeList: TemplateNode[] = [];
|
|
291
|
+
|
|
292
|
+
if (storagePane.markdown && storagePane.markdown.nodes) {
|
|
293
|
+
for (const storageNode of storagePane.markdown.nodes) {
|
|
294
|
+
const processedNodes = processStorageNode(storageNode, markdownId);
|
|
295
|
+
flatNodeList.push(...processedNodes);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let liveBgPane: ArtpackImageNode | BgImageNode | VisualBreakNode | undefined =
|
|
300
|
+
undefined;
|
|
301
|
+
if (storagePane.bgPane) {
|
|
302
|
+
const bgPaneId = ulid();
|
|
303
|
+
liveBgPane = {
|
|
304
|
+
...storagePane.bgPane,
|
|
305
|
+
id: bgPaneId,
|
|
306
|
+
parentId: paneId,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const liveTemplatePane: TemplatePane = {
|
|
311
|
+
...storagePane,
|
|
312
|
+
id: paneId,
|
|
313
|
+
parentId: '',
|
|
314
|
+
markdown: {
|
|
315
|
+
...(storagePane.markdown || {
|
|
316
|
+
nodeType: 'Markdown',
|
|
317
|
+
type: 'markdown',
|
|
318
|
+
defaultClasses: {},
|
|
319
|
+
parentClasses: [],
|
|
320
|
+
}),
|
|
321
|
+
id: markdownId,
|
|
322
|
+
markdownId: markdownId,
|
|
323
|
+
parentId: paneId,
|
|
324
|
+
nodes: flatNodeList,
|
|
325
|
+
},
|
|
326
|
+
bgPane: liveBgPane,
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
return liveTemplatePane;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Helper to convert a style object { "px": "4", "fontBOLD": "bold" } to "px-4 font-bold"
|
|
333
|
+
function classObjectToString(
|
|
334
|
+
classObj: Record<string, string> | undefined
|
|
335
|
+
): string {
|
|
336
|
+
if (!classObj) return '';
|
|
337
|
+
|
|
338
|
+
return Object.entries(classObj)
|
|
339
|
+
.map(([key, value]) => {
|
|
340
|
+
const definition = tailwindClasses[key];
|
|
341
|
+
if (!definition) return ''; // Ignore keys not in our definitions
|
|
342
|
+
|
|
343
|
+
if (definition.useKeyAsClass) {
|
|
344
|
+
return value; // e.g., for 'fontBOLD', value is 'font-bold'
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Handle negative values
|
|
348
|
+
if (typeof value === 'string' && value.startsWith('-')) {
|
|
349
|
+
return `-${definition.prefix}${value.substring(1)}`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return `${definition.prefix}${value}`;
|
|
353
|
+
})
|
|
354
|
+
.filter(Boolean)
|
|
355
|
+
.join(' ');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Translates a TemplatePane from the design library into an AI-compatible JSON shell
|
|
360
|
+
* for the hybrid AI copy generation path.
|
|
361
|
+
* @param template The TemplatePane object selected by the user.
|
|
362
|
+
* @returns A JSON string compatible with the AI's second-stage prompt.
|
|
363
|
+
*/
|
|
364
|
+
export function convertTemplateToAIShell(template: TemplatePane): string {
|
|
365
|
+
const shell: any = {
|
|
366
|
+
bgColour: template.bgColour || '#ffffff',
|
|
367
|
+
parentClasses: [],
|
|
368
|
+
defaultClasses: {},
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// 1. Process parentClasses (layout)
|
|
372
|
+
if (template.markdown?.parentClasses) {
|
|
373
|
+
shell.parentClasses = template.markdown.parentClasses.map((layer) => {
|
|
374
|
+
const newLayer: { mobile?: string; tablet?: string; desktop?: string } =
|
|
375
|
+
{};
|
|
376
|
+
if (layer.mobile && Object.keys(layer.mobile).length > 0) {
|
|
377
|
+
newLayer.mobile = classObjectToString(layer.mobile);
|
|
378
|
+
}
|
|
379
|
+
if (layer.tablet && Object.keys(layer.tablet).length > 0) {
|
|
380
|
+
newLayer.tablet = classObjectToString(layer.tablet);
|
|
381
|
+
}
|
|
382
|
+
if (layer.desktop && Object.keys(layer.desktop).length > 0) {
|
|
383
|
+
newLayer.desktop = classObjectToString(layer.desktop);
|
|
384
|
+
}
|
|
385
|
+
return newLayer;
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// 2. Process defaultClasses (typography, etc.)
|
|
390
|
+
if (template.markdown?.defaultClasses) {
|
|
391
|
+
for (const tag in template.markdown.defaultClasses) {
|
|
392
|
+
const styles = template.markdown.defaultClasses[tag];
|
|
393
|
+
const newTagStyles: {
|
|
394
|
+
mobile?: string;
|
|
395
|
+
tablet?: string;
|
|
396
|
+
desktop?: string;
|
|
397
|
+
} = {};
|
|
398
|
+
|
|
399
|
+
if (styles.mobile && Object.keys(styles.mobile).length > 0) {
|
|
400
|
+
newTagStyles.mobile = classObjectToString(styles.mobile);
|
|
401
|
+
}
|
|
402
|
+
if (styles.tablet && Object.keys(styles.tablet).length > 0) {
|
|
403
|
+
newTagStyles.tablet = classObjectToString(styles.tablet);
|
|
404
|
+
}
|
|
405
|
+
if (styles.desktop && Object.keys(styles.desktop).length > 0) {
|
|
406
|
+
newTagStyles.desktop = classObjectToString(styles.desktop);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (Object.keys(newTagStyles).length > 0) {
|
|
410
|
+
shell.defaultClasses[tag] = newTagStyles;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return JSON.stringify(shell, null, 2);
|
|
416
|
+
}
|
package/utils/inject-files.ts
CHANGED
|
@@ -127,6 +127,12 @@ export async function injectTemplateFiles(
|
|
|
127
127
|
),
|
|
128
128
|
dest: 'src/components/compositor/nodes/Pane_eraser.tsx',
|
|
129
129
|
},
|
|
130
|
+
{
|
|
131
|
+
src: resolve(
|
|
132
|
+
'../templates/src/components/compositor/nodes/Pane_DesignLibrary.tsx'
|
|
133
|
+
),
|
|
134
|
+
dest: 'src/components/compositor/nodes/Pane_DesignLibrary.tsx',
|
|
135
|
+
},
|
|
130
136
|
{
|
|
131
137
|
src: resolve(
|
|
132
138
|
'../templates/src/components/compositor/nodes/Pane_layout.tsx'
|
|
@@ -429,6 +435,12 @@ export async function injectTemplateFiles(
|
|
|
429
435
|
),
|
|
430
436
|
dest: 'src/components/edit/pane/AddPanePanel_codehook.tsx',
|
|
431
437
|
},
|
|
438
|
+
{
|
|
439
|
+
src: resolve(
|
|
440
|
+
'../templates/src/components/edit/pane/RestylePaneModal.tsx'
|
|
441
|
+
),
|
|
442
|
+
dest: 'src/components/edit/pane/RestylePaneModal.tsx',
|
|
443
|
+
},
|
|
432
444
|
{
|
|
433
445
|
src: resolve('../templates/src/components/edit/pane/AiPaneGenerator.tsx'),
|
|
434
446
|
dest: 'src/components/edit/pane/AiPaneGenerator.tsx',
|
|
@@ -437,6 +449,24 @@ export async function injectTemplateFiles(
|
|
|
437
449
|
src: resolve('../templates/src/components/edit/pane/AiPanePreview.tsx'),
|
|
438
450
|
dest: 'src/components/edit/pane/AiPanePreview.tsx',
|
|
439
451
|
},
|
|
452
|
+
{
|
|
453
|
+
src: resolve(
|
|
454
|
+
'../templates/src/components/edit/pane/steps/CopyInputStep.tsx'
|
|
455
|
+
),
|
|
456
|
+
dest: 'src/components/edit/pane/steps/CopyInputStep.tsx',
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
src: resolve(
|
|
460
|
+
'../templates/src/components/edit/pane/steps/DesignLibraryStep.tsx'
|
|
461
|
+
),
|
|
462
|
+
dest: 'src/components/edit/pane/steps/DesignLibraryStep.tsx',
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
src: resolve(
|
|
466
|
+
'../templates/src/components/edit/pane/steps/AiDesignStep.tsx'
|
|
467
|
+
),
|
|
468
|
+
dest: 'src/components/edit/pane/steps/AiDesignStep.tsx',
|
|
469
|
+
},
|
|
440
470
|
{
|
|
441
471
|
src: resolve(
|
|
442
472
|
'../templates/src/components/edit/pane/AddPanePanel_newCustomCopy.tsx'
|
|
@@ -629,6 +659,10 @@ export async function injectTemplateFiles(
|
|
|
629
659
|
src: resolve('../templates/src/utils/compositor/typeGuards.ts'),
|
|
630
660
|
dest: 'src/utils/compositor/typeGuards.ts',
|
|
631
661
|
},
|
|
662
|
+
{
|
|
663
|
+
src: resolve('../templates/src/utils/compositor/designLibraryHelper.ts'),
|
|
664
|
+
dest: 'src/utils/compositor/designLibraryHelper.ts',
|
|
665
|
+
},
|
|
632
666
|
{
|
|
633
667
|
src: resolve('../templates/src/utils/compositor/domHelpers.ts'),
|
|
634
668
|
dest: 'src/utils/compositor/domHelpers.ts',
|
|
@@ -1372,6 +1406,12 @@ export async function injectTemplateFiles(
|
|
|
1372
1406
|
src: resolve('../templates/src/components/edit/state/SaveModal.tsx'),
|
|
1373
1407
|
dest: 'src/components/edit/state/SaveModal.tsx',
|
|
1374
1408
|
},
|
|
1409
|
+
{
|
|
1410
|
+
src: resolve(
|
|
1411
|
+
'../templates/src/components/edit/state/SaveToLibraryModal.tsx'
|
|
1412
|
+
),
|
|
1413
|
+
dest: 'src/components/edit/state/SaveToLibraryModal.tsx',
|
|
1414
|
+
},
|
|
1375
1415
|
{
|
|
1376
1416
|
src: resolve('../templates/src/components/edit/state/StylesMemory.tsx'),
|
|
1377
1417
|
dest: 'src/components/edit/state/StylesMemory.tsx',
|