astro-tractstack 2.1.3 → 2.2.1

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
@@ -1,100 +1,42 @@
1
- import {
2
- memo,
3
- type ReactElement,
4
- useEffect,
5
- useState,
6
- createElement,
7
- type MouseEvent,
8
- } from 'react';
1
+ import { memo, useEffect, type JSX, type ReactNode } from 'react';
9
2
  import { useStore } from '@nanostores/react';
10
3
  import { getCtx } from '@/stores/nodes';
4
+ import { viewportKeyStore } from '@/stores/storykeep';
11
5
  import {
12
- settingsPanelStore,
13
- styleElementInfoStore,
14
- viewportKeyStore,
15
- } from '@/stores/storykeep';
16
- import { isGridLayoutNode, getType } from '@/utils/compositor/typeGuards';
17
- import { NodeWithGuid } from './NodeWithGuid';
18
- import PanelVisibilityWrapper from './PanelVisibilityWrapper';
6
+ isAddressableNode,
7
+ getNodeDisplayMode,
8
+ isTopLevelBlockNode,
9
+ parseCodeHook,
10
+ } from '@/utils/compositor/nodesHelper';
11
+ import { isPaneNode, isGridLayoutNode } from '@/utils/compositor/typeGuards';
12
+ import { PaneAddMode } from '@/types/compositorTypes';
13
+ import { NodeOverlay } from './tools/NodeOverlay';
14
+ import PanelVisibilityWrapper from '@/components/compositor/PanelVisibilityWrapper';
19
15
  import { Pane } from './nodes/Pane';
20
- import { PaneEraser } from './nodes/Pane_eraser';
21
- import { PaneLayout } from './nodes/Pane_layout';
16
+ import { CreativePane } from './nodes/CreativePane';
22
17
  import { Markdown } from './nodes/Markdown';
23
- import { BgPaneWrapper } from './nodes/BgPaneWrapper';
24
18
  import { StoryFragment } from './nodes/StoryFragment';
25
- import { TagElement } from './nodes/TagElement';
19
+ import { GridLayout } from './nodes/GridLayout';
26
20
  import { Widget } from './nodes/Widget';
27
- import { NodeText } from './nodes/tagElements/NodeText';
21
+ import { NodeBasicTag } from './nodes/tagElements/NodeBasicTag';
22
+ import { NodeImg } from './nodes/tagElements/NodeImg';
28
23
  import { NodeA } from './nodes/tagElements/NodeA';
29
- import { NodeAEraser } from './nodes/tagElements/NodeA_eraser';
30
24
  import { NodeButton } from './nodes/tagElements/NodeButton';
31
- import { NodeButtonEraser } from './nodes/tagElements/NodeButton_eraser';
32
- import { NodeImg } from './nodes/tagElements/NodeImg';
33
- import { NodeBasicTag } from './nodes/tagElements/NodeBasicTag';
34
- import { NodeBasicTagInsert } from './nodes/tagElements/NodeBasicTag_insert';
35
- import { NodeBasicTagEraser } from './nodes/tagElements/NodeBasicTag_eraser';
36
- import { NodeBasicTagSettings } from './nodes/tagElements/NodeBasicTag_settings';
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';
25
+ import { NodeText } from './nodes/tagElements/NodeText';
26
+ import { BgPaneWrapper } from './nodes/BgPaneWrapper';
41
27
  import AddPanePanel from '@/components/edit/pane/AddPanePanel';
42
28
  import ConfigPanePanel from '@/components/edit/pane/ConfigPanePanel';
43
29
  import StoryFragmentConfigPanel from '@/components/edit/storyfragment/StoryFragmentConfigPanel';
44
30
  import StoryFragmentTitlePanel from '@/components/edit/storyfragment/StoryFragmentPanel_title';
45
31
  import ContextPanePanel from '@/components/edit/context/ContextPaneConfig';
46
32
  import ContextPaneTitlePanel from '@/components/edit/context/ContextPaneConfig_title';
47
- import { regexpHook } from '@/constants';
48
- import { RenderChildren } from './nodes/RenderChildren';
33
+
34
+ import type { NodeProps } from '@/types/nodeProps';
49
35
  import type {
50
- StoryFragmentNode,
51
- PaneNode,
52
- BaseNode,
53
36
  FlatNode,
37
+ PaneNode,
38
+ StoryFragmentNode,
54
39
  } from '@/types/compositorTypes';
