astro-tractstack 2.0.8 → 2.0.10
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 +4 -6
- package/package.json +1 -1
- package/templates/css/custom.css +0 -6
- package/templates/src/components/codehooks/EpinetDurationSelector.tsx +1 -1
- package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +2 -1
- package/templates/src/components/codehooks/ProductGridSetup.tsx +4 -4
- package/templates/src/components/compositor/Compositor.tsx +335 -16
- package/templates/src/components/compositor/Node.tsx +86 -6
- package/templates/src/components/compositor/nodes/RenderChildren.tsx +3 -6
- package/templates/src/components/compositor/nodes/tagElements/NodeA.tsx +2 -1
- package/templates/src/components/compositor/nodes/tagElements/NodeAnchorComponent.tsx +11 -19
- package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag.tsx +120 -6
- package/templates/src/components/compositor/nodes/tagElements/NodeButton.tsx +1 -1
- package/templates/src/components/compositor/nodes/tagElements/NodeText.tsx +78 -8
- package/templates/src/components/edit/SettingsPanel.tsx +1 -1
- package/templates/src/components/edit/ToolMode.tsx +93 -22
- package/templates/src/components/edit/pane/AddPanePanel_break.tsx +2 -1
- package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +2 -1
- package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +1 -1
- package/templates/src/components/edit/pane/PageGen_preview.tsx +2 -1
- package/templates/src/components/edit/panels/StyleElementPanel_update.tsx +9 -5
- package/templates/src/components/edit/state/SaveModal.tsx +84 -14
- package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +2 -2
- package/templates/src/components/search/SearchModal.tsx +2 -1
- package/templates/src/components/search/SearchResults.tsx +2 -1
- package/templates/src/components/search/SearchWrapper.tsx +1 -1
- package/templates/src/components/storykeep/Dashboard_Analytics.tsx +1 -1
- package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +3 -5
- package/templates/src/components/storykeep/controls/content/BeliefTable.tsx +1 -1
- package/templates/src/components/storykeep/controls/content/MenuTable.tsx +1 -1
- package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +1 -1
- package/templates/src/components/widgets/ImpressionWrapper.tsx +1 -1
- package/templates/src/hooks/useFormState.ts +3 -4
- package/templates/src/stores/nodes.ts +813 -19
- package/templates/src/stores/selection.ts +41 -0
- package/templates/src/types/compositorTypes.ts +1 -0
- package/templates/src/types/nodeProps.ts +12 -0
- package/templates/src/utils/compositor/nodesHelper.ts +2 -2
- package/utils/inject-files.ts +4 -6
- package/templates/src/components/compositor/elements/PlayButton.tsx +0 -19
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NodeAnchorComponent } from './NodeAnchorComponent';
|
|
2
2
|
import type { NodeProps } from '@/types/nodeProps';
|
|
3
3
|
|
|
4
|
-
export const NodeA = (props: NodeProps) =>
|
|
4
|
+
export const NodeA = (props: NodeProps) =>
|
|
5
|
+
NodeAnchorComponent({ ...props, isSelectableText: false }, 'a');
|
|
@@ -2,7 +2,6 @@ import { useEffect, useRef, type RefObject, type MouseEvent } from 'react';
|
|
|
2
2
|
import { getCtx } from '@/stores/nodes';
|
|
3
3
|
import { viewportKeyStore } from '@/stores/storykeep';
|
|
4
4
|
import { RenderChildren } from '../RenderChildren';
|
|
5
|
-
import { PlayButton } from '@/components/compositor/elements/PlayButton';
|
|
6
5
|
import type { FlatNode } from '@/types/compositorTypes';
|
|
7
6
|
import type { NodeProps } from '@/types/nodeProps';
|
|
8
7
|
|
|
@@ -13,11 +12,12 @@ export const NodeAnchorComponent = (props: NodeProps, tagName: string) => {
|
|
|
13
12
|
const childNodeIDs = ctx.getChildNodeIDs(node?.parentId ?? '');
|
|
14
13
|
const linkRef = useRef<HTMLAnchorElement | HTMLButtonElement>(null);
|
|
15
14
|
|
|
16
|
-
//
|
|
17
|
-
const isVideo = !!node.buttonPayload?.bunnyPayload;
|
|
18
|
-
|
|
19
|
-
// Get previous and next siblings for spacing logic
|
|
15
|
+
// Get current position and next sibling for spacing logic
|
|
20
16
|
const currentIndex = childNodeIDs.indexOf(nodeId);
|
|
17
|
+
|
|
18
|
+
// Determine if a leading zero-width space is needed when this is the first child.
|
|
19
|
+
const needsLeadingSpace = currentIndex === 0;
|
|
20
|
+
|
|
21
21
|
const nextNode =
|
|
22
22
|
currentIndex < childNodeIDs.length - 1
|
|
23
23
|
? (ctx.allNodes.get().get(childNodeIDs[currentIndex + 1]) as FlatNode)
|
|
@@ -188,12 +188,15 @@ export const NodeAnchorComponent = (props: NodeProps, tagName: string) => {
|
|
|
188
188
|
const isEditMode = [`text`].includes(ctx.toolModeValStore.get().value);
|
|
189
189
|
|
|
190
190
|
// Create appropriate element based on tagName
|
|
191
|
+
let baseClasses = ctx.getNodeClasses(nodeId, viewportKeyStore.get().value);
|
|
192
|
+
baseClasses += ' outline outline-1 outline-dotted outline-gray-400/60';
|
|
191
193
|
if (tagName === 'a') {
|
|
192
194
|
return (
|
|
193
195
|
<>
|
|
196
|
+
{needsLeadingSpace && '\u200B'}
|
|
194
197
|
<a
|
|
195
198
|
ref={linkRef as RefObject<HTMLAnchorElement>}
|
|
196
|
-
className={
|
|
199
|
+
className={baseClasses}
|
|
197
200
|
href={node.href}
|
|
198
201
|
onClick={handleClick}
|
|
199
202
|
onDoubleClick={handleDoubleClick}
|
|
@@ -205,12 +208,6 @@ export const NodeAnchorComponent = (props: NodeProps, tagName: string) => {
|
|
|
205
208
|
children={ctx.getChildNodeIDs(nodeId)}
|
|
206
209
|
nodeProps={props}
|
|
207
210
|
/>
|
|
208
|
-
{isVideo && (
|
|
209
|
-
<>
|
|
210
|
-
{` `}
|
|
211
|
-
<PlayButton />
|
|
212
|
-
</>
|
|
213
|
-
)}
|
|
214
211
|
</a>
|
|
215
212
|
{needsTrailingSpace && ' '}
|
|
216
213
|
</>
|
|
@@ -218,9 +215,10 @@ export const NodeAnchorComponent = (props: NodeProps, tagName: string) => {
|
|
|
218
215
|
} else {
|
|
219
216
|
return (
|
|
220
217
|
<>
|
|
218
|
+
{needsLeadingSpace && '\u200B'}
|
|
221
219
|
<button
|
|
222
220
|
ref={linkRef as RefObject<HTMLButtonElement>}
|
|
223
|
-
className={
|
|
221
|
+
className={baseClasses}
|
|
224
222
|
onClick={handleClick}
|
|
225
223
|
onDoubleClick={handleDoubleClick}
|
|
226
224
|
data-editable-button="true"
|
|
@@ -234,12 +232,6 @@ export const NodeAnchorComponent = (props: NodeProps, tagName: string) => {
|
|
|
234
232
|
children={ctx.getChildNodeIDs(nodeId)}
|
|
235
233
|
nodeProps={props}
|
|
236
234
|
/>
|
|
237
|
-
{isVideo && (
|
|
238
|
-
<>
|
|
239
|
-
{` `}
|
|
240
|
-
<PlayButton />
|
|
241
|
-
</>
|
|
242
|
-
)}
|
|
243
235
|
</button>
|
|
244
236
|
{needsTrailingSpace && ' '}
|
|
245
237
|
</>
|
|
@@ -9,8 +9,15 @@ import {
|
|
|
9
9
|
useState,
|
|
10
10
|
createElement,
|
|
11
11
|
} from 'react';
|
|
12
|
+
import { useStore } from '@nanostores/react';
|
|
13
|
+
import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
|
|
14
|
+
import PaintBrushIcon from '@heroicons/react/24/outline/PaintBrushIcon';
|
|
12
15
|
import { getCtx } from '@/stores/nodes';
|
|
13
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
viewportKeyStore,
|
|
18
|
+
isEditingStore,
|
|
19
|
+
settingsPanelStore,
|
|
20
|
+
} from '@/stores/storykeep';
|
|
14
21
|
import TabIndicator from './TabIndicator';
|
|
15
22
|
import { RenderChildren } from '../RenderChildren';
|
|
16
23
|
import {
|
|
@@ -32,13 +39,30 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
32
39
|
const ctx = getCtx(props);
|
|
33
40
|
const Tag = ctx.showGuids.get() ? `div` : props.tagName;
|
|
34
41
|
|
|
35
|
-
|
|
42
|
+
if (props.tagName === 'span') {
|
|
43
|
+
const node = ctx.allNodes.get().get(props.nodeId);
|
|
44
|
+
const children = ctx.parentNodes.get().get(props.nodeId);
|
|
45
|
+
|
|
46
|
+
if (VERBOSE)
|
|
47
|
+
console.log(
|
|
48
|
+
'%c[NodeBasicTag] RENDERING SPAN',
|
|
49
|
+
'color: purple; font-weight: bold;',
|
|
50
|
+
{
|
|
51
|
+
nodeId: props.nodeId,
|
|
52
|
+
node: node ? cloneDeep(node) : 'NODE NOT FOUND',
|
|
53
|
+
childrenIds: children ? cloneDeep(children) : 'CHILDREN NOT FOUND',
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
36
58
|
const [editState, setEditState] = useState<EditState>('viewing');
|
|
37
59
|
const [showTabIndicator, setShowTabIndicator] = useState(false);
|
|
38
60
|
const elementRef = useRef<HTMLElement | null>(null);
|
|
39
61
|
const originalContentRef = useRef<string>('');
|
|
40
62
|
const cursorPositionRef = useRef<{ node: Node; offset: number } | null>(null);
|
|
41
63
|
|
|
64
|
+
const { value: toolModeVal } = useStore(ctx.toolModeValStore);
|
|
65
|
+
|
|
42
66
|
// Get node data
|
|
43
67
|
const node = ctx.allNodes.get().get(nodeId) as FlatNode;
|
|
44
68
|
const children = ctx.getChildNodeIDs(nodeId);
|
|
@@ -228,8 +252,83 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
228
252
|
}
|
|
229
253
|
}, [editState]);
|
|
230
254
|
|
|
231
|
-
// For formatting nodes and
|
|
232
|
-
if (['em', 'strong', '
|
|
255
|
+
// For formatting nodes <em> and <strong> and <span>
|
|
256
|
+
if (['em', 'strong', 'span'].includes(props.tagName)) {
|
|
257
|
+
const isEditorActive = toolModeVal === 'styles';
|
|
258
|
+
const handleStyleClick = (e: MouseEvent<HTMLButtonElement>) => {
|
|
259
|
+
e.preventDefault();
|
|
260
|
+
e.stopPropagation();
|
|
261
|
+
settingsPanelStore.set({
|
|
262
|
+
action: 'style-element',
|
|
263
|
+
nodeId: nodeId,
|
|
264
|
+
expanded: true,
|
|
265
|
+
});
|
|
266
|
+
};
|
|
267
|
+
const handleUnwrapClick = (e: MouseEvent<HTMLButtonElement>) => {
|
|
268
|
+
e.preventDefault();
|
|
269
|
+
e.stopPropagation();
|
|
270
|
+
ctx.unwrapNode(nodeId);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
let baseClasses = ctx.getNodeClasses(nodeId, viewportKeyStore.get().value);
|
|
274
|
+
baseClasses += ' outline outline-1 outline-dotted outline-gray-400/60';
|
|
275
|
+
|
|
276
|
+
return createElement(
|
|
277
|
+
Tag,
|
|
278
|
+
{
|
|
279
|
+
className: baseClasses,
|
|
280
|
+
onClick: (e: MouseEvent) => {
|
|
281
|
+
if (isEditableMode) {
|
|
282
|
+
ctx.setClickedNodeId(nodeId);
|
|
283
|
+
} else {
|
|
284
|
+
ctx.setClickedNodeId(nodeId);
|
|
285
|
+
e.stopPropagation();
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
'data-node-id': nodeId,
|
|
289
|
+
tabIndex: isEditableMode ? -1 : undefined,
|
|
290
|
+
style: {
|
|
291
|
+
position: isEditorActive ? 'relative' : undefined,
|
|
292
|
+
outlineOffset: '1px',
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
[
|
|
296
|
+
<RenderChildren key="children" children={children} nodeProps={props} />,
|
|
297
|
+
isEditorActive && (
|
|
298
|
+
<span
|
|
299
|
+
key="chip"
|
|
300
|
+
className="absolute z-10 flex select-none gap-x-1"
|
|
301
|
+
data-attr="exclude"
|
|
302
|
+
style={{ top: '-0.9rem', left: '0' }}
|
|
303
|
+
>
|
|
304
|
+
{props.tagName === 'span' && (
|
|
305
|
+
<button
|
|
306
|
+
type="button"
|
|
307
|
+
onClick={handleStyleClick}
|
|
308
|
+
className="flex h-4 w-4 items-center justify-center rounded-full bg-blue-100/90 text-blue-700 shadow-sm hover:bg-blue-300/50 focus:outline-none"
|
|
309
|
+
aria-label="Style selection"
|
|
310
|
+
data-attr="exclude"
|
|
311
|
+
>
|
|
312
|
+
<PaintBrushIcon className="h-3 w-3" data-attr="exclude" />
|
|
313
|
+
</button>
|
|
314
|
+
)}
|
|
315
|
+
<button
|
|
316
|
+
type="button"
|
|
317
|
+
onClick={handleUnwrapClick}
|
|
318
|
+
className="flex h-4 w-4 items-center justify-center rounded-full bg-gray-100/90 text-gray-700 shadow-sm hover:bg-gray-300/50 focus:outline-none"
|
|
319
|
+
aria-label="Remove formatting"
|
|
320
|
+
data-attr="exclude"
|
|
321
|
+
>
|
|
322
|
+
<XMarkIcon className="h-3.5 w-3.5" data-attr="exclude" />
|
|
323
|
+
</button>
|
|
324
|
+
</span>
|
|
325
|
+
),
|
|
326
|
+
]
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// For interactive elements like <a> and <button>
|
|
331
|
+
if (['a', 'button'].includes(props.tagName)) {
|
|
233
332
|
return createElement(
|
|
234
333
|
Tag,
|
|
235
334
|
{
|
|
@@ -267,7 +366,11 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
267
366
|
const saveAndExit = () => {
|
|
268
367
|
if (editState !== 'editing') return;
|
|
269
368
|
|
|
270
|
-
|
|
369
|
+
// Use an in-memory element to safely parse and strip UI chrome from the content.
|
|
370
|
+
const tempDiv = document.createElement('div');
|
|
371
|
+
tempDiv.innerHTML = elementRef.current?.innerHTML || '';
|
|
372
|
+
tempDiv.querySelectorAll('[data-attr="exclude"]');
|
|
373
|
+
const currentContent = tempDiv.innerHTML;
|
|
271
374
|
|
|
272
375
|
if (currentContent !== originalContentRef.current) {
|
|
273
376
|
try {
|
|
@@ -553,6 +656,10 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
553
656
|
const clickTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
554
657
|
|
|
555
658
|
const handleClick = (e: MouseEvent) => {
|
|
659
|
+
if (toolModeVal === 'styles') {
|
|
660
|
+
console.log(`skipping handleClick on purpose`);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
556
663
|
if (
|
|
557
664
|
isEditableMode &&
|
|
558
665
|
(e.target instanceof HTMLAnchorElement ||
|
|
@@ -674,7 +781,14 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
674
781
|
'data-node-id': nodeId,
|
|
675
782
|
'data-placeholder': isPlaceholder,
|
|
676
783
|
},
|
|
677
|
-
<RenderChildren
|
|
784
|
+
<RenderChildren
|
|
785
|
+
children={children}
|
|
786
|
+
nodeProps={{
|
|
787
|
+
...props,
|
|
788
|
+
nodeId: nodeId,
|
|
789
|
+
isSelectableText: props.isSelectableText,
|
|
790
|
+
}}
|
|
791
|
+
/>
|
|
678
792
|
)}
|
|
679
793
|
{showTabIndicator && editState === 'editing' && (
|
|
680
794
|
<TabIndicator onTab={createNextParagraph} parentNodeId={nodeId} />
|
|
@@ -2,4 +2,4 @@ import { NodeAnchorComponent } from './NodeAnchorComponent';
|
|
|
2
2
|
import type { NodeProps } from '@/types/nodeProps';
|
|
3
3
|
|
|
4
4
|
export const NodeButton = (props: NodeProps) =>
|
|
5
|
-
NodeAnchorComponent(props, 'button');
|
|
5
|
+
NodeAnchorComponent({ ...props, isSelectableText: false }, 'button');
|
|
@@ -1,18 +1,88 @@
|
|
|
1
1
|
import { getCtx } from '@/stores/nodes';
|
|
2
2
|
import type { FlatNode } from '@/types/compositorTypes';
|
|
3
3
|
import type { NodeProps } from '@/types/nodeProps';
|
|
4
|
+
import { useStore } from '@nanostores/react';
|
|
5
|
+
import { selectionStore } from '@/stores/selection';
|
|
4
6
|
|
|
5
7
|
export const NodeText = (props: NodeProps) => {
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const ctx = getCtx(props);
|
|
9
|
+
const node = ctx.allNodes.get().get(props.nodeId) as FlatNode;
|
|
10
|
+
const { value: toolModeVal } = useStore(ctx.toolModeValStore);
|
|
11
|
+
const selection = useStore(selectionStore);
|
|
12
|
+
|
|
11
13
|
if (!node) return <>ERROR MISSING NODE</>;
|
|
12
14
|
|
|
13
|
-
// Only add a space if we're not empty and don't end with a space
|
|
14
15
|
const text = node.copy || '';
|
|
15
|
-
const needsSpace = text && !text.endsWith(' ') && !isLink;
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
if (toolModeVal === 'styles' && props.isSelectableText) {
|
|
18
|
+
let charOffset = 0;
|
|
19
|
+
const wordSpans = text.split(/(\s+)/).map((segment, index) => {
|
|
20
|
+
const startOffset = charOffset;
|
|
21
|
+
const endOffset = charOffset + segment.length;
|
|
22
|
+
charOffset = endOffset;
|
|
23
|
+
|
|
24
|
+
if (segment.trim() === '') {
|
|
25
|
+
return (
|
|
26
|
+
<span
|
|
27
|
+
key={index}
|
|
28
|
+
data-parent-text-node-id={props.nodeId}
|
|
29
|
+
data-start-char-offset={startOffset}
|
|
30
|
+
data-end-char-offset={endOffset}
|
|
31
|
+
>
|
|
32
|
+
{segment}
|
|
33
|
+
</span>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let isInSelection = false;
|
|
38
|
+
const currentNodeId = props.nodeId;
|
|
39
|
+
|
|
40
|
+
// Show outline if EITHER dragging OR selection is finalized and active
|
|
41
|
+
// AND the selection is within this current text node.
|
|
42
|
+
if (
|
|
43
|
+
(selection.isDragging || selection.isActive) &&
|
|
44
|
+
selection.startNodeId &&
|
|
45
|
+
selection.endNodeId &&
|
|
46
|
+
selection.startNodeId === currentNodeId && // Selection must start in this node
|
|
47
|
+
selection.endNodeId === currentNodeId // Selection must end in this node
|
|
48
|
+
) {
|
|
49
|
+
const { startCharOffset, endCharOffset } = selection;
|
|
50
|
+
|
|
51
|
+
let selStartChar = startCharOffset;
|
|
52
|
+
let selEndChar = endCharOffset;
|
|
53
|
+
|
|
54
|
+
// Handle backward selection within this single node
|
|
55
|
+
if (startCharOffset > endCharOffset) {
|
|
56
|
+
selStartChar = endCharOffset;
|
|
57
|
+
selEndChar = startCharOffset;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check if current span falls within the character range
|
|
61
|
+
if (endOffset > selStartChar && startOffset < selEndChar) {
|
|
62
|
+
isInSelection = true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<span
|
|
68
|
+
key={index}
|
|
69
|
+
className={
|
|
70
|
+
isInSelection ? 'outline-dotted outline-2 outline-blue-600' : ''
|
|
71
|
+
}
|
|
72
|
+
data-parent-text-node-id={props.nodeId}
|
|
73
|
+
data-start-char-offset={startOffset}
|
|
74
|
+
data-end-char-offset={endOffset}
|
|
75
|
+
>
|
|
76
|
+
{segment}
|
|
77
|
+
</span>
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return <>{wordSpans}</>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (text.trim() === '') {
|
|
85
|
+
return <>{'\u00A0'}</>;
|
|
86
|
+
}
|
|
87
|
+
return <>{text}</>;
|
|
18
88
|
};
|
|
@@ -27,7 +27,7 @@ const SettingsPanel = ({ config, availableCodeHooks }: SettingsPanelProps) => {
|
|
|
27
27
|
|
|
28
28
|
return (
|
|
29
29
|
<div
|
|
30
|
-
className="bg-mydarkgrey
|
|
30
|
+
className="bg-mydarkgrey flex h-full min-w-96 max-w-sm flex-col rounded-xl bg-opacity-20 p-0.5 backdrop-blur-sm"
|
|
31
31
|
style={
|
|
32
32
|
{
|
|
33
33
|
animation: window.matchMedia(
|
|
@@ -6,9 +6,13 @@ import TrashIcon from '@heroicons/react/24/outline/TrashIcon';
|
|
|
6
6
|
import ArrowsUpDownIcon from '@heroicons/react/24/outline/ArrowsUpDownIcon';
|
|
7
7
|
import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
|
|
8
8
|
import BugAntIcon from '@heroicons/react/24/outline/BugAntIcon';
|
|
9
|
+
import LinkIcon from '@heroicons/react/24/solid/LinkIcon';
|
|
10
|
+
import XMarkIcon from '@heroicons/react/24/solid/XMarkIcon';
|
|
9
11
|
import { settingsPanelStore } from '@/stores/storykeep';
|
|
10
12
|
import { getCtx } from '@/stores/nodes';
|
|
13
|
+
import { classNames } from '@/utils/helpers';
|
|
11
14
|
import type { ToolModeVal } from '@/types/compositorTypes';
|
|
15
|
+
import { selectionStore, resetSelectionStore } from '@/stores/selection';
|
|
12
16
|
|
|
13
17
|
const storykeepToolModes = [
|
|
14
18
|
{
|
|
@@ -50,12 +54,15 @@ interface StoryKeepToolModeProps {
|
|
|
50
54
|
const StoryKeepToolMode = ({ isContext }: StoryKeepToolModeProps) => {
|
|
51
55
|
const ctx = getCtx();
|
|
52
56
|
const { value: toolModeVal } = useStore(ctx.toolModeValStore);
|
|
57
|
+
const $selection = useStore(selectionStore);
|
|
53
58
|
const showGuids = useStore(ctx.showGuids);
|
|
54
59
|
const navRef = useRef<HTMLElement>(null);
|
|
55
60
|
|
|
56
61
|
const hasTitle = useStore(ctx.hasTitle);
|
|
57
62
|
const hasPanes = useStore(ctx.hasPanes);
|
|
58
63
|
|
|
64
|
+
const isSelectionActive = $selection.isActive;
|
|
65
|
+
|
|
59
66
|
const className =
|
|
60
67
|
'w-8 h-8 py-1 rounded-xl bg-white text-myblue hover:bg-mygreen/20 hover:text-black hover:rotate-3 cursor-pointer transition-all';
|
|
61
68
|
const classNameActive = 'w-8 h-8 py-1.5 rounded-md bg-myblue text-white';
|
|
@@ -77,11 +84,27 @@ const StoryKeepToolMode = ({ isContext }: StoryKeepToolModeProps) => {
|
|
|
77
84
|
ctx.notifyNode('root');
|
|
78
85
|
};
|
|
79
86
|
|
|
87
|
+
const handleStyleClick = () => {
|
|
88
|
+
selectionStore.setKey('pendingAction', 'style');
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const handleLinkClick = () => {
|
|
92
|
+
selectionStore.setKey('pendingAction', 'link');
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const handleCancelClick = () => {
|
|
96
|
+
resetSelectionStore();
|
|
97
|
+
};
|
|
98
|
+
|
|
80
99
|
useEffect(() => {
|
|
81
100
|
const handleEscapeKey = (event: KeyboardEvent) => {
|
|
82
101
|
if (event.key === 'Escape') {
|
|
83
|
-
|
|
84
|
-
|
|
102
|
+
if (selectionStore.get().isActive) {
|
|
103
|
+
resetSelectionStore();
|
|
104
|
+
} else {
|
|
105
|
+
ctx.toolModeValStore.set({ value: 'text' });
|
|
106
|
+
ctx.notifyNode('root');
|
|
107
|
+
}
|
|
85
108
|
}
|
|
86
109
|
};
|
|
87
110
|
|
|
@@ -101,31 +124,79 @@ const StoryKeepToolMode = ({ isContext }: StoryKeepToolModeProps) => {
|
|
|
101
124
|
<nav
|
|
102
125
|
id="mainNav"
|
|
103
126
|
ref={navRef}
|
|
104
|
-
className=
|
|
127
|
+
className={classNames(
|
|
128
|
+
'z-102 bg-mywhite md:bg-mywhite/70 fixed bottom-0 left-0 right-0 p-1.5 md:bottom-2 md:right-auto md:h-auto md:w-auto md:rounded-r-xl md:border md:border-black/5 md:p-2 md:shadow-lg md:backdrop-blur-sm',
|
|
129
|
+
isSelectionActive ? `outline-dashed outline-4 outline-red-600` : ``
|
|
130
|
+
)}
|
|
105
131
|
>
|
|
106
|
-
|
|
107
|
-
<div className="
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
132
|
+
{!isSelectionActive && (
|
|
133
|
+
<div className="flex flex-wrap justify-around gap-4 py-0.5 md:flex-nowrap md:justify-start md:gap-4 md:p-0">
|
|
134
|
+
<div className="text-mydarkgrey text-center text-sm font-bold">
|
|
135
|
+
mode:
|
|
136
|
+
<div className="font-action text-myblue pt-1.5 text-center text-xs">
|
|
137
|
+
{currentToolMode.title}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
{storykeepToolModes.map(({ key, Icon, description }) => (
|
|
141
|
+
<div title={description} key={key}>
|
|
142
|
+
{key === toolModeVal ? (
|
|
143
|
+
<Icon className={classNameActive} />
|
|
144
|
+
) : (
|
|
145
|
+
<Icon
|
|
146
|
+
className={className}
|
|
147
|
+
onClick={() => handleClick(key)}
|
|
148
|
+
/>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
))}
|
|
152
|
+
<div title="Toggle debug node ids" key="debug">
|
|
153
|
+
<BugAntIcon
|
|
154
|
+
className={showGuids ? classNameDebugActive : className}
|
|
155
|
+
onClick={handleDebugToggle}
|
|
156
|
+
/>
|
|
111
157
|
</div>
|
|
112
158
|
</div>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
159
|
+
)}
|
|
160
|
+
|
|
161
|
+
{isSelectionActive && (
|
|
162
|
+
<div className="flex items-center justify-around gap-x-2 py-0.5 md:justify-start md:p-0">
|
|
163
|
+
<div className="text-mydarkgrey text-center text-sm font-bold">
|
|
164
|
+
mode:
|
|
165
|
+
<div className="font-action text-myblue pt-1.5 text-center text-xs">
|
|
166
|
+
Action Pending
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
<div className="flex gap-x-1 rounded-lg bg-gray-100 p-2 shadow-inner">
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
onClick={handleStyleClick}
|
|
173
|
+
className="flex h-8 w-8 items-center justify-center rounded-lg bg-blue-100 text-blue-700 shadow-sm hover:bg-blue-200"
|
|
174
|
+
aria-label="Style selection"
|
|
175
|
+
title="Add custom styles"
|
|
176
|
+
>
|
|
177
|
+
<PaintBrushIcon className="h-5 w-5" />
|
|
178
|
+
</button>
|
|
179
|
+
<button
|
|
180
|
+
type="button"
|
|
181
|
+
onClick={handleLinkClick}
|
|
182
|
+
className="flex h-8 w-8 items-center justify-center rounded-lg bg-blue-100 text-blue-700 shadow-sm hover:bg-blue-200"
|
|
183
|
+
aria-label="Create link"
|
|
184
|
+
title="Hyperlink"
|
|
185
|
+
>
|
|
186
|
+
<LinkIcon className="h-5 w-5" />
|
|
187
|
+
</button>
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
onClick={handleCancelClick}
|
|
191
|
+
className="flex h-8 w-8 items-center justify-center rounded-lg bg-gray-200 text-gray-700 shadow-sm hover:bg-gray-300"
|
|
192
|
+
aria-label="Cancel selection"
|
|
193
|
+
title="Cancel Selection"
|
|
194
|
+
>
|
|
195
|
+
<XMarkIcon className="h-5 w-5" />
|
|
196
|
+
</button>
|
|
120
197
|
</div>
|
|
121
|
-
))}
|
|
122
|
-
<div title="Toggle debug node ids" key="debug">
|
|
123
|
-
<BugAntIcon
|
|
124
|
-
className={showGuids ? classNameDebugActive : className}
|
|
125
|
-
onClick={handleDebugToggle}
|
|
126
|
-
/>
|
|
127
198
|
</div>
|
|
128
|
-
|
|
199
|
+
)}
|
|
129
200
|
</nav>
|
|
130
201
|
</>
|
|
131
202
|
);
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { useEffect, useState, useMemo } from 'react';
|
|
3
3
|
import { Combobox } from '@ark-ui/react';
|
|
4
4
|
import { createListCollection } from '@ark-ui/react/collection';
|
|
5
|
-
import
|
|
5
|
+
import ChevronUpDownIcon from '@heroicons/react/20/solid/ChevronUpDownIcon';
|
|
6
|
+
import CheckIcon from '@heroicons/react/20/solid/CheckIcon';
|
|
6
7
|
import { NodesContext } from '@/stores/nodes';
|
|
7
8
|
import {
|
|
8
9
|
PanesPreviewGenerator,
|
|
@@ -3,7 +3,8 @@ import { useStore } from '@nanostores/react';
|
|
|
3
3
|
import { ulid } from 'ulid';
|
|
4
4
|
import { Combobox } from '@ark-ui/react';
|
|
5
5
|
import { createListCollection } from '@ark-ui/react/collection';
|
|
6
|
-
import
|
|
6
|
+
import ChevronUpDownIcon from '@heroicons/react/20/solid/ChevronUpDownIcon';
|
|
7
|
+
import CheckIcon from '@heroicons/react/20/solid/CheckIcon';
|
|
7
8
|
import { codehookMapStore, fullContentMapStore } from '@/stores/storykeep';
|
|
8
9
|
import { getCtx } from '@/stores/nodes';
|
|
9
10
|
import { findUniqueSlug } from '@/utils/helpers';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect, useMemo } from 'react';
|
|
2
2
|
import { Combobox } from '@ark-ui/react';
|
|
3
3
|
import { createListCollection } from '@ark-ui/react/collection';
|
|
4
|
-
import
|
|
4
|
+
import ChevronUpDownIcon from '@heroicons/react/20/solid/ChevronUpDownIcon';
|
|
5
5
|
import { fullContentMapStore } from '@/stores/storykeep';
|
|
6
6
|
import { NodesContext, getCtx } from '@/stores/nodes';
|
|
7
7
|
import {
|
|
@@ -2,7 +2,8 @@ import { useState, useEffect } from 'react';
|
|
|
2
2
|
import { Select } from '@ark-ui/react/select';
|
|
3
3
|
import { Portal } from '@ark-ui/react/portal';
|
|
4
4
|
import { createListCollection } from '@ark-ui/react/collection';
|
|
5
|
-
import
|
|
5
|
+
import ChevronUpDownIcon from '@heroicons/react/20/solid/ChevronUpDownIcon';
|
|
6
|
+
import CheckIcon from '@heroicons/react/20/solid/CheckIcon';
|
|
6
7
|
import { NodesContext } from '@/stores/nodes';
|
|
7
8
|
import { createEmptyStorykeep } from '@/utils/compositor/nodesHelper';
|
|
8
9
|
import { brandColourStore, preferredThemeStore } from '@/stores/storykeep';
|
|
@@ -56,7 +56,8 @@ const StyleElementUpdatePanel = ({
|
|
|
56
56
|
// Initialize values from current node state
|
|
57
57
|
useEffect(() => {
|
|
58
58
|
const hasOverride = node.overrideClasses?.mobile?.[className] !== undefined;
|
|
59
|
-
|
|
59
|
+
const isSpan = node.tagName === 'span';
|
|
60
|
+
setIsOverridden(isSpan || hasOverride);
|
|
60
61
|
|
|
61
62
|
styleElementInfoStore.set({
|
|
62
63
|
markdownParentId: parentNode.id,
|
|
@@ -278,12 +279,15 @@ const StyleElementUpdatePanel = ({
|
|
|
278
279
|
type="checkbox"
|
|
279
280
|
checked={isOverridden}
|
|
280
281
|
onChange={(e) => handleToggleOverride(e.target.checked)}
|
|
281
|
-
|
|
282
|
+
disabled={node.tagName === 'span'}
|
|
283
|
+
className="border-mydarkgrey text-myorange focus:ring-myorange h-4 w-4 rounded disabled:opacity-50"
|
|
282
284
|
/>
|
|
283
285
|
<span className="text-mydarkgrey text-sm">
|
|
284
|
-
{
|
|
285
|
-
? '
|
|
286
|
-
:
|
|
286
|
+
{node.tagName === 'span'
|
|
287
|
+
? 'Styling this selection (Override mode).'
|
|
288
|
+
: isOverridden
|
|
289
|
+
? 'Override mode. Styling this element only.'
|
|
290
|
+
: 'You are in quick styles mode. Click to override this element.'}
|
|
287
291
|
</span>
|
|
288
292
|
</div>
|
|
289
293
|
|