astro-tractstack 2.0.16 → 2.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -470,6 +470,12 @@ async function w(t, e, c) {
470
470
  ),
471
471
  dest: "src/components/edit/pane/steps/AiDesignStep.tsx"
472
472
  },
473
+ {
474
+ src: t(
475
+ "../templates/src/components/edit/pane/steps/DirectInjectStep.tsx"
476
+ ),
477
+ dest: "src/components/edit/pane/steps/DirectInjectStep.tsx"
478
+ },
473
479
  {
474
480
  src: t(
475
481
  "../templates/src/components/edit/pane/AddPanePanel_newCustomCopy.tsx"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.0.16",
3
+ "version": "2.0.17",
4
4
  "description": "Astro integration for TractStack - redeeming the web from boring experiences",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -31,15 +31,17 @@ export default function SandboxLauncher() {
31
31
  <p className="mt-4 text-lg text-gray-600">
32
32
  Create an interactive webpage in a sandbox! No credit card required.
33
33
  </p>
34
- <p className="mt-8 text-sm text-gray-500">
35
- Already connected?{' '}
36
- <a
37
- href="/storykeep/profile"
38
- className="font-bold text-blue-600 underline hover:text-blue-500"
39
- >
40
- Unlock your profile
41
- </a>
42
- </p>
34
+ {!profileExists && (
35
+ <p className="mt-8 text-sm text-gray-500">
36
+ Already connected?{' '}
37
+ <a
38
+ href="/storykeep/profile"
39
+ className="font-bold text-blue-600 underline hover:text-blue-500"
40
+ >
41
+ Unlock your profile
42
+ </a>
43
+ </p>
44
+ )}
43
45
  </div>
44
46
 
45
47
  {/* Column 2: The Action (Switches between Form and Button) */}
@@ -68,7 +68,7 @@ export const Pane_DesignLibrary = (props: NodeProps) => {
68
68
  }}
69
69
  >
70
70
  <div className="absolute left-2 top-2 z-10 flex flex-col gap-y-2">
71
- {props.isSandboxMode && (
71
+ {!props.isSandboxMode && (
72
72
  <button
73
73
  title="Save Pane to Design Library"
74
74
  onClick={handleSaveClick}
@@ -19,6 +19,7 @@ import {
19
19
  mergeCopyIntoTemplate,
20
20
  convertTemplateToAIShell,
21
21
  } from '@/utils/compositor/designLibraryHelper';
22
+ import { DirectInjectStep } from './steps/DirectInjectStep';
22
23
 
23
24
  type Step =
24
25
  | 'initial'
@@ -26,7 +27,8 @@ type Step =
26
27
  | 'designLibrary'
27
28
  | 'aiDesign'
28
29
  | 'loading'
29
- | 'error';
30
+ | 'error'
31
+ | 'directInject';
30
32
 
31
33
  type InitialChoice = 'library' | 'ai' | 'blank';
32
34
  type CopyMode = 'prompt' | 'raw';
@@ -160,6 +162,8 @@ const AddPaneNewPanel = ({
160
162
  setError(null);
161
163
  if (step === 'copyInput') {
162
164
  setStep('initial');
165
+ } else if (step === 'directInject') {
166
+ setStep('aiDesign');
163
167
  } else if (step === 'designLibrary' || step === 'aiDesign' || 'error') {
164
168
  setStep('copyInput');
165
169
  }
@@ -175,18 +179,18 @@ const AddPaneNewPanel = ({
175
179
 
176
180
  const handleBlankSlate = () => {
177
181
  const blankTemplate: TemplatePane = {
178
- id: '', // ctx will assign
182
+ id: '',
179
183
  nodeType: 'Pane',
180
- parentId: '', // ctx will assign
184
+ parentId: '',
181
185
  title: 'New Pane',
182
186
  slug: '',
183
187
  isDecorative: false,
184
188
  markdown: {
185
- id: '', // ctx will assign
189
+ id: '',
186
190
  nodeType: 'Markdown',
187
- parentId: '', // ctx will assign
191
+ parentId: '',
188
192
  type: 'markdown',
189
- markdownId: '', // ctx will assign
193
+ markdownId: '',
190
194
  defaultClasses: {},
191
195
  parentClasses: [],
192
196
  nodes: [],
@@ -196,10 +200,9 @@ const AddPaneNewPanel = ({
196
200
  };
197
201
 
198
202
  const handleDesignLibrarySelect = async (entry: DesignLibraryEntry) => {
199
- // This flow is for "Design Library + Provide Copy"
200
203
  if (copyMode === 'raw') {
201
204
  const liveTemplate = convertStorageToLiveTemplate(
202
- mergeCopyIntoTemplate(entry.template, []) // Start with blank copy
205
+ mergeCopyIntoTemplate(entry.template, [])
203
206
  );
204
207
  if (liveTemplate.markdown) {
205
208
  liveTemplate.markdown.markdownBody = copyValue;
@@ -208,12 +211,10 @@ const AddPaneNewPanel = ({
208
211
  return;
209
212
  }
210
213
 
211
- // This flow is for "Design Library + Write a Prompt" (Hybrid AI)
212
214
  if (copyMode === 'prompt') {
213
215
  setError(null);
214
216
  setStep('loading');
215
217
  try {
216
- // 1. Get the full, rich template from the library
217
218
  const liveTemplate = convertStorageToLiveTemplate(entry.template);
218
219
  if (!liveTemplate.markdown) {
219
220
  throw new Error(
@@ -221,7 +222,6 @@ const AddPaneNewPanel = ({
221
222
  );
222
223
  }
223
224
 
224
- // 2. Create the simplified shell for the AI
225
225
  const shellJson = convertTemplateToAIShell(liveTemplate);
226
226
  if (!shellJson || shellJson === '{}') {
227
227
  throw new Error(
@@ -229,7 +229,6 @@ const AddPaneNewPanel = ({
229
229
  );
230
230
  }
231
231
 
232
- // 3. Get the AI to write copy based on the shell and prompt
233
232
  const copyPromptDetails = prompts.aiPaneCopyPrompt;
234
233
  const layout = 'Text Only';
235
234
  const formattedCopyPrompt = copyPromptDetails.user_template
@@ -248,16 +247,12 @@ const AddPaneNewPanel = ({
248
247
  isSandboxMode
249
248
  );
250
249
 
251
- // 4. Parse ONLY the AI-generated HTML into content nodes
252
250
  const newNodes = parseAiCopyHtml(copyResult, liveTemplate.markdown.id);
253
251
 
254
- // 5. Create the final pane by cloning the original rich template
255
252
  const finalPane = cloneDeep(liveTemplate);
256
253
 
257
- // 6. Inject the new AI content, preserving the original rich design
258
254
  finalPane.markdown!.nodes = newNodes;
259
255
 
260
- // 7. Apply the complete, correctly merged pane
261
256
  handleApplyTemplate(finalPane);
262
257
  } catch (err: any) {
263
258
  setError(err.message || 'Failed to generate AI copy for this design.');
@@ -281,7 +276,7 @@ const AddPaneNewPanel = ({
281
276
  try {
282
277
  const shellPromptDetails = prompts.aiPaneShellPrompt;
283
278
  const copyPromptDetails = prompts.aiPaneCopyPrompt;
284
- const layout = 'Text Only'; // Hardcoded for this simplified AI path
279
+ const layout = 'Text Only';
285
280
 
286
281
  const formattedShellPrompt = shellPromptDetails.user_template
287
282
  .replace('{{DESIGN_INPUT}}', designInput)
@@ -317,6 +312,7 @@ const AddPaneNewPanel = ({
317
312
  }, [aiDesignConfig, copyMode, promptValue, copyValue, isSandboxMode]);
318
313
 
319
314
  const handleApplyTemplate = async (template: TemplatePane) => {
315
+ console.log(template);
320
316
  if (!ctx) return;
321
317
  try {
322
318
  const insertTemplate = cloneDeep(template);
@@ -356,8 +352,6 @@ const AddPaneNewPanel = ({
356
352
  }
357
353
  };
358
354
 
359
- // --- Render Logic ---
360
-
361
355
  const renderInitialStep = () => (
362
356
  <div className="p-4">
363
357
  <h3 className="font-action mb-4 text-center text-xl font-bold text-gray-800">
@@ -369,7 +363,7 @@ const AddPaneNewPanel = ({
369
363
  className="group flex flex-col items-center space-y-3 rounded-lg border bg-white p-6 text-center shadow-sm transition-all hover:border-cyan-600 hover:shadow-lg"
370
364
  >
371
365
  <SwatchIcon className="h-10 w-10 text-gray-500 transition-colors group-hover:text-cyan-600" />
372
- <h4 className="font-semibold text-gray-800">Use Design Library</h4>
366
+ <h4 className="font-bold text-gray-800">Use Design Library</h4>
373
367
  <p className="text-sm text-gray-600">
374
368
  Start with a pre-made design and add your own content.
375
369
  </p>
@@ -380,7 +374,7 @@ const AddPaneNewPanel = ({
380
374
  className="group flex flex-col items-center space-y-3 rounded-lg border bg-white p-6 text-center shadow-sm transition-all hover:border-cyan-600 hover:shadow-lg"
381
375
  >
382
376
  <SparklesIcon className="h-10 w-10 text-gray-500 transition-colors group-hover:text-cyan-600" />
383
- <h4 className="font-semibold text-gray-800">Design with AI</h4>
377
+ <h4 className="font-bold text-gray-800">Design with AI</h4>
384
378
  <p className="text-sm text-gray-600">
385
379
  Let AI generate a complete design and copy from your prompt.
386
380
  </p>
@@ -391,7 +385,7 @@ const AddPaneNewPanel = ({
391
385
  className="group flex flex-col items-center space-y-3 rounded-lg border bg-white p-6 text-center shadow-sm transition-all hover:border-cyan-600 hover:shadow-lg"
392
386
  >
393
387
  <DocumentPlusIcon className="h-10 w-10 text-gray-500 transition-colors group-hover:text-cyan-600" />
394
- <h4 className="font-semibold text-gray-800">Blank Slate</h4>
388
+ <h4 className="font-bold text-gray-800">Blank Slate</h4>
395
389
  <p className="text-sm text-gray-600">
396
390
  Add a simple, empty pane to build from scratch.
397
391
  </p>
@@ -432,6 +426,17 @@ const AddPaneNewPanel = ({
432
426
  Continue →
433
427
  </button>
434
428
  </div>
429
+ {initialChoice === `ai` && !isSandboxMode && (
430
+ <div className="mt-6 text-center text-sm text-gray-600">
431
+ ADVANCED:{' '}
432
+ <button
433
+ onClick={() => setStep('directInject')}
434
+ className="font-bold text-cyan-700 underline hover:text-cyan-900 focus:outline-none"
435
+ >
436
+ Direct Inject
437
+ </button>
438
+ </div>
439
+ )}
435
440
  </div>
436
441
  );
437
442
 
@@ -476,6 +481,10 @@ const AddPaneNewPanel = ({
476
481
  </div>
477
482
  );
478
483
 
484
+ const renderDirectInjectStep = () => (
485
+ <DirectInjectStep onBack={handleBack} onCreatePane={handleApplyTemplate} />
486
+ );
487
+
479
488
  const renderLoading = () => (
480
489
  <div className="flex min-h-[300px] flex-col items-center justify-center space-y-4 p-6">
481
490
  <div className="h-8 w-8 animate-spin rounded-full border-b-2 border-cyan-600"></div>
@@ -509,6 +518,8 @@ const AddPaneNewPanel = ({
509
518
  return renderAiDesignStep();
510
519
  case 'loading':
511
520
  return renderLoading();
521
+ case 'directInject':
522
+ return renderDirectInjectStep();
512
523
  case 'error':
513
524
  return renderError();
514
525
  default:
@@ -0,0 +1,96 @@
1
+ import { useState } from 'react';
2
+ import { parseAiPane } from '@/utils/compositor/aiPaneParser';
3
+ import type { TemplatePane } from '@/types/compositorTypes';
4
+
5
+ interface DirectInjectStepProps {
6
+ onBack: () => void;
7
+ onCreatePane: (template: TemplatePane) => void;
8
+ }
9
+
10
+ export const DirectInjectStep = ({
11
+ onBack,
12
+ onCreatePane,
13
+ }: DirectInjectStepProps) => {
14
+ const [shellJson, setShellJson] = useState('');
15
+ const [copyHtml, setCopyHtml] = useState('');
16
+ const [error, setError] = useState<string | null>(null);
17
+
18
+ const handleCreate = () => {
19
+ setError(null);
20
+ if (!shellJson.trim() || !copyHtml.trim()) {
21
+ setError('Both Shell JSON and Inner HTML must be provided.');
22
+ return;
23
+ }
24
+
25
+ try {
26
+ const finalPane = parseAiPane(shellJson, copyHtml, 'DirectInject');
27
+ onCreatePane(finalPane);
28
+ } catch (err: any) {
29
+ console.error('Direct Inject Error:', err);
30
+ setError(
31
+ `Failed to parse inputs: ${err.message || 'Unknown error'}. Check console.`
32
+ );
33
+ }
34
+ };
35
+
36
+ return (
37
+ <div className="space-y-6 p-4">
38
+ <div className="space-y-4">
39
+ <div>
40
+ <label
41
+ htmlFor="shellJson"
42
+ className="block text-sm font-bold text-gray-700"
43
+ >
44
+ Shell JSON
45
+ </label>
46
+ <textarea
47
+ id="shellJson"
48
+ rows={10}
49
+ value={shellJson}
50
+ onChange={(e) => setShellJson(e.target.value)}
51
+ 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"
52
+ placeholder={`{ "bgColour": "#ffffff", "parentClasses": [...], "defaultClasses": {...} }`}
53
+ />
54
+ </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
+ </div>
72
+
73
+ {error && (
74
+ <div className="rounded-md bg-red-50 p-4">
75
+ <p className="text-sm font-bold text-red-800">{error}</p>
76
+ </div>
77
+ )}
78
+
79
+ <div className="flex justify-between">
80
+ <button
81
+ onClick={onBack}
82
+ className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-bold text-gray-700 shadow-sm hover:bg-gray-50"
83
+ >
84
+ ← Back
85
+ </button>
86
+ <button
87
+ onClick={handleCreate}
88
+ disabled={!shellJson.trim() || !copyHtml.trim()}
89
+ 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
+ >
91
+ Create Pane
92
+ </button>
93
+ </div>
94
+ </div>
95
+ );
96
+ };
@@ -18,6 +18,7 @@ export const reservedSlugs = [
18
18
  `404`,
19
19
  `transcribe`,
20
20
  `sitemap`,
21
+ `sandbox`,
21
22
  `robots`,
22
23
  `llm`,
23
24
  ];
@@ -60,6 +60,8 @@ let BUTTON_CLASS_LOOKUP: Map<string, { key: string; value: string }> | null =
60
60
  null;
61
61
 
62
62
  const ALLOWED_TAGS = new Set([
63
+ 'ul',
64
+ 'li',
63
65
  'h2',
64
66
  'h3',
65
67
  'h4',
@@ -69,6 +71,7 @@ const ALLOWED_TAGS = new Set([
69
71
  'em',
70
72
  'strong',
71
73
  'button',
74
+ 'a',
72
75
  ]);
73
76
 
74
77
  function buildKeyNormalizationLookup(): Map<string, string> {
@@ -309,7 +312,10 @@ function walkDom(
309
312
  if (tagName === 'button') {
310
313
  let finalParentId = parentId;
311
314
 
312
- if (parentId === markdownId) {
315
+ const parentDomEl = el.parentNode as Element;
316
+ const parentTagName = parentDomEl?.tagName?.toLowerCase();
317
+
318
+ if (parentId === markdownId || parentTagName === 'li') {
313
319
  const pNodeId = ulid();
314
320
  const pNode: TemplateNode = {
315
321
  id: pNodeId,
@@ -330,7 +336,7 @@ function walkDom(
330
336
  id: ulid(),
331
337
  nodeType: 'TagElement',
332
338
  parentId: finalParentId,
333
- tagName: 'a', // Buttons are converted to anchor tags for our system
339
+ tagName: 'a',
334
340
  href: '#',
335
341
  buttonPayload: {
336
342
  ...buttonPayload,
@@ -471,6 +471,12 @@ export async function injectTemplateFiles(
471
471
  ),
472
472
  dest: 'src/components/edit/pane/steps/AiDesignStep.tsx',
473
473
  },
474
+ {
475
+ src: resolve(
476
+ '../templates/src/components/edit/pane/steps/DirectInjectStep.tsx'
477
+ ),
478
+ dest: 'src/components/edit/pane/steps/DirectInjectStep.tsx',
479
+ },
474
480
  {
475
481
  src: resolve(
476
482
  '../templates/src/components/edit/pane/AddPanePanel_newCustomCopy.tsx'