astro-tractstack 2.0.29 → 2.0.30

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.0.29",
3
+ "version": "2.0.30",
4
4
  "description": "Astro integration for TractStack - redeeming the web from boring experiences",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { useState, useCallback } from 'react';
2
+ import { useState, useCallback, useMemo, useEffect } from 'react';
3
3
  import DocumentPlusIcon from '@heroicons/react/24/outline/DocumentPlusIcon';
4
4
  import SparklesIcon from '@heroicons/react/24/outline/SparklesIcon';
5
5
  import SwatchIcon from '@heroicons/react/24/outline/SwatchIcon';
@@ -12,7 +12,7 @@ import prompts from '@/constants/prompts.json';
12
12
  import type { DesignLibraryEntry } from '@/types/tractstack';
13
13
  import { PaneAddMode, type TemplatePane } from '@/types/compositorTypes';
14
14
  import { useStore } from '@nanostores/react';
15
- import { CopyInputStep } from './steps/CopyInputStep';
15
+ import { CopyInputStep, type CopyMode } from './steps/CopyInputStep';
16
16
  import { DesignLibraryStep } from './steps/DesignLibraryStep';
17
17
  import { AiDesignStep, type AiDesignConfig } from './steps/AiDesignStep';
18
18
  import { parseAiPane, parseAiCopyHtml } from '@/utils/compositor/aiPaneParser';
@@ -21,6 +21,8 @@ import {
21
21
  convertTemplateToAIShell,
22
22
  } from '@/utils/compositor/designLibraryHelper';
23
23
  import { DirectInjectStep } from './steps/DirectInjectStep';
24
+ import BooleanToggle from '@/components/form/BooleanToggle';
25
+ import EnumSelect from '@/components/form/EnumSelect';
24
26
 
25
27
  type Step =
26
28
  | 'initial'
@@ -33,7 +35,6 @@ type Step =
33
35
  | 'directInject';
34
36
 
35
37
  type InitialChoice = 'library' | 'ai' | 'blank';
36
- type CopyMode = 'prompt' | 'raw';
37
38
  type LayoutChoice = 'standard' | 'grid';
38
39
  type ColumnPresetKey = 'left' | 'right';
39
40
 
