astro-tractstack 2.0.9 → 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 +70 -17
- 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 +627 -21
- 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 {
|
|
@@ -21,8 +28,6 @@ import { cloneDeep } from '@/utils/helpers';
|
|
|
21
28
|
import { PatchOp } from '@/stores/nodesHistory';
|
|
22
29
|
import type { FlatNode, PaneNode } from '@/types/compositorTypes';
|
|
23
30
|
import type { NodeProps } from '@/types/nodeProps';
|
|
24
|
-
import { useStore } from '@nanostores/react';
|
|
25
|
-
import { XMarkIcon } from '@heroicons/react/20/solid';
|
|
26
31
|
|
|
27
32
|
export type NodeTagProps = NodeProps & { tagName: keyof JSX.IntrinsicElements };
|
|
28
33
|
|
|
@@ -34,7 +39,22 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
34
39
|
const ctx = getCtx(props);
|
|
35
40
|
const Tag = ctx.showGuids.get() ? `div` : props.tagName;
|
|
36
41
|
|
|
37
|
-
|
|
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
|
+
|
|
38
58
|
const [editState, setEditState] = useState<EditState>('viewing');
|
|
39
59
|
const [showTabIndicator, setShowTabIndicator] = useState(false);
|
|
40
60
|
const elementRef = useRef<HTMLElement | null>(null);
|
|
@@ -232,10 +252,18 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
232
252
|
}
|
|
233
253
|
}, [editState]);
|
|
234
254
|
|
|
235
|
-
// For formatting nodes <em> and <strong>
|
|
236
|
-
if (['em', 'strong'].includes(props.tagName)) {
|
|
237
|
-
const isEditorActive = toolModeVal === '
|
|
238
|
-
|
|
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
|
+
};
|
|
239
267
|
const handleUnwrapClick = (e: MouseEvent<HTMLButtonElement>) => {
|
|
240
268
|
e.preventDefault();
|
|
241
269
|
e.stopPropagation();
|
|
@@ -243,10 +271,7 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
243
271
|
};
|
|
244
272
|
|
|
245
273
|
let baseClasses = ctx.getNodeClasses(nodeId, viewportKeyStore.get().value);
|
|
246
|
-
|
|
247
|
-
if (isEditorActive) {
|
|
248
|
-
baseClasses += ' outline outline-1 outline-dotted outline-gray-400/60';
|
|
249
|
-
}
|
|
274
|
+
baseClasses += ' outline outline-1 outline-dotted outline-gray-400/60';
|
|
250
275
|
|
|
251
276
|
return createElement(
|
|
252
277
|
Tag,
|
|
@@ -272,16 +297,29 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
272
297
|
isEditorActive && (
|
|
273
298
|
<span
|
|
274
299
|
key="chip"
|
|
275
|
-
className="absolute z-10 select-none"
|
|
276
|
-
|
|
300
|
+
className="absolute z-10 flex select-none gap-x-1"
|
|
301
|
+
data-attr="exclude"
|
|
302
|
+
style={{ top: '-0.9rem', left: '0' }}
|
|
277
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
|
+
)}
|
|
278
315
|
<button
|
|
279
316
|
type="button"
|
|
280
317
|
onClick={handleUnwrapClick}
|
|
281
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"
|
|
282
319
|
aria-label="Remove formatting"
|
|
320
|
+
data-attr="exclude"
|
|
283
321
|
>
|
|
284
|
-
<XMarkIcon className="h-3.5 w-3.5" />
|
|
322
|
+
<XMarkIcon className="h-3.5 w-3.5" data-attr="exclude" />
|
|
285
323
|
</button>
|
|
286
324
|
</span>
|
|
287
325
|
),
|
|
@@ -328,7 +366,11 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
328
366
|
const saveAndExit = () => {
|
|
329
367
|
if (editState !== 'editing') return;
|
|
330
368
|
|
|
331
|
-
|
|
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;
|
|
332
374
|
|
|
333
375
|
if (currentContent !== originalContentRef.current) {
|
|
334
376
|
try {
|
|
@@ -614,6 +656,10 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
614
656
|
const clickTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
615
657
|
|
|
616
658
|
const handleClick = (e: MouseEvent) => {
|
|
659
|
+
if (toolModeVal === 'styles') {
|
|
660
|
+
console.log(`skipping handleClick on purpose`);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
617
663
|
if (
|
|
618
664
|
isEditableMode &&
|
|
619
665
|
(e.target instanceof HTMLAnchorElement ||
|
|
@@ -735,7 +781,14 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
735
781
|
'data-node-id': nodeId,
|
|
736
782
|
'data-placeholder': isPlaceholder,
|
|
737
783
|
},
|
|
738
|
-
<RenderChildren
|
|
784
|
+
<RenderChildren
|
|
785
|
+
children={children}
|
|
786
|
+
nodeProps={{
|
|
787
|
+
...props,
|
|
788
|
+
nodeId: nodeId,
|
|
789
|
+
isSelectableText: props.isSelectableText,
|
|
790
|
+
}}
|
|
791
|
+
/>
|
|
739
792
|
)}
|
|
740
793
|
{showTabIndicator && editState === 'editing' && (
|
|
741
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
|
|