astro-tractstack 2.0.12 → 2.0.14
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 +22 -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 +18 -2
- 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.tsx +5 -1
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +4 -1
- package/templates/src/components/edit/pane/AiPaneGenerator.tsx +264 -94
- package/templates/src/components/edit/pane/AiPanePreview.tsx +60 -210
- package/templates/src/components/edit/pane/PageGen.tsx +1 -1
- package/templates/src/components/edit/pane/PageGenSelector.tsx +4 -0
- package/templates/src/components/edit/pane/RestylePaneModal.tsx +573 -0
- package/templates/src/components/edit/state/SaveToLibraryModal.tsx +205 -0
- package/templates/src/constants/prompts.json +3 -3
- 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 +39 -13
- package/templates/src/utils/compositor/designLibraryHelper.ts +331 -0
- package/templates/src/utils/compositor/processMarkdown.ts +1 -1
- package/utils/inject-files.ts +22 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { useState, useMemo, useEffect } from 'react';
|
|
2
|
+
import type { BrandConfig } from '@/types/tractstack';
|
|
3
|
+
import { savePaneToLibrary } from '@/utils/compositor/designLibraryHelper';
|
|
4
|
+
import StringInput from '@/components/form/StringInput';
|
|
5
|
+
import { CheckIcon } from '@heroicons/react/20/solid';
|
|
6
|
+
|
|
7
|
+
interface SaveToLibraryModalProps {
|
|
8
|
+
paneId: string;
|
|
9
|
+
config: BrandConfig;
|
|
10
|
+
tenantId: string;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type CopyMode = 'retain' | 'lorem' | 'blank';
|
|
15
|
+
type SaveState = 'idle' | 'saving' | 'saved';
|
|
16
|
+
|
|
17
|
+
const copyOptions: { id: CopyMode; title: string; description: string }[] = [
|
|
18
|
+
{
|
|
19
|
+
id: 'retain',
|
|
20
|
+
title: 'Retain Copy',
|
|
21
|
+
description: 'Save the design with all current text and content.',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'lorem',
|
|
25
|
+
title: 'Lorem Ipsum',
|
|
26
|
+
description:
|
|
27
|
+
'Save the design structure, replacing text with placeholders and removing overrides.',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'blank',
|
|
31
|
+
title: 'Blank',
|
|
32
|
+
description: 'Save the design structure with no content nodes.',
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const OTHER_CATEGORY = 'other';
|
|
37
|
+
|
|
38
|
+
export function SaveToLibraryModal({
|
|
39
|
+
paneId,
|
|
40
|
+
config,
|
|
41
|
+
tenantId,
|
|
42
|
+
onClose,
|
|
43
|
+
}: SaveToLibraryModalProps) {
|
|
44
|
+
const [title, setTitle] = useState('');
|
|
45
|
+
const [selectedCategory, setSelectedCategory] = useState(OTHER_CATEGORY);
|
|
46
|
+
const [customCategory, setCustomCategory] = useState('');
|
|
47
|
+
const [copyMode, setCopyMode] = useState<CopyMode>('retain');
|
|
48
|
+
const [saveState, setSaveState] = useState<SaveState>('idle');
|
|
49
|
+
const [error, setError] = useState('');
|
|
50
|
+
|
|
51
|
+
const categories = useMemo(() => {
|
|
52
|
+
const cats =
|
|
53
|
+
config.DESIGN_LIBRARY?.map((item) => item.category).filter(
|
|
54
|
+
(v, i, a) => a.indexOf(v) === i
|
|
55
|
+
) || [];
|
|
56
|
+
return [...cats, OTHER_CATEGORY];
|
|
57
|
+
}, [config.DESIGN_LIBRARY]);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (saveState === 'saved') {
|
|
61
|
+
const timer = setTimeout(() => {
|
|
62
|
+
onClose();
|
|
63
|
+
}, 2000);
|
|
64
|
+
return () => clearTimeout(timer);
|
|
65
|
+
}
|
|
66
|
+
}, [saveState, onClose]);
|
|
67
|
+
|
|
68
|
+
const handleSave = async () => {
|
|
69
|
+
const finalCategory =
|
|
70
|
+
selectedCategory === OTHER_CATEGORY ? customCategory : selectedCategory;
|
|
71
|
+
if (!title || !finalCategory) {
|
|
72
|
+
setError('Title and category are required.');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
setError('');
|
|
76
|
+
setSaveState('saving');
|
|
77
|
+
|
|
78
|
+
const formData = {
|
|
79
|
+
title: title,
|
|
80
|
+
category: finalCategory,
|
|
81
|
+
copyMode: copyMode,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const success = await savePaneToLibrary(paneId, tenantId, config, formData);
|
|
85
|
+
|
|
86
|
+
if (success) {
|
|
87
|
+
setSaveState('saved');
|
|
88
|
+
} else {
|
|
89
|
+
setSaveState('idle');
|
|
90
|
+
setError('Failed to save template. Please try again.');
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div
|
|
96
|
+
className="z-105 fixed inset-0 flex items-center justify-center bg-black/50"
|
|
97
|
+
onClick={saveState === 'idle' ? onClose : undefined}
|
|
98
|
+
>
|
|
99
|
+
<div
|
|
100
|
+
className="w-full max-w-lg rounded-lg bg-white p-6 shadow-xl"
|
|
101
|
+
onClick={(e) => e.stopPropagation()}
|
|
102
|
+
>
|
|
103
|
+
<div className="flex items-center justify-between">
|
|
104
|
+
<h2 className="text-lg font-medium text-gray-900">
|
|
105
|
+
Save Pane to Library
|
|
106
|
+
</h2>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div className="mt-4 space-y-4">
|
|
110
|
+
<StringInput label="Title" value={title} onChange={setTitle} />
|
|
111
|
+
|
|
112
|
+
<div>
|
|
113
|
+
<label
|
|
114
|
+
htmlFor="category-select"
|
|
115
|
+
className="block text-sm font-medium text-gray-700"
|
|
116
|
+
>
|
|
117
|
+
Category
|
|
118
|
+
</label>
|
|
119
|
+
<select
|
|
120
|
+
id="category-select"
|
|
121
|
+
value={selectedCategory}
|
|
122
|
+
onChange={(e) => setSelectedCategory(e.target.value)}
|
|
123
|
+
className="mt-1 block w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-cyan-500 focus:outline-none focus:ring-cyan-500 sm:text-sm"
|
|
124
|
+
>
|
|
125
|
+
{categories.map((cat) => (
|
|
126
|
+
<option key={cat} value={cat}>
|
|
127
|
+
{cat === OTHER_CATEGORY ? 'New Category...' : cat}
|
|
128
|
+
</option>
|
|
129
|
+
))}
|
|
130
|
+
</select>
|
|
131
|
+
{selectedCategory === OTHER_CATEGORY && (
|
|
132
|
+
<StringInput
|
|
133
|
+
label="New Category Name"
|
|
134
|
+
value={customCategory}
|
|
135
|
+
onChange={setCustomCategory}
|
|
136
|
+
className="mt-2"
|
|
137
|
+
/>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div>
|
|
142
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
143
|
+
Content Mode
|
|
144
|
+
</label>
|
|
145
|
+
<fieldset className="mt-2">
|
|
146
|
+
<legend className="sr-only">Copy Mode</legend>
|
|
147
|
+
<div className="space-y-2">
|
|
148
|
+
{copyOptions.map((option) => (
|
|
149
|
+
<div key={option.id} className="flex items-center">
|
|
150
|
+
<input
|
|
151
|
+
id={option.id}
|
|
152
|
+
name="copy-mode"
|
|
153
|
+
type="radio"
|
|
154
|
+
value={option.id}
|
|
155
|
+
checked={copyMode === option.id}
|
|
156
|
+
onChange={() => setCopyMode(option.id)}
|
|
157
|
+
className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
|
|
158
|
+
/>
|
|
159
|
+
<label
|
|
160
|
+
htmlFor={option.id}
|
|
161
|
+
className="ml-3 block text-sm font-medium text-gray-700"
|
|
162
|
+
>
|
|
163
|
+
{option.title}
|
|
164
|
+
<p className="text-xs text-gray-500">
|
|
165
|
+
{option.description}
|
|
166
|
+
</p>
|
|
167
|
+
</label>
|
|
168
|
+
</div>
|
|
169
|
+
))}
|
|
170
|
+
</div>
|
|
171
|
+
</fieldset>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{error && <p className="text-sm text-red-600">{error}</p>}
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div className="mt-6 flex justify-end space-x-3">
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
disabled={saveState !== 'idle'}
|
|
181
|
+
className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 disabled:opacity-50"
|
|
182
|
+
onClick={onClose}
|
|
183
|
+
>
|
|
184
|
+
Cancel
|
|
185
|
+
</button>
|
|
186
|
+
<button
|
|
187
|
+
type="button"
|
|
188
|
+
disabled={saveState !== 'idle'}
|
|
189
|
+
className="flex min-w-36 items-center justify-center rounded-md border border-transparent bg-cyan-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-cyan-700 disabled:cursor-not-allowed disabled:opacity-50"
|
|
190
|
+
onClick={handleSave}
|
|
191
|
+
>
|
|
192
|
+
{saveState === 'idle' && 'Save to Library'}
|
|
193
|
+
{saveState === 'saving' && 'Saving...'}
|
|
194
|
+
{saveState === 'saved' && (
|
|
195
|
+
<>
|
|
196
|
+
<CheckIcon className="mr-2 h-5 w-5" />
|
|
197
|
+
Saved
|
|
198
|
+
</>
|
|
199
|
+
)}
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
},
|
|
42
42
|
"aiPaneShellPrompt": {
|
|
43
43
|
"system": "You are an expert web designer. Your task is to generate the structural design for a component as a single JSON object. Respond *only* with the JSON.",
|
|
44
|
-
"user_template": "Generate the design JSON for a component with the following characteristics:\n\nDesign Style: \"{{DESIGN_INPUT}}\"\nLayout Type: \"{{LAYOUT_TYPE}}\"\n\nCRITICAL RULES:\n1. You must respond with a JSON object with keys: `bgColour`, `parentClasses`, `defaultClasses`.\n2. The `parentClasses` value *must* be structured with our internal responsive object schema (e.g., `[ { \"mobile\": { \"px\": \"4\", \"py\": \"12\" } } ]`).\n3. The `defaultClasses` value *must* be structured with responsive keys containing
|
|
44
|
+
"user_template": "Generate the design JSON for a component with the following characteristics:\n\nDesign Style: \"{{DESIGN_INPUT}}\"\nLayout Type: \"{{LAYOUT_TYPE}}\"\n\nCRITICAL RULES:\n1. You must respond with a JSON object with keys: `bgColour`, `parentClasses`, `defaultClasses`.\n2. The `parentClasses` value *must* be structured with our internal responsive object schema (e.g., `[ { \"mobile\": { \"px\": \"4\", \"py\": \"12\" } } ]`).\n3. The `defaultClasses` value *must* be structured with responsive keys (`mobile`, `tablet`, `desktop`) containing Tailwind class strings.\n4. Ensure the selected `bgColour` provides **high contrast** (meeting at least WCAG AA standards - 4.5:1 for normal text, 3:1 for large text) with the primary text colors defined in `defaultClasses`.\n\nEXAMPLE:\n{\n \"bgColour\": \"#050710\",\n \"parentClasses\": [\n { \"mobile\": { \"px\": \"6\", \"py\": \"24\" }, \"tablet\": { \"px\": \"8\", \"py\": \"32\" } },\n { \"mobile\": { \"mx\": \"auto\", \"maxW\": \"2xl\", \"textALIGN\": \"center\" }, \"tablet\": { \"maxW\": \"4xl\" } }\n ],\n \"defaultClasses\": {\n \"h2\": { \"mobile\": \"text-4xl font-bold tracking-tight text-white mt-4\", \"tablet\": \"text-6xl\", \"desktop\": \"text-7xl\" },\n \"p\": { \"mobile\": \"text-lg leading-8 text-gray-300 mt-6\", \"tablet\": \"text-xl\", \"desktop\": \"\" }\n }\n}"
|
|
45
45
|
},
|
|
46
46
|
"aiPaneCopyPrompt": {
|
|
47
|
-
"system": "You are an expert **web designer and copywriter**. Your task is to generate a single, visually compelling block of HTML content. You must ensure the content is well-written, engaging, and **
|
|
48
|
-
"user_template": "Here is the design 'shell' and 'theme' (bgColour, parentClasses, and defaultClasses) you must write your HTML for. Your HTML will be placed *inside* this shell. Use the `defaultClasses` as your base theme for styling:\n{{SHELL_JSON}}\n\nNow, generate the HTML content based on these inputs:\n\nContent Prompt: \"{{COPY_INPUT}}\"\nDesign Style: **strictly for visual reference** when choosing element styles **DO NOT** include any words or concepts from the `Design Style` input in the written copy text itself.\"{{DESIGN_INPUT}}\"\nLayout Type: \"{{LAYOUT_TYPE}}\"\n\nCRITICAL RULES:\n1. You are responsible for the **inner layout and visual rhythm**. You MUST add appropriate vertical margins (e.g., `mt-4`, `mt-6`, `mt-8`) directly to any HTML block elements that *deviate* from the default spacing. **Elements must not touch.**\n2. You **MUST NOT** use `<h1>` tags. You must use `<h2>`, `<h3>`, and `<p>` tags for all text content.\n3. For responsive styles, you *must* only use `md:` and `xl:` prefixes.\n4. To make headlines pop, you MUST wrap key words in `<span>` tags with creative classes (e.g., gradient text, different colors).\n5. **All text**, even short links or phrases (like 'Learn more →'), **must** be wrapped in a block element like `<p>` or `<button>`.\n6. You MUST include at least one `<button>` tag for the primary call-to-action.\n7. Respond *only* with the raw HTML.\n\nEXAMPLE of a good, well-spaced response:\n<h2 class=\"text-4xl font-bold tracking-tight text-white md:text-6xl\"><span class=\"bg-gradient-to-r from-purple-500 to-indigo-400 bg-clip-text text-transparent\">Own the Art.</span> Possess the Reality.</h2>\n<p class=\"mt-6 text-lg leading-8 text-gray-300 md:text-xl\">Every Sneaky Productions NFT is your key. This is where digital rarity meets tangible legacy.</p>\n<button class=\"mt-8 rounded-md bg-indigo-600 px-5 py-3 text-base font-semibold text-white shadow-sm hover:bg-indigo-500\">Secure Your Drop</button>\n<p class=\"mt-4 text-sm text-gray-400\">Learn more <span>→</span></p>"
|
|
47
|
+
"system": "You are an expert **web designer and copywriter**. Your task is to generate a single, visually compelling block of HTML content. You must ensure the content is well-written, engaging, **beautifully spaced**, and **highly readable**.",
|
|
48
|
+
"user_template": "Here is the design 'shell' and 'theme' (bgColour, parentClasses, and defaultClasses) you must write your HTML for. Your HTML will be placed *inside* this shell. Use the `defaultClasses` as your base theme for styling:\n{{SHELL_JSON}}\n\nNow, generate the HTML content based on these inputs:\n\nContent Prompt: \"{{COPY_INPUT}}\"\nDesign Style: **strictly for visual reference** when choosing element styles **DO NOT** include any words or concepts from the `Design Style` input in the written copy text itself.\"{{DESIGN_INPUT}}\"\nLayout Type: \"{{LAYOUT_TYPE}}\"\n\nCRITICAL RULES:\n1. You are responsible for the **inner layout and visual rhythm**. You MUST add appropriate vertical margins (e.g., `mt-4`, `mt-6`, `mt-8`) directly to any HTML block elements that *deviate* from the default spacing. **Elements must not touch.**\n2. You **MUST NOT** use `<h1>` tags. You must use `<h2>`, `<h3>`, and `<p>` tags for all text content.\n3. For responsive styles, you *must* only use `md:` and `xl:` prefixes.\n4. To make headlines pop, you MUST wrap key words in `<span>` tags with creative classes (e.g., gradient text, different colors).\n5. **All text**, even short links or phrases (like 'Learn more →'), **must** be wrapped in a block element like `<p>` or `<button>`.\n6. You MUST include at least one `<button>` tag for the primary call-to-action.\n7. Verify that **all text elements**, including text within `<span>` tags, `<button>` elements, and any elements using override classes, maintain **high contrast** (meeting at least WCAG AA standards - 4.5:1 for normal text, 3:1 for large text) against the `bgColour` provided in the `SHELL_JSON`. **Prioritize readability above all else**.\n8. Respond *only* with the raw HTML.\n\nEXAMPLE of a good, well-spaced response:\n<h2 class=\"text-4xl font-bold tracking-tight text-white md:text-6xl\"><span class=\"bg-gradient-to-r from-purple-500 to-indigo-400 bg-clip-text text-transparent\">Own the Art.</span> Possess the Reality.</h2>\n<p class=\"mt-6 text-lg leading-8 text-gray-300 md:text-xl\">Every Sneaky Productions NFT is your key. This is where digital rarity meets tangible legacy.</p>\n<button class=\"mt-8 rounded-md bg-indigo-600 px-5 py-3 text-base font-semibold text-white shadow-sm hover:bg-indigo-500\">Secure Your Drop</button>\n<p class=\"mt-4 text-sm text-gray-400\">Learn more <span>→</span></p>"
|
|
49
49
|
}
|
|
50
50
|
}
|
|
@@ -19,6 +19,8 @@ export interface SelectionStoreState extends SelectionRange {
|
|
|
19
19
|
isActive: boolean;
|
|
20
20
|
selectionBox: SelectionBox | null;
|
|
21
21
|
pendingAction: 'style' | 'link' | null;
|
|
22
|
+
isRestyleModalOpen: boolean;
|
|
23
|
+
paneToRestyleId: string | null;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
const DEFAULT_SELECTION_STATE: SelectionStoreState = {
|
|
@@ -32,6 +34,8 @@ const DEFAULT_SELECTION_STATE: SelectionStoreState = {
|
|
|
32
34
|
endCharOffset: 0,
|
|
33
35
|
selectionBox: null,
|
|
34
36
|
pendingAction: null,
|
|
37
|
+
isRestyleModalOpen: false,
|
|
38
|
+
paneToRestyleId: null,
|
|
35
39
|
};
|
|
36
40
|
|
|
37
41
|
export const selectionStore = map<SelectionStoreState>(DEFAULT_SELECTION_STATE);
|
|
@@ -11,6 +11,7 @@ export type ToolModeVal =
|
|
|
11
11
|
| 'eraser'
|
|
12
12
|
| 'move'
|
|
13
13
|
| 'layout'
|
|
14
|
+
| 'designLibrary'
|
|
14
15
|
| 'debug';
|
|
15
16
|
|
|
16
17
|
export const toolAddModes = [
|
|
@@ -404,7 +405,56 @@ export type TemplatePane = PaneNode & {
|
|
|
404
405
|
id?: string;
|
|
405
406
|
parentId?: string;
|
|
406
407
|
markdown?: TemplateMarkdown;
|
|
407
|
-
bgPane?: VisualBreakNode | ArtpackImageNode;
|
|
408
|
+
bgPane?: VisualBreakNode | ArtpackImageNode | BgImageNode;
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
export type StorageArtpackImageNode = Omit<
|
|
412
|
+
ArtpackImageNode,
|
|
413
|
+
'id' | 'parentId' | 'isChanged'
|
|
414
|
+
>;
|
|
415
|
+
export type StorageBgImageNode = Omit<
|
|
416
|
+
BgImageNode,
|
|
417
|
+
'id' | 'parentId' | 'isChanged' | 'base64Data'
|
|
418
|
+
>;
|
|
419
|
+
export type StorageVisualBreakNode = Omit<
|
|
420
|
+
VisualBreakNode,
|
|
421
|
+
'id' | 'parentId' | 'isChanged'
|
|
422
|
+
>;
|
|
423
|
+
|
|
424
|
+
export type StorageBgPane =
|
|
425
|
+
| StorageArtpackImageNode
|
|
426
|
+
| StorageBgImageNode
|
|
427
|
+
| StorageVisualBreakNode;
|
|
428
|
+
|
|
429
|
+
export type StorageNode = Omit<
|
|
430
|
+
FlatNode,
|
|
431
|
+
'id' | 'parentId' | 'isChanged' | 'base64Data' | 'isPlaceholder'
|
|
432
|
+
> & {
|
|
433
|
+
nodes?: StorageNode[];
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
export type StorageMarkdown = Omit<
|
|
437
|
+
MarkdownPaneFragmentNode,
|
|
438
|
+
'id' | 'parentId' | 'isChanged' | 'markdownId' | 'parentCss' | 'nodes'
|
|
439
|
+
> & {
|
|
440
|
+
nodes?: StorageNode[];
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
export type StoragePane = Omit<
|
|
444
|
+
PaneNode,
|
|
445
|
+
| 'id'
|
|
446
|
+
| 'parentId'
|
|
447
|
+
| 'isChanged'
|
|
448
|
+
| 'created'
|
|
449
|
+
| 'changed'
|
|
450
|
+
| 'heldBeliefs'
|
|
451
|
+
| 'withheldBeliefs'
|
|
452
|
+
| 'codeHookTarget'
|
|
453
|
+
| 'codeHookPayload'
|
|
454
|
+
| 'markdown'
|
|
455
|
+
> & {
|
|
456
|
+
markdown?: StorageMarkdown;
|
|
457
|
+
bgPane?: StorageBgPane;
|
|
408
458
|
};
|
|
409
459
|
|
|
410
460
|
export interface LinkNode extends FlatNode {
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
import type { StoragePane } from './compositorTypes';
|
|
2
|
+
|
|
3
|
+
export type DesignLibraryEntry = {
|
|
4
|
+
category: string;
|
|
5
|
+
title: string;
|
|
6
|
+
template: StoragePane;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type DesignLibraryConfig = DesignLibraryEntry[];
|
|
10
|
+
|
|
2
11
|
export interface BaseComponentProps {
|
|
3
|
-
/** Additional CSS classes */
|
|
4
12
|
class?: string;
|
|
5
|
-
|
|
6
|
-
/** Inline styles */
|
|
7
13
|
style?: React.CSSProperties | string;
|
|
8
|
-
|
|
9
|
-
/** Component ID */
|
|
10
14
|
id?: string;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
|
-
// HTMX-specific attributes for components
|
|
14
17
|
export interface HTMXAttributes {
|
|
15
18
|
'hx-get'?: string;
|
|
16
19
|
'hx-post'?: string;
|
|
@@ -148,39 +151,40 @@ export interface FullContentMapItem {
|
|
|
148
151
|
}
|
|
149
152
|
|
|
150
153
|
export interface BrandConfig {
|
|
151
|
-
|
|
152
|
-
SITE_INIT
|
|
153
|
-
WORDMARK_MODE
|
|
154
|
-
OPEN_DEMO
|
|
155
|
-
STYLES_VER
|
|
156
|
-
HOME_SLUG
|
|
157
|
-
TRACTSTACK_HOME_SLUG
|
|
158
|
-
THEME
|
|
159
|
-
BRAND_COLOURS
|
|
160
|
-
SOCIALS
|
|
161
|
-
LOGO
|
|
162
|
-
WORDMARK
|
|
163
|
-
FAVICON
|
|
164
|
-
SITE_URL
|
|
165
|
-
SLOGAN
|
|
166
|
-
FOOTER
|
|
167
|
-
OG
|
|
168
|
-
OGLOGO
|
|
169
|
-
OGTITLE
|
|
170
|
-
OGAUTHOR
|
|
171
|
-
OGDESC
|
|
154
|
+
TENANT_ID: string;
|
|
155
|
+
SITE_INIT?: boolean;
|
|
156
|
+
WORDMARK_MODE?: string;
|
|
157
|
+
OPEN_DEMO?: boolean;
|
|
158
|
+
STYLES_VER?: number;
|
|
159
|
+
HOME_SLUG?: string;
|
|
160
|
+
TRACTSTACK_HOME_SLUG?: string;
|
|
161
|
+
THEME?: string; // e.g., "light-bold"
|
|
162
|
+
BRAND_COLOURS?: string; // e.g., "10120d,fcfcfc,f58333,c8df8c,293f58,a7b1b7,393d34,e3e3e3"
|
|
163
|
+
SOCIALS?: string; // e.g., "github|https://github.com/user,twitter|https://twitter.com/user"
|
|
164
|
+
LOGO?: string;
|
|
165
|
+
WORDMARK?: string;
|
|
166
|
+
FAVICON?: string;
|
|
167
|
+
SITE_URL?: string;
|
|
168
|
+
SLOGAN?: string;
|
|
169
|
+
FOOTER?: string;
|
|
170
|
+
OG?: string;
|
|
171
|
+
OGLOGO?: string;
|
|
172
|
+
OGTITLE?: string;
|
|
173
|
+
OGAUTHOR?: string;
|
|
174
|
+
OGDESC?: string;
|
|
172
175
|
LOGO_BASE64?: string;
|
|
173
176
|
WORDMARK_BASE64?: string;
|
|
174
177
|
OG_BASE64?: string;
|
|
175
178
|
OGLOGO_BASE64?: string;
|
|
176
179
|
FAVICON_BASE64?: string;
|
|
177
|
-
GTAG
|
|
180
|
+
GTAG?: string;
|
|
178
181
|
KNOWN_RESOURCES?: KnownResourcesConfig;
|
|
179
|
-
|
|
182
|
+
DESIGN_LIBRARY?: DesignLibraryConfig;
|
|
183
|
+
HAS_AAI?: boolean;
|
|
180
184
|
}
|
|
181
185
|
|
|
182
186
|
export interface BrandConfigState {
|
|
183
|
-
|
|
187
|
+
tenantId: string;
|
|
184
188
|
siteInit: boolean;
|
|
185
189
|
wordmarkMode: string;
|
|
186
190
|
openDemo: boolean;
|
|
@@ -208,6 +212,7 @@ export interface BrandConfigState {
|
|
|
208
212
|
faviconBase64?: string;
|
|
209
213
|
gtag: string;
|
|
210
214
|
knownResources: KnownResourcesConfig;
|
|
215
|
+
designLibrary?: DesignLibraryConfig;
|
|
211
216
|
hasAAI: boolean;
|
|
212
217
|
}
|
|
213
218
|
|
|
@@ -7,9 +7,11 @@ export async function saveBrandConfig(
|
|
|
7
7
|
brandConfig: BrandConfig
|
|
8
8
|
): Promise<BrandConfig> {
|
|
9
9
|
const api = new TractStackAPI(tenantId);
|
|
10
|
+
const config: any = brandConfig;
|
|
11
|
+
delete config.TENANT_ID;
|
|
10
12
|
try {
|
|
11
13
|
const response = await api.put('/api/v1/config/brand', {
|
|
12
|
-
...
|
|
14
|
+
...config,
|
|
13
15
|
SITE_INIT: true,
|
|
14
16
|
});
|
|
15
17
|
if (!response.success) {
|
|
@@ -39,6 +41,7 @@ export async function getBrandConfig(tenantId: string): Promise<BrandConfig> {
|
|
|
39
41
|
) {
|
|
40
42
|
// Return empty/default config when backend is down
|
|
41
43
|
return {
|
|
44
|
+
TENANT_ID: tenantId,
|
|
42
45
|
SITE_INIT: false,
|
|
43
46
|
WORDMARK_MODE: '',
|
|
44
47
|
BRAND_COLOURS: '',
|
|
@@ -61,16 +64,18 @@ export async function getBrandConfig(tenantId: string): Promise<BrandConfig> {
|
|
|
61
64
|
GTAG: '',
|
|
62
65
|
STYLES_VER: 1,
|
|
63
66
|
KNOWN_RESOURCES: {},
|
|
67
|
+
DESIGN_LIBRARY: [],
|
|
64
68
|
HAS_AAI: false,
|
|
65
69
|
} as BrandConfig;
|
|
66
70
|
}
|
|
67
71
|
throw new Error(response.error || 'Failed to get brand configuration');
|
|
68
72
|
}
|
|
69
|
-
return response.data;
|
|
73
|
+
return { ...response.data, TENANT_ID: tenantId };
|
|
70
74
|
} catch (error) {
|
|
71
75
|
// If it's a network error (backend down), return default config
|
|
72
76
|
if (error instanceof TypeError && error.message.includes('fetch failed')) {
|
|
73
77
|
return {
|
|
78
|
+
TENANT_ID: tenantId,
|
|
74
79
|
SITE_INIT: false,
|
|
75
80
|
WORDMARK_MODE: '',
|
|
76
81
|
BRAND_COLOURS: '',
|
|
@@ -93,6 +98,7 @@ export async function getBrandConfig(tenantId: string): Promise<BrandConfig> {
|
|
|
93
98
|
GTAG: '',
|
|
94
99
|
STYLES_VER: 1,
|
|
95
100
|
KNOWN_RESOURCES: {},
|
|
101
|
+
DESIGN_LIBRARY: [],
|
|
96
102
|
HAS_AAI: false,
|
|
97
103
|
} as BrandConfig;
|
|
98
104
|
}
|
|
@@ -11,6 +11,7 @@ export function convertToLocalState(
|
|
|
11
11
|
brandConfig: BrandConfig
|
|
12
12
|
): BrandConfigState {
|
|
13
13
|
return {
|
|
14
|
+
tenantId: brandConfig.TENANT_ID || `default`,
|
|
14
15
|
siteInit: brandConfig.SITE_INIT ?? false,
|
|
15
16
|
wordmarkMode: brandConfig.WORDMARK_MODE ?? '',
|
|
16
17
|
brandColours: brandConfig.BRAND_COLOURS
|
|
@@ -37,6 +38,7 @@ export function convertToLocalState(
|
|
|
37
38
|
gtag: brandConfig.GTAG ?? '',
|
|
38
39
|
stylesVer: brandConfig.STYLES_VER ?? 1,
|
|
39
40
|
knownResources: brandConfig.KNOWN_RESOURCES ?? {},
|
|
41
|
+
designLibrary: brandConfig.DESIGN_LIBRARY ?? undefined,
|
|
40
42
|
hasAAI: brandConfig.HAS_AAI ?? false,
|
|
41
43
|
};
|
|
42
44
|
}
|
|
@@ -49,6 +51,7 @@ export function convertToBackendFormat(
|
|
|
49
51
|
localState: BrandConfigState
|
|
50
52
|
): BrandConfig {
|
|
51
53
|
return {
|
|
54
|
+
TENANT_ID: localState.tenantId,
|
|
52
55
|
SITE_INIT: localState.siteInit,
|
|
53
56
|
WORDMARK_MODE: localState.wordmarkMode,
|
|
54
57
|
BRAND_COLOURS: localState.brandColours.join(','),
|
|
@@ -66,6 +69,7 @@ export function convertToBackendFormat(
|
|
|
66
69
|
OGDESC: localState.ogdesc,
|
|
67
70
|
GTAG: localState.gtag,
|
|
68
71
|
KNOWN_RESOURCES: localState.knownResources,
|
|
72
|
+
DESIGN_LIBRARY: localState.designLibrary,
|
|
69
73
|
HAS_AAI: localState.hasAAI,
|
|
70
74
|
|
|
71
75
|
// ALWAYS send asset paths (current state)
|
|
@@ -250,7 +250,12 @@ function sanitizeButtonClasses(
|
|
|
250
250
|
return buttonPayload;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
function walkDom(
|
|
253
|
+
function walkDom(
|
|
254
|
+
domNode: Node,
|
|
255
|
+
parentId: string,
|
|
256
|
+
parsedNodes: ParsedNode[],
|
|
257
|
+
markdownId: string
|
|
258
|
+
) {
|
|
254
259
|
if (domNode.nodeType === Node.TEXT_NODE) {
|
|
255
260
|
const copy = domNode.textContent || '';
|
|
256
261
|
// Preserve leading/trailing spaces unless the *entire* content is just whitespace.
|
|
@@ -305,17 +310,37 @@ function walkDom(domNode: Node, parentId: string, parsedNodes: ParsedNode[]) {
|
|
|
305
310
|
const tagName = el.tagName.toLowerCase();
|
|
306
311
|
|
|
307
312
|
if (!ALLOWED_TAGS.has(tagName)) {
|
|
308
|
-
el.childNodes.forEach((child) =>
|
|
313
|
+
el.childNodes.forEach((child) =>
|
|
314
|
+
walkDom(child, parentId, parsedNodes, markdownId)
|
|
315
|
+
);
|
|
309
316
|
return;
|
|
310
317
|
}
|
|
311
318
|
|
|
312
319
|
if (tagName === 'button') {
|
|
320
|
+
let finalParentId = parentId;
|
|
321
|
+
|
|
322
|
+
if (parentId === markdownId) {
|
|
323
|
+
const pNodeId = ulid();
|
|
324
|
+
const pNode: TemplateNode = {
|
|
325
|
+
id: pNodeId,
|
|
326
|
+
nodeType: 'TagElement',
|
|
327
|
+
parentId: parentId,
|
|
328
|
+
tagName: 'p',
|
|
329
|
+
overrideClasses: {},
|
|
330
|
+
};
|
|
331
|
+
parsedNodes.push({
|
|
332
|
+
flatNode: pNode,
|
|
333
|
+
responsiveClasses: {},
|
|
334
|
+
});
|
|
335
|
+
finalParentId = pNodeId;
|
|
336
|
+
}
|
|
337
|
+
|
|
313
338
|
const buttonPayload = sanitizeButtonClasses(el.getAttribute('class'));
|
|
314
339
|
|
|
315
340
|
const flatNode: TemplateNode = {
|
|
316
341
|
id: ulid(),
|
|
317
342
|
nodeType: 'TagElement',
|
|
318
|
-
parentId:
|
|
343
|
+
parentId: finalParentId,
|
|
319
344
|
tagName: 'a',
|
|
320
345
|
overrideClasses: {},
|
|
321
346
|
href: '#',
|
|
@@ -330,7 +355,9 @@ function walkDom(domNode: Node, parentId: string, parsedNodes: ParsedNode[]) {
|
|
|
330
355
|
responsiveClasses: {},
|
|
331
356
|
});
|
|
332
357
|
|
|
333
|
-
el.childNodes.forEach((child) =>
|
|
358
|
+
el.childNodes.forEach((child) =>
|
|
359
|
+
walkDom(child, flatNode.id, parsedNodes, markdownId)
|
|
360
|
+
);
|
|
334
361
|
return;
|
|
335
362
|
}
|
|
336
363
|
|
|
@@ -353,7 +380,9 @@ function walkDom(domNode: Node, parentId: string, parsedNodes: ParsedNode[]) {
|
|
|
353
380
|
responsiveClasses: responsive,
|
|
354
381
|
});
|
|
355
382
|
|
|
356
|
-
el.childNodes.forEach((child) =>
|
|
383
|
+
el.childNodes.forEach((child) =>
|
|
384
|
+
walkDom(child, flatNode.id, parsedNodes, markdownId)
|
|
385
|
+
);
|
|
357
386
|
}
|
|
358
387
|
|
|
359
388
|
function findMostCommonClasses(nodes: ParsedNode[]): ResponsiveClasses {
|
|
@@ -476,18 +505,15 @@ export const parseAiPane = (
|
|
|
476
505
|
const paneId = ulid();
|
|
477
506
|
const markdownId = ulid();
|
|
478
507
|
|
|
479
|
-
// --- MODIFICATION START ---
|
|
480
|
-
// Normalize the keys within parentClasses using the new helper
|
|
481
508
|
const transformedParentClasses: ParentClassesPayload = (
|
|
482
509
|
shell.parentClasses || []
|
|
483
510
|
).map(
|
|
484
511
|
(layer): ParentClassLayer => ({
|
|
485
|
-
mobile: normalizeKeys(layer.mobile),
|
|
486
|
-
tablet: normalizeKeys(layer.tablet),
|
|
487
|
-
desktop: normalizeKeys(layer.desktop),
|
|
512
|
+
mobile: normalizeKeys(layer.mobile),
|
|
513
|
+
tablet: normalizeKeys(layer.tablet),
|
|
514
|
+
desktop: normalizeKeys(layer.desktop),
|
|
488
515
|
})
|
|
489
516
|
);
|
|
490
|
-
// --- MODIFICATION END ---
|
|
491
517
|
|
|
492
518
|
const shellDefaults = parseDefaultClassesFromShell(shell.defaultClasses);
|
|
493
519
|
|
|
@@ -497,12 +523,12 @@ export const parseAiPane = (
|
|
|
497
523
|
parentId: paneId,
|
|
498
524
|
type: 'markdown',
|
|
499
525
|
markdownId: ulid(),
|
|
500
|
-
parentClasses: transformedParentClasses,
|
|
526
|
+
parentClasses: transformedParentClasses,
|
|
501
527
|
defaultClasses: shellDefaults,
|
|
502
528
|
};
|
|
503
529
|
|
|
504
530
|
const allParsedNodes: ParsedNode[] = [];
|
|
505
|
-
walkDom(doc.body, markdownId, allParsedNodes);
|
|
531
|
+
walkDom(doc.body, markdownId, allParsedNodes, markdownId);
|
|
506
532
|
|
|
507
533
|
const templateNodes: TemplateNode[] = [];
|
|
508
534
|
const nodesByTag = new Map<string, ParsedNode[]>();
|