astro-tractstack 2.0.19 → 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.
Files changed (54) hide show
  1. package/dist/index.js +6 -32
  2. package/package.json +1 -1
  3. package/templates/src/components/codehooks/BunnyVideoSetup.tsx +1 -4
  4. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +0 -4
  5. package/templates/src/components/codehooks/ListContentSetup.tsx +1 -8
  6. package/templates/src/components/codehooks/ProductCardSetup.tsx +0 -2
  7. package/templates/src/components/codehooks/ProductGridSetup.tsx +0 -2
  8. package/templates/src/components/compositor/Compositor.tsx +3 -6
  9. package/templates/src/components/compositor/Node.tsx +13 -32
  10. package/templates/src/components/compositor/NodeWithGuid.tsx +49 -5
  11. package/templates/src/components/compositor/nodes/Pane.tsx +4 -21
  12. package/templates/src/components/compositor/nodes/Pane_DesignLibrary.tsx +27 -7
  13. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag.tsx +3 -1
  14. package/templates/src/components/compositor/preview/OgImagePreview.tsx +0 -5
  15. package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +5 -6
  16. package/templates/src/components/edit/PanelSwitch.tsx +3 -24
  17. package/templates/src/components/edit/SettingsPanel.tsx +0 -1
  18. package/templates/src/components/edit/ToolMode.tsx +6 -14
  19. package/templates/src/components/edit/pane/AddPanePanel.tsx +58 -25
  20. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +140 -133
  21. package/templates/src/components/edit/pane/AddPanePanel_paste.tsx +111 -0
  22. package/templates/src/components/edit/pane/RestylePaneModal.tsx +231 -282
  23. package/templates/src/components/edit/pane/steps/AiDesignStep.tsx +0 -5
  24. package/templates/src/components/edit/pane/steps/DesignLibraryStep.tsx +4 -13
  25. package/templates/src/components/edit/panels/StyleBreakPanel.tsx +1 -3
  26. package/templates/src/components/edit/panels/StyleElementPanel_update.tsx +0 -6
  27. package/templates/src/components/edit/panels/StyleImagePanel_update.tsx +0 -3
  28. package/templates/src/components/edit/panels/StyleLiElementPanel_update.tsx +0 -4
  29. package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +8 -5
  30. package/templates/src/components/edit/panels/StyleLinkPanel_update.tsx +1 -2
  31. package/templates/src/components/edit/panels/StyleParentPanel.tsx +1 -3
  32. package/templates/src/components/edit/panels/StyleParentPanel_update.tsx +2 -5
  33. package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +2 -8
  34. package/templates/src/components/edit/panels/StyleWidgetPanel_update.tsx +0 -4
  35. package/templates/src/components/edit/state/SaveToLibraryModal.tsx +29 -16
  36. package/templates/src/components/edit/storyfragment/StoryFragmentConfigPanel.tsx +9 -26
  37. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +7 -16
  38. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +5 -6
  39. package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +0 -5
  40. package/templates/src/components/fields/BackgroundImageWrapper.tsx +1 -7
  41. package/templates/src/components/fields/ColorPickerCombo.tsx +8 -12
  42. package/templates/src/components/fields/ViewportComboBox.tsx +4 -6
  43. package/templates/src/stores/nodes.ts +14 -6
  44. package/templates/src/stores/storykeep.ts +3 -3
  45. package/templates/src/types/compositorTypes.ts +2 -0
  46. package/templates/src/utils/compositor/TemplatePanes.ts +0 -76
  47. package/templates/src/utils/compositor/aiPaneParser.ts +3 -1
  48. package/templates/src/utils/compositor/designLibraryHelper.ts +523 -203
  49. package/templates/src/utils/helpers.ts +5 -4
  50. package/utils/inject-files.ts +6 -32
  51. package/templates/src/components/compositor/preview/VisualBreakPreview.tsx +0 -154
  52. package/templates/src/components/edit/pane/PageGen_preview.tsx +0 -511
  53. package/templates/src/utils/compositor/processMarkdown.ts +0 -445
  54. package/templates/src/utils/compositor/templateMarkdownStyles.ts +0 -1273