55
- import { PaneAddMode } from '@/types/compositorTypes';
56
- import { handleClickEventDefault } from '@/utils/compositor/handleClickEvent';
57
- import { selectionStore } from '@/stores/selection';
58
- import type { NodeProps, SelectionOrigin } from '@/types/nodeProps';
59
-
60
- const VERBOSE = false;
61
-
62
- function parseCodeHook(node: BaseNode | FlatNode) {
63
- if ('codeHookParams' in node && Array.isArray(node.codeHookParams)) {
64
- const hookMatch = node.copy?.match(regexpHook);
65
-
66
- if (!hookMatch) return null;
67
-
68
- return {
69
- hook: hookMatch[1],
70
- value1: node.codeHookParams[0] || null,
71
- value2: node.codeHookParams[1] || null,
72
- value3: node.codeHookParams[2] || '',
73
- };
74
- }
75
-
76
- if ('children' in node && Array.isArray(node.children)) {
77
- const firstChild = node.children[0];
78
- if (!firstChild?.value) return null;
79
-
80
- const regexpValues = /((?:[^\\|]+|\\\|?)+)/g;
81
- const hookMatch = firstChild.value.match(regexpHook);
82
-
83
- if (!hookMatch) return null;
84
-
85
- const hook = hookMatch[1];
86
- const hookValuesRaw = hookMatch[2].match(regexpValues);
87
-
88
- return {
89
- hook,
90
- value1: hookValuesRaw?.[0] || null,
91
- value2: hookValuesRaw?.[1] || null,
92
- value3: hookValuesRaw?.[2] || '',
93
- };
94
- }
95
-
96
- return null;
97
- }
98
40
 
99
41
  const EmptyPageHandler = (props: NodeProps) => {
100
42
  const ctx = getCtx(props);
@@ -113,455 +55,185 @@ const EmptyPageHandler = (props: NodeProps) => {
113
55
  );
114
56
  };
115
57
 
