astro-tractstack 2.0.16 → 2.0.18

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 (65) hide show
  1. package/dist/index.js +24 -0
  2. package/package.json +1 -1
  3. package/templates/custom/with-examples/SandboxLauncher.tsx +11 -9
  4. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +1 -1
  5. package/templates/src/components/codehooks/ListContentSetup.tsx +1 -1
  6. package/templates/src/components/compositor/Compositor.tsx +1 -0
  7. package/templates/src/components/compositor/Node.tsx +41 -17
  8. package/templates/src/components/compositor/nodes/GhostInsertBlock.tsx +9 -6
  9. package/templates/src/components/compositor/nodes/GridLayout.tsx +124 -0
  10. package/templates/src/components/compositor/nodes/GridLayout_eraser.tsx +33 -0
  11. package/templates/src/components/compositor/nodes/Markdown.tsx +67 -37
  12. package/templates/src/components/compositor/nodes/Markdown_eraser.tsx +56 -0
  13. package/templates/src/components/compositor/nodes/Pane_DesignLibrary.tsx +1 -1
  14. package/templates/src/components/compositor/preview/FeaturedArticlePreview.tsx +8 -2
  15. package/templates/src/components/edit/PanelSwitch.tsx +232 -75
  16. package/templates/src/components/edit/SettingsPanel.tsx +0 -1
  17. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +3 -3
  18. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +184 -151
  19. package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +2 -2
  20. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -7
  21. package/templates/src/components/edit/pane/PanePanel_impression.tsx +1 -1
  22. package/templates/src/components/edit/pane/RestylePaneModal.tsx +8 -5
  23. package/templates/src/components/edit/pane/steps/AiDesignStep.tsx +6 -6
  24. package/templates/src/components/edit/pane/steps/CopyInputStep.tsx +3 -3
  25. package/templates/src/components/edit/pane/steps/DesignLibraryStep.tsx +4 -4
  26. package/templates/src/components/edit/pane/steps/DirectInjectStep.tsx +96 -0
  27. package/templates/src/components/edit/panels/StyleElementPanel.tsx +11 -4
  28. package/templates/src/components/edit/panels/StyleElementPanel_add.tsx +8 -8
  29. package/templates/src/components/edit/panels/StyleElementPanel_remove.tsx +14 -4
  30. package/templates/src/components/edit/panels/StyleElementPanel_update.tsx +16 -4
  31. package/templates/src/components/edit/panels/StyleImagePanel.tsx +8 -3
  32. package/templates/src/components/edit/panels/StyleImagePanel_add.tsx +9 -2
  33. package/templates/src/components/edit/panels/StyleImagePanel_remove.tsx +5 -2
  34. package/templates/src/components/edit/panels/StyleImagePanel_update.tsx +5 -2
  35. package/templates/src/components/edit/panels/StyleLiElementPanel.tsx +7 -3
  36. package/templates/src/components/edit/panels/StyleLiElementPanel_add.tsx +9 -2
  37. package/templates/src/components/edit/panels/StyleLiElementPanel_remove.tsx +5 -2
  38. package/templates/src/components/edit/panels/StyleLiElementPanel_update.tsx +5 -2
  39. package/templates/src/components/edit/panels/StyleParentPanel.tsx +530 -171
  40. package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +77 -42
  41. package/templates/src/components/edit/panels/StyleParentPanel_deleteLayer.tsx +38 -22
  42. package/templates/src/components/edit/panels/StyleParentPanel_remove.tsx +171 -66
  43. package/templates/src/components/edit/panels/StyleParentPanel_update.tsx +166 -98
  44. package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +7 -3
  45. package/templates/src/components/edit/panels/StyleWidgetPanel_add.tsx +9 -2
  46. package/templates/src/components/edit/panels/StyleWidgetPanel_remove.tsx +5 -2
  47. package/templates/src/components/edit/panels/StyleWidgetPanel_update.tsx +6 -2
  48. package/templates/src/components/edit/state/SaveModal.tsx +10 -2
  49. package/templates/src/components/edit/state/SaveToLibraryModal.tsx +6 -6
  50. package/templates/src/components/fields/PaneBreakShapeSelector.tsx +1 -1
  51. package/templates/src/components/widgets/ImpressionWrapper.tsx +4 -1
  52. package/templates/src/constants/prompts.json +1 -1
  53. package/templates/src/constants.ts +1 -0
  54. package/templates/src/stores/nodes.ts +110 -33
  55. package/templates/src/stores/storykeep.ts +3 -1
  56. package/templates/src/types/compositorTypes.ts +37 -2
  57. package/templates/src/utils/compositor/TemplateNodes.ts +8 -0
  58. package/templates/src/utils/compositor/aiPaneParser.ts +8 -2
  59. package/templates/src/utils/compositor/nodesHelper.ts +229 -0
  60. package/templates/src/utils/compositor/reduceNodesClassNames.ts +40 -1
  61. package/templates/src/utils/compositor/typeGuards.ts +7 -0
  62. package/templates/src/utils/etl/extractor.ts +1 -5
  63. package/templates/src/utils/etl/index.ts +1 -0
  64. package/templates/src/utils/etl/transformer.ts +70 -25
  65. package/utils/inject-files.ts +24 -0
