astro-tractstack 2.1.3 → 2.2.0
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/README.md +54 -266
- package/bin/create-tractstack.js +9 -6
- package/dist/index.js +109 -71
- package/package.json +4 -2
- package/templates/css/custom.css +5 -0
- package/templates/icons/code.svg +18 -0
- package/templates/icons/li.svg +4 -0
- package/templates/icons/link.svg +22 -0
- package/templates/icons/p.svg +3 -0
- package/templates/src/client/app.js +80 -1
- package/templates/src/components/Footer.astro +1 -1
- package/templates/src/components/codehooks/BunnyVideoSetup.tsx +6 -6
- package/templates/src/components/codehooks/EpinetDurationSelector.tsx +3 -3
- package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +1 -1
- package/templates/src/components/codehooks/ListContentSetup.tsx +2 -2
- package/templates/src/components/codehooks/ProductCardSetup.tsx +1 -1
- package/templates/src/components/codehooks/ProductGridSetup.tsx +2 -2
- package/templates/src/components/codehooks/SandboxRegisterForm.tsx +3 -3
- package/templates/src/components/compositor/Compositor.tsx +25 -9
- package/templates/src/components/compositor/Node.tsx +168 -496
- package/templates/src/components/compositor/PanelVisibilityWrapper.tsx +1 -0
- package/templates/src/components/compositor/elements/SignUp.tsx +1 -1
- package/templates/src/components/compositor/elements/YouTubeWrapper.tsx +2 -0
- package/templates/src/components/compositor/nodes/CreativePane.tsx +262 -0
- package/templates/src/components/compositor/nodes/GhostInsertBlock.tsx +4 -6
- package/templates/src/components/compositor/nodes/GridLayout.tsx +4 -2
- package/templates/src/components/compositor/nodes/Markdown.tsx +18 -3
- package/templates/src/components/compositor/nodes/Pane.tsx +11 -5
- package/templates/src/components/compositor/nodes/RenderChildren.tsx +1 -1
- package/templates/src/components/compositor/nodes/tagElements/NodeAnchorComponent.tsx +5 -5
- package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag.tsx +90 -42
- package/templates/src/components/compositor/nodes/tagElements/NodeImg.tsx +2 -0
- package/templates/src/components/compositor/nodes/tagElements/NodeText.tsx +27 -1
- package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +10 -8
- package/templates/src/components/compositor/tools/NodeOverlay.tsx +224 -0
- package/templates/src/components/compositor/tools/PaneOverlay.tsx +122 -0
- package/templates/src/components/edit/Header.tsx +68 -9
- package/templates/src/components/edit/PanelSwitch.tsx +42 -4
- package/templates/src/components/edit/SettingsPanel.tsx +2 -3
- package/templates/src/components/edit/ToolMode.tsx +1 -31
- package/templates/src/components/edit/pane/AddPanePanel_break.tsx +2 -2
- package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +1 -1
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +193 -659
- package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +15 -82
- package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +95 -45
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +137 -49
- package/templates/src/components/edit/pane/RestylePaneModal.tsx +1 -1
- package/templates/src/components/edit/pane/steps/AiCreativeDesignStep.tsx +375 -0
- package/templates/src/components/edit/pane/steps/AiDesignStep.tsx +1 -23
- package/templates/src/components/edit/pane/steps/AiLibraryCopyStep.tsx +327 -0
- package/templates/src/components/edit/pane/steps/AiRefineDesignStep.tsx +267 -0
- package/templates/src/components/edit/pane/steps/AiStandardDesignStep.tsx +371 -0
- package/templates/src/components/edit/pane/steps/CopyInputStep.tsx +201 -76
- package/templates/src/components/edit/pane/steps/CreativeInjectStep.tsx +141 -0
- package/templates/src/components/edit/panels/CreativeImagePanel.tsx +435 -0
- package/templates/src/components/edit/panels/CreativeLinkPanel.tsx +110 -0
- package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +1 -1
- package/templates/src/components/edit/panels/StyleParentPanel.tsx +118 -126
- package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +3 -2
- package/templates/src/components/edit/panels/StyleParentPanel_deleteLayer.tsx +1 -0
- package/templates/src/components/edit/panels/StyleParentPanel_remove.tsx +3 -1
- package/templates/src/components/edit/panels/StyleParentPanel_update.tsx +3 -1
- package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +1 -1
- package/templates/src/components/edit/state/SaveModal.tsx +19 -787
- package/templates/src/components/edit/state/SaveToLibraryModal.tsx +2 -2
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_menu.tsx +1 -1
- package/templates/src/components/edit/widgets/BunnyWidget.tsx +5 -5
- package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +1 -1
- package/templates/src/components/edit/widgets/SignupWidget.tsx +1 -1
- package/templates/src/components/fields/ActionBuilderTimeSelector.tsx +1 -1
- package/templates/src/components/fields/ArtpackImage.tsx +11 -3
- package/templates/src/components/fields/BackgroundImage.tsx +8 -0
- package/templates/src/components/fields/BackgroundImageWrapper.tsx +15 -9
- package/templates/src/components/fields/ImageUpload.tsx +6 -0
- package/templates/src/components/form/ActionBuilderField.tsx +15 -5
- package/templates/src/components/form/ActionBuilderSlugSelector.tsx +1 -1
- package/templates/src/components/form/ColorPicker.tsx +1 -1
- package/templates/src/components/form/EnumSelect.tsx +1 -1
- package/templates/src/components/form/NumberInput.tsx +1 -1
- package/templates/src/components/form/StringArrayInput.tsx +1 -1
- package/templates/src/components/form/StringInput.tsx +1 -1
- package/templates/src/components/form/UnsavedChangesBar.tsx +1 -1
- package/templates/src/components/form/advanced/APIConfigSection.tsx +2 -2
- package/templates/src/components/form/advanced/AuthConfigSection.tsx +2 -2
- package/templates/src/components/profile/ProfileCreate.tsx +1 -1
- package/templates/src/components/profile/ProfileEdit.tsx +1 -1
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +2 -2
- package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +1 -1
- package/templates/src/components/storykeep/controls/content/ContentSummary.tsx +2 -2
- package/templates/src/components/storykeep/controls/content/KnownResourceTable.tsx +1 -1
- package/templates/src/components/storykeep/controls/content/ManageContent.tsx +6 -6
- package/templates/src/components/storykeep/controls/content/MenuForm.tsx +1 -1
- package/templates/src/components/storykeep/controls/content/PaneTable.tsx +358 -0
- package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +1 -1
- package/templates/src/constants/prompts.json +18 -10
- package/templates/src/constants.ts +3 -0
- package/templates/src/hooks/usePaneFragments.ts +60 -0
- package/templates/src/lib/session.ts +71 -16
- package/templates/src/pages/[...slug].astro +4 -46
- package/templates/src/pages/api/css.ts +149 -0
- package/templates/src/pages/maint.astro +1 -1
- package/templates/src/pages/storykeep/login.astro +2 -2
- package/templates/src/stores/nodes.ts +162 -49
- package/templates/src/stores/orphanAnalysis.ts +6 -30
- package/templates/src/stores/previews.ts +7 -0
- package/templates/src/stores/storykeep.ts +0 -8
- package/templates/src/types/compositorTypes.ts +53 -10
- package/templates/src/utils/compositor/aiGeneration.ts +93 -0
- package/templates/src/utils/compositor/allowInsert.ts +2 -0
- package/templates/src/utils/compositor/htmlAst.ts +704 -0
- package/templates/src/utils/compositor/nodesHelper.ts +281 -102
- package/templates/src/utils/compositor/savePipeline.ts +893 -0
- package/templates/src/utils/etl/index.ts +3 -0
- package/templates/src/utils/etl/transformer.ts +10 -0
- package/templates/src/utils/helpers.ts +101 -0
- package/utils/inject-files.ts +100 -62
- package/templates/icons/text.svg +0 -6
- package/templates/src/components/compositor/NodeWithGuid.tsx +0 -69
- package/templates/src/components/compositor/nodes/GridLayout_eraser.tsx +0 -33
- package/templates/src/components/compositor/nodes/Markdown_eraser.tsx +0 -56
- package/templates/src/components/compositor/nodes/Pane_DesignLibrary.tsx +0 -269
- package/templates/src/components/compositor/nodes/Pane_eraser.tsx +0 -186
- package/templates/src/components/compositor/nodes/Pane_layout.tsx +0 -79
- package/templates/src/components/compositor/nodes/tagElements/NodeA_eraser.tsx +0 -26
- package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_eraser.tsx +0 -61
- package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_insert.tsx +0 -120
- package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_settings.tsx +0 -62
- package/templates/src/components/compositor/nodes/tagElements/NodeButton_eraser.tsx +0 -26
|
@@ -24,13 +24,19 @@ import { RenderChildren } from '../RenderChildren';
|
|
|
24
24
|
import {
|
|
25
25
|
processRichTextToNodes,
|
|
26
26
|
getTemplateNode,
|
|
27
|
+
isAddressableNode,
|
|
28
|
+
canEditText,
|
|
29
|
+
getNodeDisplayMode,
|
|
27
30
|
} from '@/utils/compositor/nodesHelper';
|
|
28
31
|
import { cloneDeep, classNames } from '@/utils/helpers';
|
|
29
32
|
import { PatchOp } from '@/stores/nodesHistory';
|
|
30
33
|
import type { FlatNode, PaneNode } from '@/types/compositorTypes';
|
|
31
34
|
import type { NodeProps } from '@/types/nodeProps';
|
|
32
35
|
|
|
33
|
-
export type NodeTagProps = NodeProps & {
|
|
36
|
+
export type NodeTagProps = NodeProps & {
|
|
37
|
+
tagName: keyof JSX.IntrinsicElements;
|
|
38
|
+
style?: any;
|
|
39
|
+
};
|
|
34
40
|
|
|
35
41
|
type EditState = 'viewing' | 'editing';
|
|
36
42
|
const VERBOSE = false;
|
|
@@ -39,9 +45,9 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
39
45
|
const nodeId = props.nodeId;
|
|
40
46
|
const ctx = getCtx(props);
|
|
41
47
|
const settingsPanel = useStore(settingsPanelStore);
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
const Tag = ['em', 'strong', 'a', 'button', 'img'].includes(props.tagName)
|
|
49
|
+
? props.tagName
|
|
50
|
+
: 'div';
|
|
45
51
|
|
|
46
52
|
if (props.tagName === 'span') {
|
|
47
53
|
const node = ctx.allNodes.get().get(props.nodeId);
|
|
@@ -66,14 +72,16 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
66
72
|
const cursorPositionRef = useRef<{ node: Node; offset: number } | null>(null);
|
|
67
73
|
|
|
68
74
|
const { value: toolModeVal } = useStore(ctx.toolModeValStore);
|
|
75
|
+
const toolAddMode = useStore(ctx.toolAddModeStore).value;
|
|
69
76
|
|
|
70
77
|
// Get node data
|
|
71
78
|
const node = ctx.allNodes.get().get(nodeId) as FlatNode;
|
|
72
79
|
const children = ctx.getChildNodeIDs(nodeId);
|
|
73
|
-
const isEditableMode =
|
|
74
|
-
const supportsEditing =
|
|
80
|
+
const isEditableMode = toolModeVal === 'text';
|
|
81
|
+
const supportsEditing = canEditText(node, ctx);
|
|
75
82
|
const isPlaceholder = node?.isPlaceholder === true;
|
|
76
83
|
const isEmpty = elementRef.current?.textContent?.trim() === '';
|
|
84
|
+
const isInline = getNodeDisplayMode(node, viewportKeyStore.get().value, ctx);
|
|
77
85
|
|
|
78
86
|
// Auto-enter edit mode for new placeholder nodes
|
|
79
87
|
useEffect(() => {
|
|
@@ -258,8 +266,6 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
258
266
|
|
|
259
267
|
// For formatting nodes <em> and <strong> and <span>
|
|
260
268
|
if (['em', 'strong', 'span'].includes(props.tagName)) {
|
|
261
|
-
const isEditorActive = ['styles', 'text'].includes(toolModeVal);
|
|
262
|
-
const isEditorEnabled = toolModeVal === 'styles';
|
|
263
269
|
const handleStyleClick = (e: MouseEvent<HTMLButtonElement>) => {
|
|
264
270
|
e.preventDefault();
|
|
265
271
|
e.stopPropagation();
|
|
@@ -300,32 +306,35 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
300
306
|
}
|
|
301
307
|
},
|
|
302
308
|
'data-node-id': nodeId,
|
|
309
|
+
'data-tag': props.tagName,
|
|
303
310
|
tabIndex: isEditableMode ? -1 : undefined,
|
|
304
311
|
style: {
|
|
305
|
-
position:
|
|
312
|
+
position: 'relative',
|
|
306
313
|
outlineOffset: '1px',
|
|
314
|
+
display: isInline ? 'inline-block' : undefined,
|
|
307
315
|
},
|
|
308
316
|
},
|
|
309
317
|
[
|
|
310
318
|
<RenderChildren key="children" children={children} nodeProps={props} />,
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
319
|
+
<span
|
|
320
|
+
key={`toolbar-${nodeId}`}
|
|
321
|
+
className="absolute z-10 flex select-none gap-x-1"
|
|
322
|
+
data-attr="exclude"
|
|
323
|
+
style={{ top: '-0.9rem', left: '0' }}
|
|
324
|
+
>
|
|
325
|
+
{props.tagName === 'span' && toolModeVal === `text` && (
|
|
326
|
+
<button
|
|
327
|
+
type="button"
|
|
328
|
+
onClick={handleStyleClick}
|
|
329
|
+
className="flex h-4 w-4 items-center justify-center rounded-full bg-blue-100 bg-opacity-90 text-blue-700 shadow-sm hover:bg-blue-300 focus:outline-none"
|
|
330
|
+
aria-label="Style this"
|
|
331
|
+
title="Style this"
|
|
332
|
+
data-attr="exclude"
|
|
333
|
+
>
|
|
334
|
+
<PaintBrushIcon className="h-3 w-3" data-attr="exclude" />
|
|
335
|
+
</button>
|
|
336
|
+
)}
|
|
337
|
+
{props.tagName === 'span' && toolModeVal === `insert` && (
|
|
329
338
|
<button
|
|
330
339
|
type="button"
|
|
331
340
|
onClick={handleWordCarouselClick}
|
|
@@ -335,7 +344,8 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
335
344
|
? 'bg-green-100 text-green-700 hover:bg-green-300'
|
|
336
345
|
: 'bg-gray-100 bg-opacity-90 text-gray-700 hover:bg-gray-300'
|
|
337
346
|
)}
|
|
338
|
-
aria-label="
|
|
347
|
+
aria-label="Word Carousel"
|
|
348
|
+
title="Word Carousel"
|
|
339
349
|
data-attr="exclude"
|
|
340
350
|
>
|
|
341
351
|
<ChatBubbleBottomCenterTextIcon
|
|
@@ -343,17 +353,20 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
343
353
|
data-attr="exclude"
|
|
344
354
|
/>
|
|
345
355
|
</button>
|
|
356
|
+
)}
|
|
357
|
+
{props.tagName === 'span' && toolModeVal === `text` && (
|
|
346
358
|
<button
|
|
347
359
|
type="button"
|
|
348
360
|
onClick={handleUnwrapClick}
|
|
349
361
|
className="flex h-4 w-4 items-center justify-center rounded-full bg-gray-100 bg-opacity-90 text-gray-700 shadow-sm hover:bg-gray-300 focus:outline-none"
|
|
350
362
|
aria-label="Remove formatting"
|
|
363
|
+
title="Remove formatting"
|
|
351
364
|
data-attr="exclude"
|
|
352
365
|
>
|
|
353
366
|
<XMarkIcon className="h-3.5 w-3.5" data-attr="exclude" />
|
|
354
367
|
</button>
|
|
355
|
-
|
|
356
|
-
|
|
368
|
+
)}
|
|
369
|
+
</span>,
|
|
357
370
|
]
|
|
358
371
|
);
|
|
359
372
|
}
|
|
@@ -373,6 +386,7 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
373
386
|
}
|
|
374
387
|
},
|
|
375
388
|
'data-node-id': nodeId,
|
|
389
|
+
'data-tag': props.tagName,
|
|
376
390
|
tabIndex: isEditableMode ? -1 : undefined,
|
|
377
391
|
},
|
|
378
392
|
<RenderChildren children={children} nodeProps={props} />
|
|
@@ -380,7 +394,13 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
380
394
|
}
|
|
381
395
|
|
|
382
396
|
const startEditing = () => {
|
|
383
|
-
if (
|
|
397
|
+
if (
|
|
398
|
+
!isEditableMode ||
|
|
399
|
+
!supportsEditing ||
|
|
400
|
+
editState === 'editing' ||
|
|
401
|
+
!canEditText(node, ctx)
|
|
402
|
+
)
|
|
403
|
+
return;
|
|
384
404
|
|
|
385
405
|
originalContentRef.current = elementRef.current?.innerHTML || '';
|
|
386
406
|
setEditState('editing');
|
|
@@ -395,13 +415,35 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
395
415
|
};
|
|
396
416
|
|
|
397
417
|
const saveAndExit = () => {
|
|
418
|
+
if (VERBOSE)
|
|
419
|
+
console.log(`[DEBUG] saveAndExit triggered for nodeId: ${nodeId}`, {
|
|
420
|
+
editState,
|
|
421
|
+
toolMode: toolModeVal,
|
|
422
|
+
hasSettingsPanel: !!settingsPanel,
|
|
423
|
+
activeSignal: settingsPanel?.action,
|
|
424
|
+
});
|
|
398
425
|
if (editState !== 'editing') return;
|
|
399
|
-
|
|
400
|
-
// Use an in-memory element to safely parse and strip UI chrome from the content.
|
|
401
426
|
const tempDiv = document.createElement('div');
|
|
427
|
+
if (VERBOSE)
|
|
428
|
+
console.log(`[DEBUG] Raw HTML before sanitization:`, tempDiv.innerHTML);
|
|
402
429
|
tempDiv.innerHTML = elementRef.current?.innerHTML || '';
|
|
403
|
-
tempDiv.querySelectorAll(
|
|
430
|
+
const chromeElements = tempDiv.querySelectorAll(
|
|
431
|
+
'.compositor-chrome, [data-attr="exclude"]'
|
|
432
|
+
);
|
|
433
|
+
chromeElements.forEach((el) => el.remove());
|
|
434
|
+
const wrappers = tempDiv.querySelectorAll(
|
|
435
|
+
'.compositor-wrapper, [data-node-overlay]'
|
|
436
|
+
);
|
|
437
|
+
wrappers.forEach((el) => {
|
|
438
|
+
while (el.firstChild) {
|
|
439
|
+
el.parentNode?.insertBefore(el.firstChild, el);
|
|
440
|
+
}
|
|
441
|
+
el.remove();
|
|
442
|
+
});
|
|
443
|
+
|
|
404
444
|
const currentContent = tempDiv.innerHTML;
|
|
445
|
+
if (VERBOSE)
|
|
446
|
+
console.log(`[DEBUG] Sanitized HTML sent to Parser:`, currentContent);
|
|
405
447
|
|
|
406
448
|
if (currentContent !== originalContentRef.current) {
|
|
407
449
|
try {
|
|
@@ -636,9 +678,14 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
636
678
|
const relatedTarget = e.relatedTarget as HTMLElement | null;
|
|
637
679
|
if (VERBOSE)
|
|
638
680
|
console.log(`[NodeBasicTag] Blur event, relatedTarget:`, e.relatedTarget);
|
|
681
|
+
|
|
682
|
+
// Check if the focus moved to the Settings Panel or Toolbar
|
|
683
|
+
const isSettingsControl = relatedTarget?.closest('#settingsControls');
|
|
684
|
+
|
|
639
685
|
if (
|
|
640
686
|
relatedTarget?.hasAttribute('data-tab-indicator') ||
|
|
641
|
-
(relatedTarget && elementRef.current?.contains(relatedTarget))
|
|
687
|
+
(relatedTarget && elementRef.current?.contains(relatedTarget)) ||
|
|
688
|
+
isSettingsControl
|
|
642
689
|
) {
|
|
643
690
|
return;
|
|
644
691
|
}
|
|
@@ -688,8 +735,7 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
688
735
|
const clickTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
689
736
|
|
|
690
737
|
const handleClick = (e: MouseEvent) => {
|
|
691
|
-
if (
|
|
692
|
-
console.log(`skipping handleClick on purpose`);
|
|
738
|
+
if (!canEditText(node, ctx)) {
|
|
693
739
|
return;
|
|
694
740
|
}
|
|
695
741
|
if (
|
|
@@ -709,6 +755,7 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
709
755
|
|
|
710
756
|
// Delay single-click behavior to see if double-click follows
|
|
711
757
|
clickTimeoutRef.current = setTimeout(() => {
|
|
758
|
+
if (!isAddressableNode(node, ctx)) return;
|
|
712
759
|
if (isEditableMode && supportsEditing && editState === 'viewing') {
|
|
713
760
|
const selection = window.getSelection();
|
|
714
761
|
if (selection && selection.rangeCount > 0) {
|
|
@@ -788,11 +835,6 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
788
835
|
if (settingsPanel?.nodeId === nodeId) {
|
|
789
836
|
outlineClasses +=
|
|
790
837
|
' outline-4 outline-dotted outline-orange-400 outline-offset-2';
|
|
791
|
-
} else if (toolMode === 'styles') {
|
|
792
|
-
outlineClasses += ' hover:outline hover:outline-2 hover:outline-black';
|
|
793
|
-
if (['span', 'strong', 'em'].includes(props.tagName)) {
|
|
794
|
-
outlineClasses += ' outline outline-1 outline-dotted outline-black';
|
|
795
|
-
}
|
|
796
838
|
}
|
|
797
839
|
const editingClasses =
|
|
798
840
|
editState === 'editing'
|
|
@@ -816,11 +858,17 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
816
858
|
onClick: handleClick,
|
|
817
859
|
onDoubleClick: handleDoubleClick,
|
|
818
860
|
style: {
|
|
861
|
+
userSelect:
|
|
862
|
+
toolModeVal === 'insert' && toolAddMode === 'span'
|
|
863
|
+
? 'none'
|
|
864
|
+
: undefined,
|
|
819
865
|
cursor: isEditableMode && supportsEditing ? 'text' : 'default',
|
|
820
866
|
minHeight: isPlaceholder ? '1.5em' : undefined,
|
|
867
|
+
display: isInline ? 'inline-block' : undefined,
|
|
821
868
|
},
|
|
822
869
|
'data-node-id': nodeId,
|
|
823
870
|
'data-placeholder': isPlaceholder,
|
|
871
|
+
'data-tag': props.tagName,
|
|
824
872
|
},
|
|
825
873
|
<RenderChildren
|
|
826
874
|
children={children}
|
|
@@ -3,28 +3,53 @@ import type { FlatNode } from '@/types/compositorTypes';
|
|
|
3
3
|
import type { NodeProps } from '@/types/nodeProps';
|
|
4
4
|
import { useStore } from '@nanostores/react';
|
|
5
5
|
import { selectionStore } from '@/stores/selection';
|
|
6
|
+
import type { MouseEvent } from 'react';
|
|
6
7
|
|
|
7
8
|
export const NodeText = (props: NodeProps) => {
|
|
8
9
|
const ctx = getCtx(props);
|
|
9
10
|
const node = ctx.allNodes.get().get(props.nodeId) as FlatNode;
|
|
10
11
|
const { value: toolModeVal } = useStore(ctx.toolModeValStore);
|
|
12
|
+
const toolAddMode = useStore(ctx.toolAddModeStore).value;
|
|
11
13
|
const selection = useStore(selectionStore);
|
|
12
14
|
|
|
13
15
|
if (!node) return <>ERROR MISSING NODE</>;
|
|
14
16
|
|
|
15
17
|
const text = node.copy || '';
|
|
16
18
|
|
|
17
|
-
if (
|
|
19
|
+
if (
|
|
20
|
+
toolModeVal === 'insert' &&
|
|
21
|
+
toolAddMode === `span` &&
|
|
22
|
+
props.isSelectableText
|
|
23
|
+
) {
|
|
18
24
|
let charOffset = 0;
|
|
19
25
|
const wordSpans = text.split(/(\s+)/).map((segment, index) => {
|
|
20
26
|
const startOffset = charOffset;
|
|
21
27
|
const endOffset = charOffset + segment.length;
|
|
22
28
|
charOffset = endOffset;
|
|
23
29
|
|
|
30
|
+
const handleMouseDown = (e: MouseEvent<HTMLElement>) => {
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
e.stopPropagation();
|
|
33
|
+
if (props.onDragStart) {
|
|
34
|
+
props.onDragStart(
|
|
35
|
+
{
|
|
36
|
+
blockNodeId: node.parentId!,
|
|
37
|
+
lcaNodeId: node.parentId!,
|
|
38
|
+
startNodeId: props.nodeId,
|
|
39
|
+
startCharOffset: startOffset,
|
|
40
|
+
endNodeId: props.nodeId,
|
|
41
|
+
endCharOffset: startOffset,
|
|
42
|
+
},
|
|
43
|
+
e
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
24
48
|
if (segment.trim() === '') {
|
|
25
49
|
return (
|
|
26
50
|
<span
|
|
27
51
|
key={index}
|
|
52
|
+
onMouseDown={handleMouseDown}
|
|
28
53
|
data-parent-text-node-id={props.nodeId}
|
|
29
54
|
data-start-char-offset={startOffset}
|
|
30
55
|
data-end-char-offset={endOffset}
|
|
@@ -69,6 +94,7 @@ export const NodeText = (props: NodeProps) => {
|
|
|
69
94
|
className={
|
|
70
95
|
isInSelection ? 'outline-dotted outline-2 outline-blue-600' : ''
|
|
71
96
|
}
|
|
97
|
+
onMouseDown={handleMouseDown} // <--- ADDED THIS
|
|
72
98
|
data-parent-text-node-id={props.nodeId}
|
|
73
99
|
data-start-char-offset={startOffset}
|
|
74
100
|
data-end-char-offset={endOffset}
|
|
@@ -22,7 +22,7 @@ function hashString(str: string): string {
|
|
|
22
22
|
for (let i = 0; i < str.length; i++) {
|
|
23
23
|
const char = str.charCodeAt(i);
|
|
24
24
|
hash = (hash << 5) - hash + char;
|
|
25
|
-
hash = hash & hash;
|
|
25
|
+
hash = hash & hash;
|
|
26
26
|
}
|
|
27
27
|
return hash.toString(36);
|
|
28
28
|
}
|
|
@@ -37,7 +37,7 @@ export const PaneSnapshotGenerator = ({
|
|
|
37
37
|
const [isGenerating, setIsGenerating] = useState(false);
|
|
38
38
|
|
|
39
39
|
useEffect(() => {
|
|
40
|
-
if (!htmlString
|
|
40
|
+
if (!htmlString) return;
|
|
41
41
|
|
|
42
42
|
const cacheKey = `${id}-${hashString(htmlString)}-${outputWidth}`;
|
|
43
43
|
if (snapshotCache.has(cacheKey)) {
|
|
@@ -81,7 +81,6 @@ export const PaneSnapshotGenerator = ({
|
|
|
81
81
|
const brandColors =
|
|
82
82
|
brandConfigStore.get()?.BRAND_COLOURS?.split(',') || [];
|
|
83
83
|
|
|
84
|
-
// Get all existing CSS links from current document
|
|
85
84
|
const existingCssLinks = Array.from(
|
|
86
85
|
document.querySelectorAll('link[rel="stylesheet"]')
|
|
87
86
|
)
|
|
@@ -131,7 +130,6 @@ export const PaneSnapshotGenerator = ({
|
|
|
131
130
|
(iframeDoc as any).write(fullHtml);
|
|
132
131
|
iframeDoc.close();
|
|
133
132
|
|
|
134
|
-
// Wait for CSS to load
|
|
135
133
|
await new Promise((resolve) => {
|
|
136
134
|
if (iframeDoc.readyState === 'complete') {
|
|
137
135
|
resolve(void 0);
|
|
@@ -140,7 +138,6 @@ export const PaneSnapshotGenerator = ({
|
|
|
140
138
|
}
|
|
141
139
|
});
|
|
142
140
|
|
|
143
|
-
// Additional wait for rendering
|
|
144
141
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
145
142
|
|
|
146
143
|
const iframeBody = iframeDoc.body;
|
|
@@ -195,16 +192,21 @@ export const PaneSnapshotGenerator = ({
|
|
|
195
192
|
onComplete(id, snapshotData);
|
|
196
193
|
} catch (error) {
|
|
197
194
|
console.error(`Snapshot generation failed for ${id}:`, error);
|
|
198
|
-
|
|
195
|
+
|
|
196
|
+
const fallbackData: SnapshotData = {
|
|
197
|
+
imageData: '/static.jpg',
|
|
198
|
+
height: 300,
|
|
199
|
+
};
|
|
200
|
+
snapshotCache.set(cacheKey, fallbackData);
|
|
201
|
+
onComplete(id, fallbackData);
|
|
199
202
|
} finally {
|
|
200
203
|
setIsGenerating(false);
|
|
201
204
|
}
|
|
202
205
|
};
|
|
203
206
|
|
|
204
207
|
generateSnapshot();
|
|
205
|
-
}, [id, htmlString,
|
|
208
|
+
}, [id, htmlString, onComplete, onError, outputWidth]);
|
|
206
209
|
|
|
207
|
-
// Show spinner while generating
|
|
208
210
|
if (isGenerating) {
|
|
209
211
|
return (
|
|
210
212
|
<div className="flex h-24 items-center justify-center">
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useState,
|
|
3
|
+
useRef,
|
|
4
|
+
useEffect,
|
|
5
|
+
type MouseEvent,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import { useStore } from '@nanostores/react';
|
|
9
|
+
import PencilSquareIcon from '@heroicons/react/24/outline/PencilSquareIcon';
|
|
10
|
+
import TrashIcon from '@heroicons/react/24/outline/TrashIcon';
|
|
11
|
+
import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
|
|
12
|
+
import { getCtx } from '@/stores/nodes';
|
|
13
|
+
import { settingsPanelStore } from '@/stores/storykeep';
|
|
14
|
+
import { handleClickEventDefault } from '@/utils/compositor/handleClickEvent';
|
|
15
|
+
import { getTemplateNode } from '@/utils/compositor/nodesHelper';
|
|
16
|
+
import { classNames } from '@/utils/helpers';
|
|
17
|
+
import type { NodeProps } from '@/types/nodeProps';
|
|
18
|
+
import type { FlatNode } from '@/types/compositorTypes';
|
|
19
|
+
|
|
20
|
+
interface NodeOverlayProps extends NodeProps {
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
isTopLevel: boolean;
|
|
23
|
+
isInline?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const getIconForTag = (tagName?: string): string | null => {
|
|
27
|
+
const t = tagName?.toLowerCase() || '';
|
|
28
|
+
if (['h2', 'h3', 'h4', 'h5', 'h6', 'p', 'li', 'code'].includes(t)) {
|
|
29
|
+
return `/icons/${t}.svg`;
|
|
30
|
+
}
|
|
31
|
+
if (t === 'img' || t === 'image') return '/icons/image.svg';
|
|
32
|
+
if (t === 'a' || t === 'button') return '/icons/link.svg';
|
|
33
|
+
return null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const NodeOverlay = ({
|
|
37
|
+
nodeId,
|
|
38
|
+
children,
|
|
39
|
+
isTopLevel,
|
|
40
|
+
isInline,
|
|
41
|
+
...props
|
|
42
|
+
}: NodeOverlayProps) => {
|
|
43
|
+
const ctx = getCtx({ nodeId, ...props });
|
|
44
|
+
const toolMode = useStore(ctx.toolModeValStore).value;
|
|
45
|
+
const toolAddMode = useStore(ctx.toolAddModeStore).value;
|
|
46
|
+
const settingsPanel = useStore(settingsPanelStore);
|
|
47
|
+
const [hoverZone, setHoverZone] = useState<'before' | 'after' | null>(null);
|
|
48
|
+
|
|
49
|
+
// put a contentEditable={false} component inside a tree that inherits contentEditable={true}.
|
|
50
|
+
const chromeRef = useRef<HTMLDivElement>(null);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (chromeRef.current) {
|
|
54
|
+
chromeRef.current.contentEditable = 'false';
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const node = ctx.allNodes.get().get(nodeId) as FlatNode;
|
|
59
|
+
if (!node) return <>{children}</>;
|
|
60
|
+
|
|
61
|
+
const zIndexClass = isTopLevel ? 'z-101 hover:z-103' : 'z-101 hover:z-104';
|
|
62
|
+
|
|
63
|
+
const isSelected = settingsPanel?.nodeId === nodeId;
|
|
64
|
+
|
|
65
|
+
const outlineClass = isSelected
|
|
66
|
+
? 'outline outline-4 outline-dotted outline-orange-400 outline-offset-2'
|
|
67
|
+
: 'hover:outline hover:outline-2 hover:outline-dotted hover:outline-cyan-500 hover:outline-offset-2';
|
|
68
|
+
|
|
69
|
+
const handleEditClick = (e: MouseEvent) => {
|
|
70
|
+
e.stopPropagation();
|
|
71
|
+
handleClickEventDefault(node, true);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const handleDeleteClick = (e: MouseEvent) => {
|
|
75
|
+
e.stopPropagation();
|
|
76
|
+
ctx.deleteNode(nodeId);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const handleCopyIdClick = (e: MouseEvent) => {
|
|
80
|
+
e.stopPropagation();
|
|
81
|
+
navigator.clipboard.writeText(nodeId);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleInsert = (location: 'before' | 'after', e: MouseEvent) => {
|
|
85
|
+
e.stopPropagation();
|
|
86
|
+
|
|
87
|
+
const tagName = toolAddMode || 'p';
|
|
88
|
+
const templateNode = getTemplateNode(tagName);
|
|
89
|
+
|
|
90
|
+
if (templateNode) {
|
|
91
|
+
ctx.addTemplateNode(nodeId, templateNode, nodeId, location);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const canInsert =
|
|
96
|
+
toolMode === 'insert'
|
|
97
|
+
? ctx.allowInsert(nodeId, toolAddMode || 'p')
|
|
98
|
+
: { allowInsertBefore: false, allowInsertAfter: false };
|
|
99
|
+
|
|
100
|
+
const iconSrc = getIconForTag(node.tagName);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
className={classNames(
|
|
105
|
+
'compositor-wrapper group relative transition-all duration-200',
|
|
106
|
+
zIndexClass,
|
|
107
|
+
toolMode === 'text' ? outlineClass : ''
|
|
108
|
+
)}
|
|
109
|
+
style={isInline ? { display: 'inline-block' } : {}}
|
|
110
|
+
data-node-overlay={nodeId}
|
|
111
|
+
>
|
|
112
|
+
{children}
|
|
113
|
+
|
|
114
|
+
{/* Text Mode: Tool Cart */}
|
|
115
|
+
{toolMode === 'text' && (
|
|
116
|
+
<div
|
|
117
|
+
ref={chromeRef}
|
|
118
|
+
className="node-overlay compositor-chrome absolute flex gap-1 opacity-10 transition-opacity duration-200 group-hover:opacity-100"
|
|
119
|
+
style={{
|
|
120
|
+
top: '-24px',
|
|
121
|
+
right: 0,
|
|
122
|
+
zIndex: 10,
|
|
123
|
+
}}
|
|
124
|
+
onClick={(e) => e.stopPropagation()}
|
|
125
|
+
data-attr="exclude"
|
|
126
|
+
>
|
|
127
|
+
<button
|
|
128
|
+
onClick={handleCopyIdClick}
|
|
129
|
+
className="flex h-6 w-auto min-w-px cursor-help items-center justify-center px-1"
|
|
130
|
+
title={`${node.tagName}: ${nodeId}`}
|
|
131
|
+
>
|
|
132
|
+
{iconSrc && (
|
|
133
|
+
<img
|
|
134
|
+
src={iconSrc}
|
|
135
|
+
alt={node.tagName || 'node'}
|
|
136
|
+
className="h-3.5 w-auto"
|
|
137
|
+
/>
|
|
138
|
+
)}
|
|
139
|
+
</button>
|
|
140
|
+
|
|
141
|
+
<button
|
|
142
|
+
onClick={handleEditClick}
|
|
143
|
+
className="flex h-6 w-6 items-center justify-center rounded-full bg-cyan-500 text-white shadow-md hover:scale-110 hover:bg-cyan-600"
|
|
144
|
+
title="Edit Styles"
|
|
145
|
+
>
|
|
146
|
+
<PencilSquareIcon className="h-3.5 w-3.5" />
|
|
147
|
+
</button>
|
|
148
|
+
|
|
149
|
+
<button
|
|
150
|
+
onClick={handleDeleteClick}
|
|
151
|
+
className="flex h-6 w-6 items-center justify-center rounded-full bg-red-500 text-white shadow-md hover:scale-110 hover:bg-red-600"
|
|
152
|
+
title="Delete Element"
|
|
153
|
+
>
|
|
154
|
+
<TrashIcon className="h-3.5 w-3.5" />
|
|
155
|
+
</button>
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
|
|
159
|
+
{/* Insert Mode: Split Drop Zones */}
|
|
160
|
+
{toolMode === 'insert' && toolAddMode !== `span` && (
|
|
161
|
+
<div
|
|
162
|
+
className="compositor-chrome absolute inset-0 z-50 flex flex-col"
|
|
163
|
+
data-attr="exclude"
|
|
164
|
+
>
|
|
165
|
+
{/* Top / Before Zone */}
|
|
166
|
+
<div
|
|
167
|
+
className={classNames(
|
|
168
|
+
'flex-1 transition-colors duration-200',
|
|
169
|
+
canInsert.allowInsertBefore
|
|
170
|
+
? 'cursor-pointer hover:bg-blue-500/10'
|
|
171
|
+
: 'cursor-not-allowed opacity-0'
|
|
172
|
+
)}
|
|
173
|
+
onMouseEnter={() => setHoverZone('before')}
|
|
174
|
+
onMouseLeave={() => setHoverZone(null)}
|
|
175
|
+
onClick={(e) =>
|
|
176
|
+
canInsert.allowInsertBefore && handleInsert('before', e)
|
|
177
|
+
}
|
|
178
|
+
>
|
|
179
|
+
{canInsert.allowInsertBefore && (
|
|
180
|
+
<div
|
|
181
|
+
className={classNames(
|
|
182
|
+
'absolute left-1/2 top-0 -translate-x-1/2 -translate-y-1/2 transform transition-opacity duration-200',
|
|
183
|
+
hoverZone === 'before' ? 'opacity-100' : 'opacity-40'
|
|
184
|
+
)}
|
|
185
|
+
>
|
|
186
|
+
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-blue-600 text-white shadow-sm">
|
|
187
|
+
<PlusIcon className="h-3.5 w-3.5" />
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
{/* Bottom / After Zone */}
|
|
194
|
+
<div
|
|
195
|
+
className={classNames(
|
|
196
|
+
'flex-1 transition-colors duration-200',
|
|
197
|
+
canInsert.allowInsertAfter
|
|
198
|
+
? 'cursor-pointer hover:bg-blue-500/10'
|
|
199
|
+
: 'cursor-not-allowed opacity-0'
|
|
200
|
+
)}
|
|
201
|
+
onMouseEnter={() => setHoverZone('after')}
|
|
202
|
+
onMouseLeave={() => setHoverZone(null)}
|
|
203
|
+
onClick={(e) =>
|
|
204
|
+
canInsert.allowInsertAfter && handleInsert('after', e)
|
|
205
|
+
}
|
|
206
|
+
>
|
|
207
|
+
{canInsert.allowInsertAfter && (
|
|
208
|
+
<div
|
|
209
|
+
className={classNames(
|
|
210
|
+
'absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2 transform transition-opacity duration-200',
|
|
211
|
+
hoverZone === 'after' ? 'opacity-100' : 'opacity-40'
|
|
212
|
+
)}
|
|
213
|
+
>
|
|
214
|
+
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-blue-600 text-white shadow-sm">
|
|
215
|
+
<PlusIcon className="h-3.5 w-3.5" />
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
};
|