116
- const getElement = (
117
- node: BaseNode | FlatNode,
118
- props: NodeProps
119
- ): ReactElement => {
120
- if (node === undefined) return <></>;
121
- const isPreview = getCtx(props).rootNodeId.get() === `tmp`;
122
- const hasPanes = useStore(getCtx(props).hasPanes);
123
- const sharedProps = {
124
- ...props,
125
- nodeId: node.id,
126
- isSandboxMode: props.isSandboxMode,
127
- };
128
- const type = getType(node);
129
- const toolModeVal = getCtx(props).toolModeValStore.get().value;
130
- const settingsPanel = useStore(settingsPanelStore);
58
+ export const Node = memo((props: NodeProps) => {
59
+ const ctx = getCtx(props);
60
+ const node = ctx.allNodes.get().get(props.nodeId);
61
+ const viewport = useStore(viewportKeyStore).value;
62
+ const isPreview = ctx.rootNodeId.get() === `tmp`;
63
+ const hasPanes = useStore(ctx.hasPanes);
131
64
 
132
- switch (type) {
133
- case 'Markdown': {
134
- if (toolModeVal === 'eraser') {
135
- const parentNode = node.parentId
136
- ? getCtx(props).allNodes.get().get(node.parentId)
137
- : null;
138
- if (parentNode && isGridLayoutNode(parentNode)) {
139
- return <MarkdownEraser {...sharedProps} />;
140
- }
141
- }
142
- return <Markdown {...sharedProps} />;
143
- }
65
+ if (!node) return null;
66
+
67
+ let element: ReactNode = null;
144
68
 
145
- case 'StoryFragment': {
146
- const sf = node as StoryFragmentNode;
147
- if (!isPreview) getCtx(props).hasTitle.set(!(!sf.slug || !sf.title));
69
+ // 1. Root Types (StoryFragment & Pane) - handled with specific early returns or complex wrapping
70
+ if (node.nodeType === 'StoryFragment') {
71
+ const sf = node as StoryFragmentNode;
72
+ if (!isPreview) ctx.hasTitle.set(!(!sf.slug || !sf.title));
148
73
 
74
+ if (!(sf.slug && sf.title)) {
149
75
  return (
150
- <>
151
- {!(sf.slug && sf.title) ? (
76
+ <div
77
+ className="fixed inset-0 overflow-y-auto bg-black bg-opacity-75"
78
+ style={{ zIndex: 9005 }}
79
+ >
80
+ <div className="flex min-h-screen items-center justify-center p-1.5">
152
81
  <div
153
- className="fixed inset-0 overflow-y-auto bg-black bg-opacity-75"
154
- style={{ zIndex: 9005 }}
155
- >
156
- <div className="flex min-h-screen items-center justify-center p-1.5">
157
- <div
158
- className="fixed inset-0 bg-black opacity-65"
159
- onClick={() => (window.location.href = '/storykeep')}
160
- />
161
-
162
- <div className="relative w-full max-w-4xl rounded-lg bg-white shadow-xl">
163
- <div className="absolute right-4 top-4 z-10">
164
- <button
165
- onClick={() => (window.location.href = '/storykeep')}
166
- className="flex h-10 w-10 items-center justify-center rounded-full bg-white bg-opacity-90 shadow-lg transition-all duration-200 hover:bg-opacity-100"
167
- title="Cancel and return to StoryKeep"
168
- aria-label="Cancel and return to StoryKeep"
169
- >
170
- <svg
171
- width="24"
172
- height="24"
173
- viewBox="0 0 24 24"
174
- fill="none"
175
- stroke="currentColor"
176
- strokeWidth="2"
177
- strokeLinecap="round"
178
- strokeLinejoin="round"
179
- >
180
- <line x1="18" y1="6" x2="6" y2="18" />
181
- <line x1="6" y1="6" x2="18" y2="18" />
182
- </svg>
183
- </button>
184
- </div>
185
-
186
- <div className="p-6">
187
- <StoryFragmentTitlePanel nodeId={props.nodeId} />
188
- </div>
189
- </div>
82
+ className="fixed inset-0 bg-black opacity-65"
83
+ onClick={() => (window.location.href = '/storykeep')}
84
+ />
85
+ <div className="relative w-full max-w-4xl rounded-lg bg-white shadow-xl">
86
+ <div className="p-6">
87
+ <StoryFragmentTitlePanel nodeId={props.nodeId} />
190
88
  </div>
191
89
  </div>
192
- ) : !hasPanes && sf.slug && sf.title && !isPreview ? (
193
- <EmptyPageHandler {...sharedProps} />
194
- ) : (
195
- <>
196
- <PanelVisibilityWrapper
197
- nodeId={props.nodeId}
198
- panelType="storyfragment"
199
- ctx={getCtx(props)}
200
- >
201
- <StoryFragmentConfigPanel
202
- nodeId={props.nodeId}
203
- isSandboxMode={props.isSandboxMode || false}
204
- />
205
- </PanelVisibilityWrapper>
206
- <StoryFragment {...sharedProps} />
207
- </>
208
- )}
209
- </>
90
+ </div>
91
+ </div>
210
92
  );
211
93
  }
212
94
 
213
- case 'Pane': {
214
- const paneNodes = getCtx(props).getChildNodeIDs(node.id);
215
- const paneNode = node as PaneNode;
216
- if (paneNode.isContextPane) {
217
- if (!isPreview)
218
- getCtx(props).hasTitle.set(!(!paneNode.slug || !paneNode.title));
95
+ if (!hasPanes && !isPreview) {
96
+ return <EmptyPageHandler {...props} />;
97
+ }
98
+
99
+ return (
100
+ <>
101
+ <StoryFragmentConfigPanel
102
+ nodeId={props.nodeId}
103
+ isSandboxMode={props.isSandboxMode || false}
104
+ />
105
+ <StoryFragment {...props} />
106
+ </>
107
+ );
108
+ } else if (isPaneNode(node)) {
109
+ const paneNode = node as PaneNode;
110
+ const isHtmlAstPane = !!paneNode.htmlAst;
111
+ const paneNodes = ctx.getChildNodeIDs(node.id);
112
+
113
+ if (paneNode.isContextPane) {
114
+ if (!isPreview) ctx.hasTitle.set(!(!paneNode.slug || !paneNode.title));
115
+
116
+ if (!isPreview && !(paneNode.slug && paneNode.title)) {
219
117
  return (
220
- <>
221
- {!isPreview && !(paneNode.slug && paneNode.title) ? (
222
- <div
223
- className="fixed inset-0 overflow-y-auto bg-black bg-opacity-75"
224
- style={{ zIndex: 9005 }}
225
- >
226
- <div className="flex min-h-screen items-center justify-center p-1.5">
227
- <div className="relative w-full max-w-4xl rounded-lg bg-white shadow-xl">
228
- <div className="p-6">
229
- <ContextPaneTitlePanel nodeId={props.nodeId} />
230
- </div>
231
- </div>
118
+ <div
119
+ className="fixed inset-0 overflow-y-auto bg-black bg-opacity-75"
120
+ style={{ zIndex: 9005 }}
121
+ >
122
+ <div className="flex min-h-screen items-center justify-center p-1.5">
123
+ <div className="relative w-full max-w-4xl rounded-lg bg-white shadow-xl">
124
+ <div className="p-6">
125
+ <ContextPaneTitlePanel nodeId={props.nodeId} />
232
126
  </div>
233
127
  </div>
234
- ) : !isPreview ? (
235
- <ContextPanePanel nodeId={node.id} />
236
- ) : null}
237
- <div>
238
- {toolModeVal === 'designLibrary' ? (
239
- <Pane_DesignLibrary {...sharedProps} />
240
- ) : (
241
- <Pane {...sharedProps} />
242
- )}
243
- {!isPreview &&
244
- paneNode.slug &&
245
- paneNode.title &&
246
- paneNodes.length === 0 && (
247
- <PanelVisibilityWrapper
248
- nodeId={node.id}
249
- panelType="add"
250
- ctx={getCtx(props)}
251
- >
252
- <AddPanePanel
253
- nodeId={node.id}
254
- first={true}
255
- ctx={getCtx(props)}
256
- isContextPane={true}
257
- />
258
- </PanelVisibilityWrapper>
259
- )}
260
128
  </div>
261
- </>
129
+ </div>
262
130
  );
263
131
  }
264
132
 
265
- const storyFragmentId = getCtx(props).getClosestNodeTypeFromId(
266
- node.id,
267
- 'StoryFragment'
268
- );
269
- const storyFragment = getCtx(props)
270
- .allNodes.get()
271
- .get(storyFragmentId) as StoryFragmentNode;
272
- const firstPane =
273
- storyFragment?.paneIds?.length && storyFragment.paneIds[0];
274
- if (isPreview) return <Pane {...sharedProps} />;
275
- return (
276
- <>
277
- {storyFragment && firstPane === node.id && (
278
- <PanelVisibilityWrapper
279
- nodeId={`${node.id}-0`}
280
- panelType="add"
281
- ctx={getCtx(props)}
282
- >
283
- <AddPanePanel nodeId={node.id} first={true} ctx={getCtx(props)} />
133
+ if (!isPreview && !paneNodes.length) {
134
+ // Context Pane Empty State
135
+ return (
136
+ <>
137
+ <ContextPanePanel nodeId={node.id} />
138
+ <PanelVisibilityWrapper nodeId={node.id} panelType="add" ctx={ctx}>
139
+ <Pane {...props} />
284
140
  </PanelVisibilityWrapper>
285
- )}
286
- <div className="py-0.5">
287
- <PanelVisibilityWrapper
141
+ <AddPanePanel
288
142
  nodeId={node.id}
289
- panelType="settings"
290
- ctx={getCtx(props)}
291
- >
292
- <ConfigPanePanel nodeId={node.id} />
293
- </PanelVisibilityWrapper>
294
- {toolModeVal === `eraser` ? (
295
- <PaneEraser {...sharedProps} />
296
- ) : toolModeVal === `layout` ? (
297
- <PaneLayout {...sharedProps} />
298
- ) : toolModeVal === 'designLibrary' ? (
299
- <Pane_DesignLibrary {...sharedProps} />
300
- ) : (
301
- <Pane {...sharedProps} />
302
- )}
303
- </div>
304
- <PanelVisibilityWrapper
305
- nodeId={node.id}
306
- panelType="add"
307
- ctx={getCtx(props)}
308
- >
309
- <AddPanePanel nodeId={node.id} first={false} ctx={getCtx(props)} />
310
- </PanelVisibilityWrapper>
311
- </>
312
- );
313
- }
314
-
315
- case 'BgPane':
316
- return <BgPaneWrapper {...sharedProps} />;
317
- case 'GridLayoutNode': {
318
- if (toolModeVal === 'eraser') {
319
- return <GridLayoutEraser {...sharedProps} />;
320
- }
321
- return <GridLayout {...sharedProps} />;
322
- }
323
- case 'TagElement':
324
- return <TagElement {...sharedProps} />;
325
- // tag elements
326
- case 'h2':
327
- case 'h3':
328
- case 'h4':
329
- case 'ol':
330
- case 'ul':
331
- case 'li':
332
- case 'aside':
333
- case 'p': {
334
- if (toolModeVal === 'styles') {
335
- let className = getCtx(props).getNodeClasses(
336
- node.id,
337
- viewportKeyStore.get().value
338
- );
339
- if (settingsPanel?.nodeId === node.id) {
340
- className +=
341
- ' outline-4 outline-dotted outline-orange-400 outline-offset-2';
342
- }
343
-
344
- const handleElementClick = (e: MouseEvent<HTMLElement>) => {
345
- e.stopPropagation();
346
- const { isActive } = selectionStore.get();
347
- if (isActive) {
348
- // A drag just finished. The user's intent was to select text.
349
- // Do NOT open the panel.
350
- return;
351
- }
352
- // else
353
- handleClickEventDefault(node as FlatNode, true);
354
- };
355
-
356
- const handleMouseDown = (e: MouseEvent<HTMLElement>) => {
357
- if (VERBOSE)
358
- console.log('[Node.tsx] handleMouseDown FIRED', { event: e });
359
- if (!props.onDragStart) {
360
- if (VERBOSE)
361
- console.log(
362
- '[Node.tsx] handleMouseDown ABORTED: no onDragStart prop'
363
- );
364
- return;
365
- }
366
- e.preventDefault();
367
- if (VERBOSE) console.log('[Node.tsx] preventDefault called');
368
-
369
- const target = e.target as HTMLElement;
370
- if (VERBOSE) console.log('[Node.tsx] mousedown target:', target);
371
- const textNodeElement = target.closest('[data-parent-text-node-id]');
372
-
373
- if (textNodeElement) {
374
- const parentTextNodeId = textNodeElement.getAttribute(
375
- 'data-parent-text-node-id'
376
- );
377
- const startCharOffset = parseInt(
378
- textNodeElement.getAttribute('data-start-char-offset') || '0',
379
- 10
380
- );
381
-
382
- if (parentTextNodeId) {
383
- const origin: SelectionOrigin = {
384
- blockNodeId: node.id,
385
- lcaNodeId: node.id,
386
- startNodeId: parentTextNodeId,
387
- startCharOffset: startCharOffset,
388
- endNodeId: parentTextNodeId,
389
- endCharOffset: startCharOffset,
390
- };
391
- props.onDragStart(origin, e);
392
- }
393
- }
394
- };
395
-
396
- const children = getCtx(props).getChildNodeIDs(node.id);
397
-
398
- // Propagate props to children, but explicitly enable text selection
399
- // and remove the onDragStart handler to prevent it from firing on child elements.
400
- const childProps: NodeProps = {
401
- ...props,
402
- onDragStart: undefined,
403
- isSelectableText: true,
404
- };
405
-
406
- return createElement(
407
- type,
408
- {
409
- className: className,
410
- onMouseDown: handleMouseDown,
411
- onClick: handleElementClick,
412
- 'data-node-id': node.id,
413
- style: { userSelect: 'none' },
414
- },
415
- <RenderChildren children={children} nodeProps={childProps} />
143
+ first={true}
144
+ ctx={ctx}
145
+ isContextPane={true}
146
+ />
147
+ </>
416
148
  );
417
149
  }
418
-
419
- if (toolModeVal === `insert`)
420
- return <NodeBasicTagInsert {...sharedProps} tagName={type} />;
421
- else if (toolModeVal === `eraser`)
422
- return <NodeBasicTagEraser {...sharedProps} tagName={type} />;
423
- else if (toolModeVal === `move`)
424
- return <NodeBasicTagSettings {...sharedProps} tagName={type} />;
425
- return <NodeBasicTag {...sharedProps} tagName={type} />;
426
150
  }
427
151
 
428
- case 'span':
429
- case 'strong':
430
- case 'em':
431
- return <NodeBasicTag {...sharedProps} tagName={type} />;
152
+ // Resolve Content
153
+ const content = isHtmlAstPane ? (
154
+ <CreativePane nodeId={props.nodeId} htmlAst={paneNode.htmlAst!} />
155
+ ) : (
156
+ <Pane {...props} />
157
+ );
432
158
 
433
- case 'text':
434
- return <NodeText {...sharedProps} />;
435
- case 'button': {
436
- if (toolModeVal === `eraser`)
437
- return <NodeButtonEraser {...sharedProps} />;
438
- return <NodeButton {...sharedProps} isSelectableText={false} />;
439
- }
440
- case 'a': {
441
- if (toolModeVal === `eraser`) return <NodeAEraser {...sharedProps} />;
442
- return <NodeA {...sharedProps} isSelectableText={false} />;
443
- }
444
- case 'img':
445
- return <NodeImg {...sharedProps} />;
446
- case 'code': {
447
- const hookData = parseCodeHook(node);
448
- return hookData ? <Widget {...hookData} {...sharedProps} /> : <></>;
449
- }
450
- case 'impression':
451
- return <></>;
452
- default:
453
- console.warn(`Node.tsx miss on ${type}`, node);
454
- return <></>;
159
+ element = (
160
+ <>
161
+ <div className="py-0.5">
162
+ <ConfigPanePanel
163
+ nodeId={props.nodeId}
164
+ isHtmlAstPane={isHtmlAstPane}
165
+ />
166
+ <PanelVisibilityWrapper
167
+ nodeId={props.nodeId}
168
+ panelType="settings"
169
+ ctx={ctx}
170
+ >
171
+ {content}
172
+ </PanelVisibilityWrapper>
173
+ </div>
174
+ <AddPanePanel nodeId={props.nodeId} first={false} ctx={ctx} />
175
+ </>
176
+ );
455
177
  }
456
- };
457
-
458
- const Node = memo((props: NodeProps) => {
459
- const node = getCtx(props).allNodes.get().get(props.nodeId) as FlatNode;
460
- const isPreview = getCtx(props).rootNodeId.get() === `tmp`;
461
- const settingsPanel = useStore(settingsPanelStore);
462
- const toolModeVal = getCtx(props).toolModeValStore.get().value;
463
-
464
- const {
465
- markdownParentId,
466
- tagName: styleTagName,
467
- overrideNodeId,
468
- } = useStore(styleElementInfoStore, {
469
- keys: ['markdownParentId', 'tagName', 'overrideNodeId'],
470
- });
471
-
472
- // Subscribe to edit lock state for this node
473
- const [isEditLocked, setIsEditLocked] = useState(false);
474
-
475
- useEffect(() => {
476
- // Check initial state
477
- setIsEditLocked(getCtx(props).isEditLocked(props.nodeId));
478
-
479
- // Subscribe to changes in edit lock
480
- const unsubscribe = getCtx(props).editingNodeId.subscribe((editingId) => {
481
- setIsEditLocked(editingId === props.nodeId);
482
- });
483
-
484
- return () => unsubscribe();
485
- }, [props.nodeId]);
486
-
487
- useEffect(() => {
488
- // Only subscribe to notifications if not edit-locked
489
- if (!isEditLocked) {
490
- const unsubscribe = getCtx(props).notifications.subscribe(
491
- props.nodeId,
492
- () => {}
493
- );
494
- return () => unsubscribe();
495
- }
496
- }, [props.nodeId, isEditLocked]);
497
-
498
- const nodeTagName = node?.tagName || '';
499
- const isBlockTag = ['h2', 'h3', 'h4', 'ol', 'ul', 'li', 'p'].includes(
500
- nodeTagName
501
- );
502
-
503
- const parentNode = node?.parentId
504
- ? getCtx(props).allNodes.get().get(node.parentId)
505
- : null;
506
- const isTopLevelBlock = isBlockTag && parentNode?.nodeType === 'Markdown';
507
-
508
- const closestMarkdownId = getCtx(props).getClosestNodeTypeFromId(
509
- props.nodeId,
510
- 'Markdown'
511
- );
512
- const isEditableMode = [`text`].includes(
513
- getCtx(props).toolModeValStore.get().value
514
- );
515
- const isStylesMode = [`styles`].includes(
516
- getCtx(props).toolModeValStore.get().value
517
- );
518
-
519
- const isHighlighted =
520
- isTopLevelBlock &&
521
- closestMarkdownId === markdownParentId &&
522
- nodeTagName === styleTagName &&
523
- !isEditableMode;
524
-
525
- const isOverride = overrideNodeId === props.nodeId;
526
-
527
- const highlightStyle = isHighlighted
528
- ? {
529
- outline: isOverride
530
- ? '3.5px dotted rgba(255, 165, 0, 0.85)'
531
- : '2.5px dashed rgba(0, 0, 0, 0.3)',
178
+ // 2. Content Types
179
+ else if (node.nodeType === 'BgPane') {
180
+ element = <BgPaneWrapper {...props} />;
181
+ } else if (node.nodeType === 'Markdown') {
182
+ element = <Markdown {...props} />;
183
+ } else if (isGridLayoutNode(node)) {
184
+ element = <GridLayout {...props} />;
185
+ } else if (node.nodeType === 'TagElement') {
186
+ const flatNode = node as FlatNode;
187
+
188
+ switch (flatNode.tagName) {
189
+ case 'text':
190
+ element = <NodeText {...props} />;
191
+ break;
192
+ case 'img':
193
+ element = <NodeImg {...props} />;
194
+ break;
195
+ case 'a':
196
+ element = <NodeA {...props} />;
197
+ break;
198
+ case 'button':
199
+ element = <NodeButton {...props} />;
200
+ break;
201
+ case 'code': {
202
+ const hookData = parseCodeHook(node);
203
+ element = hookData ? (
204
+ <Widget
205
+ {...props}
206
+ hook={hookData.hook}
207
+ value1={hookData.value1}
208
+ value2={hookData.value2}
209
+ value3={hookData.value3}
210
+ />
211
+ ) : null;
212
+ break;
532
213
  }
533
- : {};
534
- const hoverClasses = isStylesMode
535
- ? 'hover:outline hover:outline-2 hover:outline-black'
536
- : '';
537
-
538
- const element = getElement(node, props);
539
- const isPanelActive =
540
- settingsPanel?.action === 'style-parent' &&
541
- settingsPanel?.nodeId === props.nodeId;
542
- const isStyleableContainer =
543
- node?.nodeType === 'Markdown' || isGridLayoutNode(node);
544
-
545
- if (isPanelActive && isStyleableContainer) {
546
- const highlightStyle = {
547
- outline: '3.5px dotted rgba(255, 165, 0, 0.85)',
548
- };
549
- return <div style={highlightStyle}>{element}</div>;
214
+ default:
215
+ element = (
216
+ <NodeBasicTag
217
+ {...props}
218
+ tagName={flatNode.tagName as keyof JSX.IntrinsicElements}
219
+ isSelectableText={true}
220
+ />
221
+ );
222
+ break;
223
+ }
550
224
  }
551
225
 
552
- if (!isPreview && toolModeVal === `debug`) {
553
- return <NodeWithGuid {...props} element={element} />;
554
- }
226
+ // 3. Apply Overlay if addressable
227
+ if (element && isAddressableNode(node, ctx)) {
228
+ const isInline = getNodeDisplayMode(node, viewport, ctx);
229
+ const isTopLevel = isTopLevelBlockNode(node, ctx);
555
230
 
556
- if (isTopLevelBlock) {
557
231
  return (
558
- <div className={hoverClasses} style={highlightStyle}>
232
+ <NodeOverlay isInline={isInline} isTopLevel={isTopLevel} {...props}>
559
233
  {element}
560
- </div>
234
+ </NodeOverlay>
561
235
  );
562
236
  }
563
237
 
564
- return element;
238
+ return <>{element}</>;
565
239
  });
566
-
567
- export default Node;