astro-tractstack 2.2.1 → 2.2.3

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 (68) hide show
  1. package/package.json +1 -1
  2. package/templates/src/components/codehooks/BunnyVideoSetup.tsx +0 -1
  3. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +0 -1
  4. package/templates/src/components/codehooks/ListContentSetup.tsx +0 -1
  5. package/templates/src/components/codehooks/ProductCardSetup.tsx +0 -1
  6. package/templates/src/components/codehooks/ProductGridSetup.tsx +0 -1
  7. package/templates/src/components/compositor/Compositor.tsx +0 -1
  8. package/templates/src/components/compositor/Node.tsx +157 -133
  9. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag.tsx +2 -4
  10. package/templates/src/components/edit/Header.tsx +2 -6
  11. package/templates/src/components/edit/context/ContextPaneConfig_slug.tsx +1 -1
  12. package/templates/src/components/edit/context/ContextPaneConfig_title.tsx +0 -1
  13. package/templates/src/components/edit/pane/AddPanePanel_break.tsx +1 -0
  14. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +8 -12
  15. package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +9 -6
  16. package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +7 -69
  17. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +2 -2
  18. package/templates/src/components/edit/pane/PanePanel_impression.tsx +0 -4
  19. package/templates/src/components/edit/pane/PanePanel_path.tsx +0 -1
  20. package/templates/src/components/edit/pane/PanePanel_title.tsx +1 -2
  21. package/templates/src/components/edit/pane/RestylePaneModal.tsx +1 -4
  22. package/templates/src/components/edit/pane/steps/AiCreativeDesignStep.tsx +0 -3
  23. package/templates/src/components/edit/pane/steps/AiRefineDesignStep.tsx +2 -2
  24. package/templates/src/components/edit/pane/steps/AiStandardDesignStep.tsx +173 -80
  25. package/templates/src/components/edit/pane/steps/CreativeInjectStep.tsx +0 -5
  26. package/templates/src/components/edit/pane/steps/DesignLibraryStep.tsx +2 -1
  27. package/templates/src/components/edit/panels/StyleBreakPanel.tsx +1 -4
  28. package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +0 -1
  29. package/templates/src/components/edit/panels/StyleElementPanel.tsx +1 -1
  30. package/templates/src/components/edit/panels/StyleElementPanel_remove.tsx +1 -4
  31. package/templates/src/components/edit/panels/StyleElementPanel_update.tsx +3 -3
  32. package/templates/src/components/edit/panels/StyleImagePanel.tsx +3 -3
  33. package/templates/src/components/edit/panels/StyleImagePanel_remove.tsx +1 -4
  34. package/templates/src/components/edit/panels/StyleImagePanel_update.tsx +3 -4
  35. package/templates/src/components/edit/panels/StyleLiElementPanel_remove.tsx +1 -4
  36. package/templates/src/components/edit/panels/StyleLiElementPanel_update.tsx +3 -3
  37. package/templates/src/components/edit/panels/StyleLinkPanel.tsx +1 -1
  38. package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +1 -1
  39. package/templates/src/components/edit/panels/StyleLinkPanel_remove.tsx +1 -1
  40. package/templates/src/components/edit/panels/StyleLinkPanel_update.tsx +1 -1
  41. package/templates/src/components/edit/panels/StyleParentPanel.tsx +0 -7
  42. package/templates/src/components/edit/panels/StyleParentPanel_deleteLayer.tsx +0 -2
  43. package/templates/src/components/edit/panels/StyleParentPanel_remove.tsx +0 -2
  44. package/templates/src/components/edit/panels/StyleParentPanel_update.tsx +0 -2
  45. package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +0 -3
  46. package/templates/src/components/edit/panels/StyleWidgetPanel_remove.tsx +1 -4
  47. package/templates/src/components/edit/panels/StyleWidgetPanel_update.tsx +3 -4
  48. package/templates/src/components/edit/panels/StyleWordCarouselPanel.tsx +0 -2
  49. package/templates/src/components/edit/state/StylesMemory.tsx +3 -9
  50. package/templates/src/components/edit/storyfragment/StoryFragmentConfigPanel.tsx +0 -1
  51. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_menu.tsx +0 -2
  52. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +0 -2
  53. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +0 -1
  54. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_title.tsx +0 -1
  55. package/templates/src/components/fields/ArtpackImage.tsx +0 -7
  56. package/templates/src/components/fields/BackgroundImage.tsx +0 -14
  57. package/templates/src/components/fields/BackgroundImageWrapper.tsx +0 -5
  58. package/templates/src/components/fields/ImageUpload.tsx +0 -3
  59. package/templates/src/pages/[...slug]/edit.astro +0 -1
  60. package/templates/src/pages/sandbox.astro +0 -1
  61. package/templates/src/stores/nodes.ts +278 -312
  62. package/templates/src/stores/nodesHistory.ts +59 -24
  63. package/templates/src/utils/api/setupHelpers.ts +1 -1
  64. package/templates/src/utils/compositor/aiPaneParser.ts +57 -0
  65. package/templates/src/utils/compositor/designLibraryHelper.ts +1 -3
  66. package/templates/src/utils/compositor/htmlAst.ts +109 -2
  67. package/templates/src/utils/compositor/nodesHelper.ts +1 -9
  68. package/templates/src/utils/compositor/savePipeline.ts +1 -4
