astro-tractstack 2.0.43 → 2.0.45

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,4 +1,3 @@
1
- import { useEffect } from 'react';
2
1
  import BooleanToggle from '@/components/form/BooleanToggle';
3
2
  import EnumSelect from '@/components/form/EnumSelect';
4
3
 
@@ -10,13 +9,32 @@ interface PromptOption {
10
9
  }
11
10
 
12
11
  interface CopyInputStepProps {
12
+ layoutChoice: 'standard' | 'grid';
13
13
  copyMode: CopyMode;
14
14
  onCopyModeChange: (mode: CopyMode) => void;
15
+
16
+ topic: string;
17
+ onTopicChange: (value: string) => void;
18
+
19
+ showAdvancedPrompts: boolean;
20
+ onShowAdvancedPromptsChange: (value: boolean) => void;
21
+
15
22
  promptValue: string;
16
23
  onPromptValueChange: (value: string) => void;
17
24
  copyValue: string;
18
25
  onCopyValueChange: (value: string) => void;
19
- defaultPrompt?: string;
26
+
27
+ overallPrompt: string;
28
+ onOverallPromptChange: (value: string) => void;
29
+ promptValueCol1: string;
30
+ onPromptValueCol1Change: (value: string) => void;
31
+ promptValueCol2: string;
32
+ onPromptValueCol2Change: (value: string) => void;
33
+ col1Copy: string;
34
+ onCol1CopyChange: (value: string) => void;
35
+ col2Copy: string;
36
+ onCol2CopyChange: (value: string) => void;
37
+
20
38
  hasRetainedContent?: boolean;
21
39
  promptOptions: PromptOption[];
22
40
  selectedPromptId: string;
@@ -27,13 +45,27 @@ interface CopyInputStepProps {
27
45
  }
28
46
 
29
47
  export const CopyInputStep = ({
48
+ layoutChoice,
30
49
  copyMode,
31
50
  onCopyModeChange,
51
+ topic,
52
+ onTopicChange,
53
+ showAdvancedPrompts,
54
+ onShowAdvancedPromptsChange,
32
55
  promptValue,
33
56
  onPromptValueChange,
34
57
  copyValue,
35
58
  onCopyValueChange,
36
- defaultPrompt,
59
+ overallPrompt,
60
+ onOverallPromptChange,
61
+ promptValueCol1,
62
+ onPromptValueCol1Change,
63
+ promptValueCol2,
64
+ onPromptValueCol2Change,
65
+ col1Copy,
66
+ onCol1CopyChange,
67
+ col2Copy,
68
+ onCol2CopyChange,
37
69
  hasRetainedContent = false,
38
70
  promptOptions,
39
71
  selectedPromptId,
@@ -42,77 +74,182 @@ export const CopyInputStep = ({
42
74
  onIsAiStylingChange,
43
75
  showStyleToggle = true,
44
76
  }: CopyInputStepProps) => {
45
- useEffect(() => {
46
- if (defaultPrompt && !promptValue) {
47
- onPromptValueChange(defaultPrompt);
48
- }
49
- }, [defaultPrompt, promptValue, onPromptValueChange]);
50
-
51
- return (
52
- <div className="space-y-4 rounded-lg bg-gray-50 p-4 shadow-inner">
53
- <label className="block text-lg font-bold text-gray-800">
54
- 1. Provide Content
55
- </label>
56
- <div className="my-2 flex flex-wrap gap-4">
77
+ const renderModeSelection = () => (
78
+ <div className="my-2 flex flex-wrap gap-4">
79
+ <div className="flex items-center space-x-2">
80
+ <input
81
+ type="radio"
82
+ id="copy-prompt-mode"
83
+ name="copyModeOptions"
84
+ value="prompt"
85
+ checked={copyMode === 'prompt'}
86
+ onChange={(e) => onCopyModeChange(e.target.value as CopyMode)}
87
+ className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
88
+ />
89
+ <label
90
+ htmlFor="copy-prompt-mode"
91
+ className="text-sm font-bold text-gray-700"
92
+ >
93
+ Write a prompt
94
+ </label>
95
+ </div>
96
+ <div className="flex items-center space-x-2">
97
+ <input
98
+ type="radio"
99
+ id="copy-raw-mode"
100
+ name="copyModeOptions"
101
+ value="raw"
102
+ checked={copyMode === 'raw'}
103
+ onChange={(e) => onCopyModeChange(e.target.value as CopyMode)}
104
+ className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
105
+ />
106
+ <label
107
+ htmlFor="copy-raw-mode"
108
+ className="text-sm font-bold text-gray-700"
109
+ >
110
+ Provide Copy (Markdown)
111
+ </label>
112
+ </div>
113
+ {hasRetainedContent && (
57
114
  <div className="flex items-center space-x-2">
58
115
  <input
59
116
  type="radio"
60
- id="copy-prompt-mode"
117
+ id="copy-original-mode"
61
118
  name="copyModeOptions"
62
- value="prompt"
63
- checked={copyMode === 'prompt'}
119
+ value="original"
120
+ checked={copyMode === 'original'}
64
121
  onChange={(e) => onCopyModeChange(e.target.value as CopyMode)}
65
122
  className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
66
123
  />
67
124
  <label
68
- htmlFor="copy-prompt-mode"
125
+ htmlFor="copy-original-mode"
69
126
  className="text-sm font-bold text-gray-700"
70
127
  >
71
- Write a prompt
128
+ Use Original
72
129
  </label>
73
130
  </div>
74
- <div className="flex items-center space-x-2">
75
- <input
76
- type="radio"
77
- id="copy-raw-mode"
78
- name="copyModeOptions"
79
- value="raw"
80
- checked={copyMode === 'raw'}
81
- onChange={(e) => onCopyModeChange(e.target.value as CopyMode)}
82
- className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
83
- />
84
- <label
85
- htmlFor="copy-raw-mode"
86
- className="text-sm font-bold text-gray-700"
87
- >
88
- Provide Copy (Markdown)
89
- </label>
131
+ )}
132
+ </div>
133
+ );
134
+
135
+ const renderPromptMode = () => (
136
+ <>
137
+ <div className="mb-4">
138
+ <EnumSelect
139
+ label="Section Type"
140
+ value={selectedPromptId}
141
+ onChange={onSelectedPromptIdChange}
142
+ options={promptOptions}
143
+ placeholder="Select a type..."
144
+ className="w-full"
145
+ />
146
+ </div>
147
+
148
+ <div className="mb-4">
149
+ <label className="mb-2 block text-sm font-bold text-gray-700">
150
+ Topic / Context
151
+ </label>
152
+ <textarea
153
+ value={topic}
154
+ onChange={(e) => onTopicChange(e.target.value)}
155
+ placeholder="e.g. a SaaS product for team collaboration"
156
+ rows={2}
157
+ className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
158
+ />
159
+ </div>
160
+
161
+ <div className="mb-4 flex items-center">
162
+ <BooleanToggle
163
+ label="Advanced: Edit Full Prompts"
164
+ value={showAdvancedPrompts}
165
+ onChange={onShowAdvancedPromptsChange}
166
+ size="sm"
167
+ />
168
+ </div>
169
+
170
+ {showAdvancedPrompts && (
171
+ <div className="mt-4 space-y-4 rounded-md border border-gray-200 bg-white p-4">
172
+ {layoutChoice === 'standard' ? (
173
+ <div>
174
+ <label className="mb-2 block text-sm font-bold text-gray-700">
175
+ Full Prompt
176
+ </label>
177
+ <textarea
178
+ value={promptValue}
179
+ onChange={(e) => onPromptValueChange(e.target.value)}
180
+ rows={4}
181
+ className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
182
+ />
183
+ <p className="mt-1 text-xs text-gray-500">
184
+ Leave [topic] as it will be replaced with your prompt.
185
+ </p>
186
+ </div>
187
+ ) : (
188
+ <>
189
+ <div>
190
+ <label className="mb-2 block text-sm font-bold text-gray-700">
191
+ Overall Component Brief
192
+ </label>
193
+ <textarea
194
+ value={overallPrompt}
195
+ onChange={(e) => onOverallPromptChange(e.target.value)}
196
+ rows={3}
197
+ className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
198
+ />
199
+ </div>
200
+ <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
201
+ <div>
202
+ <label className="mb-2 block text-sm font-bold text-gray-700">
203
+ Left Column Prompt
204
+ </label>
205
+ <textarea
206
+ value={promptValueCol1}
207
+ onChange={(e) => onPromptValueCol1Change(e.target.value)}
208
+ rows={4}
209
+ className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
210
+ />
211
+ </div>
212
+ <div>
213
+ <label className="mb-2 block text-sm font-bold text-gray-700">
214
+ Right Column Prompt
215
+ </label>
216
+ <textarea
217
+ value={promptValueCol2}
218
+ onChange={(e) => onPromptValueCol2Change(e.target.value)}
219
+ rows={4}
220
+ className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
221
+ />
222
+ </div>
223
+ </div>
224
+ </>
225
+ )}
90
226
  </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"
227
+ )}
228
+ </>
229
+ );
230
+
231
+ const renderRawMode = () => (
232
+ <>
233
+ <div className="mb-2 flex items-center justify-between">
234
+ <p className="text-sm text-gray-500">
235
+ Provide your raw copy here. Use Markdown.
236
+ </p>
237
+ {showStyleToggle && (
238
+ <div className="flex items-center">
239
+ <BooleanToggle
240
+ label="Style with AI"
241
+ value={isAiStyling}
242
+ onChange={onIsAiStylingChange}
243
+ size="sm"
101
244
  />
102
- <label
103
- htmlFor="copy-original-mode"
104
- className="text-sm font-bold text-gray-700"
105
- >
106
- Use Original
107
- </label>
108
245
  </div>
109
246
  )}
110
247
  </div>
111
248
 
112
- {(copyMode === 'prompt' || (copyMode === 'raw' && isAiStyling)) && (
249
+ {isAiStyling && (
113
250
  <div className="mb-4">
114
251
  <EnumSelect
115
- label="Section Type"
252
+ label="Section Type (for Styling)"
116
253
  value={selectedPromptId}
117
254
  onChange={onSelectedPromptIdChange}
118
255
  options={promptOptions}
@@ -122,49 +259,54 @@ export const CopyInputStep = ({
122
259
  </div>
123
260
  )}
124
261
 
125
- {copyMode === 'prompt' && (
126
- <>
127
- <p className="mb-2 text-sm text-gray-500">
128
- Let the AI write the copy based on your prompt.
129
- </p>
130
- <textarea
131
- id="copy-prompt"
132
- value={promptValue}
133
- onChange={(e) => onPromptValueChange(e.target.value)}
134
- placeholder="e.g., A hero section for a SaaS product that helps teams collaborate..."
135
- rows={4}
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"
137
- />
138
- </>
139
- )}
140
-
141
- {copyMode === 'raw' && (
142
- <>
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
- )}
262
+ {layoutChoice === 'standard' ? (
263
+ <textarea
264
+ id="raw-copy"
265
+ value={copyValue}
266
+ onChange={(e) => onCopyValueChange(e.target.value)}
267
+ placeholder="## My Awesome Headline..."
268
+ rows={6}
269
+ 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"
270
+ />
271
+ ) : (
272
+ <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
273
+ <div>
274
+ <label className="mb-2 block text-sm font-bold text-gray-700">
275
+ Left Column Markdown
276
+ </label>
277
+ <textarea
278
+ value={col1Copy}
279
+ onChange={(e) => onCol1CopyChange(e.target.value)}
280
+ rows={8}
281
+ 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"
282
+ />
157
283
  </div>
158
- <textarea
159
- id="raw-copy"
160
- value={copyValue}
161
- onChange={(e) => onCopyValueChange(e.target.value)}
162
- placeholder="## My Awesome Headline..."
163
- rows={6}
164
- 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"
165
- />
166
- </>
284
+ <div>
285
+ <label className="mb-2 block text-sm font-bold text-gray-700">
286
+ Right Column Markdown
287
+ </label>
288
+ <textarea
289
+ value={col2Copy}
290
+ onChange={(e) => onCol2CopyChange(e.target.value)}
291
+ rows={8}
292
+ 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"
293
+ />
294
+ </div>
295
+ </div>
167
296
  )}
297
+ </>
298
+ );
299
+
300
+ return (
301
+ <>
302
+ <label className="block text-lg font-bold text-gray-800">
303
+ Content Configuration
304
+ </label>
305
+
306
+ {renderModeSelection()}
307
+
308
+ {copyMode === 'prompt' && renderPromptMode()}
309
+ {copyMode === 'raw' && renderRawMode()}
168
310
 
169
311
  {copyMode === 'original' && (
170
312
  <div className="rounded-md border border-blue-200 bg-blue-50 p-4 text-blue-700">
@@ -173,6 +315,6 @@ export const CopyInputStep = ({
173
315
  </p>
174
316
  </div>
175
317
  )}
176
- </div>
318
+ </>
177
319
  );
178
320
  };
@@ -1,8 +1,12 @@
1
1
  import { useState, useEffect } from 'react';
2
+ import { useStore } from '@nanostores/react';
3
+ import { Portal } from '@ark-ui/react';
4
+ import SparklesIcon from '@heroicons/react/24/outline/SparklesIcon';
2
5
  import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
3
6
  import ArrowUturnLeftIcon from '@heroicons/react/24/outline/ArrowUturnLeftIcon';
4
7
  import ChevronLeftIcon from '@heroicons/react/24/outline/ChevronLeftIcon';
5
8
  import ChevronRightIcon from '@heroicons/react/24/outline/ChevronRightIcon';
9
+ import { selectionStore } from '@/stores/selection';
6
10
  import {
7
11
  settingsPanelStore,
8
12
  stylePanelTargetMemoryStore,
@@ -18,6 +22,7 @@ import {
18
22
  import SelectedTailwindClass from '@/components/fields/SelectedTailwindClass';
19
23
  import BackgroundImageWrapper from '@/components/fields/BackgroundImageWrapper';
20
24
  import ColorPickerCombo from '@/components/fields/ColorPickerCombo';
25
+ import { AiRestylePaneModal } from '@/components/edit/pane/AiRestylePaneModal';
21
26
  import { cloneDeep } from '@/utils/helpers';
22
27
  import {
23
28
  convertToGrid,
@@ -60,6 +65,7 @@ const StyleParentPanel = ({
60
65
  const [selectedTargetIndex, setSelectedTargetIndex] = useState(0);
61
66
 
62
67
  const ctx = getCtx();
68
+ const { isAiRestyleModalOpen } = useStore(selectionStore);
63
69
 
64
70
  useEffect(() => {
65
71
  if (
@@ -518,6 +524,19 @@ const StyleParentPanel = ({
518
524
  })}
519
525
  </div>
520
526
  </div>
527
+ <div className="space-y-3 border-t border-gray-200 pt-4">
528
+ <button
529
+ onClick={() => {
530
+ ctx.toolModeValStore.set({ value: 'styles' });
531
+ selectionStore.setKey('paneToRestyleId', paneNode.id);
532
+ selectionStore.setKey('isAiRestyleModalOpen', true);
533
+ }}
534
+ className="flex w-full items-center justify-center gap-2 rounded bg-purple-600 px-4 py-2 text-sm font-bold text-white hover:bg-purple-700"
535
+ >
536
+ <SparklesIcon className="h-5 w-5" />
537
+ Re-Color this Pane
538
+ </button>
539
+ </div>
521
540
  </div>
522
541
  );
523
542
  };
@@ -645,7 +664,16 @@ const StyleParentPanel = ({
645
664
  }
646
665
  };
647
666
 
648
- return <div className="space-y-4">{renderContent()}</div>;
667
+ return (
668
+ <div className="space-y-4">
669
+ {renderContent()}
670
+ {isAiRestyleModalOpen && (
671
+ <Portal>
672
+ <AiRestylePaneModal />
673
+ </Portal>
674
+ )}
675
+ </div>
676
+ );
649
677
  };
650
678
 
651
679
  export default StyleParentPanel;
@@ -560,6 +560,71 @@ export class NodesContext {
560
560
  );
561
561
  }
562
562
 
563
+ applyShellToPane(paneId: string, template: TemplatePane) {
564
+ const allNodes = new Map(this.allNodes.get());
565
+ const paneNode = allNodes.get(paneId) as PaneNode;
566
+ if (!paneNode) return;
567
+
568
+ if (template.bgColour) {
569
+ paneNode.bgColour = template.bgColour;
570
+ paneNode.isChanged = true;
571
+ }
572
+
573
+ const childrenIds = this.getChildNodeIDs(paneId);
574
+
575
+ const gridLayoutNode = childrenIds
576
+ .map((id) => allNodes.get(id))
577
+ .find((n) => n?.nodeType === 'GridLayoutNode') as
578
+ | GridLayoutNode
579
+ | undefined;
580
+
581
+ const markdownNodes = childrenIds
582
+ .map((id) => allNodes.get(id))
583
+ .filter((n) => n?.nodeType === 'Markdown') as MarkdownPaneFragmentNode[];
584
+
585
+ if (gridLayoutNode && template.gridLayout) {
586
+ if (template.gridLayout.parentClasses) {
587
+ gridLayoutNode.parentClasses = template.gridLayout.parentClasses;
588
+ }
589
+ if (template.gridLayout.defaultClasses) {
590
+ gridLayoutNode.defaultClasses = template.gridLayout.defaultClasses;
591
+ }
592
+ gridLayoutNode.isChanged = true;
593
+
594
+ if (
595
+ template.gridLayout.nodes &&
596
+ Array.isArray(template.gridLayout.nodes)
597
+ ) {
598
+ const columnIds = this.getChildNodeIDs(gridLayoutNode.id);
599
+
600
+ columnIds.forEach((colId, index) => {
601
+ const templateCol = template.gridLayout!.nodes![index];
602
+ if (templateCol && templateCol.gridClasses) {
603
+ const liveColNode = allNodes.get(colId) as MarkdownPaneFragmentNode;
604
+ if (liveColNode) {
605
+ liveColNode.gridClasses = templateCol.gridClasses;
606
+ liveColNode.isChanged = true;
607
+ }
608
+ }
609
+ });
610
+ }
611
+ } else if (markdownNodes.length > 0 && template.markdown) {
612
+ const primaryMarkdown = markdownNodes[0];
613
+
614
+ if (template.markdown.parentClasses) {
615
+ primaryMarkdown.parentClasses = template.markdown.parentClasses;
616
+ }
617
+ if (template.markdown.defaultClasses) {
618
+ primaryMarkdown.defaultClasses = template.markdown.defaultClasses;
619
+ }
620
+ primaryMarkdown.isChanged = true;
621
+ }
622
+
623
+ this.allNodes.set(allNodes);
624
+ this.notifyNode(paneId);
625
+ this.notifyNode('root');
626
+ }
627
+
563
628
  /**
564
629
  * Splits a text node at a given character offset.
565
630
  * This is a robust function that correctly handles splits at offset 0
@@ -20,6 +20,7 @@ export interface SelectionStoreState extends SelectionRange {
20
20
  selectionBox: SelectionBox | null;
21
21
  pendingAction: 'style' | 'link' | 'carousel' | null;
22
22
  isRestyleModalOpen: boolean;
23
+ isAiRestyleModalOpen: boolean;
23
24
  paneToRestyleId: string | null;
24
25
  }
25
26
 
@@ -35,6 +36,7 @@ const DEFAULT_SELECTION_STATE: SelectionStoreState = {
35
36
  selectionBox: null,
36
37
  pendingAction: null,
37
38
  isRestyleModalOpen: false,
39
+ isAiRestyleModalOpen: false,
38
40
  paneToRestyleId: null,
39
41
  };
40
42
 
@@ -477,6 +477,12 @@ export async function injectTemplateFiles(
477
477
  ),
478
478
  dest: 'src/components/edit/pane/RestylePaneModal.tsx',
479
479
  },
480
+ {
481
+ src: resolve(
482
+ '../templates/src/components/edit/pane/AiRestylePaneModal.tsx'
483
+ ),
484
+ dest: 'src/components/edit/pane/AiRestylePaneModal.tsx',
485
+ },
480
486
  {
481
487
  src: resolve(
482
488
  '../templates/src/components/edit/pane/steps/CopyInputStep.tsx'