package/dist/index.js CHANGED
@@ -104,6 +104,18 @@ async function w(t, e, c) {
104
104
  ),
105
105
  dest: "src/components/compositor/nodes/RenderChildren.tsx"
106
106
  },
107
+ {
108
+ src: t(
109
+ "../templates/src/components/compositor/nodes/GridLayout.tsx"
110
+ ),
111
+ dest: "src/components/compositor/nodes/GridLayout.tsx"
112
+ },
113
+ {
114
+ src: t(
115
+ "../templates/src/components/compositor/nodes/GridLayout_eraser.tsx"
116
+ ),
117
+ dest: "src/components/compositor/nodes/GridLayout_eraser.tsx"
118
+ },
107
119
  {
108
120
  src: t(
109
121
  "../templates/src/components/compositor/nodes/GhostInsertBlock.tsx"
@@ -154,6 +166,12 @@ async function w(t, e, c) {
154
166
  src: t("../templates/src/components/compositor/nodes/Markdown.tsx"),
155
167
  dest: "src/components/compositor/nodes/Markdown.tsx"
156
168
  },
169
+ {
170
+ src: t(
171
+ "../templates/src/components/compositor/nodes/Markdown_eraser.tsx"
172
+ ),
173
+ dest: "src/components/compositor/nodes/Markdown_eraser.tsx"
174
+ },
157
175
  {
158
176
  src: t(
159
177
  "../templates/src/components/compositor/nodes/BgPaneWrapper.tsx"
@@ -470,6 +488,12 @@ async function w(t, e, c) {
470
488
  ),
471
489
  dest: "src/components/edit/pane/steps/AiDesignStep.tsx"
472
490
  },
491
+ {
492
+ src: t(
493
+ "../templates/src/components/edit/pane/steps/DirectInjectStep.tsx"
494
+ ),
495
+ dest: "src/components/edit/pane/steps/DirectInjectStep.tsx"
496
+ },
473
497
  {
474
498
  src: t(
475
499
  "../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.18",
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) */}
@@ -204,7 +204,7 @@ const FeaturedArticleSetup = ({
204
204
 
205
205
  if (!isPanelOpen) {
206
206
  return (
207
- <div className="flex min-h-[200px] w-full flex-col items-center justify-center space-y-6 rounded-lg bg-slate-50 p-6">
207
+ <div className="flex min-h-48 w-full flex-col items-center justify-center space-y-6 rounded-lg bg-slate-50 p-6">
208
208
  <button
209
209
  onClick={() => setIsPanelOpen(true)}
210
210
  className="rounded-lg bg-cyan-600 px-6 py-3 font-bold text-white shadow-md transition-colors hover:bg-cyan-700"
@@ -236,7 +236,7 @@ const ListContentSetup = ({
236
236
  // If panel is not open, show only the configuration button
237
237
  if (!isPanelOpen) {
238
238
  return (
239
- <div className="flex min-h-[200px] w-full flex-col items-center justify-center space-y-6 rounded-lg bg-slate-50 p-6">
239
+ <div className="flex min-h-48 w-full flex-col items-center justify-center space-y-6 rounded-lg bg-slate-50 p-6">
240
240
  <button
241
241
  onClick={() => setIsPanelOpen(true)}
242
242
  className="rounded-lg bg-cyan-600 px-6 py-3 font-bold text-white shadow-md transition-colors hover:bg-cyan-700"
@@ -57,6 +57,7 @@ const VERBOSE = false;
57
57
  const LOG_PREFIX = '[Compositor] ';
58
58
 
59
59
  export const Compositor = (props: CompositorProps) => {
60
+ //console.log(props.nodes)
60
61
  const [initialized, setInitialized] = useState(false);
61
62
  const [updateCounter, setUpdateCounter] = useState(0);
62
63
  const [isLoading, setIsLoading] = useState(true);
@@ -8,8 +8,12 @@ import {
8
8
  } from 'react';
9
9
  import { useStore } from '@nanostores/react';
10
10
  import { getCtx } from '@/stores/nodes';
11
- import { styleElementInfoStore, viewportKeyStore } from '@/stores/storykeep';
12
- import { getType } from '@/utils/compositor/typeGuards';
11
+ import {
12
+ settingsPanelStore,
13
+ styleElementInfoStore,
14
+ viewportKeyStore,
15
+ } from '@/stores/storykeep';
16
+ import { isGridLayoutNode, getType } from '@/utils/compositor/typeGuards';
13
17
  import { NodeWithGuid } from './NodeWithGuid';
14
18
  import PanelVisibilityWrapper from './PanelVisibilityWrapper';
15
19
  import { Pane } from './nodes/Pane';
@@ -31,6 +35,9 @@ import { NodeBasicTagInsert } from './nodes/tagElements/NodeBasicTag_insert';
31
35
  import { NodeBasicTagEraser } from './nodes/tagElements/NodeBasicTag_eraser';
32
36
  import { NodeBasicTagSettings } from './nodes/tagElements/NodeBasicTag_settings';
33
37
  import { Pane_DesignLibrary } from './nodes/Pane_DesignLibrary';
38
+ import { GridLayout } from './nodes/GridLayout';
39
+ import { GridLayoutEraser } from './nodes/GridLayout_eraser';
40
+ import { MarkdownEraser } from './nodes/Markdown_eraser';
34
41
  import AddPanePanel from '@/components/edit/pane/AddPanePanel';
35
42
  import ConfigPanePanel from '@/components/edit/pane/ConfigPanePanel';
36
43
  import StoryFragmentConfigPanel from '@/components/edit/storyfragment/StoryFragmentConfigPanel';
@@ -124,8 +131,18 @@ const getElement = (
124
131
  const type = getType(node);
125
132
 
126
133
  switch (type) {
127
- case 'Markdown':
134
+ case 'Markdown': {
135
+ const toolModeVal = getCtx(props).toolModeValStore.get().value;
136
+ if (toolModeVal === 'eraser') {
137
+ const parentNode = node.parentId
138
+ ? getCtx(props).allNodes.get().get(node.parentId)
139
+ : null;
140
+ if (parentNode && isGridLayoutNode(parentNode)) {
141
+ return <MarkdownEraser {...sharedProps} />;
142
+ }
143
+ }
128
144
  return <Markdown {...sharedProps} />;
145
+ }
129
146
 
130
147
  case 'StoryFragment': {
131
148
  const sf = node as StoryFragmentNode;
@@ -308,6 +325,13 @@ const getElement = (
308
325
 
309
326
  case 'BgPane':
310
327
  return <BgPaneWrapper {...sharedProps} />;
328
+ case 'GridLayoutNode': {
329
+ const toolModeVal = getCtx(props).toolModeValStore.get().value;
330
+ if (toolModeVal === 'eraser') {
331
+ return <GridLayoutEraser {...sharedProps} />;
332
+ }
333
+ return <GridLayout {...sharedProps} />;
334
+ }
311
335
  case 'TagElement':
312
336
  return <TagElement {...sharedProps} />;
313
337
  // tag elements
@@ -328,27 +352,14 @@ const getElement = (
328
352
  );
329
353
 
330
354
  const handleElementClick = (e: MouseEvent<HTMLElement>) => {
331
- // 1. ALWAYS stop the event from bubbling up to the Pane.
332
355
  e.stopPropagation();
333
-
334
- // 2. Check the selection store. The handleMouseUp in Compositor.tsx
335
- // has already run by the time this 'click' event fires.
336
- //
337
- // - If it was a drag, Compositor.tsx set 'isActive: true' (line 267).
338
- // - If it was a click, Compositor.tsx called 'resetSelectionStore'
339
- // (line 242), so 'isActive: false'.
340
- //
341
356
  const { isActive } = selectionStore.get();
342
-
343
357
  if (isActive) {
344
358
  // A drag just finished. The user's intent was to select text.
345
359
  // Do NOT open the panel.
346
360
  return;
347
361
  }
348
-
349
- // 3. 'isActive' was false. This was a genuine click.
350
- // We can safely open the settings panel.
351
- // 'node' is already in scope from the getElement function.
362
+ // else
352
363
  handleClickEventDefault(node as FlatNode, true);
353
364
  };
354
365
 
@@ -459,6 +470,7 @@ const getElement = (
459
470
  const Node = memo((props: NodeProps) => {
460
471
  const node = getCtx(props).allNodes.get().get(props.nodeId) as FlatNode;
461
472
  const isPreview = getCtx(props).rootNodeId.get() === `tmp`;
473
+ const settingsPanel = useStore(settingsPanelStore);
462
474
 
463
475
  const {
464
476
  markdownParentId,
@@ -535,6 +547,18 @@ const Node = memo((props: NodeProps) => {
535
547
  : '';
536
548
 
537
549
  const element = getElement(node, props);
550
+ const isPanelActive =
551
+ settingsPanel?.action === 'style-parent' &&
552
+ settingsPanel?.nodeId === props.nodeId;
553
+ const isStyleableContainer =
554
+ node?.nodeType === 'Markdown' || isGridLayoutNode(node);
555
+
556
+ if (isPanelActive && isStyleableContainer) {
557
+ const highlightStyle = {
558
+ outline: '3.5px dotted rgba(255, 165, 0, 0.85)',
559
+ };
560
+ return <div style={highlightStyle}>{element}</div>;
561
+ }
538
562
 
539
563
  if (!isPreview && getCtx(props).showGuids.get()) {
540
564
  return <NodeWithGuid {...props} element={element} />;
@@ -1,4 +1,4 @@
1
- import { memo, useMemo, useState } from 'react';
1
+ import { memo, useMemo, useState, type MouseEvent } from 'react';
2
2
  import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
3
3
  import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
4
4
  import {
@@ -29,6 +29,7 @@ export const GhostInsertBlock = memo((props: GhostInsertBlockProps) => {
29
29
  const lastChildNode = lastChildId
30
30
  ? (getCtx(props).allNodes.get().get(lastChildId) as FlatNode)
31
31
  : null;
32
+ const toolModeVal = getCtx(props).toolModeValStore.get().value;
32
33
 
33
34
  const allowedModes = useMemo(() => {
34
35
  if (isEmpty) {
@@ -50,7 +51,7 @@ export const GhostInsertBlock = memo((props: GhostInsertBlockProps) => {
50
51
  ) as ToolAddMode[];
51
52
  }, [isEmpty, lastChildId, parentNode, lastChildNode, $toolAddModes]);
52
53
 
53
- const handleInsert = (mode: ToolAddMode, e: React.MouseEvent) => {
54
+ const handleInsert = (mode: ToolAddMode, e: MouseEvent) => {
54
55
  e.stopPropagation();
55
56
  const templateNode = getTemplateNode(mode);
56
57
  let newNodeId: string | null = null;
@@ -144,7 +145,7 @@ export const GhostInsertBlock = memo((props: GhostInsertBlockProps) => {
144
145
  }
145
146
 
146
147
  return (
147
- <div className="my-4">
148
+ <div className="my-4 p-3.5 md:p-6">
148
149
  {showInsertOptions ? (
149
150
  <div className="rounded-lg border-2 border-cyan-600 bg-white p-3 text-gray-800 shadow-lg">
150
151
  <div className="mb-3 flex items-center justify-between border-b pb-2">
@@ -168,9 +169,11 @@ export const GhostInsertBlock = memo((props: GhostInsertBlockProps) => {
168
169
  ) : (
169
170
  <button
170
171
  onClick={(e) => {
171
- e.stopPropagation();
172
- settingsPanelStore.set(null);
173
- setShowInsertOptions(true);
172
+ if (toolModeVal !== `eraser`) {
173
+ e.stopPropagation();
174
+ settingsPanelStore.set(null);
175
+ setShowInsertOptions(true);
176
+ }
174
177
  }}
175
178
  className="group w-full rounded-lg border-2 border-dashed border-cyan-500 bg-cyan-50 p-6 transition-colors hover:bg-cyan-100 dark:border-cyan-600 dark:bg-cyan-900 dark:hover:bg-cyan-800"
176
179
  >
@@ -0,0 +1,124 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { getCtx } from '@/stores/nodes';
3
+ import { RenderChildren } from './RenderChildren';
4
+ import { isGridLayoutNode } from '@/utils/compositor/typeGuards';
5
+ import type { NodeProps } from '@/types/nodeProps';
6
+ import type { ParentClassesPayload } from '@/types/compositorTypes';
7
+ import { viewportKeyStore } from '@/stores/storykeep';
8
+
9
+ export const GridLayout = (props: NodeProps) => {
10
+ const ctx = getCtx(props);
11
+ const node = ctx.allNodes.get().get(props.nodeId);
12
+
13
+ const [currentViewport, setCurrentViewport] = useState(
14
+ viewportKeyStore.get().value
15
+ );
16
+
17
+ useEffect(() => {
18
+ const unsubscribeViewport = viewportKeyStore.subscribe((newViewport) => {
19
+ setCurrentViewport(newViewport.value);
20
+ });
21
+ return () => unsubscribeViewport();
22
+ }, []);
23
+
24
+ if (!node || !isGridLayoutNode(node)) {
25
+ return <></>;
26
+ }
27
+
28
+ let isHidden = false;
29
+ switch (currentViewport) {
30
+ case 'mobile':
31
+ isHidden = !!node.hiddenViewportMobile;
32
+ break;
33
+ case 'tablet':
34
+ isHidden = !!node.hiddenViewportTablet;
35
+ break;
36
+ case 'desktop':
37
+ isHidden = !!node.hiddenViewportDesktop;
38
+ break;
39
+ }
40
+
41
+ if (isHidden) {
42
+ return (
43
+ <div
44
+ onClick={(e) => {
45
+ ctx.setClickedNodeId(props.nodeId, true);
46
+ e.stopPropagation();
47
+ }}
48
+ className="group my-4 w-full cursor-pointer rounded-lg border-2 border-dashed border-gray-400 bg-gray-100 p-6 transition-colors hover:bg-gray-200"
49
+ style={{ position: 'relative', zIndex: 10 }}
50
+ >
51
+ <div className="text-center font-bold text-gray-800">
52
+ Hidden on this Viewport
53
+ </div>
54
+ </div>
55
+ );
56
+ }
57
+
58
+ let gridCols = node.gridColumns.mobile;
59
+ switch (currentViewport) {
60
+ case 'tablet':
61
+ gridCols = node.gridColumns.tablet;
62
+ break;
63
+ case 'desktop':
64
+ gridCols = node.gridColumns.desktop;
65
+ break;
66
+ }
67
+ const gridClassName = `grid grid-cols-${gridCols}`;
68
+
69
+ const children = ctx.getChildNodeIDs(props.nodeId);
70
+
71
+ let nodesToRender: JSX.Element = (
72
+ <div className={gridClassName} style={{ position: 'relative', zIndex: 10 }}>
73
+ <RenderChildren children={children} nodeProps={props} />
74
+ </div>
75
+ );
76
+
77
+ if (
78
+ 'parentClasses' in node &&
79
+ (node.parentClasses as ParentClassesPayload)?.length > 0
80
+ ) {
81
+ const parentClassesLength = (node.parentClasses as ParentClassesPayload)
82
+ .length;
83
+ for (let i = parentClassesLength; i > 0; --i) {
84
+ nodesToRender = (
85
+ <div
86
+ onClick={(e) => {
87
+ if (e.target !== e.currentTarget) {
88
+ return;
89
+ }
90
+ getCtx(props).setClickedParentLayer(i);
91
+ getCtx(props).setClickedNodeId(props.nodeId);
92
+ e.stopPropagation();
93
+ }}
94
+ onDoubleClick={(e) => {
95
+ if (e.target !== e.currentTarget) {
96
+ return;
97
+ }
98
+ getCtx(props).setClickedParentLayer(i);
99
+ getCtx(props).setClickedNodeId(props.nodeId, true);
100
+ e.stopPropagation();
101
+ }}
102
+ className={getCtx(props).getNodeClasses(
103
+ props.nodeId,
104
+ currentViewport,
105
+ i - 1
106
+ )}
107
+ style={
108
+ i === parentClassesLength
109
+ ? { position: 'relative', zIndex: 10 }
110
+ : undefined
111
+ }
112
+ >
113
+ {nodesToRender}
114
+ </div>
115
+ );
116
+ }
117
+ } else {
118
+ nodesToRender = (
119
+ <div style={{ position: 'relative', zIndex: 10 }}>{nodesToRender}</div>
120
+ );
121
+ }
122
+
123
+ return nodesToRender;
124
+ };
@@ -0,0 +1,33 @@
1
+ import { getCtx } from '@/stores/nodes';
2
+ import type { NodeProps } from '@/types/nodeProps';
3
+ import { GridLayout } from './GridLayout';
4
+ import type { MouseEvent } from 'react';
5
+
6
+ export function GridLayoutEraser(props: NodeProps) {
7
+ const ctx = getCtx(props);
8
+
9
+ const handleClick = (e: MouseEvent<HTMLDivElement>) => {
10
+ e.stopPropagation();
11
+ e.preventDefault();
12
+
13
+ const node = ctx.allNodes.get().get(props.nodeId);
14
+ if (!node) return;
15
+
16
+ if (
17
+ window.confirm(
18
+ 'Are you sure you want to delete this grid layout and all its columns?'
19
+ )
20
+ ) {
21
+ ctx.deleteNode(props.nodeId);
22
+ }
23
+ };
24
+
25
+ return (
26
+ <div
27
+ className="outline-dashed outline-2 outline-red-500 hover:bg-red-500/10"
28
+ onClick={handleClick}
29
+ >
30
+ <GridLayout {...props} />
31
+ </div>
32
+ );
33
+ }
@@ -3,6 +3,7 @@ import { getCtx } from '@/stores/nodes';
3
3
  import { viewportKeyStore } from '@/stores/storykeep';
4
4
  import { RenderChildren } from './RenderChildren';
5
5
  import { GhostInsertBlock } from './GhostInsertBlock';
6
+ import { processGridClassesToString } from '@/utils/compositor/reduceNodesClassNames';
6
7
  import type { NodeProps } from '@/types/nodeProps';
7
8
  import type {
8
9
  MarkdownPaneFragmentNode,
@@ -13,40 +14,64 @@ import type {
13
14
 
14
15
  export const Markdown = (props: NodeProps) => {
15
16
  const id = props.nodeId;
16
- const toolModeVal = getCtx(props).toolModeValStore.get().value;
17
- const node = getCtx(props)
18
- .allNodes.get()
19
- .get(props.nodeId) as MarkdownPaneFragmentNode;
20
- const isPreview = getCtx(props).rootNodeId.get() === `tmp`;
21
- const children = getCtx(props).getChildNodeIDs(props.nodeId);
17
+ const ctx = getCtx(props);
18
+ const node = ctx.allNodes.get().get(id) as MarkdownPaneFragmentNode;
19
+ const isPreview = ctx.rootNodeId.get() === `tmp`;
20
+ const children = ctx.getChildNodeIDs(id);
22
21
  const isEmpty = children.length === 0;
23
22
  const lastChildId =
24
23
  children.length > 0 ? children[children.length - 1] : null;
25
24
 
26
- // Track the viewport value in state so we can react to changes
27
25
  const [currentViewport, setCurrentViewport] = useState(
28
26
  viewportKeyStore.get().value
29
27
  );
30
28
 
31
- // Subscribe to viewportKeyStore changes
32
29
  useEffect(() => {
33
30
  const unsubscribeViewport = viewportKeyStore.subscribe((newViewport) => {
34
31
  setCurrentViewport(newViewport.value);
35
32
  });
36
-
37
- return () => {
38
- unsubscribeViewport();
39
- };
33
+ return () => unsubscribeViewport();
40
34
  }, []);
41
35
 
42
- // Check for positioned background image
43
- const allNodes = getCtx(props).allNodes.get();
44
- const parentPaneId = node.parentId;
36
+ if (!node) return null;
37
+
38
+ let isHidden = false;
39
+ switch (currentViewport) {
40
+ case 'mobile':
41
+ isHidden = !!node.hiddenViewportMobile;
42
+ break;
43
+ case 'tablet':
44
+ isHidden = !!node.hiddenViewportTablet;
45
+ break;
46
+ case 'desktop':
47
+ isHidden = !!node.hiddenViewportDesktop;
48
+ break;
49
+ }
50
+
51
+ if (isHidden) {
52
+ return (
53
+ <div
54
+ onClick={(e) => {
55
+ ctx.setClickedNodeId(props.nodeId, true);
56
+ e.stopPropagation();
57
+ }}
58
+ className="group my-4 w-full cursor-pointer rounded-lg border-2 border-dashed border-gray-400 bg-gray-100 p-6 transition-colors hover:bg-gray-200"
59
+ style={{ position: 'relative', zIndex: 10 }}
60
+ >
61
+ <div className="text-center font-bold text-gray-800">
62
+ Hidden on this Viewport
63
+ </div>
64
+ </div>
65
+ );
66
+ }
67
+
68
+ const allNodes = ctx.allNodes.get();
69
+ const parentPaneId = ctx.getClosestNodeTypeFromId(id, 'Pane');
45
70
  const bgNode = parentPaneId
46
71
  ? (() => {
47
- const childNodeIds = getCtx(props).getChildNodeIDs(parentPaneId);
72
+ const childNodeIds = ctx.getChildNodeIDs(parentPaneId);
48
73
  return childNodeIds
49
- .map((id) => allNodes.get(id))
74
+ .map((childId) => allNodes.get(childId))
50
75
  .find(
51
76
  (n) =>
52
77
  n?.nodeType === 'BgPane' &&
@@ -58,18 +83,14 @@ export const Markdown = (props: NodeProps) => {
58
83
  })()
59
84
  : undefined;
60
85
 
61
- // Helper function for size styles - NO MODIFIERS needed since React rerenders on viewport change
62
86
  function getSizeClasses(
63
87
  size: string,
64
88
  side: 'image' | 'content',
65
89
  viewport: string
66
90
  ): string {
67
- // Mobile always gets full width (stacked layout)
68
91
  if (viewport === 'mobile') {
69
92
  return 'w-full';
70
93
  }
71
-
72
- // Desktop/tablet get fractional widths
73
94
  switch (size) {
74
95
  case 'narrow':
75
96
  return side === 'image' ? 'w-1/3' : 'w-2/3';
@@ -83,7 +104,6 @@ export const Markdown = (props: NodeProps) => {
83
104
  const useFlexLayout =
84
105
  bgNode && (bgNode.position === 'left' || bgNode.position === 'right');
85
106
 
86
- // Set flex direction based on currentViewport
87
107
  const flexDirection =
88
108
  currentViewport === 'mobile'
89
109
  ? 'flex-col'
@@ -91,20 +111,24 @@ export const Markdown = (props: NodeProps) => {
91
111
  ? 'flex-row-reverse'
92
112
  : 'flex-row';
93
113
 
114
+ const gridClassName = processGridClassesToString(node.gridClasses);
115
+
94
116
  let nodesToRender = (
95
117
  <>
96
118
  {useFlexLayout ? (
97
119
  <div
98
120
  className={`flex flex-nowrap items-center justify-center gap-6 md:gap-10 xl:gap-12 ${flexDirection}`}
99
121
  >
100
- {/* Image Side - NO MODIFIERS because React rerenders on viewport change */}
101
122
  <div
102
- className={`relative overflow-hidden ${getSizeClasses(bgNode.size || 'equal', 'image', currentViewport)}`}
123
+ className={`relative overflow-hidden ${getSizeClasses(
124
+ bgNode.size || 'equal',
125
+ 'image',
126
+ currentViewport
127
+ )}`}
103
128
  >
104
129
  <RenderChildren children={[bgNode.id]} nodeProps={props} />
105
130
  </div>
106
131
 
107
- {/* Content Side - NO MODIFIERS because React rerenders on viewport change */}
108
132
  <div
109
133
  className={getSizeClasses(
110
134
  bgNode.size || 'equal',
@@ -113,7 +137,7 @@ export const Markdown = (props: NodeProps) => {
113
137
  )}
114
138
  >
115
139
  <RenderChildren children={children} nodeProps={props} />
116
- {!isPreview && [`text`, `insert`].includes(toolModeVal) && (
140
+ {!isPreview && (
117
141
  <GhostInsertBlock
118
142
  nodeId={props.nodeId}
119
143
  ctx={props.ctx}
@@ -126,7 +150,7 @@ export const Markdown = (props: NodeProps) => {
126
150
  ) : (
127
151
  <>
128
152
  <RenderChildren children={children} nodeProps={props} />
129
- {!isPreview && [`text`, `insert`].includes(toolModeVal) && (
153
+ {!isPreview && (
130
154
  <GhostInsertBlock
131
155
  nodeId={props.nodeId}
132
156
  ctx={props.ctx}
@@ -149,16 +173,22 @@ export const Markdown = (props: NodeProps) => {
149
173
  nodesToRender = (
150
174
  <div
151
175
  onClick={(e) => {
152
- getCtx(props).setClickedParentLayer(i);
153
- getCtx(props).setClickedNodeId(props.nodeId);
176
+ if (e.target !== e.currentTarget) {
177
+ return;
178
+ }
179
+ ctx.setClickedParentLayer(i);
180
+ ctx.setClickedNodeId(props.nodeId);
154
181
  e.stopPropagation();
155
182
  }}
156
183
  onDoubleClick={(e) => {
157
- getCtx(props).setClickedParentLayer(i);
158
- getCtx(props).setClickedNodeId(props.nodeId, true);
184
+ if (e.target !== e.currentTarget) {
185
+ return;
186
+ }
187
+ ctx.setClickedParentLayer(i);
188
+ ctx.setClickedNodeId(props.nodeId, true);
159
189
  e.stopPropagation();
160
190
  }}
161
- className={getCtx(props).getNodeClasses(id, currentViewport, i - 1)}
191
+ className={ctx.getNodeClasses(id, currentViewport, i - 1)}
162
192
  style={
163
193
  i === parentClassesLength
164
194
  ? { position: 'relative', zIndex: 10 }
@@ -169,11 +199,11 @@ export const Markdown = (props: NodeProps) => {
169
199
  </div>
170
200
  );
171
201
  }
172
- } else {
173
- nodesToRender = (
174
- <div style={{ position: 'relative', zIndex: 10 }}>{nodesToRender}</div>
175
- );
176
202
  }
177
203
 
178
- return <>{nodesToRender}</>;
204
+ return (
205
+ <div className={gridClassName} style={{ position: 'relative', zIndex: 10 }}>
206
+ {nodesToRender}
207
+ </div>
208
+ );
179
209
  };