astro-tractstack 2.0.29 → 2.0.31

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.
@@ -1,6 +1,13 @@
1
1
  import { useEffect } from 'react';
2
+ import BooleanToggle from '@/components/form/BooleanToggle';
3
+ import EnumSelect from '@/components/form/EnumSelect';
2
4
 
3
- type CopyMode = 'prompt' | 'raw';
5
+ export type CopyMode = 'prompt' | 'raw' | 'original';
6
+
7
+ interface PromptOption {
8
+ label: string;
9
+ value: string;
10
+ }
4
11
 
5
12
  interface CopyInputStepProps {
6
13
  copyMode: CopyMode;
@@ -10,6 +17,13 @@ interface CopyInputStepProps {
10
17
  copyValue: string;
11
18
  onCopyValueChange: (value: string) => void;
12
19
  defaultPrompt?: string;
20
+ hasRetainedContent?: boolean;
21
+ promptOptions: PromptOption[];
22
+ selectedPromptId: string;
23
+ onSelectedPromptIdChange: (id: string) => void;
24
+ isAiStyling: boolean;
25
+ onIsAiStylingChange: (checked: boolean) => void;
26
+ showStyleToggle?: boolean;
13
27
  }
14
28
 
15
29
  export const CopyInputStep = ({
@@ -20,9 +34,15 @@ export const CopyInputStep = ({
20
34
  copyValue,
21
35
  onCopyValueChange,
22
36
  defaultPrompt,
37
+ hasRetainedContent = false,
38
+ promptOptions,
39
+ selectedPromptId,
40
+ onSelectedPromptIdChange,
41
+ isAiStyling,
42
+ onIsAiStylingChange,
43
+ showStyleToggle = true,
23
44
  }: CopyInputStepProps) => {
24
45
  useEffect(() => {
25
- // Pre-populate the prompt field if a default is provided and the field is empty
26
46
  if (defaultPrompt && !promptValue) {
27
47
  onPromptValueChange(defaultPrompt);
28
48
  }
@@ -33,7 +53,7 @@ export const CopyInputStep = ({
33
53
  <label className="block text-lg font-bold text-gray-800">
34
54
  1. Provide Content
35
55
  </label>
36
- <div className="my-2 flex space-x-4">
56
+ <div className="my-2 flex flex-wrap gap-4">
37
57
  <div className="flex items-center space-x-2">
38
58
  <input
39
59
  type="radio"
@@ -68,9 +88,41 @@ export const CopyInputStep = ({
68
88
  Provide Copy (Markdown)
69
89
  </label>
70
90
  </div>
91
+ {hasRetainedContent && (
92
+ <div className="flex items-center space-x-2">
93
+ <input
94
+ type="radio"
95
+ id="copy-original-mode"
96
+ name="copyModeOptions"
97
+ value="original"
98
+ checked={copyMode === 'original'}
99
+ onChange={(e) => onCopyModeChange(e.target.value as CopyMode)}
100
+ className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
101
+ />
102
+ <label
103
+ htmlFor="copy-original-mode"
104
+ className="text-sm font-bold text-gray-700"
105
+ >
106
+ Use Original
107
+ </label>
108
+ </div>
109
+ )}
71
110
  </div>
72
111
 
73
- {copyMode === 'prompt' ? (
112
+ {(copyMode === 'prompt' || (copyMode === 'raw' && isAiStyling)) && (
113
+ <div className="mb-4">
114
+ <EnumSelect
115
+ label="Section Type"
116
+ value={selectedPromptId}
117
+ onChange={onSelectedPromptIdChange}
118
+ options={promptOptions}
119
+ placeholder="Select a type..."
120
+ className="w-full"
121
+ />
122
+ </div>
123
+ )}
124
+
125
+ {copyMode === 'prompt' && (
74
126
  <>
75
127
  <p className="mb-2 text-sm text-gray-500">
76
128
  Let the AI write the copy based on your prompt.
@@ -84,12 +136,25 @@ export const CopyInputStep = ({
84
136
  className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
85
137
  />
86
138
  </>
87
- ) : (
139
+ )}
140
+
141
+ {copyMode === 'raw' && (
88
142
  <>
89
- <p className="mb-2 text-sm text-gray-500">
90
- Provide your raw copy here. Use Markdown for formatting (e.g., ##
91
- Headline, **bold**).
92
- </p>
143
+ <div className="mb-2 flex items-center justify-between">
144
+ <p className="text-sm text-gray-500">
145
+ Provide your raw copy here. Use Markdown.
146
+ </p>
147
+ {showStyleToggle && (
148
+ <div className="flex items-center">
149
+ <BooleanToggle
150
+ label="Style with AI"
151
+ value={isAiStyling}
152
+ onChange={onIsAiStylingChange}
153
+ size="sm"
154
+ />
155
+ </div>
156
+ )}
157
+ </div>
93
158
  <textarea
94
159
  id="raw-copy"
95
160
  value={copyValue}
@@ -100,6 +165,14 @@ export const CopyInputStep = ({
100
165
  />
101
166
  </>
102
167
  )}
168
+
169
+ {copyMode === 'original' && (
170
+ <div className="rounded-md border border-blue-200 bg-blue-50 p-4 text-blue-700">
171
+ <p className="text-sm">
172
+ The original text saved with this design will be used.
173
+ </p>
174
+ </div>
175
+ )}
103
176
  </div>
104
177
  );
105
178
  };
@@ -5,25 +5,41 @@ import type { TemplatePane } from '@/types/compositorTypes';
5
5
  interface DirectInjectStepProps {
6
6
  onBack: () => void;
7
7
  onCreatePane: (template: TemplatePane) => void;
8
+ layout: 'standard' | 'grid';
8
9
  }
9
10
 
10
11
  export const DirectInjectStep = ({
11
12
  onBack,
12
13
  onCreatePane,
14
+ layout,
13
15
  }: DirectInjectStepProps) => {
14
16
  const [shellJson, setShellJson] = useState('');
15
- const [copyHtml, setCopyHtml] = useState('');
17
+ const [columnContent, setColumnContent] = useState<string[]>(
18
+ layout === 'grid' ? ['', ''] : ['']
19
+ );
16
20
  const [error, setError] = useState<string | null>(null);
17
21
 
22
+ const handleContentChange = (index: number, value: string) => {
23
+ const newContent = [...columnContent];
24
+ newContent[index] = value;
25
+ setColumnContent(newContent);
26
+ };
27
+
18
28
  const handleCreate = () => {
19
29
  setError(null);
20
- if (!shellJson.trim() || !copyHtml.trim()) {
21
- setError('Both Shell JSON and Inner HTML must be provided.');
30
+ if (!shellJson.trim()) {
31
+ setError('Shell JSON must be provided.');
32
+ return;
33
+ }
34
+ if (columnContent.some((c) => !c.trim())) {
35
+ setError('All content fields must be filled.');
22
36
  return;
23
37
  }
24
38
 
25
39
  try {
26
- const finalPane = parseAiPane(shellJson, copyHtml, 'DirectInject');
40
+ const contentPayload =
41
+ layout === 'standard' ? columnContent[0] : columnContent;
42
+ const finalPane = parseAiPane(shellJson, contentPayload, 'DirectInject');
27
43
  onCreatePane(finalPane);
28
44
  } catch (err: any) {
29
45
  console.error('Direct Inject Error:', err);
@@ -52,22 +68,27 @@ export const DirectInjectStep = ({
52
68
  placeholder={`{ "bgColour": "#ffffff", "parentClasses": [...], "defaultClasses": {...} }`}
53
69
  />
54
70
  </div>
55
- <div>
56
- <label
57
- htmlFor="copyHtml"
58
- className="block text-sm font-bold text-gray-700"
59
- >
60
- Inner HTML
61
- </label>
62
- <textarea
63
- id="copyHtml"
64
- rows={10}
65
- value={copyHtml}
66
- onChange={(e) => setCopyHtml(e.target.value)}
67
- className="mt-1 block w-full rounded-md border-gray-300 p-2 font-mono text-sm shadow-sm focus:border-cyan-500 focus:ring-cyan-500"
68
- placeholder={`<h2 class="...">...</h2>\n<p class="...">...</p>`}
69
- />
70
- </div>
71
+
72
+ {columnContent.map((content, index) => (
73
+ <div key={index}>
74
+ <label
75
+ htmlFor={`copyHtml-${index}`}
76
+ className="block text-sm font-bold text-gray-700"
77
+ >
78
+ {layout === 'grid'
79
+ ? `Inner HTML (Column ${index + 1})`
80
+ : 'Inner HTML'}
81
+ </label>
82
+ <textarea
83
+ id={`copyHtml-${index}`}
84
+ rows={layout === 'grid' ? 6 : 10}
85
+ value={content}
86
+ onChange={(e) => handleContentChange(index, e.target.value)}
87
+ className="mt-1 block w-full rounded-md border-gray-300 p-2 font-mono text-sm shadow-sm focus:border-cyan-500 focus:ring-cyan-500"
88
+ placeholder={`<h2 class="...">...</h2>\n<p class="...">...</p>`}
89
+ />
90
+ </div>
91
+ ))}
71
92
  </div>
72
93
 
73
94
  {error && (
@@ -85,7 +106,6 @@ export const DirectInjectStep = ({
85
106
  </button>
86
107
  <button
87
108
  onClick={handleCreate}
88
- disabled={!shellJson.trim() || !copyHtml.trim()}
89
109
  className="rounded-md bg-cyan-600 px-4 py-2 text-sm font-bold text-white shadow-sm hover:bg-cyan-700 disabled:cursor-not-allowed disabled:bg-gray-400"
90
110
  >
91
111
  Create Pane
@@ -4,6 +4,8 @@ import { savePaneToLibrary } from '@/utils/compositor/designLibraryHelper';
4
4
  import { convertToBackendFormat } from '@/utils/api/brandHelpers';
5
5
  import StringInput from '@/components/form/StringInput';
6
6
  import { brandConfigStore } from '@/stores/storykeep';
7
+ import { getCtx } from '@/stores/nodes';
8
+ import type { FlatNode } from '@/types/compositorTypes';
7
9
 
8
10
  interface SaveToLibraryModalProps {
9
11
  paneId: string;
@@ -42,6 +44,7 @@ export function SaveToLibraryModal({
42
44
  const [selectedCategory, setSelectedCategory] = useState(OTHER_CATEGORY);
43
45
  const [customCategory, setCustomCategory] = useState('');
44
46
  const [copyMode, setCopyMode] = useState<CopyMode>('retain');
47
+ const [locked, setLocked] = useState(false);
45
48
  const [saveState, setSaveState] = useState<SaveState>('idle');
46
49
  const [error, setError] = useState('');
47
50
 
@@ -54,6 +57,34 @@ export function SaveToLibraryModal({
54
57
  return [...cats, OTHER_CATEGORY];
55
58
  }, [designLibrary]);
56
59
 
60
+ useEffect(() => {
61
+ const ctx = getCtx();
62
+ const childIds = ctx.getChildNodeIDs(paneId);
63
+
64
+ const hasWidget = (ids: string[]): boolean => {
65
+ for (const id of ids) {
66
+ const node = ctx.allNodes.get().get(id) as FlatNode;
67
+ if (!node) continue;
68
+
69
+ // Strict check for widget based on tagName being 'code'
70
+ if (node.tagName === 'code') {
71
+ return true;
72
+ }
73
+
74
+ const children = ctx.getChildNodeIDs(id);
75
+ if (children.length > 0 && hasWidget(children)) {
76
+ return true;
77
+ }
78
+ }
79
+ return false;
80
+ };
81
+
82
+ if (hasWidget(childIds)) {
83
+ setLocked(true);
84
+ setCopyMode('retain');
85
+ }
86
+ }, [paneId]);
87
+
57
88
  useEffect(() => {
58
89
  if (saveState === 'saved') {
59
90
  const timer = setTimeout(() => {
@@ -77,6 +108,7 @@ export function SaveToLibraryModal({
77
108
  title: title,
78
109
  category: finalCategory,
79
110
  copyMode: copyMode,
111
+ locked: locked,
80
112
  };
81
113
  const brandConfig = brandConfigStore.get();
82
114
 
@@ -90,7 +122,7 @@ export function SaveToLibraryModal({
90
122
  if (newBrandConfig) {
91
123
  const backendDTO = convertToBackendFormat(newBrandConfig);
92
124
  brandConfigStore.set({
93
- ...backendDTO, // Use the converted DTO
125
+ ...backendDTO,
94
126
  TENANT_ID: brandConfig.TENANT_ID,
95
127
  });
96
128
  setSaveState('saved');
@@ -106,7 +138,7 @@ export function SaveToLibraryModal({
106
138
 
107
139
  return (
108
140
  <div
109
- className="z-105 fixed inset-0 flex items-center justify-center bg-black/50"
141
+ className="z-105 fixed inset-0 flex items-center justify-center bg-black bg-opacity-75"
110
142
  onClick={saveState === 'idle' ? onClose : undefined}
111
143
  >
112
144
  <div
@@ -151,38 +183,40 @@ export function SaveToLibraryModal({
151
183
  )}
152
184
  </div>
153
185
 
154
- <div>
155
- <label className="block text-sm font-bold text-gray-700">
156
- Content Mode
157
- </label>
158
- <fieldset className="mt-2">
159
- <legend className="sr-only">Copy Mode</legend>
160
- <div className="space-y-2">
161
- {copyOptions.map((option) => (
162
- <div key={option.id} className="flex items-center">
163
- <input
164
- id={option.id}
165
- name="copy-mode"
166
- type="radio"
167
- value={option.id}
168
- checked={copyMode === option.id}
169
- onChange={() => setCopyMode(option.id)}
170
- className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
171
- />
172
- <label
173
- htmlFor={option.id}
174
- className="ml-3 block text-sm font-bold text-gray-700"
175
- >
176
- {option.title}
177
- <p className="text-xs text-gray-500">
178
- {option.description}
179
- </p>
180
- </label>
181
- </div>
182
- ))}
183
- </div>
184
- </fieldset>
185
- </div>
186
+ {!locked && (
187
+ <div>
188
+ <label className="block text-sm font-bold text-gray-700">
189
+ Content Mode
190
+ </label>
191
+ <fieldset className="mt-2">
192
+ <legend className="sr-only">Copy Mode</legend>
193
+ <div className="space-y-2">
194
+ {copyOptions.map((option) => (
195
+ <div key={option.id} className="flex items-center">
196
+ <input
197
+ id={option.id}
198
+ name="copy-mode"
199
+ type="radio"
200
+ value={option.id}
201
+ checked={copyMode === option.id}
202
+ onChange={() => setCopyMode(option.id)}
203
+ className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
204
+ />
205
+ <label
206
+ htmlFor={option.id}
207
+ className="ml-3 block text-sm font-bold text-gray-700"
208
+ >
209
+ {option.title}
210
+ <p className="text-xs text-gray-500">
211
+ {option.description}
212
+ </p>
213
+ </label>
214
+ </div>
215
+ ))}
216
+ </div>
217
+ </fieldset>
218
+ </div>
219
+ )}
186
220
 
187
221
  {error && <p className="text-sm text-red-600">{error}</p>}
188
222
  </div>
@@ -1,43 +1,69 @@
1
1
  {
2
- "formatPrompt": "Format the response as markdown suitable for a single web page section. Use headings (##, ###, ####), paragraphs, bold (**text**), italics (*text*), and simple links ([text](url)). Do NOT include images, lists, blockquotes, code blocks, horizontal rules, or any complex markdown.",
3
- "pagePrompts": {
4
- "landing": "Write compelling copy for a landing page section. Focus on a strong headline, clear value proposition, and a call to action. Keep paragraphs concise.",
5
- "feature": "Describe a key feature or benefit. Use a clear heading, explain the feature, highlight its advantages, and optionally include a brief user testimonial.",
6
- "about": "Write an 'About Us' section. Include a brief company history or mission, introduce key team members (optional), and convey company values.",
7
- "contact": "Create a contact section. Provide essential contact information (address, phone, email), potentially include a simple contact form description (no actual form code), and mention business hours.",
8
- "faq": "Generate a Frequently Asked Questions (FAQ) section. List common questions as headings (###) and provide clear, concise answers below each.",
9
- "testimonial": "Write a customer testimonial section. Include a quote highlighting a positive experience, attribute it to a customer (name/company), and potentially add a brief context.",
10
- "cta": "Craft a strong Call to Action (CTA) section. Use an urgent headline, clearly state the desired action, and provide a compelling reason to act now."
11
- },
12
- "pagePromptsDetails": {
13
- "landing": {
14
- "title": "Landing Page",
15
- "description": "Headline, value prop, CTA"
16
- },
17
- "feature": {
18
- "title": "Feature Section",
19
- "description": "Describe a product feature"
20
- },
21
- "about": {
22
- "title": "About Us",
23
- "description": "Company mission/values"
2
+ "aiPromptsIndex": [
3
+ {
4
+ "id": "hero_standard",
5
+ "label": "Hero Section (Standard)",
6
+ "layout": "standard",
7
+ "columns": 1,
8
+ "prompts": {
9
+ "shell": "aiPaneShellPrompt",
10
+ "copy": "aiPaneCopyPrompt",
11
+ "style": "aiPaneStyleOnlyPrompt"
12
+ },
13
+ "variants": ["heroDefault"]
24
14
  },
25
- "contact": {
26
- "title": "Contact Info",
27
- "description": "How to get in touch"
15
+ {
16
+ "id": "hero_2col",
17
+ "label": "Hero Section (2-Column)",
18
+ "layout": "grid",
19
+ "columns": 2,
20
+ "prompts": {
21
+ "shell": "aiPaneShellPrompt_2cols",
22
+ "copy": "aiPaneCopyPrompt_2cols",
23
+ "style": "aiPaneStyleOnlyPrompt"
24
+ },
25
+ "variants": ["heroDefault"]
28
26
  },
29
- "faq": {
30
- "title": "FAQ",
31
- "description": "Common questions & answers"
27
+ {
28
+ "id": "article_intro",
29
+ "label": "Article Intro (Standard)",
30
+ "layout": "standard",
31
+ "columns": 1,
32
+ "prompts": {
33
+ "shell": "aiPaneShellPrompt",
34
+ "copy": "aiPaneCopyPrompt",
35
+ "style": "aiPaneStyleOnlyPrompt"
36
+ },
37
+ "variants": ["articleIntro"]
32
38
  },
33
- "testimonial": {
34
- "title": "Testimonial",
35
- "description": "Customer quote/story"
39
+ {
40
+ "id": "article_body",
41
+ "label": "Article Body (Standard)",
42
+ "layout": "standard",
43
+ "columns": 1,
44
+ "prompts": {
45
+ "shell": "aiPaneShellPrompt",
46
+ "copy": "aiPaneCopyPrompt",
47
+ "style": "aiPaneStyleOnlyPrompt"
48
+ },
49
+ "variants": ["articleBody"]
36
50
  },
37
- "cta": {
38
- "title": "Call to Action",
39
- "description": "Encourage user action"
51
+ {
52
+ "id": "section_header",
53
+ "label": "Section Header (Standard)",
54
+ "layout": "standard",
55
+ "columns": 1,
56
+ "prompts": {
57
+ "shell": "aiPaneShellPrompt",
58
+ "copy": "aiPaneCopyPrompt",
59
+ "style": "aiPaneStyleOnlyPrompt"
60
+ },
61
+ "variants": ["sectionHeader"]
40
62
  }
63
+ ],
64
+ "aiPaneStyleOnlyPrompt": {
65
+ "system": "You are an expert **frontend developer**. Your task is to convert **raw Markdown** text into semantic HTML with Tailwind CSS classes based on a provided design theme. **CRITICAL: DO NOT REWRITE, SUMMARIZE, OR CHANGE THE TEXT CONTENT.** Your job is purely structural translation (Markdown -> HTML) and aesthetic formatting.",
66
+ "user_template": "Here is the design 'shell' and 'theme' (bgColour, parentClasses, and defaultClasses) you must use. Use the `defaultClasses` as your base theme for styling:\n{{SHELL_JSON}}\n\nHere is the **RAW MARKDOWN** content you must style:\n\"{{COPY_INPUT}}\"\n\nCRITICAL RULES:\n1. **Parse the Markdown:** Convert headings (`##`, `###`) to tags (`<h2>`, `<h3>`), lists (`-`, `1.`) to (`<ul>`, `<ol>`), and formatting (`**`, `*`) to (`<strong>`, `<em>`). **DO NOT use `<h1>` tags.**\n2. **Apply Theme:** Add the Tailwind classes from the Shell's `defaultClasses` to the corresponding HTML tags.\n3. **Visual Rhythm:** 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 to ensure they do not touch.\n4. **Responsive Styles:** For responsive styles, you *must* only use `md:` and `xl:` prefixes.\n5. **Interactive Elements:** If the markdown contains a link `[text](url)` or a clear Call-to-Action phrase, style it as a `<button>` or styled `<a>` tag using the theme's button styles (if available) or high-contrast styling.\n6. **Block Wrapping:** **All text** must be wrapped in a block element like `<p>`, `<h2>`, `<h3>`, or `<li>`.\n7. **Contrast:** Verify that **all text elements** maintain **high contrast** against the `bgColour` provided in the `SHELL_JSON`. Prioritize readability.\n8. **OUTPUT ONLY THE RAW HTML.**"
41
67
  },
42
68
  "aiPaneShellPrompt": {
43
69
  "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.",
@@ -47,11 +73,14 @@
47
73
  "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
74
  "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-bold 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
75
  "heroDefault": "A compelling hero section for a website about [topic]. It should have a strong, attention-grabbing headline, a brief paragraph explaining the core value proposition, and a clear call-to-action.",
50
- "contentDefault": "A content section that follows a hero. It should elaborate on a key feature or benefit related to [topic]. Include a sub-headline and a descriptive paragraph."
76
+ "contentDefault": "A content section that follows a hero. It should elaborate on a key feature or benefit related to [topic]. Include a sub-headline and a descriptive paragraph.",
77
+ "articleIntro": "A powerful introductory paragraph (Lede) for an article about [topic]. Use large, legible typography (e.g. text-xl or text-2xl) to hook the reader immediately.",
78
+ "articleBody": "A semantic article body section about [topic]. Use H3 subheadings, prose-style paragraphs, and unordered lists to structure the content for high readability.",
79
+ "sectionHeader": "A distinct section header (sub-hero) for [topic]. Use a high-contrast background, a prominent H2 title, and a short descriptive lead paragraph to act as a visual divider."
51
80
  },
52
81
  "aiPaneShellPrompt_2cols": {
53
82
  "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.",
54
- "user_template": "Generate the design JSON for the following component. The component is a 2-column grid layout where the columns stack vertically on mobile. Your task is to design the outer container shell (`parentClasses`), the shared typography theme (`defaultClasses`), and the specific styles for the individual columns (`columns`).\n\nComponent Brief: \"{{COPY_INPUT}}\"\n\nDesign Style: \"{{DESIGN_INPUT}}\"\n\nCRITICAL RULES:\n1. You must respond with a JSON object with the top-level keys: `bgColour`, `parentClasses`, `defaultClasses`, and `columns`.\n2. The `parentClasses` value is for the OUTER container's spacing and width ONLY (e.g. `max-w-7xl`, `py-24`). It is **FORBIDDEN** to include `grid`, `grid-cols-*`, or `gap-*` properties in `parentClasses`. The application will handle the grid creation. Any response violating this rule will be rejected. The `parentClasses` value *must* be an ARRAY of objects.\n3. The `defaultClasses` value defines the theme and *must* be structured with responsive keys (`mobile`, `tablet`, `desktop`) containing Tailwind class strings.\n4. The `columns` key *must* be an array containing exactly **two** objects. Each object represents an individual column and must have a `gridClasses` key. The value for `gridClasses` is a responsive object where each key's value is a string of Tailwind classes. This is used to style the column's wrapper. Crucially, you must remember that Tailwind is mobile-first, so you must reset styles at larger breakpoints if needed. For example, to add spacing for the mobile stack that is removed on larger screens, you would use `{ \"mobile\": \"mt-12\", \"tablet\": \"mt-0\" }`.\n5. Ensure the selected `bgColour` provides **high contrast** (meeting at least WCAG AA standards) with the primary text colors defined in `defaultClasses`. Prioritize readability.\n\nEXAMPLE:\n{\n \"bgColour\": \"#0d1117\",\n \"parentClasses\": [\n { \n \"mobile\": \"mx-auto max-w-7xl\"\n },\n {\n \"mobile\": \"px-6 py-24\",\n \"tablet\": \"px-8 py-32\"\n }\n ],\n \"defaultClasses\": {\n \"h2\": {\n \"mobile\": \"text-4xl font-bold tracking-tight text-white\",\n \"tablet\": \"text-6xl\"\n },\n \"p\": {\n \"mobile\": \"text-lg leading-8 text-gray-300 mt-6\"\n }\n },\n \"columns\": [\n {\n \"gridClasses\": {\n \"mobile\": \"text-center\",\n \"tablet\": \"text-left\"\n }\n },\n {\n \"gridClasses\": {\n \"mobile\": \"flex flex-col items-center mt-12\",\n \"tablet\": \"items-start mt-0\"\n }\n }\n ]\n}"
83
+ "user_template": "Generate the design JSON for the following component. The component is a 2-column grid layout where the columns stack vertically on mobile. Your task is to design the outer container shell (`parentClasses`), the shared typography theme (`defaultClasses`), and the specific styles for the individual columns (`columns`).\n\nComponent Brief: {{COPY_INPUT}}\n\nDesign Style: {{DESIGN_INPUT}}\n\nCRITICAL RULES:\n1. You must respond with a JSON object with the top-level keys: `bgColour`, `parentClasses`, `defaultClasses`, and `columns`.\n2. The `parentClasses` value is for the OUTER container's spacing and width ONLY (e.g. `max-w-7xl`, `py-24`). It is **FORBIDDEN** to include `grid`, `grid-cols-*`, or `gap-*` properties in `parentClasses`. The application will handle the grid creation. Any response violating this rule will be rejected. The `parentClasses` value *must* be an ARRAY of objects.\n3. The `defaultClasses` value defines the theme and *must* be structured with responsive keys (`mobile`, `tablet`, `desktop`) containing Tailwind class strings.\n4. The `columns` key *must* be an array containing exactly **two** objects. Each object represents an individual column and must have a `gridClasses` key. The value for `gridClasses` is a responsive object where each key's value is a string of Tailwind classes. This is used to style the column's wrapper. Crucially, you must remember that Tailwind is mobile-first, so you must reset styles at larger breakpoints if needed. For example, to add spacing for the mobile stack that is removed on larger screens, you would use `{ \"mobile\": \"mt-12\", \"tablet\": \"mt-0\" }`.\n5. Ensure the selected `bgColour` provides **high contrast** (meeting at least WCAG AA standards) with the primary text colors defined in `defaultClasses`. Prioritize readability.\n\nEXAMPLE:\n{\n \"bgColour\": \"#0d1117\",\n \"parentClasses\": [\n { \n \"mobile\": \"mx-auto max-w-7xl\"\n },\n {\n \"mobile\": \"px-6 py-24\",\n \"tablet\": \"px-8 py-32\"\n }\n ],\n \"defaultClasses\": {\n \"h2\": {\n \"mobile\": \"text-4xl font-bold tracking-tight text-white\",\n \"tablet\": \"text-6xl\"\n },\n \"p\": {\n \"mobile\": \"text-lg leading-8 text-gray-300 mt-6\"\n }\n },\n \"columns\": [\n {\n \"gridClasses\": {\n \"mobile\": \"text-center\",\n \"tablet\": \"text-left\"\n }\n },\n {\n \"gridClasses\": {\n \"mobile\": \"flex flex-col items-center mt-12\",\n \"tablet\": \"items-start mt-0\"\n }\n }\n ]\n}"
55
84
  },
56
85
  "aiPaneCopyPrompt_2cols": {
57
86
  "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**.",
@@ -141,6 +141,9 @@ export const toolAddModes = [
141
141
  //"aside",
142
142
  ] as const;
143
143
 
144
+ export const regexpHook =
145
+ /^(identifyAs|youtube|bunny|bunnyContext|toggle|resource|belief|interactiveDisclosure|signup)\((.*)\)$/;
146
+
144
147
  export const toolAddModeTitles: Record<ToolAddMode, string> = {
145
148
  p: 'Paragraph',
146
149
  h2: 'Heading 2',
@@ -293,9 +296,6 @@ export const contactPersona = [
293
296
  },
294
297
  ];
295
298
 
296
- export const regexpHook =
297
- /^(identifyAs|youtube|bunny|bunnyContext|toggle|resource|belief|interactiveDisclosure|signup)\((.*)\)$/;
298
-
299
299
  export const biIcons = [
300
300
  '0-circle',
301
301
  '0-circle-fill',
@@ -35,6 +35,7 @@ import type {
35
35
  MenuNode,
36
36
  NodeType,
37
37
  PaneFragmentNode,
38
+ BgImageNode,
38
39
  PaneNode,
39
40
  StoryFragmentNode,
40
41
  Tag,
@@ -3199,6 +3200,9 @@ export class NodesContext {
3199
3200
  const allOriginalNodes: TemplateNode[] = [];
3200
3201
  const columnNodes: TemplateMarkdown[] = [];
3201
3202
 
3203
+ // Instantiate generator for column markdown parsing
3204
+ const markdownGen = new MarkdownGenerator(this);
3205
+
3202
3206
  duplicatedGrid.nodes?.forEach((originalColumn) => {
3203
3207
  const newColumn = cloneDeep(originalColumn);
3204
3208
  newColumn.id = ulid();
@@ -3206,12 +3210,22 @@ export class NodesContext {
3206
3210
  oldToNewIdMap.set(originalColumn.id, newColumn.id);
3207
3211
  columnNodes.push(newColumn);
3208
3212
 
3209
- originalColumn.nodes?.forEach((colNode) => {
3210
- allOriginalNodes.push(colNode);
3211
- });
3213
+ if (originalColumn.markdownBody) {
3214
+ const columnContentNodes = markdownGen.markdownToFlatNodes(
3215
+ originalColumn.markdownBody,
3216
+ newColumn.id
3217
+ ) as TemplateNode[];
3218
+ // Add generated nodes directly to allNodes
3219
+ allNodes.push(...columnContentNodes);
3220
+ } else {
3221
+ // Standard flow: collect existing nodes for remapping
3222
+ originalColumn.nodes?.forEach((colNode) => {
3223
+ allOriginalNodes.push(colNode);
3224
+ });
3225
+ }
3212
3226
  });
3213
3227
 
3214
- // Second pass: Clone all descendant nodes
3228
+ // Second pass: Clone all descendant nodes (only those from the standard flow)
3215
3229
  const allClonedDescendants = allOriginalNodes.map((originalNode) => {
3216
3230
  const newNode = cloneDeep(originalNode);
3217
3231
  newNode.id = ulid();
@@ -3251,6 +3265,15 @@ export class NodesContext {
3251
3265
  breakMobile: visualBreakPane.breakMobile,
3252
3266
  };
3253
3267
  allNodes.push(bgPaneNode);
3268
+ } else if (paneTemplate.bgPane.type === 'background-image') {
3269
+ const bgImagePane = paneTemplate.bgPane as BgImageNode;
3270
+ const bgPaneNode: BgImageNode = {
3271
+ ...bgImagePane,
3272
+ id: bgPaneId,
3273
+ nodeType: 'BgPane',
3274
+ parentId: newPaneId,
3275
+ };
3276
+ allNodes.push(bgPaneNode);
3254
3277
  } else if (paneTemplate.bgPane.type === 'artpack-image') {
3255
3278
  const artpackBgPane = paneTemplate.bgPane as ArtpackImageNode;
3256
3279
  const bgPaneNode: ArtpackImageNode = {
@@ -281,12 +281,15 @@ export interface MarkdownPaneFragmentNode extends PaneFragmentNode {
281
281
  parentClasses?: ParentClassesPayload;
282
282
  parentCss?: string[];
283
283
  gridClasses?: DefaultClassValue;
284
+ gridCss?: string;
284
285
  }
285
286
 
286
287
  export interface GridLayoutNode extends PaneFragmentNode {
287
288
  nodeType: 'GridLayoutNode';
288
289
  type: 'grid-layout';
289
290
  parentClasses?: ParentClassesPayload;
291
+ parentCss?: string;
292
+ gridCss?: string;
290
293
  defaultClasses?: Record<
291
294
  string,
292
295
  {
@@ -4,6 +4,8 @@ export type DesignLibraryEntry = {
4
4
  category: string;
5
5
  title: string;
6
6
  markdownCount: number;
7
+ retain?: boolean;
8
+ locked?: boolean;
7
9
  template: StoragePane;
8
10
  };
9
11