astro-tractstack 2.0.26 → 2.0.28
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 +6 -0
- package/package.json +1 -1
- package/templates/src/components/compositor/Compositor.tsx +33 -1
- package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag.tsx +31 -4
- package/templates/src/components/edit/PanelSwitch.tsx +5 -0
- package/templates/src/components/edit/ToolMode.tsx +14 -0
- package/templates/src/components/edit/panels/StyleElementPanel.tsx +215 -33
- package/templates/src/components/edit/panels/StyleLinkPanel.tsx +81 -17
- package/templates/src/components/edit/panels/StyleWordCarouselPanel.tsx +181 -0
- package/templates/src/stores/nodes.ts +0 -21
- package/templates/src/stores/selection.ts +1 -1
- package/templates/src/types/compositorTypes.ts +4 -0
- package/templates/src/utils/compositor/nodesHelper.ts +3 -1
- package/templates/src/utils/etl/transformer.ts +1 -0
- package/utils/inject-files.ts +6 -0
package/dist/index.js
CHANGED
|
@@ -1394,6 +1394,12 @@ async function w(t, e, c) {
|
|
|
1394
1394
|
src: t("../templates/src/components/edit/state/StylesMemory.tsx"),
|
|
1395
1395
|
dest: "src/components/edit/state/StylesMemory.tsx"
|
|
1396
1396
|
},
|
|
1397
|
+
{
|
|
1398
|
+
src: t(
|
|
1399
|
+
"../templates/src/components/edit/panels/StyleWordCarouselPanel.tsx"
|
|
1400
|
+
),
|
|
1401
|
+
dest: "src/components/edit/panels/StyleWordCarouselPanel.tsx"
|
|
1402
|
+
},
|
|
1397
1403
|
{
|
|
1398
1404
|
src: t(
|
|
1399
1405
|
"../templates/src/components/edit/panels/StyleBreakPanel.tsx"
|
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
resetSelectionStore,
|
|
27
27
|
type SelectionStoreState,
|
|
28
28
|
} from '@/stores/selection';
|
|
29
|
-
import type { LoadData } from '@/types/compositorTypes';
|
|
29
|
+
import type { LoadData, FlatNode } from '@/types/compositorTypes';
|
|
30
30
|
import type {
|
|
31
31
|
Theme,
|
|
32
32
|
BrandConfig,
|
|
@@ -410,6 +410,38 @@ export const Compositor = (props: CompositorProps) => {
|
|
|
410
410
|
});
|
|
411
411
|
}
|
|
412
412
|
resetSelectionStore();
|
|
413
|
+
} else if ($selection.pendingAction === 'carousel') {
|
|
414
|
+
if (VERBOSE) console.log(LOG_PREFIX + 'useEffect acting on: carousel');
|
|
415
|
+
const newSpanNodeId = await ctx.wrapRangeInSpan(
|
|
416
|
+
range as SelectionStoreState,
|
|
417
|
+
'span'
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
if (newSpanNodeId) {
|
|
421
|
+
const node = ctx.allNodes.get().get(newSpanNodeId);
|
|
422
|
+
const childIds = ctx.getChildNodeIDs(newSpanNodeId);
|
|
423
|
+
let initialText = '';
|
|
424
|
+
childIds.forEach((childId) => {
|
|
425
|
+
const child = ctx.allNodes.get().get(childId) as FlatNode;
|
|
426
|
+
if (child && child.copy) initialText += child.copy;
|
|
427
|
+
});
|
|
428
|
+
const words = initialText ? [initialText] : [];
|
|
429
|
+
if (node) {
|
|
430
|
+
ctx.modifyNodes([
|
|
431
|
+
{
|
|
432
|
+
...node,
|
|
433
|
+
wordCarouselPayload: { words, speed: 2 },
|
|
434
|
+
isChanged: true,
|
|
435
|
+
} as FlatNode,
|
|
436
|
+
]);
|
|
437
|
+
settingsPanelStore.set({
|
|
438
|
+
action: 'style-word-carousel',
|
|
439
|
+
nodeId: newSpanNodeId,
|
|
440
|
+
expanded: true,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
resetSelectionStore();
|
|
413
445
|
}
|
|
414
446
|
ctx.notifyNode('root');
|
|
415
447
|
};
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { useStore } from '@nanostores/react';
|
|
13
13
|
import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
|
|
14
14
|
import PaintBrushIcon from '@heroicons/react/24/outline/PaintBrushIcon';
|
|
15
|
+
import ChatBubbleBottomCenterTextIcon from '@heroicons/react/24/outline/ChatBubbleBottomCenterTextIcon';
|
|
15
16
|
import { getCtx } from '@/stores/nodes';
|
|
16
17
|
import {
|
|
17
18
|
viewportKeyStore,
|
|
@@ -24,7 +25,7 @@ import {
|
|
|
24
25
|
processRichTextToNodes,
|
|
25
26
|
getTemplateNode,
|
|
26
27
|
} from '@/utils/compositor/nodesHelper';
|
|
27
|
-
import { cloneDeep } from '@/utils/helpers';
|
|
28
|
+
import { cloneDeep, classNames } from '@/utils/helpers';
|
|
28
29
|
import { PatchOp } from '@/stores/nodesHistory';
|
|
29
30
|
import type { FlatNode, PaneNode } from '@/types/compositorTypes';
|
|
30
31
|
import type { NodeProps } from '@/types/nodeProps';
|
|
@@ -273,6 +274,15 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
273
274
|
e.stopPropagation();
|
|
274
275
|
ctx.unwrapNode(nodeId);
|
|
275
276
|
};
|
|
277
|
+
const handleWordCarouselClick = (e: MouseEvent<HTMLButtonElement>) => {
|
|
278
|
+
e.preventDefault();
|
|
279
|
+
e.stopPropagation();
|
|
280
|
+
settingsPanelStore.set({
|
|
281
|
+
action: 'style-word-carousel',
|
|
282
|
+
nodeId: nodeId,
|
|
283
|
+
expanded: true,
|
|
284
|
+
});
|
|
285
|
+
};
|
|
276
286
|
|
|
277
287
|
let baseClasses = ctx.getNodeClasses(nodeId, viewportKeyStore.get().value);
|
|
278
288
|
baseClasses += ' outline outline-1 outline-dotted outline-black';
|
|
@@ -300,7 +310,7 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
300
310
|
<RenderChildren key="children" children={children} nodeProps={props} />,
|
|
301
311
|
isEditorEnabled && (
|
|
302
312
|
<span
|
|
303
|
-
key=
|
|
313
|
+
key={`toolbar-${nodeId}`}
|
|
304
314
|
className="absolute z-10 flex select-none gap-x-1"
|
|
305
315
|
data-attr="exclude"
|
|
306
316
|
style={{ top: '-0.9rem', left: '0' }}
|
|
@@ -309,17 +319,34 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
309
319
|
<button
|
|
310
320
|
type="button"
|
|
311
321
|
onClick={handleStyleClick}
|
|
312
|
-
className="flex h-4 w-4 items-center justify-center rounded-full bg-blue-100
|
|
322
|
+
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"
|
|
313
323
|
aria-label="Style selection"
|
|
314
324
|
data-attr="exclude"
|
|
315
325
|
>
|
|
316
326
|
<PaintBrushIcon className="h-3 w-3" data-attr="exclude" />
|
|
317
327
|
</button>
|
|
318
328
|
)}
|
|
329
|
+
<button
|
|
330
|
+
type="button"
|
|
331
|
+
onClick={handleWordCarouselClick}
|
|
332
|
+
className={classNames(
|
|
333
|
+
'flex h-4 w-4 items-center justify-center rounded-full bg-opacity-50 shadow-sm focus:outline-none',
|
|
334
|
+
node.wordCarouselPayload
|
|
335
|
+
? 'bg-green-100 text-green-700 hover:bg-green-300'
|
|
336
|
+
: 'bg-gray-100 bg-opacity-90 text-gray-700 hover:bg-gray-300'
|
|
337
|
+
)}
|
|
338
|
+
aria-label="Edit Carousel"
|
|
339
|
+
data-attr="exclude"
|
|
340
|
+
>
|
|
341
|
+
<ChatBubbleBottomCenterTextIcon
|
|
342
|
+
className="h-3 w-3"
|
|
343
|
+
data-attr="exclude"
|
|
344
|
+
/>
|
|
345
|
+
</button>
|
|
319
346
|
<button
|
|
320
347
|
type="button"
|
|
321
348
|
onClick={handleUnwrapClick}
|
|
322
|
-
className="flex h-4 w-4 items-center justify-center rounded-full bg-gray-100
|
|
349
|
+
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"
|
|
323
350
|
aria-label="Remove formatting"
|
|
324
351
|
data-attr="exclude"
|
|
325
352
|
>
|
|
@@ -34,6 +34,7 @@ import StyleLiElementPanel from './panels/StyleLiElementPanel';
|
|
|
34
34
|
import StyleLiElementAddPanel from './panels/StyleLiElementPanel_add';
|
|
35
35
|
import StyleLiElementUpdatePanel from './panels/StyleLiElementPanel_update';
|
|
36
36
|
import StyleLiElementRemovePanel from './panels/StyleLiElementPanel_remove';
|
|
37
|
+
import StyleWordCarouselPanel from './panels/StyleWordCarouselPanel';
|
|
37
38
|
import StyleCodeHookPanel from './panels/StyleCodeHookPanel';
|
|
38
39
|
import { getSettingsPanelTitle } from '@/utils/helpers';
|
|
39
40
|
import type {
|
|
@@ -270,6 +271,10 @@ const PanelSwitch = ({
|
|
|
270
271
|
);
|
|
271
272
|
break;
|
|
272
273
|
|
|
274
|
+
case 'style-word-carousel':
|
|
275
|
+
if (clickedNode) return <StyleWordCarouselPanel node={clickedNode} />;
|
|
276
|
+
break;
|
|
277
|
+
|
|
273
278
|
case 'style-image': {
|
|
274
279
|
let imageNode: FlatNode | undefined;
|
|
275
280
|
let containerNode: FlatNode | undefined;
|
|
@@ -8,6 +8,7 @@ import ArrowsUpDownIcon from '@heroicons/react/24/outline/ArrowsUpDownIcon';
|
|
|
8
8
|
import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
|
|
9
9
|
import BugAntIcon from '@heroicons/react/24/outline/BugAntIcon';
|
|
10
10
|
import LinkIcon from '@heroicons/react/24/solid/LinkIcon';
|
|
11
|
+
import ChatBubbleBottomCenterTextIcon from '@heroicons/react/24/outline/ChatBubbleBottomCenterTextIcon';
|
|
11
12
|
import XMarkIcon from '@heroicons/react/24/solid/XMarkIcon';
|
|
12
13
|
import { settingsPanelStore } from '@/stores/storykeep';
|
|
13
14
|
import { getCtx } from '@/stores/nodes';
|
|
@@ -97,6 +98,10 @@ const StoryKeepToolMode = ({ isContext }: StoryKeepToolModeProps) => {
|
|
|
97
98
|
selectionStore.setKey('pendingAction', 'link');
|
|
98
99
|
};
|
|
99
100
|
|
|
101
|
+
const handleCarouselClick = () => {
|
|
102
|
+
selectionStore.setKey('pendingAction', 'carousel');
|
|
103
|
+
};
|
|
104
|
+
|
|
100
105
|
const handleCancelClick = () => {
|
|
101
106
|
resetSelectionStore();
|
|
102
107
|
};
|
|
@@ -184,6 +189,15 @@ const StoryKeepToolMode = ({ isContext }: StoryKeepToolModeProps) => {
|
|
|
184
189
|
>
|
|
185
190
|
<LinkIcon className="h-5 w-5" />
|
|
186
191
|
</button>
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
onClick={handleCarouselClick}
|
|
195
|
+
className="flex h-8 w-8 items-center justify-center rounded-lg bg-blue-100 text-blue-700 shadow-sm hover:bg-blue-200"
|
|
196
|
+
aria-label="Create Word Carousel"
|
|
197
|
+
title="Word Carousel"
|
|
198
|
+
>
|
|
199
|
+
<ChatBubbleBottomCenterTextIcon className="h-5 w-5" />
|
|
200
|
+
</button>
|
|
187
201
|
<button
|
|
188
202
|
type="button"
|
|
189
203
|
onClick={handleCancelClick}
|
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import { useMemo, useEffect } from 'react';
|
|
1
|
+
import { useMemo, useEffect, useState } from 'react';
|
|
2
|
+
import Cog6ToothIcon from '@heroicons/react/24/outline/Cog6ToothIcon';
|
|
2
3
|
import {
|
|
3
4
|
styleElementInfoStore,
|
|
4
5
|
resetStyleElementInfo,
|
|
5
6
|
settingsPanelStore,
|
|
6
7
|
} from '@/stores/storykeep';
|
|
8
|
+
import { getCtx } from '@/stores/nodes';
|
|
7
9
|
import { StylesMemory } from '@/components/edit/state/StylesMemory';
|
|
8
10
|
import {
|
|
9
11
|
isMarkdownPaneFragmentNode,
|
|
10
12
|
isGridLayoutNode,
|
|
11
13
|
} from '@/utils/compositor/typeGuards';
|
|
14
|
+
import { getNodeText } from '@/utils/compositor/nodesHelper';
|
|
15
|
+
import { cloneDeep } from '@/utils/helpers';
|
|
16
|
+
import { processClassesForViewports } from '@/utils/compositor/reduceNodesClassNames';
|
|
12
17
|
import SelectedTailwindClass from '@/components/fields/SelectedTailwindClass';
|
|
13
18
|
import { tagTitles } from '@/types/compositorTypes';
|
|
14
19
|
import type {
|
|
@@ -18,6 +23,78 @@ import type {
|
|
|
18
23
|
GridLayoutNode,
|
|
19
24
|
} from '@/types/compositorTypes';
|
|
20
25
|
|
|
26
|
+
type SpanOverride = {
|
|
27
|
+
mobile?: Record<string, string>;
|
|
28
|
+
tablet?: Record<string, string>;
|
|
29
|
+
desktop?: Record<string, string>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const spanStyleClasses: SpanOverride[] = [
|
|
33
|
+
{
|
|
34
|
+
mobile: {
|
|
35
|
+
bgCLIP: 'text',
|
|
36
|
+
bgGradientDIRECTION: 'r',
|
|
37
|
+
gradientFrom: 'blue-600',
|
|
38
|
+
gradientTo: 'teal-500',
|
|
39
|
+
textCOLOR: 'transparent',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
mobile: {
|
|
44
|
+
textCOLOR: 'blue-600',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
mobile: {
|
|
49
|
+
bgCOLOR: 'yellow-300',
|
|
50
|
+
textCOLOR: 'slate-900',
|
|
51
|
+
px: '1',
|
|
52
|
+
rounded: 'sm',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
mobile: {
|
|
57
|
+
display: 'inline-block',
|
|
58
|
+
bgCOLOR: 'indigo-100',
|
|
59
|
+
textCOLOR: 'indigo-700',
|
|
60
|
+
textSIZE: 'xs',
|
|
61
|
+
fontWEIGHT: 'bold',
|
|
62
|
+
px: '2.5',
|
|
63
|
+
py: '0.5',
|
|
64
|
+
rounded: 'full',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
mobile: {
|
|
69
|
+
bgCLIP: 'text',
|
|
70
|
+
textCOLOR: 'transparent',
|
|
71
|
+
bgGradientDIRECTION: 'r',
|
|
72
|
+
gradientFrom: 'orange-400',
|
|
73
|
+
gradientVia: 'pink-500',
|
|
74
|
+
gradientTo: 'purple-600',
|
|
75
|
+
fontWEIGHT: 'bold',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
mobile: {
|
|
80
|
+
textDECORATION: 'underline',
|
|
81
|
+
textDECORATIONSTYLE: 'wavy',
|
|
82
|
+
textDECORATIONCOLOR: 'teal-400',
|
|
83
|
+
textDECORATIONTHICKNESS: '4',
|
|
84
|
+
textUNDERLINEOFFSET: '4',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
mobile: {
|
|
89
|
+
display: 'inline-block',
|
|
90
|
+
bgCOLOR: 'rose-500',
|
|
91
|
+
textCOLOR: 'white',
|
|
92
|
+
px: '2',
|
|
93
|
+
skew: '-3',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
|
|
21
98
|
export interface StyleElementPanelProps {
|
|
22
99
|
node: FlatNode;
|
|
23
100
|
parentNode: MarkdownPaneFragmentNode | GridLayoutNode;
|
|
@@ -29,6 +106,8 @@ const StyleElementPanel = ({
|
|
|
29
106
|
parentNode,
|
|
30
107
|
onTitleChange,
|
|
31
108
|
}: StyleElementPanelProps) => {
|
|
109
|
+
const [showPresets, setShowPresets] = useState(true);
|
|
110
|
+
|
|
32
111
|
if (
|
|
33
112
|
!node?.tagName ||
|
|
34
113
|
(!isMarkdownPaneFragmentNode(parentNode) && !isGridLayoutNode(parentNode))
|
|
@@ -39,6 +118,18 @@ const StyleElementPanel = ({
|
|
|
39
118
|
const defaultClasses = parentNode.defaultClasses?.[node.tagName];
|
|
40
119
|
const overrideClasses = node.overrideClasses;
|
|
41
120
|
|
|
121
|
+
const hasOverrides = useMemo(() => {
|
|
122
|
+
return (
|
|
123
|
+
overrideClasses &&
|
|
124
|
+
((overrideClasses.mobile &&
|
|
125
|
+
Object.keys(overrideClasses.mobile).length > 0) ||
|
|
126
|
+
(overrideClasses.tablet &&
|
|
127
|
+
Object.keys(overrideClasses.tablet).length > 0) ||
|
|
128
|
+
(overrideClasses.desktop &&
|
|
129
|
+
Object.keys(overrideClasses.desktop).length > 0))
|
|
130
|
+
);
|
|
131
|
+
}, [overrideClasses]);
|
|
132
|
+
|
|
42
133
|
const mergedClasses = useMemo(() => {
|
|
43
134
|
const result: {
|
|
44
135
|
[key: string]: {
|
|
@@ -48,7 +139,6 @@ const StyleElementPanel = ({
|
|
|
48
139
|
};
|
|
49
140
|
} = {};
|
|
50
141
|
|
|
51
|
-
// First add all default classes
|
|
52
142
|
if (defaultClasses) {
|
|
53
143
|
Object.keys(defaultClasses.mobile).forEach((className) => {
|
|
54
144
|
result[className] = {
|
|
@@ -63,7 +153,6 @@ const StyleElementPanel = ({
|
|
|
63
153
|
});
|
|
64
154
|
}
|
|
65
155
|
|
|
66
|
-
// Then overlay any override classes
|
|
67
156
|
if (overrideClasses) {
|
|
68
157
|
['mobile', 'tablet', 'desktop'].forEach((viewport) => {
|
|
69
158
|
const viewportOverrides =
|
|
@@ -113,6 +202,24 @@ const StyleElementPanel = ({
|
|
|
113
202
|
});
|
|
114
203
|
};
|
|
115
204
|
|
|
205
|
+
const applySpanPreset = (styleIndex: number) => {
|
|
206
|
+
const ctx = getCtx();
|
|
207
|
+
const allNodes = ctx.allNodes.get();
|
|
208
|
+
const targetNode = cloneDeep(allNodes.get(node.id)) as FlatNode;
|
|
209
|
+
if (!targetNode) return;
|
|
210
|
+
|
|
211
|
+
const preset = spanStyleClasses[styleIndex];
|
|
212
|
+
|
|
213
|
+
targetNode.overrideClasses = {
|
|
214
|
+
...targetNode.overrideClasses,
|
|
215
|
+
...preset,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
ctx.modifyNodes([{ ...targetNode, isChanged: true }]);
|
|
219
|
+
|
|
220
|
+
setShowPresets(false);
|
|
221
|
+
};
|
|
222
|
+
|
|
116
223
|
useEffect(() => {
|
|
117
224
|
if (
|
|
118
225
|
styleElementInfoStore.get().markdownParentId !== parentNode.id ||
|
|
@@ -139,44 +246,119 @@ const StyleElementPanel = ({
|
|
|
139
246
|
}
|
|
140
247
|
}, [node?.tagName, onTitleChange]);
|
|
141
248
|
|
|
249
|
+
const shouldShowQuickStyles =
|
|
250
|
+
node.tagName === 'span' && !hasOverrides && showPresets;
|
|
251
|
+
const nodeText = shouldShowQuickStyles ? getNodeText(node) : '';
|
|
252
|
+
|
|
142
253
|
return (
|
|
143
254
|
<div className="space-y-4">
|
|
144
|
-
{
|
|
145
|
-
<div className="
|
|
146
|
-
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
255
|
+
{node.wordCarouselPayload && (
|
|
256
|
+
<div className="pb-2">
|
|
257
|
+
<div className="text-myblack hover:bg-mygreen/20 w-fit rounded border border-slate-200 p-2 text-sm">
|
|
258
|
+
<div
|
|
259
|
+
title="Configure Word Carousel"
|
|
260
|
+
className="flex items-center gap-2 font-bold"
|
|
261
|
+
>
|
|
262
|
+
<Cog6ToothIcon className="h-4 w-4" />
|
|
263
|
+
<button
|
|
264
|
+
onClick={() =>
|
|
265
|
+
settingsPanelStore.set({
|
|
266
|
+
nodeId: node.id,
|
|
267
|
+
action: 'style-word-carousel',
|
|
268
|
+
expanded: true,
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
>
|
|
272
|
+
Configure Word Carousel
|
|
273
|
+
</button>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
159
276
|
</div>
|
|
160
277
|
)}
|
|
161
278
|
|
|
162
|
-
|
|
163
|
-
<
|
|
164
|
-
<
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
279
|
+
{shouldShowQuickStyles ? (
|
|
280
|
+
<div className="space-y-6">
|
|
281
|
+
<div className="space-y-2">
|
|
282
|
+
<h3 className="text-mydarkgrey text-sm font-bold">
|
|
283
|
+
Quick Style Selection
|
|
284
|
+
</h3>
|
|
285
|
+
<p className="text-xs text-gray-500">
|
|
286
|
+
Select a preset style for your text selection.
|
|
287
|
+
</p>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<div className="flex flex-col gap-4">
|
|
291
|
+
{spanStyleClasses.map((style, index) => {
|
|
292
|
+
const [classesPayload] = processClassesForViewports(
|
|
293
|
+
style as any,
|
|
294
|
+
{},
|
|
295
|
+
1
|
|
296
|
+
);
|
|
297
|
+
const combinedClasses = classesPayload[0] || '';
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<button
|
|
301
|
+
key={index}
|
|
302
|
+
onClick={() => applySpanPreset(index)}
|
|
303
|
+
className="group w-full text-left text-xl transition-colors hover:outline-dotted hover:outline-2 hover:outline-black"
|
|
304
|
+
>
|
|
305
|
+
<span className={combinedClasses}>
|
|
306
|
+
{nodeText || 'Sample Text'}
|
|
307
|
+
</span>
|
|
308
|
+
</button>
|
|
309
|
+
);
|
|
310
|
+
})}
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<div className="border-t border-gray-100 pt-4">
|
|
168
314
|
<button
|
|
169
|
-
onClick={() =>
|
|
170
|
-
className="text-myblue
|
|
315
|
+
onClick={() => setShowPresets(false)}
|
|
316
|
+
className="text-myblue w-full text-center text-sm underline hover:text-black"
|
|
171
317
|
>
|
|
172
|
-
|
|
318
|
+
Apply your own styles manually
|
|
173
319
|
</button>
|
|
174
|
-
</
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
) : (
|
|
323
|
+
<>
|
|
324
|
+
{Object.keys(mergedClasses).length > 0 ? (
|
|
325
|
+
<div className="flex flex-wrap gap-2">
|
|
326
|
+
{Object.entries(mergedClasses).map(([className, values]) => (
|
|
327
|
+
<SelectedTailwindClass
|
|
328
|
+
key={className}
|
|
329
|
+
name={className}
|
|
330
|
+
values={values}
|
|
331
|
+
onRemove={handleRemove}
|
|
332
|
+
onUpdate={handleUpdate}
|
|
333
|
+
/>
|
|
334
|
+
))}
|
|
335
|
+
</div>
|
|
336
|
+
) : (
|
|
337
|
+
<div className="space-y-4">
|
|
338
|
+
<em>No styles.</em>
|
|
339
|
+
</div>
|
|
340
|
+
)}
|
|
341
|
+
|
|
342
|
+
<div className="space-y-4">
|
|
343
|
+
<ul className="text-mydarkgrey flex flex-wrap gap-x-4 gap-y-1">
|
|
344
|
+
<li>
|
|
345
|
+
<em>Actions:</em>
|
|
346
|
+
</li>
|
|
347
|
+
<li>
|
|
348
|
+
<button
|
|
349
|
+
onClick={() => handleClickAdd()}
|
|
350
|
+
className="text-myblue font-bold underline hover:text-black"
|
|
351
|
+
>
|
|
352
|
+
Add Style
|
|
353
|
+
</button>
|
|
354
|
+
</li>
|
|
355
|
+
<li>
|
|
356
|
+
<StylesMemory node={node} parentNode={parentNode} />
|
|
357
|
+
</li>
|
|
358
|
+
</ul>
|
|
359
|
+
</div>
|
|
360
|
+
</>
|
|
361
|
+
)}
|
|
180
362
|
</div>
|
|
181
363
|
);
|
|
182
364
|
};
|
|
@@ -32,10 +32,13 @@ type ButtonStylePair = [ButtonStyle, ButtonStyle];
|
|
|
32
32
|
|
|
33
33
|
const buttonStyleOptions = [
|
|
34
34
|
'Plain text inline',
|
|
35
|
-
'
|
|
36
|
-
'
|
|
35
|
+
'Primary',
|
|
36
|
+
'Primary inverse',
|
|
37
|
+
'Dark',
|
|
38
|
+
'Dark inverse',
|
|
37
39
|
];
|
|
38
40
|
const buttonStyleClasses: ButtonStylePair[] = [
|
|
41
|
+
// Plain text inline
|
|
39
42
|
[
|
|
40
43
|
{
|
|
41
44
|
fontWEIGHT: ['bold'],
|
|
@@ -48,32 +51,93 @@ const buttonStyleClasses: ButtonStylePair[] = [
|
|
|
48
51
|
textCOLOR: ['brand-1'],
|
|
49
52
|
},
|
|
50
53
|
],
|
|
54
|
+
// Primary Solid Button
|
|
51
55
|
[
|
|
52
56
|
{
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
alignITEMS: ['center'],
|
|
58
|
+
bgCOLOR: ['blue-600'],
|
|
59
|
+
borderCOLOR: ['transparent'],
|
|
60
|
+
display: ['inline-flex'],
|
|
61
|
+
justifyCONTENT: ['center'],
|
|
62
|
+
px: ['6'],
|
|
63
|
+
py: ['3'],
|
|
64
|
+
rounded: ['md'],
|
|
65
|
+
textCOLOR: ['white'],
|
|
66
|
+
textSIZE: ['base'],
|
|
67
|
+
transition: ['colors'],
|
|
68
|
+
transitionDURATION: ['200'],
|
|
69
|
+
w: ['full'],
|
|
59
70
|
},
|
|
60
71
|
{
|
|
61
|
-
bgCOLOR: ['
|
|
72
|
+
bgCOLOR: ['blue-700'],
|
|
62
73
|
},
|
|
63
74
|
],
|
|
75
|
+
// Secondary Outline Button
|
|
64
76
|
[
|
|
65
77
|
{
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
78
|
+
alignITEMS: ['center'],
|
|
79
|
+
bgCOLOR: ['transparent'],
|
|
80
|
+
borderCOLOR: ['blue-600'],
|
|
81
|
+
borderWIDTH: ['2'],
|
|
82
|
+
display: ['inline-flex'],
|
|
83
|
+
justifyCONTENT: ['center'],
|
|
84
|
+
px: ['6'],
|
|
85
|
+
py: ['3'],
|
|
71
86
|
rounded: ['md'],
|
|
72
|
-
textCOLOR: ['
|
|
87
|
+
textCOLOR: ['blue-600'],
|
|
88
|
+
textSIZE: ['base'],
|
|
89
|
+
transition: ['colors'],
|
|
90
|
+
transitionDURATION: ['200'],
|
|
91
|
+
w: ['full'],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
bgCOLOR: ['blue-50'],
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
[
|
|
98
|
+
{
|
|
99
|
+
alignITEMS: ['center'],
|
|
100
|
+
bgCOLOR: ['black'],
|
|
101
|
+
borderCOLOR: ['transparent'],
|
|
102
|
+
borderWIDTH: ['2'],
|
|
103
|
+
display: ['inline-flex'],
|
|
104
|
+
justifyCONTENT: ['center'],
|
|
105
|
+
px: ['6'],
|
|
106
|
+
py: ['3'],
|
|
107
|
+
rounded: ['md'],
|
|
108
|
+
textCOLOR: ['white'],
|
|
109
|
+
textSIZE: ['base'],
|
|
110
|
+
transition: ['colors'],
|
|
111
|
+
transitionDURATION: ['200'],
|
|
112
|
+
w: ['full'],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
bgCOLOR: ['white'],
|
|
116
|
+
textCOLOR: ['black'],
|
|
117
|
+
borderCOLOR: ['black'],
|
|
118
|
+
borderWIDTH: ['2'],
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
[
|
|
122
|
+
{
|
|
123
|
+
alignITEMS: ['center'],
|
|
124
|
+
bgCOLOR: ['transparent'],
|
|
125
|
+
borderCOLOR: ['black'],
|
|
126
|
+
borderWIDTH: ['2'],
|
|
127
|
+
display: ['inline-flex'],
|
|
128
|
+
justifyCONTENT: ['center'],
|
|
129
|
+
px: ['6'],
|
|
130
|
+
py: ['3'],
|
|
131
|
+
rounded: ['md'],
|
|
132
|
+
textCOLOR: ['black'],
|
|
133
|
+
textSIZE: ['base'],
|
|
134
|
+
transition: ['colors'],
|
|
135
|
+
transitionDURATION: ['200'],
|
|
136
|
+
w: ['full'],
|
|
73
137
|
},
|
|
74
138
|
{
|
|
75
|
-
|
|
76
|
-
|
|
139
|
+
textCOLOR: ['myblack'],
|
|
140
|
+
bgCOLOR: ['slate-100'],
|
|
77
141
|
},
|
|
78
142
|
],
|
|
79
143
|
];
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { getCtx } from '@/stores/nodes';
|
|
3
|
+
import { settingsPanelStore } from '@/stores/storykeep';
|
|
4
|
+
import { processRichTextToNodes } from '@/utils/compositor/nodesHelper';
|
|
5
|
+
import type { FlatNode } from '@/types/compositorTypes';
|
|
6
|
+
|
|
7
|
+
interface StyleWordCarouselPanelProps {
|
|
8
|
+
node: FlatNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const StyleWordCarouselPanel = ({ node }: StyleWordCarouselPanelProps) => {
|
|
12
|
+
const ctx = getCtx();
|
|
13
|
+
const [words, setWords] = useState<string>('');
|
|
14
|
+
const [speed, setSpeed] = useState<number>(2);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (node.wordCarouselPayload) {
|
|
18
|
+
setWords(node.wordCarouselPayload.words.join('\n'));
|
|
19
|
+
setSpeed(node.wordCarouselPayload.speed);
|
|
20
|
+
} else {
|
|
21
|
+
// Initialize from current node text content if payload doesn't exist
|
|
22
|
+
const childIds = ctx.getChildNodeIDs(node.id);
|
|
23
|
+
const allNodes = ctx.allNodes.get();
|
|
24
|
+
const currentText = childIds
|
|
25
|
+
.map((id) => {
|
|
26
|
+
const child = allNodes.get(id) as FlatNode | undefined;
|
|
27
|
+
return child?.text || child?.copy || '';
|
|
28
|
+
})
|
|
29
|
+
.join('');
|
|
30
|
+
|
|
31
|
+
setWords(currentText || node.copy || '');
|
|
32
|
+
setSpeed(2);
|
|
33
|
+
}
|
|
34
|
+
}, [node.id, node.wordCarouselPayload]);
|
|
35
|
+
|
|
36
|
+
const saveChanges = (currentWords: string, currentSpeed: number) => {
|
|
37
|
+
const wordsArray = currentWords.split('\n').filter((w) => w.trim() !== '');
|
|
38
|
+
|
|
39
|
+
// Sync the first word to the node's text content
|
|
40
|
+
if (wordsArray.length > 0) {
|
|
41
|
+
const firstWord = wordsArray[0];
|
|
42
|
+
const childIds = ctx.getChildNodeIDs(node.id);
|
|
43
|
+
const allNodes = ctx.allNodes.get();
|
|
44
|
+
const currentText = childIds
|
|
45
|
+
.map((id) => {
|
|
46
|
+
const child = allNodes.get(id) as FlatNode | undefined;
|
|
47
|
+
return child?.text || child?.copy || '';
|
|
48
|
+
})
|
|
49
|
+
.join('');
|
|
50
|
+
|
|
51
|
+
if (currentText !== firstWord) {
|
|
52
|
+
ctx.deleteChildren(node.id);
|
|
53
|
+
// Use processRichTextToNodes to generate proper text nodes compatible with the compositor
|
|
54
|
+
const newNodes = processRichTextToNodes(
|
|
55
|
+
firstWord,
|
|
56
|
+
node.id,
|
|
57
|
+
[],
|
|
58
|
+
() => {}
|
|
59
|
+
);
|
|
60
|
+
if (newNodes.length > 0) {
|
|
61
|
+
ctx.addNodes(newNodes);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
ctx.modifyNodes([
|
|
67
|
+
{
|
|
68
|
+
...node,
|
|
69
|
+
wordCarouselPayload: {
|
|
70
|
+
words: wordsArray,
|
|
71
|
+
speed: currentSpeed,
|
|
72
|
+
},
|
|
73
|
+
isChanged: true,
|
|
74
|
+
} as FlatNode,
|
|
75
|
+
]);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const handleWordsChange = (val: string) => {
|
|
79
|
+
setWords(val);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const handleWordsBlur = () => {
|
|
83
|
+
saveChanges(words, speed);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handleSpeedChange = (val: number) => {
|
|
87
|
+
setSpeed(val);
|
|
88
|
+
saveChanges(words, val);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const handleRemove = () => {
|
|
92
|
+
// Fetch the latest node state to ensure we check current classes/properties
|
|
93
|
+
const currentNode = ctx.allNodes.get().get(node.id) as FlatNode;
|
|
94
|
+
if (!currentNode) return;
|
|
95
|
+
|
|
96
|
+
// Logic: Only fully unwrap (delete node) if it is a "naked" span
|
|
97
|
+
// If it has custom classes, overrides, or is a semantic tag (em, strong), we just strip the payload.
|
|
98
|
+
|
|
99
|
+
const isSpan = currentNode.tagName === 'span';
|
|
100
|
+
|
|
101
|
+
const hasCustomClasses =
|
|
102
|
+
!!currentNode.elementCss && currentNode.elementCss.trim().length > 0;
|
|
103
|
+
|
|
104
|
+
const hasOverrides =
|
|
105
|
+
currentNode.overrideClasses &&
|
|
106
|
+
(Object.keys(currentNode.overrideClasses.mobile || {}).length > 0 ||
|
|
107
|
+
Object.keys(currentNode.overrideClasses.tablet || {}).length > 0 ||
|
|
108
|
+
Object.keys(currentNode.overrideClasses.desktop || {}).length > 0);
|
|
109
|
+
|
|
110
|
+
// Condition: It is a span, and it has NO custom styling or overrides.
|
|
111
|
+
if (isSpan && !hasCustomClasses && !hasOverrides) {
|
|
112
|
+
// "Remove outright"
|
|
113
|
+
ctx.unwrapNode(node.id);
|
|
114
|
+
} else {
|
|
115
|
+
// Keep the tag/styles, but remove the carousel functionality
|
|
116
|
+
ctx.modifyNodes([
|
|
117
|
+
{
|
|
118
|
+
...currentNode,
|
|
119
|
+
wordCarouselPayload: undefined,
|
|
120
|
+
isChanged: true,
|
|
121
|
+
} as FlatNode,
|
|
122
|
+
]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
settingsPanelStore.set(null);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div className="flex flex-col gap-4">
|
|
130
|
+
<div>
|
|
131
|
+
<label className="mb-1 block text-xs font-bold text-gray-500">
|
|
132
|
+
Words (one per line)
|
|
133
|
+
</label>
|
|
134
|
+
<textarea
|
|
135
|
+
className="focus:border-myblue focus:ring-myblue w-full rounded border border-gray-300 p-2 text-sm focus:ring-1"
|
|
136
|
+
rows={5}
|
|
137
|
+
value={words}
|
|
138
|
+
onChange={(e) => handleWordsChange(e.target.value)}
|
|
139
|
+
onBlur={handleWordsBlur}
|
|
140
|
+
placeholder="Enter words..."
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
<div>
|
|
144
|
+
<label className="mb-1 block text-xs font-bold text-gray-500">
|
|
145
|
+
Speed (seconds)
|
|
146
|
+
</label>
|
|
147
|
+
<div className="flex gap-2">
|
|
148
|
+
{[1, 2, 3].map((s) => (
|
|
149
|
+
<button
|
|
150
|
+
key={s}
|
|
151
|
+
onClick={() => handleSpeedChange(s)}
|
|
152
|
+
className={`rounded px-3 py-1 text-sm font-bold transition-colors ${
|
|
153
|
+
speed === s
|
|
154
|
+
? 'bg-myblue text-white'
|
|
155
|
+
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
|
156
|
+
}`}
|
|
157
|
+
>
|
|
158
|
+
{s}s
|
|
159
|
+
</button>
|
|
160
|
+
))}
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
<div className="mt-2 flex items-center justify-between border-t border-gray-100 pt-2">
|
|
164
|
+
<button
|
|
165
|
+
onClick={() => settingsPanelStore.set(null)}
|
|
166
|
+
className="text-xs text-gray-400 underline hover:text-gray-600"
|
|
167
|
+
>
|
|
168
|
+
Done
|
|
169
|
+
</button>
|
|
170
|
+
<button
|
|
171
|
+
onClick={handleRemove}
|
|
172
|
+
className="text-xs text-red-400 underline hover:text-red-600"
|
|
173
|
+
>
|
|
174
|
+
Remove
|
|
175
|
+
</button>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export default StyleWordCarouselPanel;
|
|
@@ -1228,27 +1228,6 @@ export class NodesContext {
|
|
|
1228
1228
|
}
|
|
1229
1229
|
}
|
|
1230
1230
|
|
|
1231
|
-
//getStyleByViewport(
|
|
1232
|
-
// defaultClasses:
|
|
1233
|
-
// | {
|
|
1234
|
-
// mobile?: Record<string, string> | undefined;
|
|
1235
|
-
// tablet?: Record<string, string> | undefined;
|
|
1236
|
-
// desktop?: Record<string, string> | undefined;
|
|
1237
|
-
// }
|
|
1238
|
-
// | undefined,
|
|
1239
|
-
// viewport: ViewportKey
|
|
1240
|
-
//): Record<string, string> {
|
|
1241
|
-
// switch (viewport) {
|
|
1242
|
-
// case "desktop":
|
|
1243
|
-
// return defaultClasses?.desktop || {};
|
|
1244
|
-
// case "tablet":
|
|
1245
|
-
// return defaultClasses?.tablet || {};
|
|
1246
|
-
// default:
|
|
1247
|
-
// case "mobile":
|
|
1248
|
-
// return defaultClasses?.mobile || {};
|
|
1249
|
-
// }
|
|
1250
|
-
//}
|
|
1251
|
-
|
|
1252
1231
|
getNodeSlug(nodeId: string): string {
|
|
1253
1232
|
const node = this.allNodes.get().get(nodeId);
|
|
1254
1233
|
if (!node || !(`slug` in node) || typeof node.slug !== `string`) return '';
|
|
@@ -18,7 +18,7 @@ export interface SelectionStoreState extends SelectionRange {
|
|
|
18
18
|
isDragging: boolean;
|
|
19
19
|
isActive: boolean;
|
|
20
20
|
selectionBox: SelectionBox | null;
|
|
21
|
-
pendingAction: 'style' | 'link' | null;
|
|
21
|
+
pendingAction: 'style' | 'link' | 'carousel' | null;
|
|
22
22
|
isRestyleModalOpen: boolean;
|
|
23
23
|
paneToRestyleId: string | null;
|
|
24
24
|
}
|
|
@@ -399,6 +399,10 @@ export interface FlatNode extends BaseNode {
|
|
|
399
399
|
};
|
|
400
400
|
elementCss?: string;
|
|
401
401
|
buttonPayload?: ButtonPayload;
|
|
402
|
+
wordCarouselPayload?: {
|
|
403
|
+
words: string[];
|
|
404
|
+
speed: number;
|
|
405
|
+
};
|
|
402
406
|
}
|
|
403
407
|
export type ButtonPayload = {
|
|
404
408
|
buttonClasses: Record<string, string[]>;
|
|
@@ -371,8 +371,10 @@ export function processRichTextToNodes(
|
|
|
371
371
|
} else if (node.tagName === 'span') {
|
|
372
372
|
node.elementCss = matchingOriginalNode.elementCss;
|
|
373
373
|
node.overrideClasses = matchingOriginalNode.overrideClasses;
|
|
374
|
+
if (matchingOriginalNode.wordCarouselPayload) {
|
|
375
|
+
node.wordCarouselPayload = matchingOriginalNode.wordCarouselPayload;
|
|
376
|
+
}
|
|
374
377
|
}
|
|
375
|
-
console.log(node);
|
|
376
378
|
} else if (onInsertSignal) {
|
|
377
379
|
// New interactive element detected, trigger insert signal
|
|
378
380
|
onInsertSignal(node.tagName, node.id);
|
package/utils/inject-files.ts
CHANGED
|
@@ -1420,6 +1420,12 @@ export async function injectTemplateFiles(
|
|
|
1420
1420
|
src: resolve('../templates/src/components/edit/state/StylesMemory.tsx'),
|
|
1421
1421
|
dest: 'src/components/edit/state/StylesMemory.tsx',
|
|
1422
1422
|
},
|
|
1423
|
+
{
|
|
1424
|
+
src: resolve(
|
|
1425
|
+
'../templates/src/components/edit/panels/StyleWordCarouselPanel.tsx'
|
|
1426
|
+
),
|
|
1427
|
+
dest: 'src/components/edit/panels/StyleWordCarouselPanel.tsx',
|
|
1428
|
+
},
|
|
1423
1429
|
{
|
|
1424
1430
|
src: resolve(
|
|
1425
1431
|
'../templates/src/components/edit/panels/StyleBreakPanel.tsx'
|