astro-tractstack 2.0.9 → 2.0.11

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 (41) hide show
  1. package/dist/index.js +4 -6
  2. package/package.json +1 -1
  3. package/templates/css/custom.css +0 -6
  4. package/templates/src/components/codehooks/EpinetDurationSelector.tsx +1 -1
  5. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +2 -1
  6. package/templates/src/components/codehooks/ProductGridSetup.tsx +4 -4
  7. package/templates/src/components/compositor/Compositor.tsx +335 -16
  8. package/templates/src/components/compositor/Node.tsx +86 -6
  9. package/templates/src/components/compositor/nodes/RenderChildren.tsx +3 -6
  10. package/templates/src/components/compositor/nodes/tagElements/NodeA.tsx +2 -1
  11. package/templates/src/components/compositor/nodes/tagElements/NodeAnchorComponent.tsx +11 -19
  12. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag.tsx +70 -17
  13. package/templates/src/components/compositor/nodes/tagElements/NodeButton.tsx +1 -1
  14. package/templates/src/components/compositor/nodes/tagElements/NodeText.tsx +78 -8
  15. package/templates/src/components/edit/SettingsPanel.tsx +1 -1
  16. package/templates/src/components/edit/ToolMode.tsx +93 -22
  17. package/templates/src/components/edit/pane/AddPanePanel_break.tsx +2 -1
  18. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +2 -1
  19. package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +1 -1
  20. package/templates/src/components/edit/pane/PageGen_preview.tsx +2 -1
  21. package/templates/src/components/edit/panels/StyleElementPanel_update.tsx +9 -5
  22. package/templates/src/components/edit/state/SaveModal.tsx +84 -14
  23. package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +2 -2
  24. package/templates/src/components/search/SearchModal.tsx +2 -1
  25. package/templates/src/components/search/SearchResults.tsx +2 -1
  26. package/templates/src/components/search/SearchWrapper.tsx +1 -1
  27. package/templates/src/components/storykeep/Dashboard_Analytics.tsx +1 -1
  28. package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +3 -5
  29. package/templates/src/components/storykeep/controls/content/BeliefTable.tsx +1 -1
  30. package/templates/src/components/storykeep/controls/content/MenuTable.tsx +1 -1
  31. package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +1 -1
  32. package/templates/src/components/widgets/ImpressionWrapper.tsx +1 -1
  33. package/templates/src/hooks/useFormState.ts +3 -4
  34. package/templates/src/pages/sitemap.xml.ts +38 -8
  35. package/templates/src/stores/nodes.ts +627 -21
  36. package/templates/src/stores/selection.ts +41 -0
  37. package/templates/src/types/compositorTypes.ts +1 -0
  38. package/templates/src/types/nodeProps.ts +12 -0
  39. package/templates/src/utils/compositor/nodesHelper.ts +2 -2
  40. package/utils/inject-files.ts +4 -6
  41. 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) => NodeAnchorComponent(props, 'a');
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
- // Check if this is a video link
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={ctx.getNodeClasses(nodeId, viewportKeyStore.get().value)}
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={ctx.getNodeClasses(nodeId, viewportKeyStore.get().value)}
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 { viewportKeyStore, isEditingStore } from '@/stores/storykeep';
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
- // Core state
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 === 'text' || toolModeVal === 'styles';
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
- style={{ top: '-.85rem', right: '0' }}
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
- const currentContent = elementRef.current?.innerHTML || '';
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 children={children} nodeProps={props} />
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 node = getCtx(props).allNodes.get().get(props.nodeId) as FlatNode;
7
- const parentNode = node.parentId
8
- ? (getCtx(props).allNodes.get().get(node.parentId) as FlatNode)
9
- : null;
10
- const isLink = parentNode && [`a`, `button`].includes(parentNode.tagName);
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
- return <>{text.trim() === '' ? '\u00A0' : text + (needsSpace ? ' ' : '')}</>;
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 min-w-xs flex h-full max-w-sm flex-col rounded-xl bg-opacity-20 p-0.5 backdrop-blur-sm"
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
- ctx.toolModeValStore.set({ value: 'text' });
84
- ctx.notifyNode('root');
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="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"
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
- <div className="flex flex-wrap justify-around gap-4 py-0.5 md:flex-nowrap md:justify-start md:gap-4 md:p-0">
107
- <div className="text-mydarkgrey text-center text-sm font-bold">
108
- mode:
109
- <div className="font-action text-myblue pt-1.5 text-center text-xs">
110
- {currentToolMode.title}
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
- {storykeepToolModes.map(({ key, Icon, description }) => (
114
- <div title={description} key={key}>
115
- {key === toolModeVal ? (
116
- <Icon className={classNameActive} />
117
- ) : (
118
- <Icon className={className} onClick={() => handleClick(key)} />
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
- </div>
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 { ChevronUpDownIcon, CheckIcon } from '@heroicons/react/20/solid';
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 { ChevronUpDownIcon, CheckIcon } from '@heroicons/react/20/solid';
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 { ChevronUpDownIcon } from '@heroicons/react/20/solid';
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 { ChevronUpDownIcon, CheckIcon } from '@heroicons/react/20/solid';
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
- setIsOverridden(hasOverride);
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
- className="border-mydarkgrey text-myorange focus:ring-myorange h-4 w-4 rounded"
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
- {isOverridden
285
- ? 'Override mode. Styling this element only.'
286
- : 'You are in quick styles mode. Click to override this element.'}
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