astro-tractstack 2.0.20 → 2.0.21

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.20",
3
+ "version": "2.0.21",
4
4
  "description": "Astro integration for TractStack - redeeming the web from boring experiences",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -62,7 +62,6 @@ export const PanesPreviewGenerator = ({
62
62
  request.ctx,
63
63
  actualPaneId
64
64
  );
65
- console.log(previewPayload);
66
65
 
67
66
  previewPayloads.push(previewPayload);
68
67
  requestMap.set(previewPayload.id, request.id);
@@ -1,5 +1,6 @@
1
1
  import { useState } from 'react';
2
2
  import { useStore } from '@nanostores/react';
3
+ import PlusCircleIcon from '@heroicons/react/24/outline/PlusCircleIcon';
3
4
  import { settingsPanelStore } from '@/stores/storykeep';
4
5
  import AddPaneNewPanel from './AddPanePanel_new';
5
6
  import AddPaneBreakPanel from './AddPanePanel_break';
@@ -58,6 +59,21 @@ const AddPanePanel = ({
58
59
 
59
60
  return (
60
61
  <div className="add-pane-panel-wrapper border-mydarkgrey border-b-2 border-t-2 border-dotted">
62
+ {isExpanded && (
63
+ <div className="border-mylightgrey border-t border-dotted">
64
+ <div className="group flex w-full flex-wrap items-center gap-2 px-1.5 pb-0.5 pt-1.5">
65
+ <button
66
+ onClick={() => {
67
+ setMode(PaneAddMode.DEFAULT);
68
+ setIsExpanded(false);
69
+ }}
70
+ className="rounded-md bg-gray-200 px-2 py-1 text-sm font-bold text-gray-800"
71
+ >
72
+ &lt; Cancel
73
+ </button>
74
+ </div>
75
+ </div>
76
+ )}
61
77
  {mode === PaneAddMode.NEW || (!hasPanes && first && !reset) ? (
62
78
  <AddPaneNewPanel
63
79
  nodeId={nodeId}
@@ -98,12 +114,6 @@ const AddPanePanel = ({
98
114
  ) : isExpanded ? (
99
115
  <div className="border-mylightgrey border-t border-dotted">
100
116
  <div className="group flex w-full flex-wrap items-center gap-2 px-1.5 pb-0.5 pt-1.5">
101
- <button
102
- onClick={() => setIsExpanded(false)}
103
- className="rounded-md bg-gray-200 px-2 py-1 text-sm font-bold text-gray-800"
104
- >
105
- &lt; Cancel
106
- </button>
107
117
  <div className={`flex flex-wrap gap-1 transition-opacity`}>
108
118
  <button
109
119
  onClick={() => setMode(PaneAddMode.NEW)}
@@ -111,12 +121,6 @@ const AddPanePanel = ({
111
121
  >
112
122
  + Design New
113
123
  </button>
114
- <button
115
- onClick={() => setMode(PaneAddMode.PASTE)}
116
- className="rounded bg-white px-2 py-1 text-sm text-cyan-700 shadow-sm transition-colors hover:bg-cyan-700 hover:text-white"
117
- >
118
- + Paste Pane
119
- </button>
120
124
  {!isContextPane && (
121
125
  <>
122
126
  <button
@@ -143,16 +147,25 @@ const AddPanePanel = ({
143
147
  + Code Hook
144
148
  </button>
145
149
  )}
150
+ <button
151
+ onClick={() => setMode(PaneAddMode.PASTE)}
152
+ className="rounded bg-white px-2 py-1 text-sm text-cyan-700 shadow-sm transition-colors hover:bg-cyan-700 hover:text-white"
153
+ >
154
+ + Paste Pane
155
+ </button>
146
156
  </div>
147
157
  </div>
148
158
  </div>
149
159
  ) : (
150
- <div className="border-mylightgrey border-t border-dotted p-2">
160
+ <div className="border-mylightgrey flex border-t border-dotted p-0.5">
151
161
  <button
152
162
  onClick={() => setIsExpanded(true)}
153
- className="text-sm text-gray-500 underline decoration-dotted underline-offset-4 transition-colors hover:text-cyan-700"
163
+ title="Insert Pane here"
164
+ className="group w-full text-gray-500"
154
165
  >
155
- Insert Pane Here
166
+ <div className="text-mydarkgrey hover:bg-myoffwhite rounded-md transition-colors duration-150 ease-in-out hover:bg-opacity-50 hover:mix-blend-difference">
167
+ <PlusCircleIcon className="mx-auto h-8 w-8" />
168
+ </div>
156
169
  </button>
157
170
  </div>
158
171
  )}
@@ -18,7 +18,6 @@ import { AiDesignStep, type AiDesignConfig } from './steps/AiDesignStep';
18
18
  import { parseAiPane, parseAiCopyHtml } from '@/utils/compositor/aiPaneParser';
19
19
  import {
20
20
  convertStorageToLiveTemplate,
21
- mergeCopyIntoTemplate,
22
21
  convertTemplateToAIShell,
23
22
  } from '@/utils/compositor/designLibraryHelper';
24
23
  import { DirectInjectStep } from './steps/DirectInjectStep';
@@ -141,19 +140,21 @@ const AddPaneNewPanel = ({
141
140
  const [layoutChoice, setLayoutChoice] = useState<LayoutChoice>('standard');
142
141
  const [error, setError] = useState<string | null>(null);
143
142
 
143
+ // Standard / Single Column State
144
144
  const [copyMode, setCopyMode] = useState<CopyMode>('prompt');
145
145
  const [promptValue, setPromptValue] = useState('');
146
146
  const [copyValue, setCopyValue] = useState('');
147
147
 
148
+ // Grid / 2-Column State (Strictly Prompt-Only)
148
149
  const [overallPrompt, setOverallPrompt] = useState(
149
150
  prompts.aiPaneCopyPrompt_2cols.presets.heroDefault.default
150
151
  );
151
- const [copyModeCol1, setCopyModeCol1] = useState<CopyMode>('prompt');
152
- const [promptValueCol1, setPromptValueCol1] = useState('');
153
- const [copyValueCol1, setCopyValueCol1] = useState('');
154
- const [copyModeCol2, setCopyModeCol2] = useState<CopyMode>('prompt');
155
- const [promptValueCol2, setPromptValueCol2] = useState('');
156
- const [copyValueCol2, setCopyValueCol2] = useState('');
152
+ const [promptValueCol1, setPromptValueCol1] = useState(
153
+ prompts.aiPaneCopyPrompt_2cols.presets.heroDefault.left.prompt
154
+ );
155
+ const [promptValueCol2, setPromptValueCol2] = useState(
156
+ prompts.aiPaneCopyPrompt_2cols.presets.heroDefault.right.prompt
157
+ );
157
158
 
158
159
  const [selectedLibraryEntry, setSelectedLibraryEntry] =
159
160
  useState<DesignLibraryEntry | null>(null);
@@ -226,11 +227,6 @@ const AddPaneNewPanel = ({
226
227
  handleApplyTemplate(blankTemplate);
227
228
  };
228
229
 
229
- const handleDesignLibrarySelect = (entry: DesignLibraryEntry) => {
230
- setSelectedLibraryEntry(entry);
231
- setStep('copyInput');
232
- };
233
-
234
230
  const handleAiDesignContinue = () => {
235
231
  setStep('copyInput');
236
232
  };
@@ -274,6 +270,18 @@ const AddPaneNewPanel = ({
274
270
  }
275
271
  };
276
272
 
273
+ const handleDesignLibrarySelect = (entry: DesignLibraryEntry) => {
274
+ setSelectedLibraryEntry(entry);
275
+
276
+ if (entry.template.gridLayout) {
277
+ setLayoutChoice('grid');
278
+ } else {
279
+ setLayoutChoice('standard');
280
+ }
281
+
282
+ setStep('copyInput');
283
+ };
284
+
277
285
  const handleFinalGenerate = useCallback(async () => {
278
286
  setError(null);
279
287
  setStep('loading');
@@ -284,59 +292,99 @@ const AddPaneNewPanel = ({
284
292
  throw new Error('No design library item was selected.');
285
293
  }
286
294
 
287
- if (copyMode === 'raw') {
288
- const liveTemplate = convertStorageToLiveTemplate(
289
- mergeCopyIntoTemplate(selectedLibraryEntry.template, [])
290
- );
291
- if (liveTemplate.markdown) {
292
- liveTemplate.markdown.markdownBody = copyValue;
293
- }
294
- handleApplyTemplate(liveTemplate);
295
- return;
296
- }
295
+ const liveTemplate = convertStorageToLiveTemplate(
296
+ selectedLibraryEntry.template
297
+ );
298
+ const shellResult = convertTemplateToAIShell(liveTemplate);
299
+ const layout = 'Text Only';
297
300
 
298
- if (copyMode === 'prompt') {
299
- const liveTemplate = convertStorageToLiveTemplate(
300
- selectedLibraryEntry.template
301
- );
302
- if (!liveTemplate.markdown) {
303
- throw new Error(
304
- 'The selected design library item is not compatible with this workflow as it has no markdown section.'
301
+ if (layoutChoice === 'grid' && liveTemplate.gridLayout) {
302
+ const copyPromptDetails = prompts.aiPaneCopyPrompt_2cols;
303
+ const preset = copyPromptDetails.presets.heroDefault;
304
+ 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
+ },
319
+ ];
320
+
321
+ for (const item of promptsToRun) {
322
+ const columnPreset = preset[item.presetKey];
323
+ const formattedCopyPrompt = copyPromptDetails.user_template
324
+ .replace('{{SHELL_JSON}}', shellResult)
325
+ .replace('{{COPY_INPUT}}', overallPrompt)
326
+ .replace('{{COLUMN_PROMPT}}', item.prompt)
327
+ .replace(
328
+ '{{DESIGN_INPUT}}',
329
+ "N/A - Use the provided Shell JSON's design."
330
+ )
331
+ .replace('{{LAYOUT_TYPE}}', layout)
332
+ .replace('{{COLUMN_EXAMPLE}}', columnPreset.example);
333
+
334
+ const copyResult = await callAskLemurAPI(
335
+ formattedCopyPrompt,
336
+ copyPromptDetails.system || '',
337
+ false,
338
+ isSandboxMode
305
339
  );
340
+ copyResults.push(copyResult);
306
341
  }
307
342
 
308
- const shellJson = convertTemplateToAIShell(liveTemplate);
309
- if (!shellJson || shellJson === '{}') {
310
- throw new Error(
311
- 'Could not generate a valid AI shell from this design.'
312
- );
343
+ const finalPane = parseAiPane(shellResult, copyResults, layout);
344
+ handleApplyTemplate(finalPane);
345
+ } else if (layoutChoice === 'standard' && liveTemplate.markdown) {
346
+ if (copyMode === 'raw') {
347
+ liveTemplate.markdown.markdownBody = copyValue;
348
+ handleApplyTemplate(liveTemplate);
349
+ return;
313
350
  }
314
351
 
315
- const copyPromptDetails = prompts.aiPaneCopyPrompt;
316
- const layout = 'Text Only';
317
- const formattedCopyPrompt = copyPromptDetails.user_template
318
- .replace('{{COPY_INPUT}}', promptValue)
319
- .replace(
320
- '{{DESIGN_INPUT}}',
321
- "N/A - Use the provided Shell JSON's design."
322
- )
323
- .replace('{{LAYOUT_TYPE}}', layout)
324
- .replace('{{SHELL_JSON}}', shellJson);
352
+ if (copyMode === 'prompt') {
353
+ if (!shellResult || shellResult === '{}') {
354
+ throw new Error(
355
+ 'Could not generate a valid AI shell from this design.'
356
+ );
357
+ }
325
358
 
326
- const copyResult = await callAskLemurAPI(
327
- formattedCopyPrompt,
328
- copyPromptDetails.system || '',
329
- false,
330
- isSandboxMode
331
- );
359
+ const copyPromptDetails = prompts.aiPaneCopyPrompt;
360
+ const formattedCopyPrompt = copyPromptDetails.user_template
361
+ .replace('{{COPY_INPUT}}', promptValue)
362
+ .replace(
363
+ '{{DESIGN_INPUT}}',
364
+ "N/A - Use the provided Shell JSON's design."
365
+ )
366
+ .replace('{{LAYOUT_TYPE}}', layout)
367
+ .replace('{{SHELL_JSON}}', shellResult);
332
368
 
333
- const newNodes = parseAiCopyHtml(
334
- copyResult,
335
- liveTemplate.markdown.id
369
+ const copyResult = await callAskLemurAPI(
370
+ formattedCopyPrompt,
371
+ copyPromptDetails.system || '',
372
+ false,
373
+ isSandboxMode
374
+ );
375
+
376
+ const newNodes = parseAiCopyHtml(
377
+ copyResult,
378
+ liveTemplate.markdown.id
379
+ );
380
+ const finalPane = cloneDeep(liveTemplate);
381
+ finalPane.markdown!.nodes = newNodes;
382
+ handleApplyTemplate(finalPane);
383
+ }
384
+ } else {
385
+ throw new Error(
386
+ 'Template and layout mismatch. Please go back and try again.'
336
387
  );
337
- const finalPane = cloneDeep(liveTemplate);
338
- finalPane.markdown!.nodes = newNodes;
339
- handleApplyTemplate(finalPane);
340
388
  }
341
389
  } else if (initialChoice === 'ai') {
342
390
  let designInput = `Generate a design using a **${aiDesignConfig.harmony.toLowerCase()}** color scheme with a **${aiDesignConfig.theme.toLowerCase()}** theme.`;
@@ -396,32 +444,22 @@ const AddPaneNewPanel = ({
396
444
  const preset = copyPromptDetails.presets.heroDefault;
397
445
  const copyResults: string[] = [];
398
446
 
447
+ // Grid is strictly prompt-based
399
448
  const promptsToRun: {
400
449
  prompt: string;
401
- copy: string;
402
- mode: CopyMode;
403
450
  presetKey: ColumnPresetKey;
404
451
  }[] = [
405
452
  {
406
453
  prompt: promptValueCol1,
407
- copy: copyValueCol1,
408
- mode: copyModeCol1,
409
454
  presetKey: 'left',
410
455
  },
411
456
  {
412
457
  prompt: promptValueCol2,
413
- copy: copyValueCol2,
414
- mode: copyModeCol2,
415
458
  presetKey: 'right',
416
459
  },
417
460
  ];
418
461
 
419
462
  for (const item of promptsToRun) {
420
- if (item.mode === 'raw') {
421
- copyResults.push(item.copy);
422
- continue;
423
- }
424
-
425
463
  const columnPreset = preset[item.presetKey];
426
464
  const formattedCopyPrompt = copyPromptDetails.user_template
427
465
  .replace('{{SHELL_JSON}}', shellResult)
@@ -454,12 +492,8 @@ const AddPaneNewPanel = ({
454
492
  promptValue,
455
493
  copyValue,
456
494
  overallPrompt,
457
- copyModeCol1,
458
495
  promptValueCol1,
459
- copyValueCol1,
460
- copyModeCol2,
461
496
  promptValueCol2,
462
- copyValueCol2,
463
497
  isSandboxMode,
464
498
  initialChoice,
465
499
  layoutChoice,
@@ -550,58 +584,47 @@ const AddPaneNewPanel = ({
550
584
  const renderContentStep = () => {
551
585
  if (layoutChoice === 'grid') {
552
586
  const isGenerateDisabled =
553
- (copyModeCol1 === 'prompt' && !promptValueCol1.trim()) ||
554
- (copyModeCol1 === 'raw' && !copyValueCol1.trim()) ||
555
- (copyModeCol2 === 'prompt' && !promptValueCol2.trim()) ||
556
- (copyModeCol2 === 'raw' && !copyValueCol2.trim()) ||
557
- !overallPrompt.trim();
587
+ !overallPrompt.trim() ||
588
+ !promptValueCol1.trim() ||
589
+ !promptValueCol2.trim();
558
590
 
559
591
  return (
560
592
  <div className="space-y-4 p-4">
561
593
  <div>
562
- <h4 className="mb-2 block text-sm font-bold text-gray-700">
594
+ <label className="mb-2 block text-sm font-bold text-gray-700">
563
595
  Overall Component Brief
564
- </h4>
596
+ </label>
565
597
  <textarea
566
598
  value={overallPrompt}
567
599
  onChange={(e) => setOverallPrompt(e.target.value)}
568
- placeholder="e.g., A compelling hero section for a website about Tract Stack..."
569
600
  rows={3}
570
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"
571
602
  />
603
+ <p className="mt-1 text-xs text-gray-500">
604
+ This context is applied to both columns.
605
+ </p>
572
606
  </div>
573
607
  <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
574
608
  <div>
575
- <h4 className="mb-2 block text-sm font-bold text-gray-700">
576
- Left Column Content
577
- </h4>
578
- <CopyInputStep
579
- copyMode={copyModeCol1}
580
- onCopyModeChange={setCopyModeCol1}
581
- promptValue={promptValueCol1}
582
- onPromptValueChange={setPromptValueCol1}
583
- copyValue={copyValueCol1}
584
- onCopyValueChange={setCopyValueCol1}
585
- defaultPrompt={
586
- prompts.aiPaneCopyPrompt_2cols.presets.heroDefault.left.prompt
587
- }
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"
588
617
  />
589
618
  </div>
590
619
  <div>
591
- <h4 className="mb-2 block text-sm font-bold text-gray-700">
592
- Right Column Content
593
- </h4>
594
- <CopyInputStep
595
- copyMode={copyModeCol2}
596
- onCopyModeChange={setCopyModeCol2}
597
- promptValue={promptValueCol2}
598
- onPromptValueChange={setPromptValueCol2}
599
- copyValue={copyValueCol2}
600
- onCopyValueChange={setCopyValueCol2}
601
- defaultPrompt={
602
- prompts.aiPaneCopyPrompt_2cols.presets.heroDefault.right
603
- .prompt
604
- }
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"
605
628
  />
606
629
  </div>
607
630
  </div>
@@ -759,25 +782,15 @@ const AddPaneNewPanel = ({
759
782
  return (
760
783
  <div className="bg-white p-2 shadow-inner">
761
784
  <div className="group mb-2 flex w-full items-center gap-1 rounded-md bg-white p-1.5">
762
- {first ? (
763
- <div className="w-full text-center">
764
- <h2 className="font-action py-1.5 text-lg font-bold text-gray-800">
765
- Welcome to Tract Stack
766
- </h2>
767
- </div>
768
- ) : (
769
- <>
770
- <button
771
- onClick={() => setParentMode(PaneAddMode.DEFAULT, first)}
772
- className="w-fit rounded bg-gray-100 px-3 py-1 text-sm text-gray-700 transition-colors hover:bg-gray-200"
773
- >
774
- ← Go Back
775
- </button>
776
- <div className="font-action ml-4 flex-none rounded px-2 py-2.5 text-sm font-bold text-cyan-700 shadow-sm">
777
- + Design New Pane
778
- </div>
779
- </>
780
- )}
785
+ <button
786
+ onClick={() => setParentMode(PaneAddMode.DEFAULT, first)}
787
+ className="w-fit rounded bg-gray-100 px-3 py-1 text-sm text-gray-700 transition-colors hover:bg-gray-200"
788
+ >
789
+ ← Go Back
790
+ </button>
791
+ <div className="font-action ml-4 flex-none rounded px-2 py-2.5 text-sm font-bold text-cyan-700 shadow-sm">
792
+ + Design New Pane
793
+ </div>
781
794
  </div>
782
795
  <div className="min-h-96 rounded-md border bg-gray-50">
783
796
  {renderStep()}