@@ -140,12 +141,13 @@ const AddPaneNewPanel = ({
140
141
  const [layoutChoice, setLayoutChoice] = useState<LayoutChoice>('standard');
141
142
  const [error, setError] = useState<string | null>(null);
142
143
 
143
- // Standard / Single Column State
144
+ const [selectedPromptId, setSelectedPromptId] = useState<string>('');
145
+ const [isAiStyling, setIsAiStyling] = useState(false);
146
+
144
147
  const [copyMode, setCopyMode] = useState<CopyMode>('prompt');
145
148
  const [promptValue, setPromptValue] = useState('');
146
149
  const [copyValue, setCopyValue] = useState('');
147
150
 
148
- // Grid / 2-Column State (Strictly Prompt-Only)
149
151
  const [overallPrompt, setOverallPrompt] = useState(
150
152
  prompts.aiPaneCopyPrompt_2cols.presets.heroDefault.default
151
153
  );
@@ -155,6 +157,8 @@ const AddPaneNewPanel = ({
155
157
  const [promptValueCol2, setPromptValueCol2] = useState(
156
158
  prompts.aiPaneCopyPrompt_2cols.presets.heroDefault.right.prompt
157
159
  );
160
+ const [col1Copy, setCol1Copy] = useState('');
161
+ const [col2Copy, setCol2Copy] = useState('');
158
162
 
159
163
  const [selectedLibraryEntry, setSelectedLibraryEntry] =
160
164
  useState<DesignLibraryEntry | null>(null);
@@ -166,6 +170,52 @@ const AddPaneNewPanel = ({
166
170
  additionalNotes: '',
167
171
  });
168
172
 
173
+ const promptOptions = useMemo(() => {
174
+ return prompts.aiPromptsIndex
175
+ .filter((p) => p.layout === layoutChoice)
176
+ .map((p) => ({ label: p.label, value: p.id }));
177
+ }, [layoutChoice]);
178
+
179
+ useEffect(() => {
180
+ if (promptOptions.length > 0) {
181
+ const currentValid = promptOptions.find(
182
+ (p) => p.value === selectedPromptId
183
+ );
184
+ if (!currentValid) {
185
+ setSelectedPromptId(promptOptions[0].value);
186
+ }
187
+ }
188
+ }, [promptOptions, selectedPromptId]);
189
+
190
+ useEffect(() => {
191
+ if (!selectedPromptId) return;
192
+
193
+ const activeConfig = prompts.aiPromptsIndex.find(
194
+ (p) => p.id === selectedPromptId
195
+ );
196
+ if (!activeConfig) return;
197
+
198
+ const promptKey = activeConfig.prompts.copy;
199
+ const copyPromptGroup = (prompts as any)[promptKey];
200
+ if (!copyPromptGroup) return;
201
+
202
+ const variant = activeConfig.variants
203
+ ? activeConfig.variants[0]
204
+ : 'default';
205
+
206
+ if (layoutChoice === 'standard') {
207
+ const newText = copyPromptGroup[variant] || '';
208
+ setPromptValue(newText);
209
+ } else if (layoutChoice === 'grid') {
210
+ const preset = copyPromptGroup.presets?.[variant];
211
+ if (preset) {
212
+ setOverallPrompt(preset.default || '');
213
+ setPromptValueCol1(preset.left?.prompt || '');
214
+ setPromptValueCol2(preset.right?.prompt || '');
215
+ }
216
+ }
217
+ }, [selectedPromptId, layoutChoice]);
218
+
169
219
  const handleInitialChoice = (choice: InitialChoice) => {
170
220
  setInitialChoice(choice);
171
221
  setError(null);
@@ -279,6 +329,18 @@ const AddPaneNewPanel = ({
279
329
  setLayoutChoice('standard');
280
330
  }
281
331
 
332
+ if (entry.locked) {
333
+ const liveTemplate = convertStorageToLiveTemplate(entry.template);
334
+ handleApplyTemplate(liveTemplate);
335
+ return;
336
+ }
337
+
338
+ if (entry.retain) {
339
+ setCopyMode('original');
340
+ } else {
341
+ setCopyMode('prompt');
342
+ }
343
+
282
344
  setStep('copyInput');
283
345
  };
284
346
 
@@ -295,27 +357,60 @@ const AddPaneNewPanel = ({
295
357
  const liveTemplate = convertStorageToLiveTemplate(
296
358
  selectedLibraryEntry.template
297
359
  );
360
+
361
+ if (copyMode === 'original') {
362
+ handleApplyTemplate(liveTemplate);
363
+ return;
364
+ }
365
+
298
366
  const shellResult = convertTemplateToAIShell(liveTemplate);
299
367
  const layout = 'Text Only';
300
368
 
301
369
  if (layoutChoice === 'grid' && liveTemplate.gridLayout) {
370
+ if (copyMode === 'raw' && isAiStyling) {
371
+ const activeConfig =
372
+ prompts.aiPromptsIndex.find((p) => p.id === selectedPromptId) ||
373
+ prompts.aiPromptsIndex.find((p) => p.layout === 'grid') ||
374
+ prompts.aiPromptsIndex[0];
375
+
376
+ const stylePromptKey = activeConfig.prompts.style;
377
+ const stylePromptDetails = (prompts as any)[stylePromptKey];
378
+
379
+ const copyResults: string[] = [];
380
+ const rawContents = [col1Copy, col2Copy];
381
+
382
+ for (const rawContent of rawContents) {
383
+ const formattedStylePrompt = stylePromptDetails.user_template
384
+ .replace('{{SHELL_JSON}}', shellResult)
385
+ .replace('{{COPY_INPUT}}', rawContent);
386
+
387
+ const styledResult = await callAskLemurAPI(
388
+ formattedStylePrompt,
389
+ stylePromptDetails.system || '',
390
+ false,
391
+ isSandboxMode
392
+ );
393
+ copyResults.push(styledResult);
394
+ }
395
+ const finalPane = parseAiPane(shellResult, copyResults, layout);
396
+ handleApplyTemplate(finalPane);
397
+ return;
398
+ }
399
+
400
+ if (copyMode === 'raw') {
401
+ const nodes = liveTemplate.gridLayout.nodes;
402
+ if (nodes && nodes[0]) nodes[0].markdownBody = col1Copy;
403
+ if (nodes && nodes[1]) nodes[1].markdownBody = col2Copy;
404
+ handleApplyTemplate(liveTemplate);
405
+ return;
406
+ }
407
+
302
408
  const copyPromptDetails = prompts.aiPaneCopyPrompt_2cols;
303
409
  const preset = copyPromptDetails.presets.heroDefault;
304
410
  const copyResults: string[] = [];
305
-
306
- // Only prompt mode supported for grid now
307
- const promptsToRun: {
308
- prompt: string;
309
- presetKey: ColumnPresetKey;
310
- }[] = [
311
- {
312
- prompt: promptValueCol1,
313
- presetKey: 'left',
314
- },
315
- {
316
- prompt: promptValueCol2,
317
- presetKey: 'right',
318
- },
411
+ const promptsToRun = [
412
+ { prompt: promptValueCol1, presetKey: 'left' as ColumnPresetKey },
413
+ { prompt: promptValueCol2, presetKey: 'right' as ColumnPresetKey },
319
414
  ];
320
415
 
321
416
  for (const item of promptsToRun) {
@@ -339,16 +434,36 @@ const AddPaneNewPanel = ({
339
434
  );
340
435
  copyResults.push(copyResult);
341
436
  }
342
-
343
437
  const finalPane = parseAiPane(shellResult, copyResults, layout);
344
438
  handleApplyTemplate(finalPane);
345
439
  } else if (layoutChoice === 'standard' && liveTemplate.markdown) {
440
+ if (copyMode === 'raw' && isAiStyling) {
441
+ const activeConfig =
442
+ prompts.aiPromptsIndex.find((p) => p.id === selectedPromptId) ||
443
+ prompts.aiPromptsIndex[0];
444
+ const stylePromptKey = activeConfig.prompts.style;
445
+ const stylePromptDetails = (prompts as any)[stylePromptKey];
446
+
447
+ const formattedStylePrompt = stylePromptDetails.user_template
448
+ .replace('{{SHELL_JSON}}', shellResult)
449
+ .replace('{{COPY_INPUT}}', copyValue);
450
+
451
+ const styledResult = await callAskLemurAPI(
452
+ formattedStylePrompt,
453
+ stylePromptDetails.system || '',
454
+ false,
455
+ isSandboxMode
456
+ );
457
+ const finalPane = parseAiPane(shellResult, styledResult, layout);
458
+ handleApplyTemplate(finalPane);
459
+ return;
460
+ }
461
+
346
462
  if (copyMode === 'raw') {
347
463
  liveTemplate.markdown.markdownBody = copyValue;
348
464
  handleApplyTemplate(liveTemplate);
349
465
  return;
350
466
  }
351
-
352
467
  if (copyMode === 'prompt') {
353
468
  if (!shellResult || shellResult === '{}') {
354
469
  throw new Error(
@@ -387,6 +502,11 @@ const AddPaneNewPanel = ({
387
502
  );
388
503
  }
389
504
  } else if (initialChoice === 'ai') {
505
+ const activeConfig = prompts.aiPromptsIndex.find(
506
+ (p) => p.id === selectedPromptId
507
+ );
508
+ if (!activeConfig) throw new Error('Selected prompt type not found.');
509
+
390
510
  let designInput = `Generate a design using a **${aiDesignConfig.harmony.toLowerCase()}** color scheme with a **${aiDesignConfig.theme.toLowerCase()}** theme.`;
391
511
  if (aiDesignConfig.baseColor)
392
512
  designInput += ` Base the colors around **${aiDesignConfig.baseColor}**.`;
@@ -396,9 +516,11 @@ const AddPaneNewPanel = ({
396
516
  designInput += ` Refine with these notes: "${aiDesignConfig.additionalNotes}"`;
397
517
 
398
518
  const layout = 'Text Only';
519
+ const promptMap = prompts as any;
399
520
 
400
521
  if (layoutChoice === 'standard') {
401
- const shellPromptDetails = prompts.aiPaneShellPrompt;
522
+ const shellPromptKey = activeConfig.prompts.shell;
523
+ const shellPromptDetails = promptMap[shellPromptKey];
402
524
  const formattedShellPrompt = shellPromptDetails.user_template
403
525
  .replace('{{DESIGN_INPUT}}', designInput)
404
526
  .replace('{{LAYOUT_TYPE}}', layout);
@@ -410,7 +532,8 @@ const AddPaneNewPanel = ({
410
532
  isSandboxMode
411
533
  );
412
534
 
413
- const copyPromptDetails = prompts.aiPaneCopyPrompt;
535
+ const copyPromptKey = activeConfig.prompts.copy;
536
+ const copyPromptDetails = promptMap[copyPromptKey];
414
537
  const copyInputContent =
415
538
  copyMode === 'prompt' ? promptValue : copyValue;
416
539
  const formattedCopyPrompt = copyPromptDetails.user_template
@@ -428,7 +551,8 @@ const AddPaneNewPanel = ({
428
551
  const finalPane = parseAiPane(shellResult, copyResult, layout);
429
552
  handleApplyTemplate(finalPane);
430
553
  } else if (layoutChoice === 'grid') {
431
- const shellPromptDetails = prompts.aiPaneShellPrompt_2cols;
554
+ const shellPromptKey = activeConfig.prompts.shell;
555
+ const shellPromptDetails = promptMap[shellPromptKey];
432
556
  const formattedShellPrompt = shellPromptDetails.user_template
433
557
  .replace('{{COPY_INPUT}}', overallPrompt)
434
558
  .replace('{{DESIGN_INPUT}}', designInput);
@@ -440,23 +564,16 @@ const AddPaneNewPanel = ({
440
564
  isSandboxMode
441
565
  );
442
566
 
443
- const copyPromptDetails = prompts.aiPaneCopyPrompt_2cols;
444
- const preset = copyPromptDetails.presets.heroDefault;
567
+ const copyPromptKey = activeConfig.prompts.copy;
568
+ const copyPromptDetails = promptMap[copyPromptKey];
569
+ const preset =
570
+ copyPromptDetails.presets?.[activeConfig.variants[0]] ||
571
+ copyPromptDetails.presets?.heroDefault;
445
572
  const copyResults: string[] = [];
446
573
 
447
- // Grid is strictly prompt-based
448
- const promptsToRun: {
449
- prompt: string;
450
- presetKey: ColumnPresetKey;
451
- }[] = [
452
- {
453
- prompt: promptValueCol1,
454
- presetKey: 'left',
455
- },
456
- {
457
- prompt: promptValueCol2,
458
- presetKey: 'right',
459
- },
574
+ const promptsToRun = [
575
+ { prompt: promptValueCol1, presetKey: 'left' as ColumnPresetKey },
576
+ { prompt: promptValueCol2, presetKey: 'right' as ColumnPresetKey },
460
577
  ];
461
578
 
462
579
  for (const item of promptsToRun) {
@@ -491,6 +608,8 @@ const AddPaneNewPanel = ({
491
608
  copyMode,
492
609
  promptValue,
493
610
  copyValue,
611
+ col1Copy,
612
+ col2Copy,
494
613
  overallPrompt,
495
614
  promptValueCol1,
496
615
  promptValueCol2,
@@ -499,6 +618,8 @@ const AddPaneNewPanel = ({
499
618
  layoutChoice,
500
619
  selectedLibraryEntry,
501
620
  handleApplyTemplate,
621
+ selectedPromptId,
622
+ isAiStyling,
502
623
  ]);
503
624
 
504
625
  const renderInitialStep = () => (
@@ -584,50 +705,160 @@ const AddPaneNewPanel = ({
584
705
  const renderContentStep = () => {
585
706
  if (layoutChoice === 'grid') {
586
707
  const isGenerateDisabled =
587
- !overallPrompt.trim() ||
588
- !promptValueCol1.trim() ||
589
- !promptValueCol2.trim();
708
+ copyMode === 'prompt'
709
+ ? !overallPrompt.trim() ||
710
+ !promptValueCol1.trim() ||
711
+ !promptValueCol2.trim()
712
+ : copyMode === 'raw'
713
+ ? !col1Copy.trim() || !col2Copy.trim()
714
+ : false;
590
715
 
591
716
  return (
592
717
  <div className="space-y-4 p-4">
593
- <div>
594
- <label className="mb-2 block text-sm font-bold text-gray-700">
595
- Overall Component Brief
596
- </label>
597
- <textarea
598
- value={overallPrompt}
599
- onChange={(e) => setOverallPrompt(e.target.value)}
600
- rows={3}
601
- className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
602
- />
603
- <p className="mt-1 text-xs text-gray-500">
604
- This context is applied to both columns.
605
- </p>
606
- </div>
607
- <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
608
- <div>
609
- <label className="mb-2 block text-sm font-bold text-gray-700">
610
- Left Column Prompt
611
- </label>
612
- <textarea
613
- value={promptValueCol1}
614
- onChange={(e) => setPromptValueCol1(e.target.value)}
615
- rows={4}
616
- className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
718
+ <label className="block text-lg font-bold text-gray-800">
719
+ 1. Provide Content
720
+ </label>
721
+
722
+ <div className="my-2 flex flex-wrap gap-4">
723
+ <div className="flex items-center space-x-2">
724
+ <input
725
+ type="radio"
726
+ checked={copyMode === 'prompt'}
727
+ onChange={() => setCopyMode('prompt')}
728
+ className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
617
729
  />
730
+ <label className="text-sm font-bold text-gray-700">Prompt</label>
618
731
  </div>
619
- <div>
620
- <label className="mb-2 block text-sm font-bold text-gray-700">
621
- Right Column Prompt
622
- </label>
623
- <textarea
624
- value={promptValueCol2}
625
- onChange={(e) => setPromptValueCol2(e.target.value)}
626
- rows={4}
627
- className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
732
+ <div className="flex items-center space-x-2">
733
+ <input
734
+ type="radio"
735
+ checked={copyMode === 'raw'}
736
+ onChange={() => setCopyMode('raw')}
737
+ className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
628
738
  />
739
+ <label className="text-sm font-bold text-gray-700">
740
+ Manual Markdown
741
+ </label>
629
742
  </div>
743
+ {selectedLibraryEntry?.retain && (
744
+ <div className="flex items-center space-x-2">
745
+ <input
746
+ type="radio"
747
+ checked={copyMode === 'original'}
748
+ onChange={() => setCopyMode('original')}
749
+ className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
750
+ />
751
+ <label className="text-sm font-bold text-gray-700">
752
+ Use Original
753
+ </label>
754
+ </div>
755
+ )}
630
756
  </div>
757
+
758
+ {copyMode === 'raw' && initialChoice === 'library' && (
759
+ <div className="mb-4 flex items-center justify-between rounded-lg border border-gray-100 bg-gray-50 p-2">
760
+ <span className="text-sm text-gray-600">
761
+ Style this content with AI?
762
+ </span>
763
+ <div className="flex items-center">
764
+ <BooleanToggle
765
+ label="AI Styles"
766
+ value={isAiStyling}
767
+ onChange={setIsAiStyling}
768
+ size="sm"
769
+ />
770
+ </div>
771
+ </div>
772
+ )}
773
+
774
+ {copyMode === 'prompt' && (
775
+ <>
776
+ <div className="mb-4">
777
+ <EnumSelect
778
+ label="Section Type"
779
+ value={selectedPromptId}
780
+ onChange={setSelectedPromptId}
781
+ options={promptOptions}
782
+ placeholder="Select a section type..."
783
+ className="w-full"
784
+ />
785
+ </div>
786
+ <div>
787
+ <label className="mb-2 block text-sm font-bold text-gray-700">
788
+ Overall Component Brief
789
+ </label>
790
+ <textarea
791
+ value={overallPrompt}
792
+ onChange={(e) => setOverallPrompt(e.target.value)}
793
+ rows={3}
794
+ className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
795
+ />
796
+ <p className="mt-1 text-xs text-gray-500">
797
+ This context is applied to both columns.
798
+ </p>
799
+ </div>
800
+ <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
801
+ <div>
802
+ <label className="mb-2 block text-sm font-bold text-gray-700">
803
+ Left Column Prompt
804
+ </label>
805
+ <textarea
806
+ value={promptValueCol1}
807
+ onChange={(e) => setPromptValueCol1(e.target.value)}
808
+ rows={4}
809
+ className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
810
+ />
811
+ </div>
812
+ <div>
813
+ <label className="mb-2 block text-sm font-bold text-gray-700">
814
+ Right Column Prompt
815
+ </label>
816
+ <textarea
817
+ value={promptValueCol2}
818
+ onChange={(e) => setPromptValueCol2(e.target.value)}
819
+ rows={4}
820
+ className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
821
+ />
822
+ </div>
823
+ </div>
824
+ </>
825
+ )}
826
+
827
+ {copyMode === 'raw' && (
828
+ <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
829
+ <div>
830
+ <label className="mb-2 block text-sm font-bold text-gray-700">
831
+ Left Column Markdown
832
+ </label>
833
+ <textarea
834
+ value={col1Copy}
835
+ onChange={(e) => setCol1Copy(e.target.value)}
836
+ rows={8}
837
+ className="block w-full rounded-md border-gray-300 p-2 font-mono text-sm shadow-sm focus:border-cyan-500 focus:ring-cyan-500"
838
+ />
839
+ </div>
840
+ <div>
841
+ <label className="mb-2 block text-sm font-bold text-gray-700">
842
+ Right Column Markdown
843
+ </label>
844
+ <textarea
845
+ value={col2Copy}
846
+ onChange={(e) => setCol2Copy(e.target.value)}
847
+ rows={8}
848
+ className="block w-full rounded-md border-gray-300 p-2 font-mono text-sm shadow-sm focus:border-cyan-500 focus:ring-cyan-500"
849
+ />
850
+ </div>
851
+ </div>
852
+ )}
853
+
854
+ {copyMode === 'original' && (
855
+ <div className="rounded-md border border-blue-200 bg-blue-50 p-4 text-blue-700">
856
+ <p className="text-sm">
857
+ The original text saved with this design will be used.
858
+ </p>
859
+ </div>
860
+ )}
861
+
631
862
  <div className="flex justify-between">
632
863
  <button
633
864
  onClick={handleBack}
@@ -656,11 +887,18 @@ const AddPaneNewPanel = ({
656
887
  onPromptValueChange={setPromptValue}
657
888
  copyValue={copyValue}
658
889
  onCopyValueChange={setCopyValue}
890
+ hasRetainedContent={selectedLibraryEntry?.retain}
659
891
  defaultPrompt={
660
892
  first
661
893
  ? prompts.aiPaneCopyPrompt.heroDefault
662
894
  : prompts.aiPaneCopyPrompt.contentDefault
663
895
  }
896
+ promptOptions={promptOptions}
897
+ selectedPromptId={selectedPromptId}
898
+ onSelectedPromptIdChange={setSelectedPromptId}
899
+ isAiStyling={isAiStyling}
900
+ onIsAiStylingChange={setIsAiStyling}
901
+ showStyleToggle={initialChoice === 'library'}
664
902
  />
665
903
  <div className="flex justify-between">
666
904
  <button
@@ -672,7 +910,11 @@ const AddPaneNewPanel = ({
672
910
  <button
673
911
  onClick={handleFinalGenerate}
674
912
  disabled={
675
- copyMode === 'prompt' ? !promptValue.trim() : !copyValue.trim()
913
+ copyMode === 'prompt'
914
+ ? !promptValue.trim()
915
+ : copyMode === 'raw'
916
+ ? !copyValue.trim()
917
+ : false
676
918
  }
677
919
  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"
678
920
  >
@@ -732,7 +974,11 @@ const AddPaneNewPanel = ({
732
974
  );
733
975
 
734
976
  const renderDirectInjectStep = () => (
735
- <DirectInjectStep onBack={handleBack} onCreatePane={handleApplyTemplate} />
977
+ <DirectInjectStep
978
+ onBack={handleBack}
979
+ onCreatePane={handleApplyTemplate}
980
+ layout={layoutChoice}
981
+ />
736
982
  );
737
983
 
738
984
  const renderLoading = () => (
@@ -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',
@@ -3199,6 +3199,9 @@ export class NodesContext {
3199
3199
  const allOriginalNodes: TemplateNode[] = [];
3200
3200
  const columnNodes: TemplateMarkdown[] = [];
3201
3201
 
3202
+ // Instantiate generator for column markdown parsing
3203
+ const markdownGen = new MarkdownGenerator(this);
3204
+
3202
3205
  duplicatedGrid.nodes?.forEach((originalColumn) => {
3203
3206
  const newColumn = cloneDeep(originalColumn);
3204
3207
  newColumn.id = ulid();
@@ -3206,12 +3209,22 @@ export class NodesContext {
3206
3209
  oldToNewIdMap.set(originalColumn.id, newColumn.id);
3207
3210
  columnNodes.push(newColumn);
3208
3211
 
3209
- originalColumn.nodes?.forEach((colNode) => {
3210
- allOriginalNodes.push(colNode);
3211
- });
3212
+ if (originalColumn.markdownBody) {
3213
+ const columnContentNodes = markdownGen.markdownToFlatNodes(
3214
+ originalColumn.markdownBody,
3215
+ newColumn.id
3216
+ ) as TemplateNode[];
3217
+ // Add generated nodes directly to allNodes
3218
+ allNodes.push(...columnContentNodes);
3219
+ } else {
3220
+ // Standard flow: collect existing nodes for remapping
3221
+ originalColumn.nodes?.forEach((colNode) => {
3222
+ allOriginalNodes.push(colNode);
3223
+ });
3224
+ }
3212
3225
  });
3213
3226
 
3214
- // Second pass: Clone all descendant nodes
3227
+ // Second pass: Clone all descendant nodes (only those from the standard flow)
3215
3228
  const allClonedDescendants = allOriginalNodes.map((originalNode) => {
3216
3229
  const newNode = cloneDeep(originalNode);
3217
3230
  newNode.id = ulid();
@@ -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
 
@@ -62,7 +62,7 @@ function convertLiveNodeToStorageNode(
62
62
  fileId: copyMode === 'retain' ? node.fileId : undefined,
63
63
  buttonPayload: copyMode === 'retain' ? node.buttonPayload : undefined,
64
64
  codeHookParams: copyMode === 'retain' ? node.codeHookParams : undefined,
65
- elementCss: copyMode === 'retain' ? node.elementCss : undefined,
65
+ copy: copyMode === 'retain' ? node.copy : undefined,
66
66
  };
67
67
 
68
68
  const childIds = ctx.getChildNodeIDs(node.id);
@@ -500,7 +500,8 @@ function convertLivePaneToStoragePane(
500
500
  : [],
501
501
  };
502
502
  } else if (gridLayoutNode) {
503
- const { id, parentId, isChanged, ...restOfGrid } = gridLayoutNode;
503
+ const { id, parentId, isChanged, parentCss, gridCss, ...restOfGrid } =
504
+ gridLayoutNode;
504
505
  storageGridLayout = {
505
506
  ...restOfGrid,
506
507
  nodes: ctx
@@ -517,6 +518,7 @@ function convertLivePaneToStoragePane(
517
518
  isChanged,
518
519
  markdownId,
519
520
  parentCss,
521
+ gridCss,
520
522
  ...restOfColumn
521
523
  } = columnNode;
522
524
 
@@ -584,10 +586,11 @@ export async function savePaneToLibrary(
584
586
  title: string;
585
587
  category: string;
586
588
  copyMode: CopyMode;
589
+ locked?: boolean;
587
590
  }
588
591
  ): Promise<BrandConfigState | null> {
589
592
  const ctx = getCtx();
590
- const { title, category, copyMode } = formData;
593
+ const { title, category, copyMode, locked } = formData;
591
594
 
592
595
  const newStoragePane = convertLivePaneToStoragePane(paneId, ctx, {
593
596
  title,
@@ -613,6 +616,8 @@ export async function savePaneToLibrary(
613
616
  title: title,
614
617
  markdownCount: actualMarkdownCount,
615
618
  template: newStoragePane,
619
+ retain: copyMode === 'retain',
620
+ locked: !!locked,
616
621
  };
617
622
 
618
623
  const currentState: BrandConfigState = convertToLocalState(config);
@@ -93,24 +93,6 @@ export const isMarkdownPaneFragmentNode = (
93
93
  );
94
94
  };
95
95
 
96
- interface WidgetNode extends FlatNode {
97
- tagName: 'code';
98
- codeHookParams: (string | string[])[];
99
- copy: string;
100
- }
101
-
102
- export const isWidgetNode = (
103
- node: BaseNode | FlatNode | null
104
- ): node is WidgetNode => {
105
- return (
106
- node !== null &&
107
- 'tagName' in node &&
108
- node.tagName === 'code' &&
109
- 'codeHookParams' in node &&
110
- Array.isArray(node.codeHookParams)
111
- );
112
- };
113
-
114
96
  export function hasTagName(
115
97
  node: BaseNode | null | undefined
116
98
  ): node is FlatNode {