@@ -9,7 +9,7 @@ import { NodesContext, getCtx } from '@/stores/nodes';
9
9
  import { cloneDeep } from '@/utils/helpers';
10
10
  import { hasAssemblyAIStore } from '@/stores/storykeep';
11
11
  import prompts from '@/constants/prompts.json';
12
- import type { BrandConfig, DesignLibraryEntry } from '@/types/tractstack';
12
+ import type { DesignLibraryEntry } from '@/types/tractstack';
13
13
  import { PaneAddMode, type TemplatePane } from '@/types/compositorTypes';
14
14
  import { useStore } from '@nanostores/react';
15
15
  import { CopyInputStep } from './steps/CopyInputStep';
@@ -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';
@@ -120,7 +119,6 @@ interface AddPaneNewPanelProps {
120
119
  ctx?: NodesContext;
121
120
  isStoryFragment?: boolean;
122
121
  isContextPane?: boolean;
123
- config?: BrandConfig;
124
122
  isSandboxMode?: boolean;
125
123
  }
126
124
 
@@ -131,7 +129,6 @@ const AddPaneNewPanel = ({
131
129
  ctx: providedCtx,
132
130
  isStoryFragment = false,
133
131
  isContextPane = false,
134
- config,
135
132
  isSandboxMode = false,
136
133
  }: AddPaneNewPanelProps) => {
137
134
  const ctx = providedCtx || getCtx();
@@ -143,19 +140,21 @@ const AddPaneNewPanel = ({
143
140
  const [layoutChoice, setLayoutChoice] = useState<LayoutChoice>('standard');
144
141
  const [error, setError] = useState<string | null>(null);
145
142
 
143
+ // Standard / Single Column State
146
144
  const [copyMode, setCopyMode] = useState<CopyMode>('prompt');
147
145
  const [promptValue, setPromptValue] = useState('');
148
146
  const [copyValue, setCopyValue] = useState('');
149
147
 
148
+ // Grid / 2-Column State (Strictly Prompt-Only)
150
149
  const [overallPrompt, setOverallPrompt] = useState(
151
150
  prompts.aiPaneCopyPrompt_2cols.presets.heroDefault.default
152
151
  );
153
- const [copyModeCol1, setCopyModeCol1] = useState<CopyMode>('prompt');
154
- const [promptValueCol1, setPromptValueCol1] = useState('');
155
- const [copyValueCol1, setCopyValueCol1] = useState('');
156
- const [copyModeCol2, setCopyModeCol2] = useState<CopyMode>('prompt');
157
- const [promptValueCol2, setPromptValueCol2] = useState('');
158
- 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
+ );
159
158
 
160
159
  const [selectedLibraryEntry, setSelectedLibraryEntry] =
161
160
  useState<DesignLibraryEntry | null>(null);
@@ -228,11 +227,6 @@ const AddPaneNewPanel = ({
228
227
  handleApplyTemplate(blankTemplate);
229
228
  };
230
229
 
231
- const handleDesignLibrarySelect = (entry: DesignLibraryEntry) => {
232
- setSelectedLibraryEntry(entry);
233
- setStep('copyInput');
234
- };
235
-
236
230
  const handleAiDesignContinue = () => {
237
231
  setStep('copyInput');
238
232
  };
@@ -276,6 +270,18 @@ const AddPaneNewPanel = ({
276
270
  }
277
271
  };
278
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
+
279
285
  const handleFinalGenerate = useCallback(async () => {
280
286
  setError(null);
281
287
  setStep('loading');
@@ -286,59 +292,99 @@ const AddPaneNewPanel = ({
286
292
  throw new Error('No design library item was selected.');
287
293
  }
288
294
 
289
- if (copyMode === 'raw') {
290
- const liveTemplate = convertStorageToLiveTemplate(
291
- mergeCopyIntoTemplate(selectedLibraryEntry.template, [])
292
- );
293
- if (liveTemplate.markdown) {
294
- liveTemplate.markdown.markdownBody = copyValue;
295
- }
296
- handleApplyTemplate(liveTemplate);
297
- return;
298
- }
295
+ const liveTemplate = convertStorageToLiveTemplate(
296
+ selectedLibraryEntry.template
297
+ );
298
+ const shellResult = convertTemplateToAIShell(liveTemplate);
299
+ const layout = 'Text Only';
299
300
 
300
- if (copyMode === 'prompt') {
301
- const liveTemplate = convertStorageToLiveTemplate(
302
- selectedLibraryEntry.template
303
- );
304
- if (!liveTemplate.markdown) {
305
- throw new Error(
306
- '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
307
339
  );
340
+ copyResults.push(copyResult);
308
341
  }
309
342
 
310
- const shellJson = convertTemplateToAIShell(liveTemplate);
311
- if (!shellJson || shellJson === '{}') {
312
- throw new Error(
313
- 'Could not generate a valid AI shell from this design.'
314
- );
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;
315
350
  }
316
351
 
317
- const copyPromptDetails = prompts.aiPaneCopyPrompt;
318
- const layout = 'Text Only';
319
- const formattedCopyPrompt = copyPromptDetails.user_template
320
- .replace('{{COPY_INPUT}}', promptValue)
321
- .replace(
322
- '{{DESIGN_INPUT}}',
323
- "N/A - Use the provided Shell JSON's design."
324
- )
325
- .replace('{{LAYOUT_TYPE}}', layout)
326
- .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
+ }
327
358
 
328
- const copyResult = await callAskLemurAPI(
329
- formattedCopyPrompt,
330
- copyPromptDetails.system || '',
331
- false,
332
- isSandboxMode
333
- );
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);
334
368
 
335
- const newNodes = parseAiCopyHtml(
336
- copyResult,
337
- 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.'
338
387
  );
339
- const finalPane = cloneDeep(liveTemplate);
340
- finalPane.markdown!.nodes = newNodes;
341
- handleApplyTemplate(finalPane);
342
388
  }
343
389
  } else if (initialChoice === 'ai') {
344
390
  let designInput = `Generate a design using a **${aiDesignConfig.harmony.toLowerCase()}** color scheme with a **${aiDesignConfig.theme.toLowerCase()}** theme.`;
@@ -398,32 +444,22 @@ const AddPaneNewPanel = ({
398
444
  const preset = copyPromptDetails.presets.heroDefault;
399
445
  const copyResults: string[] = [];
400
446
 
447
+ // Grid is strictly prompt-based
401
448
  const promptsToRun: {
402
449
  prompt: string;
403
- copy: string;
404
- mode: CopyMode;
405
450
  presetKey: ColumnPresetKey;
406
451
  }[] = [
407
452
  {
408
453
  prompt: promptValueCol1,
409
- copy: copyValueCol1,
410
- mode: copyModeCol1,
411
454
  presetKey: 'left',
412
455
  },
413
456
  {
414
457
  prompt: promptValueCol2,
415
- copy: copyValueCol2,
416
- mode: copyModeCol2,
417
458
  presetKey: 'right',
418
459
  },
419
460
  ];
420
461
 
421
462
  for (const item of promptsToRun) {
422
- if (item.mode === 'raw') {
423
- copyResults.push(item.copy);
424
- continue;
425
- }
426
-
427
463
  const columnPreset = preset[item.presetKey];
428
464
  const formattedCopyPrompt = copyPromptDetails.user_template
429
465
  .replace('{{SHELL_JSON}}', shellResult)
@@ -456,12 +492,8 @@ const AddPaneNewPanel = ({
456
492
  promptValue,
457
493
  copyValue,
458
494
  overallPrompt,
459
- copyModeCol1,
460
495
  promptValueCol1,
461
- copyValueCol1,
462
- copyModeCol2,
463
496
  promptValueCol2,
464
- copyValueCol2,
465
497
  isSandboxMode,
466
498
  initialChoice,
467
499
  layoutChoice,
@@ -552,58 +584,47 @@ const AddPaneNewPanel = ({
552
584
  const renderContentStep = () => {
553
585
  if (layoutChoice === 'grid') {
554
586
  const isGenerateDisabled =
555
- (copyModeCol1 === 'prompt' && !promptValueCol1.trim()) ||
556
- (copyModeCol1 === 'raw' && !copyValueCol1.trim()) ||
557
- (copyModeCol2 === 'prompt' && !promptValueCol2.trim()) ||
558
- (copyModeCol2 === 'raw' && !copyValueCol2.trim()) ||
559
- !overallPrompt.trim();
587
+ !overallPrompt.trim() ||
588
+ !promptValueCol1.trim() ||
589
+ !promptValueCol2.trim();
560
590
 
561
591
  return (
562
592
  <div className="space-y-4 p-4">
563
593
  <div>
564
- <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">
565
595
  Overall Component Brief
566
- </h4>
596
+ </label>
567
597
  <textarea
568
598
  value={overallPrompt}
569
599
  onChange={(e) => setOverallPrompt(e.target.value)}
570
- placeholder="e.g., A compelling hero section for a website about Tract Stack..."
571
600
  rows={3}
572
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"
573
602
  />
603
+ <p className="mt-1 text-xs text-gray-500">
604
+ This context is applied to both columns.
605
+ </p>
574
606
  </div>
575
607
  <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
576
608
  <div>
577
- <h4 className="mb-2 block text-sm font-bold text-gray-700">
578
- Left Column Content
579
- </h4>
580
- <CopyInputStep
581
- copyMode={copyModeCol1}
582
- onCopyModeChange={setCopyModeCol1}
583
- promptValue={promptValueCol1}
584
- onPromptValueChange={setPromptValueCol1}
585
- copyValue={copyValueCol1}
586
- onCopyValueChange={setCopyValueCol1}
587
- defaultPrompt={
588
- prompts.aiPaneCopyPrompt_2cols.presets.heroDefault.left.prompt
589
- }
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"
590
617
  />
591
618
  </div>
592
619
  <div>
593
- <h4 className="mb-2 block text-sm font-bold text-gray-700">
594
- Right Column Content
595
- </h4>
596
- <CopyInputStep
597
- copyMode={copyModeCol2}
598
- onCopyModeChange={setCopyModeCol2}
599
- promptValue={promptValueCol2}
600
- onPromptValueChange={setPromptValueCol2}
601
- copyValue={copyValueCol2}
602
- onCopyValueChange={setCopyValueCol2}
603
- defaultPrompt={
604
- prompts.aiPaneCopyPrompt_2cols.presets.heroDefault.right
605
- .prompt
606
- }
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"
607
628
  />
608
629
  </div>
609
630
  </div>
@@ -672,17 +693,13 @@ const AddPaneNewPanel = ({
672
693
  ← Back to Choice
673
694
  </button>
674
695
  </div>
675
- <DesignLibraryStep
676
- config={config!}
677
- onSelect={handleDesignLibrarySelect}
678
- />
696
+ <DesignLibraryStep onSelect={handleDesignLibrarySelect} />
679
697
  </div>
680
698
  );
681
699
 
682
700
  const renderAiDesignStep = () => (
683
701
  <div className="space-y-4 p-4">
684
702
  <AiDesignStep
685
- config={config!}
686
703
  designConfig={aiDesignConfig}
687
704
  onDesignConfigChange={setAiDesignConfig}
688
705
  />
@@ -765,25 +782,15 @@ const AddPaneNewPanel = ({
765
782
  return (
766
783
  <div className="bg-white p-2 shadow-inner">
767
784
  <div className="group mb-2 flex w-full items-center gap-1 rounded-md bg-white p-1.5">
768
- {first ? (
769
- <div className="w-full text-center">
770
- <h2 className="font-action py-1.5 text-lg font-bold text-gray-800">
771
- Welcome to Tract Stack
772
- </h2>
773
- </div>
774
- ) : (
775
- <>
776
- <button
777
- onClick={() => setParentMode(PaneAddMode.DEFAULT, first)}
778
- className="w-fit rounded bg-gray-100 px-3 py-1 text-sm text-gray-700 transition-colors hover:bg-gray-200"
779
- >
780
- ← Go Back
781
- </button>
782
- <div className="font-action ml-4 flex-none rounded px-2 py-2.5 text-sm font-bold text-cyan-700 shadow-sm">
783
- + Design New Pane
784
- </div>
785
- </>
786
- )}
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>
787
794
  </div>
788
795
  <div className="min-h-96 rounded-md border bg-gray-50">
789
796
  {renderStep()}
@@ -0,0 +1,111 @@
1
+ import { useState } from 'react';
2
+ import { getCtx, type NodesContext } from '@/stores/nodes';
3
+ import { PaneAddMode, type StoragePane } from '@/types/compositorTypes';
4
+ import {
5
+ remapPaneIds,
6
+ convertStorageToLiveTemplate,
7
+ } from '@/utils/compositor/designLibraryHelper';
8
+
9
+ interface AddPanePanelPasteProps {
10
+ nodeId: string;
11
+ first: boolean;
12
+ setMode: (mode: PaneAddMode, reset?: boolean) => void;
13
+ ctx?: NodesContext;
14
+ isStoryFragment?: boolean;
15
+ isContextPane?: boolean;
16
+ }
17
+
18
+ const AddPanePanel_paste = ({
19
+ nodeId,
20
+ first,
21
+ setMode,
22
+ ctx: providedCtx,
23
+ isStoryFragment,
24
+ isContextPane,
25
+ }: AddPanePanelPasteProps) => {
26
+ const [jsonInput, setJsonInput] = useState('');
27
+ const [error, setError] = useState<string | null>(null);
28
+ const ctx = providedCtx || getCtx();
29
+
30
+ const handleCreate = () => {
31
+ setError(null);
32
+ if (!jsonInput.trim()) {
33
+ setError('Paste content cannot be empty.');
34
+ return;
35
+ }
36
+
37
+ try {
38
+ const parsedPane = JSON.parse(jsonInput) as StoragePane;
39
+ if (parsedPane.nodeType !== 'Pane') {
40
+ throw new Error('Pasted content is not a valid Pane object.');
41
+ }
42
+
43
+ const remappedPane = remapPaneIds(parsedPane);
44
+ const liveTemplate = convertStorageToLiveTemplate(remappedPane);
45
+
46
+ const ownerId =
47
+ isStoryFragment || isContextPane
48
+ ? nodeId
49
+ : ctx.getClosestNodeTypeFromId(nodeId, 'StoryFragment');
50
+
51
+ ctx.addTemplatePane(
52
+ ownerId,
53
+ liveTemplate,
54
+ nodeId,
55
+ first ? 'before' : 'after'
56
+ );
57
+ ctx.notifyNode('root');
58
+ setMode(PaneAddMode.DEFAULT, true);
59
+ } catch (err) {
60
+ const message =
61
+ err instanceof Error
62
+ ? err.message
63
+ : 'An unknown error occurred during parsing.';
64
+ setError(`Invalid Pane JSON: ${message}`);
65
+ }
66
+ };
67
+
68
+ return (
69
+ <div className="p-2 shadow-inner">
70
+ <div className="rounded-md border bg-gray-50 p-4">
71
+ <div className="flex items-center justify-between pb-4">
72
+ <div className="flex items-center gap-2">
73
+ <button
74
+ onClick={() => setMode(PaneAddMode.DEFAULT)}
75
+ className="w-fit rounded bg-gray-200 px-3 py-1 text-sm text-gray-800 transition-colors hover:bg-gray-300"
76
+ >
77
+ ← Go Back
78
+ </button>
79
+ <h3 className="font-action text-sm font-bold text-cyan-700">
80
+ Paste Pane
81
+ </h3>
82
+ </div>
83
+ </div>
84
+ <div className="space-y-4">
85
+ <p className="text-sm text-gray-600">
86
+ Paste the JSON content of a copied pane into the text area below.
87
+ </p>
88
+ <textarea
89
+ value={jsonInput}
90
+ onChange={(e) => setJsonInput(e.target.value)}
91
+ placeholder="Paste pane JSON here..."
92
+ className="h-48 w-full rounded-md border border-gray-300 bg-white p-2 font-mono text-xs focus:border-cyan-500 focus:ring-cyan-500"
93
+ spellCheck={false}
94
+ />
95
+ {error && <p className="text-sm text-red-600">{error}</p>}
96
+ <div className="flex justify-end">
97
+ <button
98
+ onClick={handleCreate}
99
+ disabled={!jsonInput.trim()}
100
+ 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"
101
+ >
102
+ Create Pane from Paste
103
+ </button>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ );
109
+ };
110
+
111
+ export default AddPanePanel_paste;