astro-tractstack 2.1.3 → 2.2.0

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 (128) hide show
  1. package/README.md +54 -266
  2. package/bin/create-tractstack.js +9 -6
  3. package/dist/index.js +109 -71
  4. package/package.json +4 -2
  5. package/templates/css/custom.css +5 -0
  6. package/templates/icons/code.svg +18 -0
  7. package/templates/icons/li.svg +4 -0
  8. package/templates/icons/link.svg +22 -0
  9. package/templates/icons/p.svg +3 -0
  10. package/templates/src/client/app.js +80 -1
  11. package/templates/src/components/Footer.astro +1 -1
  12. package/templates/src/components/codehooks/BunnyVideoSetup.tsx +6 -6
  13. package/templates/src/components/codehooks/EpinetDurationSelector.tsx +3 -3
  14. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +1 -1
  15. package/templates/src/components/codehooks/ListContentSetup.tsx +2 -2
  16. package/templates/src/components/codehooks/ProductCardSetup.tsx +1 -1
  17. package/templates/src/components/codehooks/ProductGridSetup.tsx +2 -2
  18. package/templates/src/components/codehooks/SandboxRegisterForm.tsx +3 -3
  19. package/templates/src/components/compositor/Compositor.tsx +25 -9
  20. package/templates/src/components/compositor/Node.tsx +168 -496
  21. package/templates/src/components/compositor/PanelVisibilityWrapper.tsx +1 -0
  22. package/templates/src/components/compositor/elements/SignUp.tsx +1 -1
  23. package/templates/src/components/compositor/elements/YouTubeWrapper.tsx +2 -0
  24. package/templates/src/components/compositor/nodes/CreativePane.tsx +262 -0
  25. package/templates/src/components/compositor/nodes/GhostInsertBlock.tsx +4 -6
  26. package/templates/src/components/compositor/nodes/GridLayout.tsx +4 -2
  27. package/templates/src/components/compositor/nodes/Markdown.tsx +18 -3
  28. package/templates/src/components/compositor/nodes/Pane.tsx +11 -5
  29. package/templates/src/components/compositor/nodes/RenderChildren.tsx +1 -1
  30. package/templates/src/components/compositor/nodes/tagElements/NodeAnchorComponent.tsx +5 -5
  31. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag.tsx +90 -42
  32. package/templates/src/components/compositor/nodes/tagElements/NodeImg.tsx +2 -0
  33. package/templates/src/components/compositor/nodes/tagElements/NodeText.tsx +27 -1
  34. package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +10 -8
  35. package/templates/src/components/compositor/tools/NodeOverlay.tsx +224 -0
  36. package/templates/src/components/compositor/tools/PaneOverlay.tsx +122 -0
  37. package/templates/src/components/edit/Header.tsx +68 -9
  38. package/templates/src/components/edit/PanelSwitch.tsx +42 -4
  39. package/templates/src/components/edit/SettingsPanel.tsx +2 -3
  40. package/templates/src/components/edit/ToolMode.tsx +1 -31
  41. package/templates/src/components/edit/pane/AddPanePanel_break.tsx +2 -2
  42. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +1 -1
  43. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +193 -659
  44. package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +15 -82
  45. package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +95 -45
  46. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +137 -49
  47. package/templates/src/components/edit/pane/RestylePaneModal.tsx +1 -1
  48. package/templates/src/components/edit/pane/steps/AiCreativeDesignStep.tsx +375 -0
  49. package/templates/src/components/edit/pane/steps/AiDesignStep.tsx +1 -23
  50. package/templates/src/components/edit/pane/steps/AiLibraryCopyStep.tsx +327 -0
  51. package/templates/src/components/edit/pane/steps/AiRefineDesignStep.tsx +267 -0
  52. package/templates/src/components/edit/pane/steps/AiStandardDesignStep.tsx +371 -0
  53. package/templates/src/components/edit/pane/steps/CopyInputStep.tsx +201 -76
  54. package/templates/src/components/edit/pane/steps/CreativeInjectStep.tsx +141 -0
  55. package/templates/src/components/edit/panels/CreativeImagePanel.tsx +435 -0
  56. package/templates/src/components/edit/panels/CreativeLinkPanel.tsx +110 -0
  57. package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +1 -1
  58. package/templates/src/components/edit/panels/StyleParentPanel.tsx +118 -126
  59. package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +3 -2
  60. package/templates/src/components/edit/panels/StyleParentPanel_deleteLayer.tsx +1 -0
  61. package/templates/src/components/edit/panels/StyleParentPanel_remove.tsx +3 -1
  62. package/templates/src/components/edit/panels/StyleParentPanel_update.tsx +3 -1
  63. package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +1 -1
  64. package/templates/src/components/edit/state/SaveModal.tsx +19 -787
  65. package/templates/src/components/edit/state/SaveToLibraryModal.tsx +2 -2
  66. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_menu.tsx +1 -1
  67. package/templates/src/components/edit/widgets/BunnyWidget.tsx +5 -5
  68. package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +1 -1
  69. package/templates/src/components/edit/widgets/SignupWidget.tsx +1 -1
  70. package/templates/src/components/fields/ActionBuilderTimeSelector.tsx +1 -1
  71. package/templates/src/components/fields/ArtpackImage.tsx +11 -3
  72. package/templates/src/components/fields/BackgroundImage.tsx +8 -0
  73. package/templates/src/components/fields/BackgroundImageWrapper.tsx +15 -9
  74. package/templates/src/components/fields/ImageUpload.tsx +6 -0
  75. package/templates/src/components/form/ActionBuilderField.tsx +15 -5
  76. package/templates/src/components/form/ActionBuilderSlugSelector.tsx +1 -1
  77. package/templates/src/components/form/ColorPicker.tsx +1 -1
  78. package/templates/src/components/form/EnumSelect.tsx +1 -1
  79. package/templates/src/components/form/NumberInput.tsx +1 -1
  80. package/templates/src/components/form/StringArrayInput.tsx +1 -1
  81. package/templates/src/components/form/StringInput.tsx +1 -1
  82. package/templates/src/components/form/UnsavedChangesBar.tsx +1 -1
  83. package/templates/src/components/form/advanced/APIConfigSection.tsx +2 -2
  84. package/templates/src/components/form/advanced/AuthConfigSection.tsx +2 -2
  85. package/templates/src/components/profile/ProfileCreate.tsx +1 -1
  86. package/templates/src/components/profile/ProfileEdit.tsx +1 -1
  87. package/templates/src/components/storykeep/Dashboard_Advanced.tsx +2 -2
  88. package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +1 -1
  89. package/templates/src/components/storykeep/controls/content/ContentSummary.tsx +2 -2
  90. package/templates/src/components/storykeep/controls/content/KnownResourceTable.tsx +1 -1
  91. package/templates/src/components/storykeep/controls/content/ManageContent.tsx +6 -6
  92. package/templates/src/components/storykeep/controls/content/MenuForm.tsx +1 -1
  93. package/templates/src/components/storykeep/controls/content/PaneTable.tsx +358 -0
  94. package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +1 -1
  95. package/templates/src/constants/prompts.json +18 -10
  96. package/templates/src/constants.ts +3 -0
  97. package/templates/src/hooks/usePaneFragments.ts +60 -0
  98. package/templates/src/lib/session.ts +71 -16
  99. package/templates/src/pages/[...slug].astro +4 -46
  100. package/templates/src/pages/api/css.ts +149 -0
  101. package/templates/src/pages/maint.astro +1 -1
  102. package/templates/src/pages/storykeep/login.astro +2 -2
  103. package/templates/src/stores/nodes.ts +162 -49
  104. package/templates/src/stores/orphanAnalysis.ts +6 -30
  105. package/templates/src/stores/previews.ts +7 -0
  106. package/templates/src/stores/storykeep.ts +0 -8
  107. package/templates/src/types/compositorTypes.ts +53 -10
  108. package/templates/src/utils/compositor/aiGeneration.ts +93 -0
  109. package/templates/src/utils/compositor/allowInsert.ts +2 -0
  110. package/templates/src/utils/compositor/htmlAst.ts +704 -0
  111. package/templates/src/utils/compositor/nodesHelper.ts +281 -102
  112. package/templates/src/utils/compositor/savePipeline.ts +893 -0
  113. package/templates/src/utils/etl/index.ts +3 -0
  114. package/templates/src/utils/etl/transformer.ts +10 -0
  115. package/templates/src/utils/helpers.ts +101 -0
  116. package/utils/inject-files.ts +100 -62
  117. package/templates/icons/text.svg +0 -6
  118. package/templates/src/components/compositor/NodeWithGuid.tsx +0 -69
  119. package/templates/src/components/compositor/nodes/GridLayout_eraser.tsx +0 -33
  120. package/templates/src/components/compositor/nodes/Markdown_eraser.tsx +0 -56
  121. package/templates/src/components/compositor/nodes/Pane_DesignLibrary.tsx +0 -269
  122. package/templates/src/components/compositor/nodes/Pane_eraser.tsx +0 -186
  123. package/templates/src/components/compositor/nodes/Pane_layout.tsx +0 -79
  124. package/templates/src/components/compositor/nodes/tagElements/NodeA_eraser.tsx +0 -26
  125. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_eraser.tsx +0 -61
  126. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_insert.tsx +0 -120
  127. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_settings.tsx +0 -62
  128. package/templates/src/components/compositor/nodes/tagElements/NodeButton_eraser.tsx +0 -26