@@ -1,5 +1,8 @@
1
1
  import { atom, type WritableAtom } from 'nanostores';
2
- import type { NodesContext } from '@/stores/nodes.ts';
2
+ import type { NodesContext } from '@/stores/nodes';
3
+
4
+ export const VERBOSE = false;
5
+ const COALESCE_WINDOW = 16000;
3
6
 
4
7
  export enum PatchOp {
5
8
  ADD,
@@ -19,8 +22,7 @@ export class NodesHistory {
19
22
 
20
23
  protected _ctx: NodesContext;
21
24
  protected _maxBuffer: number;
22
- private _debounceTimer: ReturnType<typeof setTimeout> | null = null;
23
- private _pendingPatch: HistoryPatch | null = null;
25
+ public lastPatchTime: number = 0;
24
26
 
25
27
  constructor(ctx: NodesContext, maxBuffer: number) {
26
28
  this._ctx = ctx;
@@ -38,30 +40,62 @@ export class NodesHistory {
38
40
  return this.history.get().length > 0 && this.headIndex.get() > 0;
39
41
  }
40
42
 
41
- addPatch(patch: HistoryPatch) {
42
- this._pendingPatch = patch;
43
+ addPatch(patch: HistoryPatch, options?: { merge?: boolean }) {
44
+ const now = Date.now();
45
+ const timeDelta = now - this.lastPatchTime;
46
+ const shouldMerge =
47
+ options?.merge !== false &&
48
+ timeDelta < COALESCE_WINDOW &&
49
+ this.headIndex.get() === 0 && // Only merge if we are at the tip of history
50
+ this.history.get().length > 0;
43
51
 
44
- if (this._debounceTimer) {
45
- clearTimeout(this._debounceTimer);
46
- }
52
+ if (shouldMerge) {
53
+ if (VERBOSE) {
54
+ console.log(
55
+ `[History] Merging patch (Delta: ${timeDelta}ms < ${COALESCE_WINDOW}ms)`
56
+ );
57
+ }
47
58
 
48
- this._debounceTimer = setTimeout(() => {
49
- if (this._pendingPatch) {
50
- // Original addPatch logic
51
- while (this.headIndex.get() !== 0) {
52
- this.history.get().shift();
53
- this.headIndex.set(this.headIndex.get() - 1);
54
- }
55
-
56
- this.history.get().unshift(this._pendingPatch);
57
- if (this.history.get().length > this._maxBuffer) {
58
- this.history.get().pop();
59
- }
60
- this.history.set([...this.history.get()]);
61
-
62
- this._pendingPatch = null;
59
+ const currentHistory = [...this.history.get()];
60
+ const previousPatch = currentHistory[0];
61
+
62
+ // Create a composite patch that executes both actions
63
+ const mergedPatch: HistoryPatch = {
64
+ op: PatchOp.REPLACE, // Merged operations are generally treated as complex replacements
65
+ undo: (ctx) => {
66
+ // Reverse order: Undo the new action, then undo the old action
67
+ patch.undo(ctx);
68
+ previousPatch.undo(ctx);
69
+ },
70
+ redo: (ctx) => {
71
+ // Forward order: Redo the old action, then redo the new action
72
+ previousPatch.redo(ctx);
73
+ patch.redo(ctx);
74
+ },
75
+ };
76
+
77
+ currentHistory[0] = mergedPatch;
78
+ this.history.set(currentHistory);
79
+ // We do NOT update lastPatchTime on merge. This keeps the window anchored to the start of the "thought".
80
+ } else {
81
+ if (VERBOSE) {
82
+ console.log(`[History] Pushing new patch (Delta: ${timeDelta}ms)`);
63
83
  }
64
- }, 300);
84
+
85
+ // If we are not at the tip (we undid something), verify we clear the future
86
+ while (this.headIndex.get() !== 0) {
87
+ this.history.get().shift();
88
+ this.headIndex.set(this.headIndex.get() - 1);
89
+ }
90
+
91
+ const newHistory = [patch, ...this.history.get()];
92
+ if (newHistory.length > this._maxBuffer) {
93
+ newHistory.pop();
94
+ }
95
+
96
+ this.history.set(newHistory);
97
+ this.lastPatchTime = now;
98
+ }
65
99
  }
66
100
 
67
101
  undo() {
@@ -81,5 +115,6 @@ export class NodesHistory {
81
115
  clearHistory() {
82
116
  this.history.set([]);
83
117
  this.headIndex.set(0);
118
+ this.lastPatchTime = 0;
84
119
  }
85
120
  }
@@ -164,7 +164,7 @@ function forceMarkAllDirty(ctx: any) {
164
164
  if (VERBOSE)
165
165
  console.log('[forceMarkAllDirty] Flagging all nodes for SaveModal...');
166
166
  const allNodes = Array.from(ctx.allNodes.get().values());
167
- const dirtyUpdates = allNodes.map((n: any) => ({ ...n, isChanged: true }));
167
+ const dirtyUpdates = allNodes.map((n: any) => ({ ...n }));
168
168
  ctx.modifyNodes(dirtyUpdates);
169
169
  }
170
170
 
@@ -747,3 +747,60 @@ export const parseAiPane = (
747
747
  // Fallback for invalid input
748
748
  throw new Error('Invalid input for parseAiPane');
749
749
  };
750
+
751
+ /**
752
+ * Generates a default design shell when AI styling is disabled.
753
+ * Provides a clean, white-label structure compatible with the parser.
754
+ */
755
+ export function createDefaultShell(layout: 'standard' | 'grid'): ShellJson {
756
+ const baseDefaults: LLMDefaultClasses = {
757
+ h2: {
758
+ mobile: 'text-3xl font-bold tracking-tight text-gray-900',
759
+ tablet: 'text-4xl',
760
+ },
761
+ h3: {
762
+ mobile: 'text-2xl font-bold tracking-tight text-gray-900',
763
+ },
764
+ p: {
765
+ mobile: 'mt-6 text-lg leading-8 text-gray-600',
766
+ },
767
+ ul: {
768
+ mobile: 'mt-6 list-disc list-outside ml-6 text-gray-600',
769
+ },
770
+ li: {
771
+ mobile: 'mb-2',
772
+ },
773
+ a: {
774
+ mobile: 'text-indigo-600 hover:text-indigo-500 font-semibold',
775
+ },
776
+ button: {
777
+ mobile:
778
+ 'rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600',
779
+ },
780
+ };
781
+
782
+ if (layout === 'grid') {
783
+ return {
784
+ bgColour: '#ffffff',
785
+ parentClasses: [
786
+ { mobile: 'mx-auto max-w-7xl' },
787
+ { mobile: 'px-6 py-24', tablet: 'px-8 py-32' },
788
+ ],
789
+ defaultClasses: baseDefaults,
790
+ columns: [
791
+ { gridClasses: { mobile: 'flex flex-col gap-y-4' } }, // Left Column
792
+ { gridClasses: { mobile: 'flex flex-col gap-y-4' } }, // Right Column
793
+ ],
794
+ };
795
+ }
796
+
797
+ // Standard Layout
798
+ return {
799
+ bgColour: '#ffffff',
800
+ parentClasses: [
801
+ { mobile: 'mx-auto max-w-3xl text-base leading-7' },
802
+ { mobile: 'px-6 py-24', tablet: 'px-8 py-32' },
803
+ ],
804
+ defaultClasses: baseDefaults,
805
+ };
806
+ }
@@ -500,8 +500,7 @@ function convertLivePaneToStoragePane(
500
500
  : [],
501
501
  };
502
502
  } else if (gridLayoutNode) {
503
- const { id, parentId, isChanged, parentCss, gridCss, ...restOfGrid } =
504
- gridLayoutNode;
503
+ const { id, parentId, parentCss, gridCss, ...restOfGrid } = gridLayoutNode;
505
504
  storageGridLayout = {
506
505
  ...restOfGrid,
507
506
  nodes: ctx
@@ -515,7 +514,6 @@ function convertLivePaneToStoragePane(
515
514
  const {
516
515
  id,
517
516
  parentId,
518
- isChanged,
519
517
  markdownId,
520
518
  parentCss,
521
519
  gridCss,
@@ -562,10 +562,13 @@ export async function regenerateCreativePane(
562
562
  } else if (updates.image === null) {
563
563
  delete targetNode.attrs['data-image'];
564
564
  }
565
-
566
565
  if (updates.src && updates.tagName === 'img') {
567
566
  targetNode.attrs.src = updates.src;
568
- if (updates.srcSet) targetNode.attrs.srcset = updates.srcSet;
567
+ if (updates.srcSet) {
568
+ targetNode.attrs.srcset = updates.srcSet;
569
+ } else {
570
+ delete targetNode.attrs.srcset;
571
+ }
569
572
  }
570
573
  if (updates.alt) {
571
574
  targetNode.attrs.alt = updates.alt;
@@ -702,3 +705,107 @@ export function extractFileIdsFromAst(payload: CreativePanePayload): string[] {
702
705
  }
703
706
  return results;
704
707
  }
708
+
709
+ /**
710
+ * Simple Markdown to HTML converter.
711
+ * Handles:
712
+ * - Headers (## through ######)
713
+ * - Unordered lists (- item)
714
+ * - Bold (**text**)
715
+ * - Italic (*text*)
716
+ * - Links ([text](url))
717
+ * - Paragraphs (everything else)
718
+ */
719
+ export function markdownToHtml(markdown: string): string {
720
+ const lines = markdown.split('\n');
721
+ let html = '';
722
+ let inList = false;
723
+
724
+ const parseInline = (text: string): string => {
725
+ return (
726
+ text
727
+ // Bold: **text**
728
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
729
+ // Italic: *text*
730
+ .replace(/\*(.*?)\*/g, '<em>$1</em>')
731
+ // Link: [text](url)
732
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
733
+ );
734
+ };
735
+
736
+ for (const line of lines) {
737
+ const trimmed = line.trim();
738
+
739
+ // Skip empty lines, but close list if open
740
+ if (!trimmed) {
741
+ if (inList) {
742
+ html += '</ul>';
743
+ inList = false;
744
+ }
745
+ continue;
746
+ }
747
+
748
+ // Headers (## to ######) - Note: Ignoring # (h1) as per standard rules
749
+ const headerMatch = trimmed.match(/^(#{2,6})\s+(.*)$/);
750
+ if (headerMatch) {
751
+ if (inList) {
752
+ html += '</ul>';
753
+ inList = false;
754
+ }
755
+ const level = headerMatch[1].length;
756
+ const content = parseInline(headerMatch[2]);
757
+ html += `<h${level}>${content}</h${level}>`;
758
+ continue;
759
+ }
760
+
761
+ // Unordered Lists (- item)
762
+ const listMatch = trimmed.match(/^-\s+(.*)$/);
763
+ if (listMatch) {
764
+ if (!inList) {
765
+ html += '<ul>';
766
+ inList = true;
767
+ }
768
+ const content = parseInline(listMatch[1]);
769
+ html += `<li>${content}</li>`;
770
+ continue;
771
+ }
772
+
773
+ // Paragraphs (default block)
774
+ if (inList) {
775
+ html += '</ul>';
776
+ inList = false;
777
+ }
778
+ html += `<p>${parseInline(trimmed)}</p>`;
779
+ }
780
+
781
+ // Close any remaining open list
782
+ if (inList) {
783
+ html += '</ul>';
784
+ }
785
+
786
+ return html;
787
+ }
788
+
789
+ /**
790
+ * Sanitizes HTML for AI processing or other non-editor contexts.
791
+ * Strips editor-specific guards, metadata, and heavy base64 data.
792
+ */
793
+ export function cleanHtml(html: string): string {
794
+ if (!html) return '';
795
+
796
+ return (
797
+ html
798
+ // 1. Revert Base64 images to static placeholder to save tokens and avoid confusion
799
+ .replace(/src="data:[^"]*"/g, 'src="/static.jpg"')
800
+ // 2. Remove Content Editable attribute
801
+ .replace(/contenteditable="true"/g, '')
802
+ .replace(/contenteditable="false"/g, '')
803
+ // 3. Remove Editor Interaction Guards
804
+ .replace(/onclick="return false;"/g, '')
805
+ .replace(/style="pointer-events: none;"/g, '')
806
+ // 4. Remove Internal Upload Metadata
807
+ .replace(/data-file-id="[^"]*"/g, '')
808
+ // 5. Clean up resulting double spaces
809
+ .replace(/\s{2,}/g, ' ')
810
+ );
811
+ }
@@ -304,7 +304,6 @@ export function createEmptyStorykeep(id: string) {
304
304
  nodeType: 'StoryFragment',
305
305
  tractStackId: 'temp',
306
306
  parentId: null,
307
- isChanged: false,
308
307
  paneIds: [],
309
308
  changed: undefined,
310
309
  slug: 'temp',
@@ -549,7 +548,6 @@ export function revertFromGrid(gridLayoutId: string) {
549
548
  markdownNodeToKeep.parentId = paneNode.id;
550
549
  markdownNodeToKeep.parentClasses = gridLayoutNode.parentClasses || [];
551
550
  markdownNodeToKeep.defaultClasses = gridLayoutNode.defaultClasses || {};
552
- markdownNodeToKeep.isChanged = true;
553
551
  newAllNodes.set(markdownNodeToKeepId, markdownNodeToKeep);
554
552
 
555
553
  const paneChildren = [...(newParentNodes.get(paneNode.id) || [])];
@@ -567,7 +565,6 @@ export function revertFromGrid(gridLayoutId: string) {
567
565
  }
568
566
 
569
567
  const updatedPaneNode = cloneDeep(paneNode);
570
- updatedPaneNode.isChanged = true;
571
568
  newAllNodes.set(paneNode.id, updatedPaneNode);
572
569
 
573
570
  ctx.allNodes.set(newAllNodes);
@@ -622,7 +619,6 @@ export function convertToGrid(markdownNodeId: string) {
622
619
  parentClasses: markdownNode.parentClasses || [],
623
620
  defaultClasses: markdownNode.defaultClasses || {},
624
621
  gridColumns: { mobile: 1, tablet: 2, desktop: 2 },
625
- isChanged: true,
626
622
  };
627
623
 
628
624
  const updatedMarkdownNode = cloneDeep(markdownNode);
@@ -630,7 +626,6 @@ export function convertToGrid(markdownNodeId: string) {
630
626
  updatedMarkdownNode.parentClasses = [];
631
627
  updatedMarkdownNode.parentCss = [];
632
628
  updatedMarkdownNode.defaultClasses = {};
633
- updatedMarkdownNode.isChanged = true;
634
629
 
635
630
  // Create a new, truly empty MarkdownNode for the second column.
636
631
  const newColumnNodeId = ulid();
@@ -642,12 +637,11 @@ export function convertToGrid(markdownNodeId: string) {
642
637
  markdownId: ulid(),
643
638
  defaultClasses: {},
644
639
  parentClasses: [],
645
- isChanged: true,
646
640
  };
647
641
 
648
642
  newAllNodes.set(gridLayoutId, newGridLayoutNode);
649
643
  newAllNodes.set(markdownNodeId, updatedMarkdownNode);
650
- newAllNodes.set(paneNode.id, { ...cloneDeep(paneNode), isChanged: true });
644
+ newAllNodes.set(paneNode.id, { ...cloneDeep(paneNode) });
651
645
  newAllNodes.set(newColumnNodeId, newColumnNode);
652
646
 
653
647
  const paneChildren = [...(newParentNodes.get(paneNode.id) || [])];
@@ -703,7 +697,6 @@ export function addColumn(gridLayoutId: string) {
703
697
  defaultClasses: {},
704
698
  parentClasses: [],
705
699
  gridClasses: { mobile: {}, tablet: {}, desktop: {} },
706
- isChanged: true,
707
700
  };
708
701
 
709
702
  newAllNodes.set(newMarkdownNodeId, newColumnNode);
@@ -714,7 +707,6 @@ export function addColumn(gridLayoutId: string) {
714
707
  newParentNodes.set(newMarkdownNodeId, []); // Set children to an empty array
715
708
 
716
709
  const updatedGridLayoutNode = cloneDeep(gridLayoutNode);
717
- updatedGridLayoutNode.isChanged = true;
718
710
  newAllNodes.set(gridLayoutId, updatedGridLayoutNode);
719
711
 
720
712
  ctx.allNodes.set(newAllNodes);
@@ -211,7 +211,6 @@ export async function executeSavePipeline(
211
211
  updatedNode.fileId = result.fileId;
212
212
  updatedNode.src = result.src;
213
213
  if (result.srcSet) updatedNode.srcSet = result.srcSet;
214
- updatedNode.isChanged = true;
215
214
  ctx.modifyNodes([updatedNode]);
216
215
 
217
216
  const localRef = fileNode as PendingFileNode;
@@ -286,9 +285,7 @@ export async function executeSavePipeline(
286
285
  }
287
286
  );
288
287
 
289
- ctx.modifyNodes([
290
- { ...pane, htmlAst: cleanAst, isChanged: true } as PaneNode,
291
- ]);
288
+ ctx.modifyNodes([{ ...pane, htmlAst: cleanAst } as PaneNode]);
292
289
  logDebug(`Creative Pane ${pane.id} assets processed and node updated.`);
293
290
  } catch (err) {
294
291
  console.error('Creative Pane Asset Upload Error:', err);