astro-tractstack 2.0.17 → 2.0.19
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.
- package/dist/index.js +18 -0
- package/package.json +1 -1
- package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +1 -1
- package/templates/src/components/codehooks/ListContentSetup.tsx +1 -1
- package/templates/src/components/compositor/Compositor.tsx +1 -0
- package/templates/src/components/compositor/Node.tsx +41 -17
- package/templates/src/components/compositor/nodes/GhostInsertBlock.tsx +9 -6
- package/templates/src/components/compositor/nodes/GridLayout.tsx +124 -0
- package/templates/src/components/compositor/nodes/GridLayout_eraser.tsx +33 -0
- package/templates/src/components/compositor/nodes/Markdown.tsx +67 -37
- package/templates/src/components/compositor/nodes/Markdown_eraser.tsx +56 -0
- package/templates/src/components/compositor/preview/FeaturedArticlePreview.tsx +8 -2
- package/templates/src/components/edit/PanelSwitch.tsx +232 -75
- package/templates/src/components/edit/SettingsPanel.tsx +0 -1
- package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +3 -3
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +402 -167
- package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +2 -2
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -7
- package/templates/src/components/edit/pane/PanePanel_impression.tsx +1 -1
- package/templates/src/components/edit/pane/RestylePaneModal.tsx +8 -5
- package/templates/src/components/edit/pane/steps/AiDesignStep.tsx +6 -6
- package/templates/src/components/edit/pane/steps/CopyInputStep.tsx +3 -3
- package/templates/src/components/edit/pane/steps/DesignLibraryStep.tsx +4 -4
- package/templates/src/components/edit/panels/StyleElementPanel.tsx +11 -4
- package/templates/src/components/edit/panels/StyleElementPanel_add.tsx +8 -8
- package/templates/src/components/edit/panels/StyleElementPanel_remove.tsx +14 -4
- package/templates/src/components/edit/panels/StyleElementPanel_update.tsx +16 -4
- package/templates/src/components/edit/panels/StyleImagePanel.tsx +7 -3
- package/templates/src/components/edit/panels/StyleImagePanel_add.tsx +9 -2
- package/templates/src/components/edit/panels/StyleImagePanel_remove.tsx +5 -2
- package/templates/src/components/edit/panels/StyleImagePanel_update.tsx +5 -2
- package/templates/src/components/edit/panels/StyleLiElementPanel.tsx +7 -3
- package/templates/src/components/edit/panels/StyleLiElementPanel_add.tsx +9 -2
- package/templates/src/components/edit/panels/StyleLiElementPanel_remove.tsx +5 -2
- package/templates/src/components/edit/panels/StyleLiElementPanel_update.tsx +5 -2
- package/templates/src/components/edit/panels/StyleParentPanel.tsx +530 -171
- package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +77 -42
- package/templates/src/components/edit/panels/StyleParentPanel_deleteLayer.tsx +38 -22
- package/templates/src/components/edit/panels/StyleParentPanel_remove.tsx +171 -66
- package/templates/src/components/edit/panels/StyleParentPanel_update.tsx +166 -98
- package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +7 -3
- package/templates/src/components/edit/panels/StyleWidgetPanel_add.tsx +9 -2
- package/templates/src/components/edit/panels/StyleWidgetPanel_remove.tsx +5 -2
- package/templates/src/components/edit/panels/StyleWidgetPanel_update.tsx +6 -2
- package/templates/src/components/edit/state/SaveModal.tsx +10 -2
- package/templates/src/components/edit/state/SaveToLibraryModal.tsx +6 -6
- package/templates/src/components/fields/PaneBreakShapeSelector.tsx +1 -1
- package/templates/src/components/widgets/ImpressionWrapper.tsx +4 -1
- package/templates/src/constants/prompts.json +23 -2
- package/templates/src/stores/nodes.ts +356 -212
- package/templates/src/stores/storykeep.ts +3 -1
- package/templates/src/types/compositorTypes.ts +56 -3
- package/templates/src/types/tractstack.ts +1 -0
- package/templates/src/utils/compositor/TemplateNodes.ts +8 -0
- package/templates/src/utils/compositor/aiPaneParser.ts +263 -83
- package/templates/src/utils/compositor/designLibraryHelper.ts +12 -9
- package/templates/src/utils/compositor/nodesHelper.ts +229 -0
- package/templates/src/utils/compositor/reduceNodesClassNames.ts +40 -1
- package/templates/src/utils/compositor/typeGuards.ts +7 -0
- package/templates/src/utils/etl/extractor.ts +1 -5
- package/templates/src/utils/etl/index.ts +1 -0
- package/templates/src/utils/etl/transformer.ts +70 -25
- package/utils/inject-files.ts +18 -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"
|
package/package.json
CHANGED
|
@@ -204,7 +204,7 @@ const FeaturedArticleSetup = ({
|
|
|
204
204
|
|
|
205
205
|
if (!isPanelOpen) {
|
|
206
206
|
return (
|
|
207
|
-
<div className="flex min-h-
|
|
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-
|
|
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 {
|
|
12
|
-
|
|
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:
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
17
|
-
const node =
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 =
|
|
72
|
+
const childNodeIds = ctx.getChildNodeIDs(parentPaneId);
|
|
48
73
|
return childNodeIds
|
|
49
|
-
.map((
|
|
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(
|
|
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 &&
|
|
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 &&
|
|
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
|
-
|
|
153
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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={
|
|
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
|
|
204
|
+
return (
|
|
205
|
+
<div className={gridClassName} style={{ position: 'relative', zIndex: 10 }}>
|
|
206
|
+
{nodesToRender}
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
179
209
|
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { getCtx } from '@/stores/nodes';
|
|
2
|
+
import { Markdown } from './Markdown';
|
|
3
|
+
import { isGridLayoutNode } from '@/utils/compositor/typeGuards';
|
|
4
|
+
import { revertFromGrid } from '@/utils/compositor/nodesHelper';
|
|
5
|
+
import type { NodeProps } from '@/types/nodeProps';
|
|
6
|
+
import type { MouseEvent } from 'react';
|
|
7
|
+
|
|
8
|
+
export function MarkdownEraser(props: NodeProps) {
|
|
9
|
+
const ctx = getCtx(props);
|
|
10
|
+
|
|
11
|
+
const handleClick = (e: MouseEvent<HTMLDivElement>) => {
|
|
12
|
+
e.stopPropagation();
|
|
13
|
+
e.preventDefault();
|
|
14
|
+
|
|
15
|
+
const node = ctx.allNodes.get().get(props.nodeId);
|
|
16
|
+
if (!node || !node.parentId) return;
|
|
17
|
+
|
|
18
|
+
const parentNode = ctx.allNodes.get().get(node.parentId);
|
|
19
|
+
if (!parentNode || !isGridLayoutNode(parentNode)) {
|
|
20
|
+
ctx.deleteNode(props.nodeId);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const children = ctx.getChildNodeIDs(parentNode.id);
|
|
25
|
+
const columnCount = children.length;
|
|
26
|
+
|
|
27
|
+
if (columnCount === 1) {
|
|
28
|
+
if (
|
|
29
|
+
window.confirm(
|
|
30
|
+
'This is the last column. Do you want to delete it and revert to a standard pane layout?'
|
|
31
|
+
)
|
|
32
|
+
) {
|
|
33
|
+
revertFromGrid(parentNode.id);
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
if (
|
|
37
|
+
window.confirm(
|
|
38
|
+
`Are you sure you want to delete this column? ${
|
|
39
|
+
columnCount - 1
|
|
40
|
+
} columns will remain.`
|
|
41
|
+
)
|
|
42
|
+
) {
|
|
43
|
+
ctx.deleteNode(props.nodeId);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
className="outline-dashed outline-2 outline-red-500 hover:bg-red-500/10"
|
|
51
|
+
onClick={handleClick}
|
|
52
|
+
>
|
|
53
|
+
<Markdown {...props} />
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -120,7 +120,10 @@ const FeaturedArticlePreview = ({ story }: FeaturedArticlePreviewProps) => {
|
|
|
120
120
|
|
|
121
121
|
{/* This is the visible UI that reacts to state changes. */}
|
|
122
122
|
{isLoading && (
|
|
123
|
-
<div
|
|
123
|
+
<div
|
|
124
|
+
style={{ aspectRatio: '4 / 3' }}
|
|
125
|
+
className="flex w-full items-center justify-center rounded-lg bg-gray-200"
|
|
126
|
+
>
|
|
124
127
|
<div className="h-12 w-12 animate-spin rounded-full border-4 border-solid border-cyan-600 border-t-transparent"></div>
|
|
125
128
|
</div>
|
|
126
129
|
)}
|
|
@@ -142,7 +145,10 @@ const FeaturedArticlePreview = ({ story }: FeaturedArticlePreviewProps) => {
|
|
|
142
145
|
className="h-auto w-full rounded-lg object-cover shadow-lg"
|
|
143
146
|
/>
|
|
144
147
|
) : (
|
|
145
|
-
<div
|
|
148
|
+
<div
|
|
149
|
+
style={{ aspectRatio: '4 / 3' }}
|
|
150
|
+
className="flex w-full items-center justify-center rounded-lg bg-red-50 p-4 text-center text-sm text-red-700"
|
|
151
|
+
>
|
|
146
152
|
Could not display preview.
|
|
147
153
|
</div>
|
|
148
154
|
)}
|