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,15 @@
1
1
  import { atom, map } from 'nanostores';
2
+ import { ulid } from 'ulid';
3
+ import { processClassesForViewports } from '@/utils/compositor/reduceNodesClassNames';
4
+ import { NotificationSystem } from '@/stores/notificationSystem';
5
+ import { cloneDeep, isDeepEqual } from '@/utils/helpers';
6
+ import { extractClassesFromNodes } from '@/utils/compositor/nodesHelper';
7
+ import { handleClickEventDefault } from '@/utils/compositor/handleClickEvent';
8
+ import allowInsert from '@/utils/compositor/allowInsert';
9
+ import { reservedSlugs } from '@/constants';
10
+ import { NodesHistory, PatchOp } from '@/stores/nodesHistory';
11
+ import { moveNodeAtLocationInContext } from '@/utils/compositor/nodesHelper';
12
+ import { MarkdownGenerator } from '@/utils/compositor/nodesMarkdownGenerator';
2
13
  import {
3
14
  hasButtonPayload,
4
15
  hasTagName,
@@ -40,22 +51,14 @@ import type {
40
51
  } from '@/types/compositorTypes';
41
52
  import type { NodeProps, WidgetProps } from '@/types/nodeProps';
42
53
  import type { CSSProperties } from 'react';
43
- import { processClassesForViewports } from '@/utils/compositor/reduceNodesClassNames';
44
- import { ulid } from 'ulid';
45
- import { NotificationSystem } from '@/stores/notificationSystem';
46
- import { cloneDeep, isDeepEqual } from '@/utils/helpers';
47
- import { extractClassesFromNodes } from '@/utils/compositor/nodesHelper';
48
- import { handleClickEventDefault } from '@/utils/compositor/handleClickEvent';
49
- import allowInsert from '@/utils/compositor/allowInsert';
50
- import { reservedSlugs } from '@/constants';
51
- import { NodesHistory, PatchOp } from '@/stores/nodesHistory';
52
- import { moveNodeAtLocationInContext } from '@/utils/compositor/nodesHelper';
53
- import { MarkdownGenerator } from '@/utils/compositor/nodesMarkdownGenerator';
54
+ import { selectionStore } from '@/stores/selection';
55
+ import type { SelectionRange, SelectionStoreState } from '@/stores/selection';
54
56
  import type { CompositorProps } from '@/components/compositor/Compositor';
55
57
 
56
58
  const blockedClickNodes = new Set<string>(['em', 'strong']);
57
59
  export const ROOT_NODE_NAME = 'root';
58
60
  export const UNDO_REDO_HISTORY_CAPACITY = 500;
61
+ const VERBOSE = false;
59
62
 
60
63
  function strippedStyles(obj: Record<string, string[]>) {
61
64
  return Object.fromEntries(
@@ -337,10 +340,17 @@ export class NodesContext {
337
340
  // click handler based on toolModeVal
338
341
  switch (toolModeVal) {
339
342
  case `styles`:
340
- handleClickEventDefault(node, dblClick, this.clickedParentLayer.get());
343
+ const selection = selectionStore.get();
344
+ if (!selection.isActive)
345
+ handleClickEventDefault(
346
+ node,
347
+ dblClick,
348
+ this.clickedParentLayer.get()
349
+ );
341
350
  break;
342
351
  case `text`:
343
352
  if (
353
+ dblClick &&
344
354
  node.nodeType === 'TagElement' &&
345
355
  'tagName' in node &&
346
356
  (node.tagName === 'a' || node.tagName === 'button')
@@ -359,12 +369,6 @@ export class NodesContext {
359
369
  dblClick,
360
370
  this.clickedParentLayer.get()
361
371
  );
362
- } else {
363
- handleClickEventDefault(
364
- node,
365
- dblClick,
366
- this.clickedParentLayer.get()
367
- );
368
372
  }
369
373
  break;
370
374
  case `eraser`:
@@ -377,6 +381,578 @@ export class NodesContext {
377
381
  this.setClickedParentLayer(null);
378
382
  }
379
383
 
384
+ public async wrapRangeInAnchor(
385
+ range: SelectionStoreState
386
+ ): Promise<string | null> {
387
+ if (
388
+ !range.startNodeId ||
389
+ !range.endNodeId ||
390
+ !range.lcaNodeId ||
391
+ !range.blockNodeId
392
+ ) {
393
+ return Promise.resolve(null);
394
+ }
395
+
396
+ const originalAllNodes = new Map(this.allNodes.get());
397
+ const originalParentNodes = new Map(this.parentNodes.get());
398
+ const paneNodeId = this.getClosestNodeTypeFromId(range.blockNodeId, 'Pane');
399
+ const originalPaneNode = this.allNodes.get().get(paneNodeId)
400
+ ? (cloneDeep(this.allNodes.get().get(paneNodeId)) as PaneNode)
401
+ : null;
402
+ const wrapperNodeId = `a-${ulid()}`;
403
+
404
+ const redoLogic = (): string | null => {
405
+ if (!range.startNodeId || !range.endNodeId || !range.lcaNodeId)
406
+ return null;
407
+
408
+ let startNodeToFind: string | null = null;
409
+ let endNodeToFind: string | null = null;
410
+
411
+ if (range.startNodeId === range.endNodeId) {
412
+ const { left: nodeSplitAtEnd } = this._splitTextNode(
413
+ range.endNodeId,
414
+ range.endCharOffset
415
+ );
416
+
417
+ const { right: middleNode } = this._splitTextNode(
418
+ nodeSplitAtEnd.id,
419
+ range.startCharOffset
420
+ );
421
+
422
+ if (!middleNode) {
423
+ console.error(
424
+ 'wrapRangeInAnchor: Single-node split failed to create a middle node.'
425
+ );
426
+ return null;
427
+ }
428
+ startNodeToFind = middleNode.id;
429
+ endNodeToFind = middleNode.id;
430
+ } else {
431
+ const { left: endNodeAfterSplit } = this._splitTextNode(
432
+ range.endNodeId,
433
+ range.endCharOffset
434
+ );
435
+ endNodeToFind = endNodeAfterSplit.id;
436
+
437
+ const { right: startNodeAfterSplit } = this._splitTextNode(
438
+ range.startNodeId,
439
+ range.startCharOffset
440
+ );
441
+
442
+ if (!startNodeAfterSplit) {
443
+ console.error(
444
+ 'wrapRangeInAnchor: Multi-node split failed to create a start node.'
445
+ );
446
+ return null;
447
+ }
448
+ startNodeToFind = startNodeAfterSplit.id;
449
+ }
450
+
451
+ const newAllNodes = new Map(this.allNodes.get());
452
+ const newParentNodes = new Map(this.parentNodes.get());
453
+ const parentChildren = newParentNodes.get(range.lcaNodeId!);
454
+
455
+ if (!parentChildren) {
456
+ console.error('wrapRangeInAnchor: Could not find parent children');
457
+ return null;
458
+ }
459
+
460
+ const startIndex = parentChildren.indexOf(startNodeToFind!);
461
+ const endIndex = parentChildren.indexOf(endNodeToFind!);
462
+
463
+ if (startIndex === -1 || endIndex === -1) {
464
+ console.error(
465
+ 'wrapRangeInAnchor: Could not find split nodes in parent',
466
+ {
467
+ startIndex,
468
+ endIndex,
469
+ startNodeToFind,
470
+ endNodeToFind,
471
+ }
472
+ );
473
+ return null;
474
+ }
475
+
476
+ const actualStartIndex = Math.min(startIndex, endIndex);
477
+ const actualEndIndex = Math.max(startIndex, endIndex);
478
+
479
+ const newParentChildren = [...parentChildren];
480
+ const nodesToWrapIds = newParentChildren.slice(
481
+ actualStartIndex,
482
+ actualEndIndex + 1
483
+ );
484
+
485
+ if (nodesToWrapIds.length === 0) {
486
+ console.error('wrapRangeInAnchor: No nodes to wrap.');
487
+ return null;
488
+ }
489
+
490
+ const nodesToWrap = nodesToWrapIds
491
+ .map((id) => newAllNodes.get(id))
492
+ .filter((n): n is BaseNode => !!n);
493
+
494
+ const wrapperNode: FlatNode = {
495
+ id: wrapperNodeId,
496
+ nodeType: 'TagElement',
497
+ parentId: range.lcaNodeId,
498
+ tagName: 'a',
499
+ href: '#',
500
+ overrideClasses: {},
501
+ };
502
+ newAllNodes.set(wrapperNode.id, wrapperNode);
503
+ newParentNodes.set(wrapperNode.id, []);
504
+
505
+ for (const node of nodesToWrap) {
506
+ const nodeToUpdate = newAllNodes.get(node.id);
507
+ if (nodeToUpdate) {
508
+ nodeToUpdate.parentId = wrapperNode.id;
509
+ }
510
+ newParentNodes.get(wrapperNode.id)!.push(node.id);
511
+ }
512
+
513
+ newParentChildren.splice(
514
+ actualStartIndex,
515
+ nodesToWrapIds.length,
516
+ wrapperNode.id
517
+ );
518
+ newParentNodes.set(range.lcaNodeId!, newParentChildren);
519
+
520
+ this.allNodes.set(newAllNodes);
521
+ this.parentNodes.set(newParentNodes);
522
+
523
+ if (originalPaneNode) {
524
+ this.modifyNodes([{ ...originalPaneNode, isChanged: true }], {
525
+ notify: false,
526
+ recordHistory: false,
527
+ });
528
+ }
529
+
530
+ this.notifyNode(range.blockNodeId!);
531
+ return wrapperNode.id;
532
+ };
533
+
534
+ const undoLogic = () => {
535
+ this.allNodes.set(originalAllNodes);
536
+ this.parentNodes.set(originalParentNodes);
537
+
538
+ if (originalPaneNode) {
539
+ this.modifyNodes([originalPaneNode], {
540
+ notify: false,
541
+ recordHistory: false,
542
+ });
543
+ }
544
+ this.notifyNode(range.blockNodeId!);
545
+ };
546
+
547
+ const newAnchorId = redoLogic();
548
+
549
+ this.history.addPatch({
550
+ op: PatchOp.REPLACE,
551
+ undo: undoLogic,
552
+ redo: redoLogic,
553
+ });
554
+
555
+ return new Promise((resolve) =>
556
+ setTimeout(() => resolve(newAnchorId), 310)
557
+ );
558
+ }
559
+
560
+ /**
561
+ * Splits a text node at a given character offset.
562
+ * This is a robust function that correctly handles splits at offset 0
563
+ * by creating an empty left node, and splits at the end by returning
564
+ * the original node as 'left' and null as 'right'.
565
+ *
566
+ * @param nodeId - The ID of the 'text' node to split.
567
+ * @param offset - The character offset at which to split.
568
+ * @returns An object containing the left and (optional) right node.
569
+ */
570
+ private _splitTextNode(
571
+ nodeId: string,
572
+ offset: number
573
+ ): { left: FlatNode; right: FlatNode | null } {
574
+ console.log(`%c[_splitTextNode] CALLED`, 'color: #f59e0b;', {
575
+ nodeId,
576
+ offset,
577
+ });
578
+
579
+ const allNodes = new Map(this.allNodes.get());
580
+ const parentNodes = new Map(this.parentNodes.get());
581
+ const originalNode = allNodes.get(nodeId) as FlatNode;
582
+
583
+ if (
584
+ !originalNode ||
585
+ originalNode.tagName !== 'text' ||
586
+ typeof originalNode.copy !== 'string' // Check for copy existence
587
+ ) {
588
+ console.warn('_splitTextNode: Invalid node or type.', {
589
+ nodeId,
590
+ offset,
591
+ originalNode,
592
+ });
593
+ return { left: originalNode, right: null };
594
+ }
595
+
596
+ const text = originalNode.copy;
597
+
598
+ // Handle split at the end of the string (no-op)
599
+ if (offset >= text.length) {
600
+ console.warn(
601
+ `%c[_splitTextNode] OFFSET >= LENGTH DETECTED. Returning right: null`,
602
+ 'color: #f59e0b;',
603
+ { nodeId, text, offset }
604
+ );
605
+ return { left: originalNode, right: null };
606
+ }
607
+
608
+ // Handle split at the beginning of the string (THE FIX)
609
+ if (offset === 0) {
610
+ console.log(
611
+ `%c[_splitTextNode] OFFSET 0 DETECTED. Creating empty left node.`,
612
+ 'color: #f59e0b; font-weight: bold;',
613
+ { nodeId, text }
614
+ );
615
+
616
+ // Create a new empty node for the "left" half
617
+ const leftNode: FlatNode = {
618
+ id: ulid(),
619
+ nodeType: 'TagElement',
620
+ parentId: originalNode.parentId,
621
+ tagName: 'text',
622
+ copy: '', // Empty text
623
+ };
624
+ allNodes.set(leftNode.id, leftNode);
625
+
626
+ // The original node becomes the "right" half
627
+ const rightNode = originalNode;
628
+
629
+ // Insert the new empty node *before* the original node
630
+ const parentChildren = parentNodes.get(leftNode.parentId!)!;
631
+ if (!parentChildren) {
632
+ console.error(
633
+ '_splitTextNode (offset 0): Could not find parent children list for',
634
+ {
635
+ parentId: leftNode.parentId,
636
+ }
637
+ );
638
+ // We still return the nodes so the operation MIGHT succeed
639
+ return { left: leftNode, right: rightNode };
640
+ }
641
+
642
+ const nodeIndex = parentChildren.indexOf(rightNode.id);
643
+ const newParentChildren = [...parentChildren];
644
+
645
+ if (nodeIndex > -1) {
646
+ newParentChildren.splice(nodeIndex, 0, leftNode.id);
647
+ } else {
648
+ console.warn(
649
+ '_splitTextNode (offset 0): Could not find node in parent, prepending.',
650
+ {
651
+ nodeId: rightNode.id,
652
+ parentId: leftNode.parentId,
653
+ }
654
+ );
655
+ newParentChildren.unshift(leftNode.id);
656
+ }
657
+
658
+ parentNodes.set(leftNode.parentId!, newParentChildren);
659
+ this.allNodes.set(allNodes);
660
+ this.parentNodes.set(parentNodes);
661
+
662
+ // Return the new empty node as 'left' and the original node as 'right'
663
+ // This allows wrapRangeInSpan to find the original node as 'middleNode'
664
+ return { left: leftNode, right: rightNode };
665
+ }
666
+
667
+ // Standard split (offset > 0 and < text.length)
668
+ console.log(
669
+ `%c[_splitTextNode] Performing standard split...`,
670
+ 'color: green;',
671
+ { text, offset }
672
+ );
673
+
674
+ const leftText = text.substring(0, offset);
675
+ const rightText = text.substring(offset);
676
+
677
+ // Modify the original node to become the 'left' node
678
+ const leftNode = cloneDeep(originalNode);
679
+ leftNode.copy = leftText;
680
+ allNodes.set(leftNode.id, leftNode);
681
+
682
+ // Create a new node for the 'right' half
683
+ const rightNode: FlatNode = {
684
+ id: ulid(),
685
+ nodeType: 'TagElement',
686
+ parentId: leftNode.parentId,
687
+ tagName: 'text',
688
+ copy: rightText,
689
+ };
690
+ allNodes.set(rightNode.id, rightNode);
691
+
692
+ const parentChildren = parentNodes.get(leftNode.parentId!)!;
693
+ if (!parentChildren) {
694
+ console.error(
695
+ '_splitTextNode (standard): Could not find parent children list for',
696
+ {
697
+ parentId: leftNode.parentId,
698
+ }
699
+ );
700
+ return { left: leftNode, right: null };
701
+ }
702
+
703
+ const nodeIndex = parentChildren.indexOf(leftNode.id);
704
+ const newParentChildren = [...parentChildren];
705
+
706
+ if (nodeIndex > -1) {
707
+ // Insert the new 'right' node immediately after the 'left' node
708
+ newParentChildren.splice(nodeIndex + 1, 0, rightNode.id);
709
+ } else {
710
+ console.warn(
711
+ '_splitTextNode (standard): Could not find node in parent, appending.',
712
+ {
713
+ nodeId: leftNode.id,
714
+ parentId: leftNode.parentId,
715
+ }
716
+ );
717
+ newParentChildren.push(rightNode.id);
718
+ }
719
+
720
+ parentNodes.set(leftNode.parentId!, newParentChildren);
721
+
722
+ this.allNodes.set(allNodes);
723
+ this.parentNodes.set(parentNodes);
724
+
725
+ return { left: leftNode, right: rightNode };
726
+ }
727
+
728
+ /**
729
+ * Wraps a range of nodes (typically text nodes) inside a new formatting
730
+ * element, like a <span>. This is an atomic operation with history support.
731
+ *
732
+ * @param range - The selection range state.
733
+ * @param tagName - The tag name for the wrapper element (e.g., 'span').
734
+ * @returns A Promise that resolves with the new wrapper node's ID, or null.
735
+ */
736
+ // Replacement for wrapRangeInSpan in src/stores/nodes.ts
737
+
738
+ public async wrapRangeInSpan(
739
+ range: SelectionRange,
740
+ tagName: 'span'
741
+ ): Promise<string | null> {
742
+ if (
743
+ !range.startNodeId ||
744
+ !range.endNodeId ||
745
+ !range.lcaNodeId ||
746
+ !range.blockNodeId
747
+ ) {
748
+ return Promise.resolve(null);
749
+ }
750
+
751
+ const originalAllNodes = new Map(this.allNodes.get());
752
+ const originalParentNodes = new Map(this.parentNodes.get());
753
+ const paneNodeId = this.getClosestNodeTypeFromId(range.blockNodeId, 'Pane');
754
+ const originalPaneNode = this.allNodes.get().get(paneNodeId)
755
+ ? (cloneDeep(this.allNodes.get().get(paneNodeId)) as PaneNode)
756
+ : null;
757
+ const wrapperNodeId = ulid();
758
+
759
+ const redoLogic = (): string | null => {
760
+ if (VERBOSE)
761
+ console.log(
762
+ '%c[wrapRangeInSpan] START',
763
+ 'color: blue; font-weight: bold;',
764
+ {
765
+ range: cloneDeep(range),
766
+ lcaChildren_BEFORE: cloneDeep(
767
+ this.parentNodes.get().get(range.lcaNodeId!)
768
+ ),
769
+ }
770
+ );
771
+ if (!range.startNodeId || !range.endNodeId || !range.lcaNodeId)
772
+ return null;
773
+
774
+ let startNodeToFind: string | null = null;
775
+ let endNodeToFind: string | null = null;
776
+
777
+ if (range.startNodeId === range.endNodeId) {
778
+ // SINGLE-NODE SELECTION
779
+ const { left: nodeSplitAtEnd } = this._splitTextNode(
780
+ range.endNodeId,
781
+ range.endCharOffset
782
+ );
783
+
784
+ const { right: middleNode } = this._splitTextNode(
785
+ nodeSplitAtEnd.id,
786
+ range.startCharOffset
787
+ );
788
+
789
+ if (!middleNode) {
790
+ console.error(
791
+ 'wrapRangeInSpan: Single-node split failed to create a middle node.'
792
+ );
793
+ return null;
794
+ }
795
+ startNodeToFind = middleNode.id;
796
+ endNodeToFind = middleNode.id;
797
+ } else {
798
+ // MULTI-NODE SELECTION
799
+ const { left: endNodeAfterSplit } = this._splitTextNode(
800
+ range.endNodeId,
801
+ range.endCharOffset
802
+ );
803
+ endNodeToFind = endNodeAfterSplit.id;
804
+
805
+ const { right: startNodeAfterSplit } = this._splitTextNode(
806
+ range.startNodeId,
807
+ range.startCharOffset
808
+ );
809
+
810
+ if (!startNodeAfterSplit) {
811
+ console.error(
812
+ 'wrapRangeInSpan: Multi-node split failed to create a start node.'
813
+ );
814
+ return null;
815
+ }
816
+ startNodeToFind = startNodeAfterSplit.id;
817
+ }
818
+ if (VERBOSE)
819
+ console.log('%c[wrapRangeInSpan] SPLIT COMPLETE', 'color: blue;', {
820
+ startNodeToFind,
821
+ endNodeToFind,
822
+ lcaChildren_AFTER_SPLIT: cloneDeep(
823
+ this.parentNodes.get().get(range.lcaNodeId!)
824
+ ),
825
+ });
826
+
827
+ const newAllNodes = new Map(this.allNodes.get());
828
+ const newParentNodes = new Map(this.parentNodes.get());
829
+ const parentChildren = newParentNodes.get(range.lcaNodeId!);
830
+
831
+ if (!parentChildren) {
832
+ console.error('wrapRangeInSpan: Could not find parent children');
833
+ return null;
834
+ }
835
+
836
+ const startIndex = parentChildren.indexOf(startNodeToFind!);
837
+ const endIndex = parentChildren.indexOf(endNodeToFind!);
838
+
839
+ if (startIndex === -1 || endIndex === -1) {
840
+ console.error('wrapRangeInSpan: Could not find split nodes in parent', {
841
+ startIndex,
842
+ endIndex,
843
+ startNodeToFind,
844
+ endNodeToFind,
845
+ });
846
+ return null;
847
+ }
848
+
849
+ const actualStartIndex = Math.min(startIndex, endIndex);
850
+ const actualEndIndex = Math.max(startIndex, endIndex);
851
+
852
+ const newParentChildren = [...parentChildren];
853
+ const nodesToWrapIds = newParentChildren.slice(
854
+ actualStartIndex,
855
+ actualEndIndex + 1
856
+ );
857
+ if (VERBOSE)
858
+ console.log('%c[wrapRangeInSpan] INDEXES', 'color: blue;', {
859
+ startIndex,
860
+ endIndex,
861
+ actualStartIndex,
862
+ actualEndIndex,
863
+ });
864
+
865
+ if (nodesToWrapIds.length === 0) {
866
+ console.error('wrapRangeInSpan: No nodes to wrap.');
867
+ return null;
868
+ }
869
+ if (VERBOSE)
870
+ console.log('%c[wrapRangeInSpan] NODES TO WRAP', 'color: orange;', {
871
+ nodesToWrapIds: cloneDeep(nodesToWrapIds),
872
+ });
873
+
874
+ const nodesToWrap = nodesToWrapIds
875
+ .map((id) => newAllNodes.get(id))
876
+ .filter((n): n is BaseNode => !!n);
877
+
878
+ const wrapperNode: FlatNode = {
879
+ id: wrapperNodeId,
880
+ nodeType: 'TagElement',
881
+ parentId: range.lcaNodeId,
882
+ tagName: tagName,
883
+ };
884
+ newAllNodes.set(wrapperNode.id, wrapperNode);
885
+ newParentNodes.set(wrapperNode.id, []);
886
+
887
+ for (const node of nodesToWrap) {
888
+ node.parentId = wrapperNode.id;
889
+ newParentNodes.get(wrapperNode.id)!.push(node.id);
890
+ }
891
+
892
+ newParentChildren.splice(
893
+ actualStartIndex,
894
+ nodesToWrapIds.length,
895
+ wrapperNode.id
896
+ );
897
+ newParentNodes.set(range.lcaNodeId!, newParentChildren);
898
+
899
+ this.allNodes.set(newAllNodes);
900
+ this.parentNodes.set(newParentNodes);
901
+
902
+ if (originalPaneNode) {
903
+ this.modifyNodes([{ ...originalPaneNode, isChanged: true }], {
904
+ notify: false,
905
+ recordHistory: false,
906
+ });
907
+ }
908
+ const lcaChildrenIds = newParentNodes.get(range.lcaNodeId!);
909
+ const finalLCAChildrenNodes = lcaChildrenIds
910
+ ? lcaChildrenIds.map((id) => newAllNodes.get(id))
911
+ : [];
912
+
913
+ const wrapperChildrenIds = newParentNodes.get(wrapperNodeId);
914
+ const finalWrapperChildrenNodes = wrapperChildrenIds
915
+ ? wrapperChildrenIds.map((id) => newAllNodes.get(id))
916
+ : [];
917
+
918
+ if (VERBOSE)
919
+ console.log(
920
+ '%c[wrapRangeInSpan] END (FINAL PAYLOAD)',
921
+ 'color: green; font-weight: bold;',
922
+ {
923
+ wrapperNodeId,
924
+ lcaChildren_FINAL: finalLCAChildrenNodes,
925
+ wrapperChildren_FINAL: finalWrapperChildrenNodes,
926
+ }
927
+ );
928
+ this.notifyNode(range.blockNodeId!);
929
+ return wrapperNode.id;
930
+ };
931
+
932
+ const undoLogic = () => {
933
+ this.allNodes.set(originalAllNodes);
934
+ this.parentNodes.set(originalParentNodes);
935
+
936
+ if (originalPaneNode) {
937
+ this.modifyNodes([originalPaneNode], {
938
+ notify: false,
939
+ recordHistory: false,
940
+ });
941
+ }
942
+ this.notifyNode(range.blockNodeId!);
943
+ };
944
+
945
+ const newSpanId = redoLogic();
946
+
947
+ this.history.addPatch({
948
+ op: PatchOp.REPLACE,
949
+ undo: undoLogic,
950
+ redo: redoLogic,
951
+ });
952
+
953
+ return new Promise((resolve) => setTimeout(() => resolve(newSpanId), 310));
954
+ }
955
+
380
956
  private clickTimer: number | null = null;
381
957
  private DOUBLE_CLICK_DELAY = 300;
382
958
  private isProcessingDoubleClick = false;
@@ -877,6 +1453,35 @@ export class NodesContext {
877
1453
  : ``
878
1454
  }`;
879
1455
  }
1456
+
1457
+ if ('tagName' in node && node.tagName === 'span') {
1458
+ const spanNode = node as FlatNode;
1459
+ const [all, mobile, tablet, desktop] = processClassesForViewports(
1460
+ { mobile: {}, tablet: {}, desktop: {} },
1461
+ spanNode.overrideClasses || {},
1462
+ 1
1463
+ );
1464
+ const outlineClass =
1465
+ this.toolModeValStore.get().value === 'styles'
1466
+ ? ' outline outline-1 outline-dotted outline-gray-400/60'
1467
+ : '';
1468
+
1469
+ const getClassString = (classes: string[]): string =>
1470
+ classes && classes.length > 0 ? classes[0] : '';
1471
+
1472
+ if (isPreview) return getClassString(desktop) + outlineClass;
1473
+ switch (viewport) {
1474
+ case 'desktop':
1475
+ return getClassString(desktop) + outlineClass;
1476
+ case 'tablet':
1477
+ return getClassString(tablet) + outlineClass;
1478
+ case 'mobile':
1479
+ return getClassString(mobile) + outlineClass;
1480
+ default:
1481
+ return getClassString(all) + outlineClass;
1482
+ }
1483
+ }
1484
+
880
1485
  const closestPaneId = this.getClosestNodeTypeFromId(
881
1486
  nodeId,
882
1487
  'Markdown'
@@ -1362,7 +1967,8 @@ export class NodesContext {
1362
1967
  });
1363
1968
  break;
1364
1969
  }
1365
- this.toolModeValStore.set({ value: 'text' });
1970
+ this.toolModeValStore.set({ value: 'styles' });
1971
+ this.notifyNode('root');
1366
1972
  }
1367
1973
 
1368
1974
  addTemplateImpressionNode(targetId: string, node: ImpressionNode) {
@@ -2455,7 +3061,7 @@ export class NodesContext {
2455
3061
  recordHistory: false,
2456
3062
  });
2457
3063
 
2458
- this.notifyNode(parentId);
3064
+ this.notifyNode(`root`);
2459
3065
  };
2460
3066
 
2461
3067
  // --- 4. Define the atomic "undo" (backward) operation ---
@@ -2470,7 +3076,7 @@ export class NodesContext {
2470
3076
  recordHistory: false,
2471
3077
  });
2472
3078
 
2473
- this.notifyNode(parentId);
3079
+ this.notifyNode(`root`);
2474
3080
  };
2475
3081
 
2476
3082
  // --- 5. Execute the operation and add to history ---