@@ -15,12 +15,15 @@ import {
15
15
  } from './TemplateNodes';
16
16
  import { getCtx, NodesContext } from '@/stores/nodes';
17
17
  import { settingsPanelStore } from '@/stores/storykeep';
18
+ import { regexpHook } from '@/constants';
18
19
  import { PatchOp } from '@/stores/nodesHistory';
19
20
  import { cloneDeep } from '@/utils/helpers';
20
- import { isPaneNode } from './typeGuards';
21
+ import { isPaneNode, isGridLayoutNode } from './typeGuards';
22
+ import { processClassesForViewports } from '@/utils/compositor/reduceNodesClassNames';
21
23
  import type {
22
24
  BaseNode,
23
25
  FlatNode,
26
+ ViewportKey,
24
27
  StoryFragmentNode,
25
28
  TemplateNode,
26
29
  ToolAddMode,
@@ -28,7 +31,6 @@ import type {
28
31
  GridLayoutNode,
29
32
  PaneNode,
30
33
  } from '@/types/compositorTypes';
31
- import type { NodeTagProps } from '@/types/nodeProps';
32
34
 
33
35
  export const getTemplateNode = (value: ToolAddMode): TemplateNode => {
34
36
  let templateNode: TemplateNode;
@@ -74,19 +76,48 @@ export const getTemplateNode = (value: ToolAddMode): TemplateNode => {
74
76
  return templateNode;
75
77
  };
76
78
 
77
- const forbiddenEditTags = new Set<string>(['em', 'strong', 'ol', 'ul']);
79
+ export const canEditText = (
80
+ node: BaseNode | FlatNode,
81
+ ctx: NodesContext
82
+ ): boolean => {
83
+ if (!node || node.nodeType !== 'TagElement') return false;
84
+
85
+ const flatNode = node as FlatNode;
86
+ const tagName = flatNode.tagName;
87
+
88
+ // 1. Explicitly forbid structural containers
89
+ if (tagName === 'ul' || tagName === 'ol') {
90
+ return false;
91
+ }
78
92
 
79
- export const canEditText = (props: NodeTagProps): boolean => {
80
- const nodeId = props.nodeId;
93
+ // 2. Surgical LI check:
94
+ // Only allow text editing if it is a standard text list item.
95
+ if (tagName === 'li') {
96
+ const childIds = ctx.getChildNodeIDs(node.id);
81
97
 
82
- const self = getCtx(props).allNodes.get().get(nodeId) as FlatNode;
83
- if (self.tagName === 'a') return false;
98
+ // If it contains an image or code widget, it is NOT a text editor.
99
+ const hasComplexContent = childIds.some((id) => {
100
+ const child = ctx.allNodes.get().get(id) as FlatNode;
101
+ return child && ['img', 'code'].includes(child.tagName);
102
+ });
84
103
 
85
- const parentIsButton = getCtx(props).getParentNodeByTagNames(nodeId, ['a']);
86
- if (parentIsButton?.length > 0) return false;
104
+ return !hasComplexContent;
105
+ }
87
106
 
88
- if (forbiddenEditTags.has(props.tagName as string)) return false;
89
- return true;
107
+ // 3. Allowed text-editing tags
108
+ const editableTags = [
109
+ 'p',
110
+ 'h2',
111
+ 'h3',
112
+ 'h4',
113
+ 'h5',
114
+ 'h6',
115
+ 'a',
116
+ 'button',
117
+ 'span',
118
+ ];
119
+
120
+ return editableTags.includes(tagName);
90
121
  };
91
122
 
92
123
  export function parseMarkdownToNodes(
@@ -133,113 +164,58 @@ export function parseMarkdownToNodes(
133
164
 
134
165
  function extractNodesFromDOM(
135
166
  element: HTMLElement,
136
- parentId: string
167
+ parentId: string,
168
+ onInsertSignal?: (tagName: string, nodeId: string) => void
137
169
  ): FlatNode[] {
138
- const result: FlatNode[] = [];
170
+ const results: FlatNode[] = [];
139
171
 
140
- // Process each child node
141
172
  Array.from(element.childNodes).forEach((child) => {
142
173
  if (child.nodeType === Node.TEXT_NODE) {
143
- // Handle text nodes - preserve text content but strip zero-width spaces
144
- let text = child.textContent;
145
-
146
- // Only skip if null or undefined, but keep empty strings and whitespace
147
- if (text !== null && text !== undefined) {
148
- // Remove zero-width spaces
149
- text = text.replace(/\u200B/g, '');
150
- if (text.trim() === '') {
151
- return;
152
- }
153
- result.push({
174
+ const text = child.textContent || '';
175
+ const sanitizedText = text.replace(/\u200B/g, '');
176
+ if (sanitizedText) {
177
+ results.push({
154
178
  id: ulid(),
155
- parentId,
156
179
  nodeType: 'TagElement',
157
180
  tagName: 'text',
158
- copy: text,
181
+ parentId,
182
+ copy: sanitizedText,
159
183
  } as FlatNode);
160
184
  }
161
185
  } else if (child.nodeType === Node.ELEMENT_NODE) {
162
186
  const elem = child as HTMLElement;
163
- const tagName = elem.tagName.toLowerCase();
187
+ const tagName =
188
+ elem.getAttribute('data-tag') || elem.tagName.toLowerCase();
164
189
 
165
- // Skip any remaining space marker spans
166
- if (
167
- tagName === 'span' &&
168
- (elem.classList.contains('space-marker') ||
169
- elem.getAttribute('style')?.includes('font-size: 0px'))
170
- ) {
171
- return;
172
- }
190
+ const originalId = elem.getAttribute('data-node-id');
191
+ const nodeId = originalId || ulid();
173
192
 
174
- // Create node for this element
175
- const nodeId = ulid();
176
193
  const node: FlatNode = {
177
194
  id: nodeId,
178
- parentId,
179
195
  nodeType: 'TagElement',
180
196
  tagName,
181
- };
197
+ parentId,
198
+ } as FlatNode;
182
199
 
183
- // Handle special attributes for different tags
184
200
  if (tagName === 'a') {
185
- // Process anchor tags
186
- node.href =
187
- (elem as HTMLAnchorElement).getAttribute('href') || undefined;
188
-
189
- // Save classes for the link
190
- const className = elem.getAttribute('class');
191
- if (className) {
192
- node.elementCss = className;
193
- }
194
-
195
- // Update attribute name for editable links
196
- if (
197
- elem.hasAttribute('data-space-protected') ||
198
- elem.hasAttribute('data-editable-link')
199
- ) {
200
- (node as any)['data-editable-link'] = 'true';
201
- }
202
- } else if (tagName === 'button') {
203
- // Process button tags - preserve all attributes
204
- const className = elem.getAttribute('class');
205
- if (className) {
206
- node.elementCss = className;
207
- }
208
-
209
- // Update attribute name for editable buttons
210
- if (
211
- elem.hasAttribute('data-space-protected') ||
212
- elem.hasAttribute('data-editable-button')
213
- ) {
214
- (node as any)['data-editable-button'] = 'true';
215
- }
201
+ node.href = elem.getAttribute('href') || '#';
202
+ }
216
203
 
217
- // Copy all data attributes
218
- Array.from(elem.attributes).forEach((attr) => {
219
- if (
220
- attr.name.startsWith('data-') &&
221
- attr.name !== 'data-space-protected' &&
222
- attr.name !== 'data-editable-button'
223
- ) {
224
- (node as any)[attr.name] = attr.value;
225
- }
226
- });
227
- } else if (tagName === 'img') {
228
- // Process image tags
229
- node.src = (elem as HTMLImageElement).getAttribute('src') || undefined;
230
- node.alt = (elem as HTMLImageElement).getAttribute('alt') || undefined;
204
+ if (tagName === 'img') {
205
+ node.src = elem.getAttribute('src') || '';
206
+ node.alt = elem.getAttribute('alt') || '';
231
207
  }
232
208
 
233
- // Add this node
234
- result.push(node);
209
+ results.push(node);
235
210
 
236
- // Process children recursively with the new nodeId as parent
237
- const childNodes = extractNodesFromDOM(elem, nodeId);
238
- result.push(...childNodes);
211
+ const childNodes = extractNodesFromDOM(elem, nodeId, onInsertSignal);
212
+ if (childNodes.length > 0) {
213
+ results.push(...childNodes);
214
+ }
239
215
  }
240
216
  });
241
217
 
242
- return result;
218
+ return results;
243
219
  }
244
220
 
245
221
  export function findLinkDestinationInHtml(html: string): string | null {
@@ -359,25 +335,29 @@ export function processRichTextToNodes(
359
335
 
360
336
  if (parsedNodes.length === 0) return [];
361
337
 
362
- // Process each node to restore interactive element properties
338
+ // Create a map for O(1) identity lookup
339
+ const originalNodesMap = new Map(originalNodes.map((n) => [n.id, n]));
340
+
363
341
  parsedNodes.forEach((node) => {
364
- if (['a', 'button', 'span'].includes(node.tagName)) {
365
- const matchingOriginalNode = findMatchingNode(node, originalNodes);
342
+ if (['a', 'button', 'span', 'img'].includes(node.tagName)) {
343
+ const matchingOriginalNode = originalNodesMap.get(node.id);
344
+
366
345
  if (matchingOriginalNode) {
346
+ // Re-hydrate the node with its payloads
367
347
  if (['a', 'button'].includes(node.tagName)) {
368
- if (matchingOriginalNode.href) {
369
- node.href = matchingOriginalNode.href;
370
- }
348
+ if (matchingOriginalNode.href) node.href = matchingOriginalNode.href;
371
349
  node.buttonPayload = matchingOriginalNode.buttonPayload;
372
350
  } else if (node.tagName === 'span') {
373
351
  node.elementCss = matchingOriginalNode.elementCss;
374
352
  node.overrideClasses = matchingOriginalNode.overrideClasses;
375
- if (matchingOriginalNode.wordCarouselPayload) {
376
- node.wordCarouselPayload = matchingOriginalNode.wordCarouselPayload;
377
- }
353
+ node.wordCarouselPayload = matchingOriginalNode.wordCarouselPayload;
354
+ } else if (node.tagName === 'img') {
355
+ // Keep original src/alt if the DOM didn't change them
356
+ if (matchingOriginalNode.src) node.src = matchingOriginalNode.src;
357
+ if (matchingOriginalNode.alt) node.alt = matchingOriginalNode.alt;
378
358
  }
379
359
  } else if (onInsertSignal) {
380
- // New interactive element detected, trigger insert signal
360
+ // Truly a new node (e.g. from a paste)
381
361
  onInsertSignal(node.tagName, node.id);
382
362
  }
383
363
  }
@@ -756,3 +736,202 @@ export function addColumn(gridLayoutId: string) {
756
736
 
757
737
  redoLogic();
758
738
  }
739
+
740
+ export function parseCodeHook(node: BaseNode | FlatNode) {
741
+ if ('codeHookParams' in node && Array.isArray(node.codeHookParams)) {
742
+ const hookMatch = node.copy?.match(regexpHook);
743
+
744
+ if (!hookMatch) return null;
745
+
746
+ return {
747
+ hook: hookMatch[1],
748
+ value1: node.codeHookParams[0] || null,
749
+ value2: node.codeHookParams[1] || null,
750
+ value3: node.codeHookParams[2] || '',
751
+ };
752
+ }
753
+
754
+ // Legacy/Children check fallback
755
+ if ('children' in node && Array.isArray((node as any).children)) {
756
+ const firstChild = (node as any).children[0];
757
+ if (!firstChild?.value) return null;
758
+
759
+ const regexpValues = /((?:[^\\|]+|\\\|?)+)/g;
760
+ const hookMatch = firstChild.value.match(regexpHook);
761
+
762
+ if (!hookMatch) return null;
763
+
764
+ const hook = hookMatch[1];
765
+ const hookValuesRaw = hookMatch[2].match(regexpValues);
766
+
767
+ return {
768
+ hook,
769
+ value1: hookValuesRaw?.[0] || null,
770
+ value2: hookValuesRaw?.[1] || null,
771
+ value3: hookValuesRaw?.[2] || '',
772
+ };
773
+ }
774
+
775
+ return null;
776
+ }
777
+
778
+ export const isAddressableNode = (
779
+ node: BaseNode | FlatNode,
780
+ ctx?: NodesContext
781
+ ): boolean => {
782
+ if (!node || node.nodeType !== 'TagElement') {
783
+ return false;
784
+ }
785
+
786
+ const flatNode = node as FlatNode;
787
+ const tagName = flatNode.tagName;
788
+
789
+ if (tagName === 'ul' || tagName === 'ol') {
790
+ return false;
791
+ }
792
+
793
+ // Handle LI: Only addressable if it is a text-item.
794
+ // If it contains an img or code widget, it is a transparent container.
795
+ if (tagName === 'li') {
796
+ if (!ctx) return false;
797
+
798
+ const childIds = ctx.getChildNodeIDs(node.id);
799
+ const hasComplexContent = childIds.some((id) => {
800
+ const child = ctx.allNodes.get().get(id) as FlatNode;
801
+ return child && ['img', 'code'].includes(child.tagName);
802
+ });
803
+
804
+ return !hasComplexContent;
805
+ }
806
+
807
+ const addressableTags = [
808
+ 'p',
809
+ 'h2',
810
+ 'h3',
811
+ 'h4',
812
+ 'h5',
813
+ 'h6',
814
+ 'img',
815
+ 'a',
816
+ 'button',
817
+ 'code',
818
+ ];
819
+
820
+ return addressableTags.includes(tagName);
821
+ };
822
+
823
+ export const isTopLevelBlockNode = (
824
+ node: BaseNode | FlatNode,
825
+ ctx: NodesContext
826
+ ): boolean => {
827
+ if (!node.parentId) return false;
828
+ const parent = ctx.allNodes.get().get(node.parentId);
829
+ if (!parent) return false;
830
+
831
+ return parent.nodeType === 'Markdown' || isGridLayoutNode(parent);
832
+ };
833
+
834
+ export const getNodeDisplayMode = (
835
+ node: BaseNode | FlatNode,
836
+ viewport: ViewportKey,
837
+ ctx: NodesContext
838
+ ): boolean => {
839
+ const flatNode = node as FlatNode;
840
+ const tagName = flatNode.tagName || '';
841
+
842
+ // 1. Check Overrides (Highest Priority)
843
+ // If the user manually added a class, we respect it immediately.
844
+ if (flatNode.overrideClasses) {
845
+ const [_, mobile, tablet, desktop] = processClassesForViewports(
846
+ { mobile: {}, tablet: {}, desktop: {} },
847
+ flatNode.overrideClasses,
848
+ 1
849
+ );
850
+ const active =
851
+ viewport === 'mobile'
852
+ ? mobile[0]
853
+ : viewport === 'tablet'
854
+ ? tablet[0]
855
+ : desktop[0];
856
+
857
+ // If explicit inline, force inline
858
+ if (active.includes('inline-block') || active.includes('inline-'))
859
+ return true;
860
+
861
+ // If explicit layout (flex, grid, block), force NOT inline (let the class work)
862
+ if (
863
+ active.includes('block') ||
864
+ active.includes('flex') ||
865
+ active.includes('grid')
866
+ )
867
+ return false;
868
+ }
869
+
870
+ // 2. PARENT CONTEXT CHECK
871
+ // If the parent is a layout container (Flex/Grid), this node is an Item.
872
+ // It should NOT be forced to inline-block. It should default to block (div).
873
+ if (node.parentId) {
874
+ const parent = ctx.allNodes.get().get(node.parentId) as any;
875
+ if (parent) {
876
+ // Check Parent's Extracted Classes (defaults + overrides)
877
+ const extracted = extractClassesFromNodes([parent]).join(' ');
878
+
879
+ // Also check Parent's Explicit Overrides directly for current viewport
880
+ let parentActiveOverrides = '';
881
+ if (parent.overrideClasses) {
882
+ const [_, pMob, pTab, pDesk] = processClassesForViewports(
883
+ { mobile: {}, tablet: {}, desktop: {} },
884
+ parent.overrideClasses,
885
+ 1
886
+ );
887
+ parentActiveOverrides =
888
+ viewport === 'mobile'
889
+ ? pMob[0]
890
+ : viewport === 'tablet'
891
+ ? pTab[0]
892
+ : pDesk[0];
893
+ }
894
+
895
+ const combinedParentClasses = `${extracted} ${parentActiveOverrides}`;
896
+
897
+ // If parent is Flex or Grid, we are an Item. Return FALSE to avoid inline-block.
898
+ if (
899
+ combinedParentClasses.includes('flex') ||
900
+ combinedParentClasses.includes('grid') ||
901
+ combinedParentClasses.includes('gap-')
902
+ ) {
903
+ return false;
904
+ }
905
+ }
906
+ }
907
+
908
+ // 3. Check Default Classes (Theme Defaults)
909
+ const markdownParentId = ctx.getClosestNodeTypeFromId(node.id, 'Markdown');
910
+ if (markdownParentId) {
911
+ const styleSourceNode = ctx.allNodes.get().get(markdownParentId) as any;
912
+ const styles = styleSourceNode?.defaultClasses?.[tagName];
913
+ if (styles) {
914
+ const defaultClassStr = Object.values(styles.mobile || {})
915
+ .flat()
916
+ .join(' ');
917
+
918
+ if (defaultClassStr.includes('inline')) {
919
+ return true;
920
+ }
921
+ }
922
+ }
923
+
924
+ // 4. Tag Default (Lowest Priority)
925
+ // Standard HTML behavior
926
+ const inlineTags = ['a', 'span', 'img', 'button', 'strong', 'em'];
927
+ if (inlineTags.includes(tagName)) {
928
+ // Exception: Top level blocks (direct children of Markdown roots) usually stack
929
+ if (isTopLevelBlockNode(node, ctx)) {
930
+ return false;
931
+ }
932
+ return true;
933
+ }
934
+
935
+ // Default to Block (False)
936
+ return false;
937
+ };