astro-tractstack 2.0.0-rc.32 → 2.0.0-rc.34
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 -4
- package/package.json +1 -1
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +16 -27
- package/templates/src/components/edit/pane/PanePanel_title.tsx +243 -76
- package/templates/src/components/edit/state/SaveModal.tsx +87 -93
- package/templates/src/components/fields/BackgroundImageWrapper.tsx +127 -35
- package/templates/src/components/form/ActionBuilderField.tsx +3 -1
- package/templates/src/components/storykeep/StoryKeepBackdrop.astro +86 -0
- package/templates/src/pages/storykeep/advanced.astro +2 -0
- package/templates/src/pages/storykeep/branding.astro +2 -0
- package/templates/src/pages/storykeep/content.astro +2 -0
- package/templates/src/pages/storykeep.astro +3 -1
- package/templates/src/stores/nodes.ts +12 -0
- package/utils/inject-files.ts +6 -4
- package/templates/src/components/edit/pane/PanePanel_slug.tsx +0 -219
package/dist/index.js
CHANGED
|
@@ -478,10 +478,6 @@ async function w(t, e, c) {
|
|
|
478
478
|
src: t("../templates/src/components/edit/pane/PanePanel_path.tsx"),
|
|
479
479
|
dest: "src/components/edit/pane/PanePanel_path.tsx"
|
|
480
480
|
},
|
|
481
|
-
{
|
|
482
|
-
src: t("../templates/src/components/edit/pane/PanePanel_slug.tsx"),
|
|
483
|
-
dest: "src/components/edit/pane/PanePanel_slug.tsx"
|
|
484
|
-
},
|
|
485
481
|
{
|
|
486
482
|
src: t("../templates/src/components/edit/pane/PanePanel_title.tsx"),
|
|
487
483
|
dest: "src/components/edit/pane/PanePanel_title.tsx"
|
|
@@ -1028,6 +1024,12 @@ async function w(t, e, c) {
|
|
|
1028
1024
|
dest: "src/components/form/advanced/APIConfigSection.tsx"
|
|
1029
1025
|
},
|
|
1030
1026
|
// StoryKeep Dashboard Components
|
|
1027
|
+
{
|
|
1028
|
+
src: t(
|
|
1029
|
+
"../templates/src/components/storykeep/StoryKeepBackdrop.astro"
|
|
1030
|
+
),
|
|
1031
|
+
dest: "src/components/storykeep/StoryKeepBackdrop.astro"
|
|
1032
|
+
},
|
|
1031
1033
|
{
|
|
1032
1034
|
src: t("../templates/src/components/storykeep/Dashboard.tsx"),
|
|
1033
1035
|
dest: "src/components/storykeep/Dashboard.tsx"
|
package/package.json
CHANGED
|
@@ -9,10 +9,13 @@ import {
|
|
|
9
9
|
isContextPaneNode,
|
|
10
10
|
hasBeliefPayload,
|
|
11
11
|
} from '@/utils/compositor/typeGuards';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
settingsPanelStore,
|
|
14
|
+
viewportKeyStore,
|
|
15
|
+
fullContentMapStore,
|
|
16
|
+
} from '@/stores/storykeep';
|
|
13
17
|
import { getCtx } from '@/stores/nodes';
|
|
14
18
|
import PaneTitlePanel from './PanePanel_title';
|
|
15
|
-
import PaneSlugPanel from './PanePanel_slug';
|
|
16
19
|
import PaneMagicPathPanel from './PanePanel_path';
|
|
17
20
|
import PaneImpressionPanel from './PanePanel_impression';
|
|
18
21
|
import { PaneConfigMode, type PaneNode } from '@/types/compositorTypes';
|
|
@@ -30,7 +33,7 @@ const ConfigPanePanel = ({ nodeId }: ConfigPanePanelProps) => {
|
|
|
30
33
|
const reorderMode = toolMode.value === `move`;
|
|
31
34
|
const isActiveMode =
|
|
32
35
|
activePaneMode.panel === 'settings' && activePaneMode.paneId === nodeId;
|
|
33
|
-
|
|
36
|
+
const $contentMap = useStore(fullContentMapStore);
|
|
34
37
|
const $viewportKey = useStore(viewportKeyStore);
|
|
35
38
|
const isMobile = $viewportKey.value === `mobile`;
|
|
36
39
|
|
|
@@ -104,8 +107,6 @@ const ConfigPanePanel = ({ nodeId }: ConfigPanePanelProps) => {
|
|
|
104
107
|
|
|
105
108
|
if (mode === PaneConfigMode.TITLE) {
|
|
106
109
|
return <PaneTitlePanel nodeId={nodeId} setMode={setSaveMode} />;
|
|
107
|
-
} else if (mode === PaneConfigMode.SLUG) {
|
|
108
|
-
return <PaneSlugPanel nodeId={nodeId} setMode={setSaveMode} />;
|
|
109
110
|
} else if (mode === PaneConfigMode.PATH) {
|
|
110
111
|
return <PaneMagicPathPanel nodeId={nodeId} setMode={setSaveMode} />;
|
|
111
112
|
} else if (mode === PaneConfigMode.IMPRESSION) {
|
|
@@ -137,28 +138,16 @@ const ConfigPanePanel = ({ nodeId }: ConfigPanePanelProps) => {
|
|
|
137
138
|
<>
|
|
138
139
|
{!isTemplate && (
|
|
139
140
|
<>
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
</button>
|
|
151
|
-
<button
|
|
152
|
-
onClick={() => setSaveMode(PaneConfigMode.SLUG)}
|
|
153
|
-
className={buttonClass}
|
|
154
|
-
>
|
|
155
|
-
Slug
|
|
156
|
-
{!isMobile && (
|
|
157
|
-
<>
|
|
158
|
-
: <strong>{paneNode.slug}</strong>
|
|
159
|
-
</>
|
|
160
|
-
)}
|
|
161
|
-
</button>
|
|
141
|
+
{$contentMap.some(
|
|
142
|
+
(item) => item.type === 'Pane' && item.id === nodeId
|
|
143
|
+
) && (
|
|
144
|
+
<button
|
|
145
|
+
onClick={() => setSaveMode(PaneConfigMode.TITLE)}
|
|
146
|
+
className={buttonClass}
|
|
147
|
+
>
|
|
148
|
+
ID
|
|
149
|
+
</button>
|
|
150
|
+
)}
|
|
162
151
|
<button
|
|
163
152
|
onClick={() => setSaveMode(PaneConfigMode.IMPRESSION)}
|
|
164
153
|
className={buttonClass}
|
|
@@ -5,10 +5,12 @@ import {
|
|
|
5
5
|
type SetStateAction,
|
|
6
6
|
type ChangeEvent,
|
|
7
7
|
} from 'react';
|
|
8
|
+
import { useStore } from '@nanostores/react';
|
|
8
9
|
import ExclamationTriangleIcon from '@heroicons/react/24/outline/ExclamationTriangleIcon';
|
|
9
10
|
import CheckIcon from '@heroicons/react/24/outline/CheckIcon';
|
|
11
|
+
import { fullContentMapStore } from '@/stores/storykeep';
|
|
10
12
|
import { getCtx } from '@/stores/nodes';
|
|
11
|
-
import { cloneDeep } from '@/utils/helpers';
|
|
13
|
+
import { cloneDeep, findUniqueSlug, titleToSlug } from '@/utils/helpers';
|
|
12
14
|
import { PaneConfigMode, type PaneNode } from '@/types/compositorTypes';
|
|
13
15
|
|
|
14
16
|
interface PaneTitlePanelProps {
|
|
@@ -18,36 +20,141 @@ interface PaneTitlePanelProps {
|
|
|
18
20
|
|
|
19
21
|
const PaneTitlePanel = ({ nodeId, setMode }: PaneTitlePanelProps) => {
|
|
20
22
|
const [title, setTitle] = useState('');
|
|
21
|
-
const [
|
|
22
|
-
const [
|
|
23
|
-
const [
|
|
23
|
+
const [slug, setSlug] = useState('');
|
|
24
|
+
const [isValidTitle, setIsValidTitle] = useState(false);
|
|
25
|
+
const [isValidSlug, setIsValidSlug] = useState(false);
|
|
26
|
+
const [warningTitle, setWarningTitle] = useState(false);
|
|
27
|
+
const [warningSlug, setWarningSlug] = useState(false);
|
|
28
|
+
const [titleCharCount, setTitleCharCount] = useState(0);
|
|
29
|
+
const [slugCharCount, setSlugCharCount] = useState(0);
|
|
30
|
+
const [slugValidationError, setSlugValidationError] = useState<string | null>(
|
|
31
|
+
null
|
|
32
|
+
);
|
|
33
|
+
const [canSaveSlug, setCanSaveSlug] = useState(false);
|
|
24
34
|
|
|
35
|
+
const $contentMap = useStore(fullContentMapStore);
|
|
25
36
|
const ctx = getCtx();
|
|
26
37
|
const allNodes = ctx.allNodes.get();
|
|
27
38
|
const paneNode = allNodes.get(nodeId) as PaneNode;
|
|
28
39
|
if (!paneNode) return null;
|
|
29
40
|
|
|
41
|
+
const existingSlugs = $contentMap
|
|
42
|
+
.filter(
|
|
43
|
+
(item) =>
|
|
44
|
+
['Pane', 'StoryFragment'].includes(item.type) && item.id !== nodeId
|
|
45
|
+
)
|
|
46
|
+
.map((item) => item.slug);
|
|
47
|
+
|
|
30
48
|
useEffect(() => {
|
|
31
49
|
setTitle(paneNode.title);
|
|
32
|
-
|
|
33
|
-
|
|
50
|
+
setSlug(paneNode.slug);
|
|
51
|
+
setTitleCharCount(paneNode.title.length);
|
|
52
|
+
setSlugCharCount(paneNode.slug.length);
|
|
53
|
+
checkSlugLiveValidity(paneNode.slug);
|
|
54
|
+
}, [paneNode.title, paneNode.slug]);
|
|
34
55
|
|
|
35
56
|
const handleTitleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
36
57
|
const newTitle = e.target.value;
|
|
37
58
|
if (newTitle.length <= 50) {
|
|
38
|
-
// Prevent more than 70 chars
|
|
39
59
|
setTitle(newTitle);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
60
|
+
setTitleCharCount(newTitle.length);
|
|
61
|
+
setIsValidTitle(newTitle.length >= 5 && newTitle.length <= 35);
|
|
62
|
+
setWarningTitle(newTitle.length > 35 && newTitle.length <= 50);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const handleSlugChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
67
|
+
const newSlug = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
68
|
+
|
|
69
|
+
if (newSlug.length <= 50) {
|
|
70
|
+
setSlug(newSlug);
|
|
71
|
+
checkSlugLiveValidity(newSlug);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const checkSlugLiveValidity = (value: string) => {
|
|
76
|
+
const length = value.length;
|
|
77
|
+
setSlugCharCount(length);
|
|
78
|
+
|
|
79
|
+
// Basic format check for allowed characters
|
|
80
|
+
if (!/^[a-z0-9-]*$/.test(value)) {
|
|
81
|
+
setSlugValidationError(
|
|
82
|
+
'Only lowercase letters, numbers, and hyphens allowed'
|
|
83
|
+
);
|
|
84
|
+
setIsValidSlug(false);
|
|
85
|
+
setCanSaveSlug(false);
|
|
86
|
+
return false;
|
|
43
87
|
}
|
|
88
|
+
|
|
89
|
+
// Length checks
|
|
90
|
+
setIsValidSlug(length >= 3 && length <= 40);
|
|
91
|
+
setWarningSlug(length > 40 && length <= 50);
|
|
92
|
+
setSlugValidationError(null);
|
|
93
|
+
|
|
94
|
+
// Check if we can save
|
|
95
|
+
if (length >= 3) {
|
|
96
|
+
const saveValidation = checkSlugSaveValidity(value);
|
|
97
|
+
setCanSaveSlug(saveValidation.isValid);
|
|
98
|
+
if (!saveValidation.isValid) {
|
|
99
|
+
setSlugValidationError(saveValidation.error || null);
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
setCanSaveSlug(false);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return true;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const checkSlugSaveValidity = (
|
|
109
|
+
value: string
|
|
110
|
+
): { isValid: boolean; error?: string } => {
|
|
111
|
+
// Strict pattern that prevents leading/trailing hyphens and multiple consecutive hyphens
|
|
112
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {
|
|
113
|
+
return {
|
|
114
|
+
isValid: false,
|
|
115
|
+
error:
|
|
116
|
+
'Slug must start and end with letters or numbers, and no consecutive hyphens',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check duplicates
|
|
121
|
+
if (existingSlugs.includes(value)) {
|
|
122
|
+
return {
|
|
123
|
+
isValid: false,
|
|
124
|
+
error: 'This slug is already in use',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { isValid: true };
|
|
44
129
|
};
|
|
45
130
|
|
|
46
131
|
const handleTitleBlur = () => {
|
|
47
132
|
if (title.length >= 5) {
|
|
48
|
-
//
|
|
133
|
+
// Auto-generate slug if slug is empty or still system-generated
|
|
134
|
+
let updatedSlug = slug;
|
|
135
|
+
if (!slug || slug === paneNode.slug) {
|
|
136
|
+
const generatedSlug = titleToSlug(title);
|
|
137
|
+
const uniqueSlug = findUniqueSlug(generatedSlug, existingSlugs);
|
|
138
|
+
updatedSlug = uniqueSlug;
|
|
139
|
+
setSlug(uniqueSlug);
|
|
140
|
+
checkSlugLiveValidity(uniqueSlug);
|
|
141
|
+
}
|
|
142
|
+
|
|
49
143
|
const ctx = getCtx();
|
|
50
|
-
const updatedNode = {
|
|
144
|
+
const updatedNode = {
|
|
145
|
+
...cloneDeep(paneNode),
|
|
146
|
+
title,
|
|
147
|
+
slug: updatedSlug,
|
|
148
|
+
isChanged: true,
|
|
149
|
+
};
|
|
150
|
+
ctx.modifyNodes([updatedNode]);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const handleSlugBlur = () => {
|
|
155
|
+
if (canSaveSlug) {
|
|
156
|
+
const ctx = getCtx();
|
|
157
|
+
const updatedNode = { ...cloneDeep(paneNode), slug, isChanged: true };
|
|
51
158
|
ctx.modifyNodes([updatedNode]);
|
|
52
159
|
}
|
|
53
160
|
};
|
|
@@ -56,7 +163,7 @@ const PaneTitlePanel = ({ nodeId, setMode }: PaneTitlePanelProps) => {
|
|
|
56
163
|
<div className="group mb-4 w-full rounded-b-md bg-white px-1.5 py-6 shadow-inner">
|
|
57
164
|
<div className="px-3.5">
|
|
58
165
|
<div className="mb-4 flex justify-between">
|
|
59
|
-
<h3 className="text-lg font-bold">Pane Title</h3>
|
|
166
|
+
<h3 className="text-lg font-bold">Pane Title & Slug</h3>
|
|
60
167
|
<button
|
|
61
168
|
onClick={() => setMode(PaneConfigMode.DEFAULT)}
|
|
62
169
|
className="text-myblue hover:text-black"
|
|
@@ -65,73 +172,133 @@ const PaneTitlePanel = ({ nodeId, setMode }: PaneTitlePanelProps) => {
|
|
|
65
172
|
</button>
|
|
66
173
|
</div>
|
|
67
174
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
<ExclamationTriangleIcon className="h-5 w-5 text-red-500" />
|
|
93
|
-
) : isValid ? (
|
|
94
|
-
<CheckIcon className="h-5 w-5 text-green-500" />
|
|
95
|
-
) : warning ? (
|
|
96
|
-
<ExclamationTriangleIcon className="h-5 w-5 text-yellow-500" />
|
|
97
|
-
) : null}
|
|
98
|
-
<span
|
|
99
|
-
className={`text-sm ${
|
|
100
|
-
charCount < 5
|
|
101
|
-
? 'text-red-500'
|
|
102
|
-
: isValid
|
|
103
|
-
? 'text-green-500'
|
|
104
|
-
: warning
|
|
105
|
-
? 'text-yellow-500'
|
|
106
|
-
: 'text-gray-500'
|
|
175
|
+
{/* Title Input */}
|
|
176
|
+
<div className="mb-6">
|
|
177
|
+
<label className="mb-2 block text-sm font-bold text-gray-700">
|
|
178
|
+
Title
|
|
179
|
+
</label>
|
|
180
|
+
<div className="relative">
|
|
181
|
+
<input
|
|
182
|
+
type="text"
|
|
183
|
+
value={title}
|
|
184
|
+
onChange={handleTitleChange}
|
|
185
|
+
onBlur={handleTitleBlur}
|
|
186
|
+
onKeyDown={(e) => {
|
|
187
|
+
if (e.key === 'Enter') {
|
|
188
|
+
e.currentTarget.blur();
|
|
189
|
+
}
|
|
190
|
+
}}
|
|
191
|
+
className={`w-full rounded-md border px-2 py-1 pr-16 ${
|
|
192
|
+
titleCharCount < 5
|
|
193
|
+
? 'border-red-500 bg-red-50'
|
|
194
|
+
: isValidTitle
|
|
195
|
+
? 'border-green-500 bg-green-50'
|
|
196
|
+
: warningTitle
|
|
197
|
+
? 'border-yellow-500 bg-yellow-50'
|
|
198
|
+
: 'border-gray-300'
|
|
107
199
|
}`}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
200
|
+
placeholder="Enter pane title (5-35 characters recommended)"
|
|
201
|
+
/>
|
|
202
|
+
<div className="absolute right-2 top-1/2 flex -translate-y-1/2 items-center gap-2">
|
|
203
|
+
{titleCharCount < 5 ? (
|
|
204
|
+
<ExclamationTriangleIcon className="h-5 w-5 text-red-500" />
|
|
205
|
+
) : isValidTitle ? (
|
|
206
|
+
<CheckIcon className="h-5 w-5 text-green-500" />
|
|
207
|
+
) : warningTitle ? (
|
|
208
|
+
<ExclamationTriangleIcon className="h-5 w-5 text-yellow-500" />
|
|
209
|
+
) : null}
|
|
210
|
+
<span
|
|
211
|
+
className={`text-sm ${
|
|
212
|
+
titleCharCount < 5
|
|
213
|
+
? 'text-red-500'
|
|
214
|
+
: isValidTitle
|
|
215
|
+
? 'text-green-500'
|
|
216
|
+
: warningTitle
|
|
217
|
+
? 'text-yellow-500'
|
|
218
|
+
: 'text-gray-500'
|
|
219
|
+
}`}
|
|
220
|
+
>
|
|
221
|
+
{titleCharCount}/50
|
|
222
|
+
</span>
|
|
223
|
+
</div>
|
|
111
224
|
</div>
|
|
112
225
|
</div>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
226
|
+
|
|
227
|
+
{/* Slug Input */}
|
|
228
|
+
<div className="mb-4">
|
|
229
|
+
<label className="mb-2 block text-sm font-bold text-gray-700">
|
|
230
|
+
Slug (URL)
|
|
231
|
+
</label>
|
|
232
|
+
<div className="relative">
|
|
233
|
+
<input
|
|
234
|
+
type="text"
|
|
235
|
+
value={slug}
|
|
236
|
+
onChange={handleSlugChange}
|
|
237
|
+
onBlur={handleSlugBlur}
|
|
238
|
+
onKeyDown={(e) => {
|
|
239
|
+
if (e.key === 'Enter') {
|
|
240
|
+
e.currentTarget.blur();
|
|
241
|
+
}
|
|
242
|
+
}}
|
|
243
|
+
className={`w-full rounded-md border px-2 py-1 pr-16 ${
|
|
244
|
+
slugValidationError || slugCharCount < 3
|
|
245
|
+
? 'border-red-500 bg-red-50'
|
|
246
|
+
: isValidSlug && canSaveSlug
|
|
247
|
+
? 'border-green-500 bg-green-50'
|
|
248
|
+
: warningSlug
|
|
249
|
+
? 'border-yellow-500 bg-yellow-50'
|
|
250
|
+
: 'border-gray-300'
|
|
251
|
+
}`}
|
|
252
|
+
placeholder="Enter pane slug (3-40 characters recommended)"
|
|
253
|
+
/>
|
|
254
|
+
<div className="absolute right-2 top-1/2 flex -translate-y-1/2 items-center gap-2">
|
|
255
|
+
{slugValidationError || slugCharCount < 3 ? (
|
|
256
|
+
<ExclamationTriangleIcon className="h-5 w-5 text-red-500" />
|
|
257
|
+
) : isValidSlug && canSaveSlug ? (
|
|
258
|
+
<CheckIcon className="h-5 w-5 text-green-500" />
|
|
259
|
+
) : warningSlug ? (
|
|
260
|
+
<ExclamationTriangleIcon className="h-5 w-5 text-yellow-500" />
|
|
261
|
+
) : null}
|
|
262
|
+
<span
|
|
263
|
+
className={`text-sm ${
|
|
264
|
+
slugValidationError || slugCharCount < 3
|
|
265
|
+
? 'text-red-500'
|
|
266
|
+
: isValidSlug && canSaveSlug
|
|
267
|
+
? 'text-green-500'
|
|
268
|
+
: warningSlug
|
|
269
|
+
? 'text-yellow-500'
|
|
270
|
+
: 'text-gray-500'
|
|
271
|
+
}`}
|
|
272
|
+
>
|
|
273
|
+
{slugCharCount}/50
|
|
127
274
|
</span>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
{slugValidationError && (
|
|
278
|
+
<div className="mt-2 text-sm text-red-600">
|
|
279
|
+
<ExclamationTriangleIcon className="mr-1 inline h-4 w-4" />
|
|
280
|
+
{slugValidationError}
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
{/* Help Text */}
|
|
286
|
+
<div className="space-y-4 text-sm text-gray-600">
|
|
287
|
+
<div>
|
|
288
|
+
<h4 className="font-semibold">Title Guidelines:</h4>
|
|
289
|
+
<ul className="ml-4 mt-1 list-disc">
|
|
290
|
+
<li>5-35 characters recommended for optimal display</li>
|
|
291
|
+
<li>Clear, descriptive title for the pane content</li>
|
|
292
|
+
</ul>
|
|
293
|
+
</div>
|
|
294
|
+
<div>
|
|
295
|
+
<h4 className="font-semibold">Slug Guidelines:</h4>
|
|
296
|
+
<ul className="ml-4 mt-1 list-disc">
|
|
297
|
+
<li>Used for analytics tracking</li>
|
|
298
|
+
<li>Only lowercase letters, numbers, and hyphens</li>
|
|
299
|
+
<li>Must start and end with letter or number</li>
|
|
300
|
+
<li>No consecutive hyphens</li>
|
|
301
|
+
</ul>
|
|
135
302
|
</div>
|
|
136
303
|
</div>
|
|
137
304
|
</div>
|
|
@@ -73,7 +73,6 @@ export default function SaveModal({
|
|
|
73
73
|
const isSaving = useRef(false);
|
|
74
74
|
const [isNavigating, setIsNavigating] = useState(false);
|
|
75
75
|
const isCreateMode = slug === 'create';
|
|
76
|
-
const contentMap = fullContentMapStore.get();
|
|
77
76
|
const pendingHomePageSlug = pendingHomePageSlugStore.get();
|
|
78
77
|
const goBackend =
|
|
79
78
|
import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
@@ -334,113 +333,108 @@ export default function SaveModal({
|
|
|
334
333
|
currentStep: 0,
|
|
335
334
|
totalSteps: dirtyPanes.length,
|
|
336
335
|
});
|
|
337
|
-
for (let i = 0; i < dirtyPanes.length; i++) {
|
|
338
|
-
const paneNode = dirtyPanes[i];
|
|
339
336
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
paneNode.id,
|
|
344
|
-
isContext
|
|
345
|
-
);
|
|
346
|
-
|
|
347
|
-
payload.optionsPayload.nodes.forEach((transformedNode) => {
|
|
348
|
-
const liveNode = ctx.allNodes.get().get(transformedNode.id);
|
|
349
|
-
if (!liveNode) return;
|
|
350
|
-
|
|
351
|
-
let needsUpdate = false;
|
|
352
|
-
let updatedNode: BaseNode = { ...liveNode };
|
|
353
|
-
|
|
354
|
-
if (
|
|
355
|
-
transformedNode.nodeType === 'TagElement' &&
|
|
356
|
-
transformedNode.elementCss
|
|
357
|
-
) {
|
|
358
|
-
const flatNode = liveNode as FlatNode;
|
|
359
|
-
if (flatNode.elementCss !== transformedNode.elementCss) {
|
|
360
|
-
(updatedNode as FlatNode).elementCss =
|
|
361
|
-
transformedNode.elementCss;
|
|
362
|
-
needsUpdate = true;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
337
|
+
const bulkPayload = dirtyPanes.map((paneNode) =>
|
|
338
|
+
transformLivePaneForSave(ctx, paneNode.id, isContext)
|
|
339
|
+
);
|
|
365
340
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
newParentCss;
|
|
384
|
-
needsUpdate = true;
|
|
385
|
-
}
|
|
341
|
+
bulkPayload.forEach((payload) => {
|
|
342
|
+
payload.optionsPayload.nodes.forEach((transformedNode) => {
|
|
343
|
+
const liveNode = ctx.allNodes.get().get(transformedNode.id);
|
|
344
|
+
if (!liveNode) return;
|
|
345
|
+
|
|
346
|
+
let needsUpdate = false;
|
|
347
|
+
let updatedNode: BaseNode = { ...liveNode };
|
|
348
|
+
|
|
349
|
+
if (
|
|
350
|
+
transformedNode.nodeType === 'TagElement' &&
|
|
351
|
+
transformedNode.elementCss
|
|
352
|
+
) {
|
|
353
|
+
const flatNode = liveNode as FlatNode;
|
|
354
|
+
if (flatNode.elementCss !== transformedNode.elementCss) {
|
|
355
|
+
(updatedNode as FlatNode).elementCss =
|
|
356
|
+
transformedNode.elementCss;
|
|
357
|
+
needsUpdate = true;
|
|
386
358
|
}
|
|
359
|
+
}
|
|
387
360
|
|
|
388
|
-
|
|
389
|
-
|
|
361
|
+
if (
|
|
362
|
+
transformedNode.nodeType === 'Markdown' &&
|
|
363
|
+
transformedNode.parentCss
|
|
364
|
+
) {
|
|
365
|
+
const markdownNode = liveNode as MarkdownPaneFragmentNode;
|
|
366
|
+
const currentParentCss = markdownNode.parentCss;
|
|
367
|
+
const newParentCss = transformedNode.parentCss as string[];
|
|
368
|
+
|
|
369
|
+
const isDifferent =
|
|
370
|
+
!currentParentCss ||
|
|
371
|
+
currentParentCss.length !== newParentCss.length ||
|
|
372
|
+
currentParentCss.some(
|
|
373
|
+
(css, index) => css !== newParentCss[index]
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
if (isDifferent) {
|
|
377
|
+
(updatedNode as MarkdownPaneFragmentNode).parentCss =
|
|
378
|
+
newParentCss;
|
|
379
|
+
needsUpdate = true;
|
|
390
380
|
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const paneExistsInBackend = contentMap.some(
|
|
394
|
-
(item) => item.type === 'Pane' && item.id === paneNode.id
|
|
395
|
-
);
|
|
396
|
-
const isCreatePaneMode = !paneExistsInBackend;
|
|
397
|
-
const endpoint = isCreatePaneMode
|
|
398
|
-
? `${goBackend}/api/v1/nodes/panes/create`
|
|
399
|
-
: `${goBackend}/api/v1/nodes/panes/${payload.id}`;
|
|
400
|
-
const method = isCreatePaneMode ? 'POST' : 'PUT';
|
|
381
|
+
}
|
|
401
382
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
383
|
+
if (needsUpdate) {
|
|
384
|
+
ctx.allNodes.get().set(transformedNode.id, updatedNode);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
});
|
|
407
388
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
'X-Tenant-ID': tenantId,
|
|
413
|
-
},
|
|
414
|
-
credentials: 'include',
|
|
415
|
-
body: JSON.stringify(payload),
|
|
416
|
-
});
|
|
389
|
+
const endpoint = `${goBackend}/api/v1/nodes/panes/bulk`;
|
|
390
|
+
addDebugMessage(
|
|
391
|
+
`Processing ${dirtyPanes.length} panes via -> POST ${endpoint}`
|
|
392
|
+
);
|
|
417
393
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
394
|
+
try {
|
|
395
|
+
const response = await fetch(endpoint, {
|
|
396
|
+
method: 'POST',
|
|
397
|
+
headers: {
|
|
398
|
+
'Content-Type': 'application/json',
|
|
399
|
+
'X-Tenant-ID': tenantId,
|
|
400
|
+
},
|
|
401
|
+
credentials: 'include',
|
|
402
|
+
body: JSON.stringify({ panes: bulkPayload }),
|
|
403
|
+
});
|
|
421
404
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
} catch (etlError) {
|
|
425
|
-
const errorMsg =
|
|
426
|
-
etlError instanceof Error ? etlError.message : 'Unknown error';
|
|
427
|
-
addDebugMessage(`Pane ${paneNode.id} ETL failed: ${errorMsg}`);
|
|
405
|
+
if (!response.ok) {
|
|
406
|
+
const errorText = await response.text();
|
|
428
407
|
throw new Error(
|
|
429
|
-
`
|
|
408
|
+
`HTTP error! status: ${response.status} - ${errorText}`
|
|
430
409
|
);
|
|
431
410
|
}
|
|
432
411
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
(completedProcessingSteps / totalProcessingSteps) *
|
|
437
|
-
PROGRESS_PHASES.PROCESSING;
|
|
438
|
-
setProgress(
|
|
439
|
-
PROGRESS_PHASES.PREPARATION +
|
|
440
|
-
PROGRESS_PHASES.UPLOADS +
|
|
441
|
-
processingProgress
|
|
412
|
+
await response.json();
|
|
413
|
+
addDebugMessage(
|
|
414
|
+
`${dirtyPanes.length} panes saved successfully via bulk endpoint.`
|
|
442
415
|
);
|
|
416
|
+
} catch (bulkError) {
|
|
417
|
+
const errorMsg =
|
|
418
|
+
bulkError instanceof Error
|
|
419
|
+
? bulkError.message
|
|
420
|
+
: 'Unknown bulk save error';
|
|
421
|
+
addDebugMessage(`Bulk pane save failed: ${errorMsg}`);
|
|
422
|
+
throw new Error(`Failed to save panes in bulk: ${errorMsg}`);
|
|
443
423
|
}
|
|
424
|
+
|
|
425
|
+
setStageProgress({
|
|
426
|
+
currentStep: dirtyPanes.length,
|
|
427
|
+
totalSteps: dirtyPanes.length,
|
|
428
|
+
});
|
|
429
|
+
completedProcessingSteps += dirtyPanes.length;
|
|
430
|
+
const processingProgress =
|
|
431
|
+
(completedProcessingSteps / totalProcessingSteps) *
|
|
432
|
+
PROGRESS_PHASES.PROCESSING;
|
|
433
|
+
setProgress(
|
|
434
|
+
PROGRESS_PHASES.PREPARATION +
|
|
435
|
+
PROGRESS_PHASES.UPLOADS +
|
|
436
|
+
processingProgress
|
|
437
|
+
);
|
|
444
438
|
}
|
|
445
439
|
|
|
446
440
|
if (!isContext && dirtyStoryFragments.length > 0) {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { useState, useCallback } from 'react';
|
|
1
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
2
2
|
import { useStore } from '@nanostores/react';
|
|
3
|
+
import { Select } from '@ark-ui/react/select';
|
|
4
|
+
import { createListCollection } from '@ark-ui/react/collection';
|
|
3
5
|
import BackgroundImage from './BackgroundImage';
|
|
4
6
|
import ArtpackImage from './ArtpackImage';
|
|
5
7
|
import ColorPickerCombo from './ColorPickerCombo';
|
|
6
8
|
import { getCtx } from '@/stores/nodes';
|
|
7
|
-
import { hasArtpacksStore } from '@/stores/storykeep';
|
|
9
|
+
import { hasArtpacksStore, settingsPanelStore } from '@/stores/storykeep';
|
|
8
10
|
import { cloneDeep } from '@/utils/helpers';
|
|
9
11
|
import type { BrandConfig } from '@/types/tractstack';
|
|
10
12
|
import type {
|
|
@@ -19,6 +21,36 @@ export interface BackgroundImageWrapperProps {
|
|
|
19
21
|
config?: BrandConfig;
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
const CheckIcon = () => (
|
|
25
|
+
<svg
|
|
26
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
27
|
+
className="h-5 w-5"
|
|
28
|
+
viewBox="0 0 20 20"
|
|
29
|
+
fill="currentColor"
|
|
30
|
+
>
|
|
31
|
+
<path
|
|
32
|
+
fillRule="evenodd"
|
|
33
|
+
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
|
34
|
+
clipRule="evenodd"
|
|
35
|
+
/>
|
|
36
|
+
</svg>
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const ChevronDownIcon = () => (
|
|
40
|
+
<svg
|
|
41
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
42
|
+
className="h-5 w-5"
|
|
43
|
+
viewBox="0 0 20 20"
|
|
44
|
+
fill="currentColor"
|
|
45
|
+
>
|
|
46
|
+
<path
|
|
47
|
+
fillRule="evenodd"
|
|
48
|
+
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
|
49
|
+
clipRule="evenodd"
|
|
50
|
+
/>
|
|
51
|
+
</svg>
|
|
52
|
+
);
|
|
53
|
+
|
|
22
54
|
const BackgroundImageWrapper = ({
|
|
23
55
|
paneId,
|
|
24
56
|
config,
|
|
@@ -27,11 +59,8 @@ const BackgroundImageWrapper = ({
|
|
|
27
59
|
const allNodes = useStore(ctx.allNodes);
|
|
28
60
|
const $artpacks = useStore(hasArtpacksStore);
|
|
29
61
|
const hasArtpacks = $artpacks && Object.keys($artpacks).length > 0;
|
|
30
|
-
|
|
31
|
-
// State to force re-renders when child components need it
|
|
32
62
|
const [, setUpdateCounter] = useState(0);
|
|
33
63
|
|
|
34
|
-
// Using useCallback to create a stable reference to the update function
|
|
35
64
|
const onUpdate = useCallback(() => {
|
|
36
65
|
setUpdateCounter((prev) => prev + 1);
|
|
37
66
|
}, []);
|
|
@@ -89,8 +118,47 @@ const BackgroundImageWrapper = ({
|
|
|
89
118
|
const position = bgNode?.position || 'background';
|
|
90
119
|
const size = bgNode?.size || 'equal';
|
|
91
120
|
|
|
121
|
+
const positionOptions = [
|
|
122
|
+
{ label: 'Background', value: 'background' },
|
|
123
|
+
{ label: 'Left', value: 'left' },
|
|
124
|
+
{ label: 'Right', value: 'right' },
|
|
125
|
+
{ label: 'Left Bleed', value: 'leftBleed' },
|
|
126
|
+
{ label: 'Right Bleed', value: 'rightBleed' },
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
const collection = useMemo(
|
|
130
|
+
() =>
|
|
131
|
+
createListCollection({
|
|
132
|
+
items: positionOptions,
|
|
133
|
+
itemToValue: (item) => item.value,
|
|
134
|
+
itemToString: (item) => item.label,
|
|
135
|
+
}),
|
|
136
|
+
[]
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const selectItemStyles = `
|
|
140
|
+
.position-item[data-highlighted] {
|
|
141
|
+
background-color: #0891b2; /* bg-cyan-600 */
|
|
142
|
+
color: white;
|
|
143
|
+
}
|
|
144
|
+
.position-item[data-highlighted] .position-indicator {
|
|
145
|
+
color: white;
|
|
146
|
+
}
|
|
147
|
+
.position-item[data-state="checked"] .position-indicator {
|
|
148
|
+
display: flex;
|
|
149
|
+
}
|
|
150
|
+
.position-item .position-indicator {
|
|
151
|
+
display: none;
|
|
152
|
+
}
|
|
153
|
+
.position-item[data-state="checked"] {
|
|
154
|
+
font-weight: bold;
|
|
155
|
+
}
|
|
156
|
+
`;
|
|
157
|
+
|
|
92
158
|
return (
|
|
93
159
|
<div className="w-full space-y-6">
|
|
160
|
+
<style>{selectItemStyles}</style>
|
|
161
|
+
|
|
94
162
|
<h3 className="text-sm font-bold text-gray-700">Background</h3>
|
|
95
163
|
|
|
96
164
|
<ColorPickerCombo
|
|
@@ -115,39 +183,64 @@ const BackgroundImageWrapper = ({
|
|
|
115
183
|
)}
|
|
116
184
|
{bgNode && (
|
|
117
185
|
<div className="w-full space-y-6">
|
|
118
|
-
{/* Position Toggle */}
|
|
119
186
|
<div className="space-y-2">
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
{(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
187
|
+
<Select.Root
|
|
188
|
+
collection={collection}
|
|
189
|
+
positioning={{ sameWidth: true }}
|
|
190
|
+
value={[position]}
|
|
191
|
+
onValueChange={(details) => {
|
|
192
|
+
const currentSignal = settingsPanelStore.get();
|
|
193
|
+
if (currentSignal) {
|
|
194
|
+
settingsPanelStore.set({
|
|
195
|
+
...currentSignal,
|
|
196
|
+
editLock: Date.now(),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
handlePositionChange(
|
|
200
|
+
details.value[0] as
|
|
201
|
+
| 'background'
|
|
202
|
+
| 'left'
|
|
203
|
+
| 'right'
|
|
204
|
+
| 'leftBleed'
|
|
205
|
+
| 'rightBleed'
|
|
206
|
+
);
|
|
207
|
+
}}
|
|
208
|
+
>
|
|
209
|
+
<Select.Label className="block text-sm font-bold text-gray-700">
|
|
210
|
+
Position
|
|
211
|
+
</Select.Label>
|
|
212
|
+
<Select.Control>
|
|
213
|
+
<Select.Trigger className="focus:border-myblue focus:ring-myblue flex w-full items-center justify-between rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-1">
|
|
214
|
+
<Select.ValueText
|
|
215
|
+
className="capitalize"
|
|
216
|
+
placeholder="Select a position"
|
|
141
217
|
/>
|
|
142
|
-
<
|
|
143
|
-
|
|
144
|
-
</
|
|
145
|
-
</
|
|
146
|
-
|
|
147
|
-
|
|
218
|
+
<Select.Indicator>
|
|
219
|
+
<ChevronDownIcon />
|
|
220
|
+
</Select.Indicator>
|
|
221
|
+
</Select.Trigger>
|
|
222
|
+
</Select.Control>
|
|
223
|
+
<Select.Positioner>
|
|
224
|
+
<Select.Content className="z-10 mt-1 w-full rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
|
225
|
+
<Select.ItemGroup>
|
|
226
|
+
{collection.items.map((item) => (
|
|
227
|
+
<Select.Item
|
|
228
|
+
key={item.value}
|
|
229
|
+
item={item}
|
|
230
|
+
className="position-item relative cursor-pointer select-none py-2 pl-10 pr-4 text-sm text-gray-900"
|
|
231
|
+
>
|
|
232
|
+
<Select.ItemText>{item.label}</Select.ItemText>
|
|
233
|
+
<Select.ItemIndicator className="position-indicator absolute inset-y-0 left-0 flex items-center pl-3 text-cyan-600">
|
|
234
|
+
<CheckIcon />
|
|
235
|
+
</Select.ItemIndicator>
|
|
236
|
+
</Select.Item>
|
|
237
|
+
))}
|
|
238
|
+
</Select.ItemGroup>
|
|
239
|
+
</Select.Content>
|
|
240
|
+
</Select.Positioner>
|
|
241
|
+
</Select.Root>
|
|
148
242
|
</div>
|
|
149
243
|
|
|
150
|
-
{/* Size Toggle - Only show when position is left or right */}
|
|
151
244
|
{position !== 'background' && (
|
|
152
245
|
<div className="space-y-2">
|
|
153
246
|
<label className="block text-sm font-bold text-gray-700">
|
|
@@ -177,7 +270,6 @@ const BackgroundImageWrapper = ({
|
|
|
177
270
|
</div>
|
|
178
271
|
)}
|
|
179
272
|
|
|
180
|
-
{/* Render the appropriate image component */}
|
|
181
273
|
{isArtpackImageNode(bgNode) ? (
|
|
182
274
|
<ArtpackImage paneId={paneId} onUpdate={onUpdate} />
|
|
183
275
|
) : (
|
|
@@ -202,7 +202,9 @@ export default function ActionBuilderField({
|
|
|
202
202
|
onChange={(e) => {
|
|
203
203
|
const newValue = e.target.value;
|
|
204
204
|
setParam1(newValue);
|
|
205
|
-
|
|
205
|
+
}}
|
|
206
|
+
onBlur={(e) => {
|
|
207
|
+
updateValue(selectedTarget, '', e.target.value);
|
|
206
208
|
}}
|
|
207
209
|
placeholder="https://..."
|
|
208
210
|
className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-cyan-700 focus:ring-cyan-700"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
let MODE = 'logo'; // 'wordmark' | 'logo'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
brandConfig: {
|
|
6
|
+
LOGO?: string;
|
|
7
|
+
WORDMARK?: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { brandConfig } = Astro.props;
|
|
12
|
+
|
|
13
|
+
const getAssetPath = (
|
|
14
|
+
configPath: string | undefined,
|
|
15
|
+
fallback: string
|
|
16
|
+
): string => {
|
|
17
|
+
if (configPath && configPath !== '') {
|
|
18
|
+
return configPath;
|
|
19
|
+
}
|
|
20
|
+
return fallback;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let assetUrl;
|
|
24
|
+
if (MODE === `wordmark`)
|
|
25
|
+
assetUrl = getAssetPath(brandConfig?.WORDMARK, '/brand/wordmark.svg');
|
|
26
|
+
else assetUrl = getAssetPath(brandConfig?.LOGO, '/brand/logo.svg');
|
|
27
|
+
|
|
28
|
+
// Generate positions programmatically for triple density
|
|
29
|
+
const generatePositions = () => {
|
|
30
|
+
const positions = [];
|
|
31
|
+
const rows = 15; // More rows to extend beyond boundaries
|
|
32
|
+
const cols = 12; // More cols to extend beyond boundaries
|
|
33
|
+
|
|
34
|
+
for (let row = 0; row < rows; row++) {
|
|
35
|
+
for (let col = 0; col < cols; col++) {
|
|
36
|
+
// Skip some positions for natural spacing
|
|
37
|
+
if ((row + col) % 3 !== 0) continue;
|
|
38
|
+
|
|
39
|
+
// Allow logos to extend beyond container edges (no margins)
|
|
40
|
+
const top = (row / (rows - 1)) * 120 - 10; // Extend 10% beyond top/bottom
|
|
41
|
+
const left = (col / (cols - 1)) * 120 - 10; // Extend 10% beyond left/right
|
|
42
|
+
const rotation = -45 + Math.random() * 90;
|
|
43
|
+
|
|
44
|
+
positions.push({
|
|
45
|
+
top: `${top}%`,
|
|
46
|
+
left: `${left}%`,
|
|
47
|
+
rotation: `${rotation}deg`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return positions;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const logoPositions = generatePositions();
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
{
|
|
59
|
+
assetUrl && (
|
|
60
|
+
<div
|
|
61
|
+
class="pointer-events-none absolute overflow-hidden rounded-2xl p-1.5 md:p-3.5"
|
|
62
|
+
style={{
|
|
63
|
+
top: '2rem',
|
|
64
|
+
left: '64rem',
|
|
65
|
+
right: '0',
|
|
66
|
+
bottom: '3.5rem',
|
|
67
|
+
opacity: '0.85',
|
|
68
|
+
//filter: 'grayscale(1)',
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
{logoPositions.map((position) => (
|
|
72
|
+
<img
|
|
73
|
+
src={assetUrl}
|
|
74
|
+
style={{
|
|
75
|
+
position: 'absolute',
|
|
76
|
+
top: position.top,
|
|
77
|
+
left: position.left,
|
|
78
|
+
width: '120px',
|
|
79
|
+
height: 'auto',
|
|
80
|
+
transform: `rotate(${position.rotation})`,
|
|
81
|
+
}}
|
|
82
|
+
/>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Layout from '@/layouts/Layout.astro';
|
|
3
|
+
import StoryKeepBackdrop from '@/components/storykeep/StoryKeepBackdrop.astro';
|
|
3
4
|
import StoryKeepDashboard from '@/components/storykeep/Dashboard';
|
|
4
5
|
import StoryKeepDashboard_Advanced from '@/components/storykeep/Dashboard_Advanced';
|
|
5
6
|
import { requireAdminOrEditor, isAuthenticated, isAdmin } from '@/utils/auth';
|
|
@@ -53,6 +54,7 @@ try {
|
|
|
53
54
|
|
|
54
55
|
<Layout title={title} slug="storykeep" isStoryKeep={true}>
|
|
55
56
|
<main id="main-content" class="min-h-screen w-full">
|
|
57
|
+
<StoryKeepBackdrop brandConfig={brandConfig} />
|
|
56
58
|
<div class="max-w-5xl p-3.5 md:p-8">
|
|
57
59
|
<StoryKeepDashboard
|
|
58
60
|
client:only="react"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Layout from '@/layouts/Layout.astro';
|
|
3
|
+
import StoryKeepBackdrop from '@/components/storykeep/StoryKeepBackdrop.astro';
|
|
3
4
|
import BrandingPageWrapper from '@/components/storykeep/state/BrandingWrapper';
|
|
4
5
|
import { requireAdminOrEditor, isAuthenticated, isAdmin } from '@/utils/auth';
|
|
5
6
|
import { getFullContentMap } from '@/stores/analytics';
|
|
@@ -42,6 +43,7 @@ try {
|
|
|
42
43
|
|
|
43
44
|
<Layout title={title} slug="storykeep" isStoryKeep={true}>
|
|
44
45
|
<main id="main-content" class="min-h-screen w-full">
|
|
46
|
+
<StoryKeepBackdrop brandConfig={brandConfig} />
|
|
45
47
|
<div class="max-w-5xl p-3.5 md:p-8">
|
|
46
48
|
<BrandingPageWrapper
|
|
47
49
|
client:only="react"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Layout from '@/layouts/Layout.astro';
|
|
3
|
+
import StoryKeepBackdrop from '@/components/storykeep/StoryKeepBackdrop.astro';
|
|
3
4
|
import StoryKeepDashboard from '@/components/storykeep/Dashboard';
|
|
4
5
|
import StoryKeepDashboard_Content from '@/components/storykeep/Dashboard_Content';
|
|
5
6
|
import { requireAdminOrEditor, isAuthenticated, isAdmin } from '@/utils/auth';
|
|
@@ -49,6 +50,7 @@ try {
|
|
|
49
50
|
|
|
50
51
|
<Layout title={title} slug="storykeep" isStoryKeep={true}>
|
|
51
52
|
<main id="main-content" class="min-h-screen w-full">
|
|
53
|
+
<StoryKeepBackdrop brandConfig={brandConfig} />
|
|
52
54
|
<div class="max-w-5xl p-3.5 md:p-8">
|
|
53
55
|
<StoryKeepDashboard
|
|
54
56
|
client:only="react"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Layout from '@/layouts/Layout.astro';
|
|
3
|
+
import StoryKeepBackdrop from '@/components/storykeep/StoryKeepBackdrop.astro';
|
|
3
4
|
import StoryKeepDashboard from '@/components/storykeep/Dashboard';
|
|
4
5
|
import StoryKeepDashboard_Analytics from '@/components/storykeep/Dashboard_Analytics';
|
|
5
6
|
import { requireAdminOrEditor, isAuthenticated, isAdmin } from '@/utils/auth';
|
|
@@ -50,7 +51,8 @@ try {
|
|
|
50
51
|
---
|
|
51
52
|
|
|
52
53
|
<Layout title={title} isStoryKeep={true} slug="storykeep">
|
|
53
|
-
<main id="main-content" class="min-h-screen w-full">
|
|
54
|
+
<main id="main-content" class="relative min-h-screen w-full">
|
|
55
|
+
<StoryKeepBackdrop brandConfig={brandConfig} />
|
|
54
56
|
<div class="max-w-5xl p-3.5 md:p-8">
|
|
55
57
|
<StoryKeepDashboard
|
|
56
58
|
client:only="react"
|
|
@@ -340,6 +340,18 @@ export class NodesContext {
|
|
|
340
340
|
handleClickEventDefault(node, dblClick, this.clickedParentLayer.get());
|
|
341
341
|
break;
|
|
342
342
|
case `text`:
|
|
343
|
+
if (
|
|
344
|
+
node.nodeType === 'TagElement' &&
|
|
345
|
+
'tagName' in node &&
|
|
346
|
+
(node.tagName === 'a' || node.tagName === 'button')
|
|
347
|
+
) {
|
|
348
|
+
this.toolModeValStore.set({ value: 'styles' });
|
|
349
|
+
handleClickEventDefault(
|
|
350
|
+
node,
|
|
351
|
+
dblClick,
|
|
352
|
+
this.clickedParentLayer.get()
|
|
353
|
+
);
|
|
354
|
+
}
|
|
343
355
|
if (dblClick && ![`Markdown`].includes(node.nodeType)) {
|
|
344
356
|
this.toolModeValStore.set({ value: 'styles' });
|
|
345
357
|
handleClickEventDefault(
|
package/utils/inject-files.ts
CHANGED
|
@@ -479,10 +479,6 @@ export async function injectTemplateFiles(
|
|
|
479
479
|
src: resolve('../templates/src/components/edit/pane/PanePanel_path.tsx'),
|
|
480
480
|
dest: 'src/components/edit/pane/PanePanel_path.tsx',
|
|
481
481
|
},
|
|
482
|
-
{
|
|
483
|
-
src: resolve('../templates/src/components/edit/pane/PanePanel_slug.tsx'),
|
|
484
|
-
dest: 'src/components/edit/pane/PanePanel_slug.tsx',
|
|
485
|
-
},
|
|
486
482
|
{
|
|
487
483
|
src: resolve('../templates/src/components/edit/pane/PanePanel_title.tsx'),
|
|
488
484
|
dest: 'src/components/edit/pane/PanePanel_title.tsx',
|
|
@@ -1046,6 +1042,12 @@ export async function injectTemplateFiles(
|
|
|
1046
1042
|
},
|
|
1047
1043
|
|
|
1048
1044
|
// StoryKeep Dashboard Components
|
|
1045
|
+
{
|
|
1046
|
+
src: resolve(
|
|
1047
|
+
'../templates/src/components/storykeep/StoryKeepBackdrop.astro'
|
|
1048
|
+
),
|
|
1049
|
+
dest: 'src/components/storykeep/StoryKeepBackdrop.astro',
|
|
1050
|
+
},
|
|
1049
1051
|
{
|
|
1050
1052
|
src: resolve('../templates/src/components/storykeep/Dashboard.tsx'),
|
|
1051
1053
|
dest: 'src/components/storykeep/Dashboard.tsx',
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, type Dispatch, type SetStateAction } from 'react';
|
|
2
|
-
import ExclamationTriangleIcon from '@heroicons/react/24/outline/ExclamationTriangleIcon';
|
|
3
|
-
import CheckIcon from '@heroicons/react/24/outline/CheckIcon';
|
|
4
|
-
import { getCtx } from '@/stores/nodes';
|
|
5
|
-
import { cloneDeep } from '@/utils/helpers';
|
|
6
|
-
import { PaneConfigMode, type PaneNode } from '@/types/compositorTypes';
|
|
7
|
-
|
|
8
|
-
interface PaneSlugPanelProps {
|
|
9
|
-
nodeId: string;
|
|
10
|
-
setMode: Dispatch<SetStateAction<PaneConfigMode>>;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const PaneSlugPanel = ({ nodeId, setMode }: PaneSlugPanelProps) => {
|
|
14
|
-
const [slug, setSlug] = useState('');
|
|
15
|
-
const [isValid, setIsValid] = useState(false);
|
|
16
|
-
const [warning, setWarning] = useState(false);
|
|
17
|
-
const [charCount, setCharCount] = useState(0);
|
|
18
|
-
const [validationError, setValidationError] = useState<string | null>(null);
|
|
19
|
-
const [canSave, setCanSave] = useState(false);
|
|
20
|
-
|
|
21
|
-
const ctx = getCtx();
|
|
22
|
-
const allNodes = ctx.allNodes.get();
|
|
23
|
-
const paneNode = allNodes.get(nodeId) as PaneNode;
|
|
24
|
-
if (!paneNode) return null;
|
|
25
|
-
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
setSlug(paneNode.slug);
|
|
28
|
-
setCharCount(paneNode.slug.length);
|
|
29
|
-
checkLiveValidity(paneNode.slug);
|
|
30
|
-
}, [paneNode.slug]);
|
|
31
|
-
|
|
32
|
-
// More permissive validation for typing
|
|
33
|
-
const checkLiveValidity = (value: string) => {
|
|
34
|
-
const length = value.length;
|
|
35
|
-
setCharCount(length);
|
|
36
|
-
|
|
37
|
-
// Basic format check for allowed characters
|
|
38
|
-
if (!/^[a-z0-9-]*$/.test(value)) {
|
|
39
|
-
setValidationError(
|
|
40
|
-
'Only lowercase letters, numbers, and hyphens allowed'
|
|
41
|
-
);
|
|
42
|
-
setIsValid(false);
|
|
43
|
-
setCanSave(false);
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Length checks
|
|
48
|
-
setIsValid(length >= 3 && length <= 40);
|
|
49
|
-
setWarning(length > 40 && length <= 50);
|
|
50
|
-
setValidationError(null);
|
|
51
|
-
|
|
52
|
-
// Check if we can save
|
|
53
|
-
if (length >= 3) {
|
|
54
|
-
const saveValidation = checkSaveValidity(value);
|
|
55
|
-
setCanSave(saveValidation.isValid);
|
|
56
|
-
if (!saveValidation.isValid) {
|
|
57
|
-
setValidationError(saveValidation.error || null);
|
|
58
|
-
}
|
|
59
|
-
} else {
|
|
60
|
-
setCanSave(false);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return true;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
// Strict validation for saving
|
|
67
|
-
const checkSaveValidity = (
|
|
68
|
-
value: string
|
|
69
|
-
): { isValid: boolean; error?: string } => {
|
|
70
|
-
// Strict pattern that prevents leading/trailing hyphens and multiple consecutive hyphens
|
|
71
|
-
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {
|
|
72
|
-
return {
|
|
73
|
-
isValid: false,
|
|
74
|
-
error:
|
|
75
|
-
'Slug must start and end with letters or numbers, and no consecutive hyphens',
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Check duplicates and reserved slugs
|
|
80
|
-
return ctx.isSlugValid(value, nodeId);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const handleSlugChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
84
|
-
const newSlug = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
85
|
-
|
|
86
|
-
if (newSlug.length <= 50) {
|
|
87
|
-
setSlug(newSlug);
|
|
88
|
-
checkLiveValidity(newSlug);
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const handleSlugBlur = () => {
|
|
93
|
-
if (canSave) {
|
|
94
|
-
const ctx = getCtx();
|
|
95
|
-
const updatedNode = { ...cloneDeep(paneNode), slug, isChanged: true };
|
|
96
|
-
ctx.modifyNodes([updatedNode]);
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
return (
|
|
101
|
-
<div className="group mb-4 w-full rounded-b-md bg-white px-1.5 py-6 shadow-inner">
|
|
102
|
-
<div className="px-3.5">
|
|
103
|
-
<div className="mb-4 flex justify-between">
|
|
104
|
-
<h3 className="text-lg font-bold">Pane Slug</h3>
|
|
105
|
-
<button
|
|
106
|
-
onClick={() => setMode(PaneConfigMode.DEFAULT)}
|
|
107
|
-
className="text-myblue hover:text-black"
|
|
108
|
-
>
|
|
109
|
-
← Go Back
|
|
110
|
-
</button>
|
|
111
|
-
</div>
|
|
112
|
-
|
|
113
|
-
<div className="relative">
|
|
114
|
-
<input
|
|
115
|
-
type="text"
|
|
116
|
-
value={slug}
|
|
117
|
-
onChange={handleSlugChange}
|
|
118
|
-
onBlur={handleSlugBlur}
|
|
119
|
-
onKeyDown={(e) => {
|
|
120
|
-
if (e.key === 'Enter') {
|
|
121
|
-
e.currentTarget.blur();
|
|
122
|
-
}
|
|
123
|
-
}}
|
|
124
|
-
className={`w-full rounded-md border px-2 py-1 pr-16 ${
|
|
125
|
-
validationError || charCount < 3
|
|
126
|
-
? 'border-red-500 bg-red-50'
|
|
127
|
-
: isValid && canSave
|
|
128
|
-
? 'border-green-500 bg-green-50'
|
|
129
|
-
: warning
|
|
130
|
-
? 'border-yellow-500 bg-yellow-50'
|
|
131
|
-
: 'border-gray-300'
|
|
132
|
-
}`}
|
|
133
|
-
placeholder="Enter pane slug (3-40 characters recommended)"
|
|
134
|
-
/>
|
|
135
|
-
<div className="absolute right-2 top-1/2 flex -translate-y-1/2 items-center gap-2">
|
|
136
|
-
{validationError || charCount < 3 ? (
|
|
137
|
-
<ExclamationTriangleIcon className="h-5 w-5 text-red-500" />
|
|
138
|
-
) : isValid && canSave ? (
|
|
139
|
-
<CheckIcon className="h-5 w-5 text-green-500" />
|
|
140
|
-
) : warning ? (
|
|
141
|
-
<ExclamationTriangleIcon className="h-5 w-5 text-yellow-500" />
|
|
142
|
-
) : null}
|
|
143
|
-
<span
|
|
144
|
-
className={`text-sm ${
|
|
145
|
-
validationError || charCount < 3
|
|
146
|
-
? 'text-red-500'
|
|
147
|
-
: isValid && canSave
|
|
148
|
-
? 'text-green-500'
|
|
149
|
-
: warning
|
|
150
|
-
? 'text-yellow-500'
|
|
151
|
-
: 'text-gray-500'
|
|
152
|
-
}`}
|
|
153
|
-
>
|
|
154
|
-
{charCount}/50
|
|
155
|
-
</span>
|
|
156
|
-
</div>
|
|
157
|
-
</div>
|
|
158
|
-
{validationError && (
|
|
159
|
-
<div className="mt-2 text-sm text-red-600">
|
|
160
|
-
<ExclamationTriangleIcon className="mr-1 inline h-4 w-4" />
|
|
161
|
-
{validationError}
|
|
162
|
-
</div>
|
|
163
|
-
)}
|
|
164
|
-
<div className="mt-4 space-y-4 text-lg">
|
|
165
|
-
<div className="text-gray-600">
|
|
166
|
-
This is solely used for analytics!
|
|
167
|
-
<ul className="ml-4 mt-1">
|
|
168
|
-
<li>
|
|
169
|
-
<CheckIcon className="inline h-4 w-4" /> Keep it concise and
|
|
170
|
-
descriptive
|
|
171
|
-
</li>
|
|
172
|
-
<li>
|
|
173
|
-
<CheckIcon className="inline h-4 w-4" /> Use lowercase letters
|
|
174
|
-
and numbers
|
|
175
|
-
</li>
|
|
176
|
-
<li>
|
|
177
|
-
<CheckIcon className="inline h-4 w-4" /> Use hyphens between
|
|
178
|
-
words
|
|
179
|
-
</li>
|
|
180
|
-
<li>
|
|
181
|
-
<CheckIcon className="inline h-4 w-4" /> Must start and end with
|
|
182
|
-
a letter or number
|
|
183
|
-
</li>
|
|
184
|
-
</ul>
|
|
185
|
-
</div>
|
|
186
|
-
<div className="py-4">
|
|
187
|
-
{charCount < 3 && (
|
|
188
|
-
<span className="text-red-500">
|
|
189
|
-
Slug must be at least 3 characters
|
|
190
|
-
</span>
|
|
191
|
-
)}
|
|
192
|
-
{charCount >= 3 && charCount < 5 && !validationError && (
|
|
193
|
-
<span className="text-gray-500">
|
|
194
|
-
Consider adding more characters for better description
|
|
195
|
-
</span>
|
|
196
|
-
)}
|
|
197
|
-
{warning && !validationError && (
|
|
198
|
-
<span className="text-yellow-500">
|
|
199
|
-
Slug is getting long - consider shortening it
|
|
200
|
-
</span>
|
|
201
|
-
)}
|
|
202
|
-
{isValid && canSave && charCount >= 5 && !validationError && (
|
|
203
|
-
<span className="text-green-500">
|
|
204
|
-
Good slug length and format!
|
|
205
|
-
</span>
|
|
206
|
-
)}
|
|
207
|
-
{isValid && !canSave && !validationError && (
|
|
208
|
-
<span className="text-gray-500">
|
|
209
|
-
Valid characters but needs proper formatting to save
|
|
210
|
-
</span>
|
|
211
|
-
)}
|
|
212
|
-
</div>
|
|
213
|
-
</div>
|
|
214
|
-
</div>
|
|
215
|
-
</div>
|
|
216
|
-
);
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
export default PaneSlugPanel;
|