astro-tractstack 2.0.2 → 2.0.5
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/package.json +1 -1
- package/templates/src/components/codehooks/EpinetWrapper.tsx +1 -1
- package/templates/src/components/codehooks/SankeyDiagram.tsx +15 -9
- package/templates/src/components/compositor/nodes/Widget.tsx +30 -20
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +20 -25
- package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +137 -74
- package/templates/src/components/storykeep/controls/content/KnownResourceForm.tsx +62 -32
- package/templates/src/layouts/Layout.astro +1 -0
- package/templates/src/pages/[...slug]/edit.astro +5 -1
- package/templates/src/pages/[...slug].astro +4 -0
- package/templates/src/utils/layout.ts +41 -11
package/package.json
CHANGED
|
@@ -353,7 +353,7 @@ const EpinetWrapper = ({
|
|
|
353
353
|
}
|
|
354
354
|
>
|
|
355
355
|
<div className="space-y-6">
|
|
356
|
-
<div className="rounded-lg bg-white p-
|
|
356
|
+
<div className="rounded-lg bg-white p-2 shadow md:p-6">
|
|
357
357
|
<div className="mb-4 flex items-center justify-between">
|
|
358
358
|
{(isLoading || status === 'loading') && (
|
|
359
359
|
<div className="flex items-center space-x-2 text-sm text-gray-500">
|
|
@@ -3,7 +3,8 @@ import * as d3 from 'd3';
|
|
|
3
3
|
import { sankey, sankeyLinkHorizontal } from 'd3-sankey';
|
|
4
4
|
|
|
5
5
|
const MAX_HEIGHT = 1600;
|
|
6
|
-
const COMPRESSED_HEIGHT = 256;
|
|
6
|
+
const COMPRESSED_HEIGHT = 256;
|
|
7
|
+
const MIN_DIAGRAM_WIDTH = 800; // Define a minimum width for the diagram
|
|
7
8
|
|
|
8
9
|
const colors = [
|
|
9
10
|
'#ef4444',
|
|
@@ -52,7 +53,7 @@ const SankeyDiagram = ({ data, isLoading = false }: SankeyDiagramProps) => {
|
|
|
52
53
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
53
54
|
const hasScrolledRef = useRef(false);
|
|
54
55
|
const [dimensions, setDimensions] = useState({
|
|
55
|
-
width:
|
|
56
|
+
width: MIN_DIAGRAM_WIDTH,
|
|
56
57
|
height: 500,
|
|
57
58
|
});
|
|
58
59
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
@@ -60,7 +61,11 @@ const SankeyDiagram = ({ data, isLoading = false }: SankeyDiagramProps) => {
|
|
|
60
61
|
useEffect(() => {
|
|
61
62
|
const updateDimensions = () => {
|
|
62
63
|
if (containerRef.current) {
|
|
63
|
-
|
|
64
|
+
// Ensure the diagram width is the larger of the container or our defined minimum
|
|
65
|
+
const containerWidth = Math.max(
|
|
66
|
+
MIN_DIAGRAM_WIDTH,
|
|
67
|
+
containerRef.current.offsetWidth
|
|
68
|
+
);
|
|
64
69
|
const nodeCount = data.nodes.length || 1;
|
|
65
70
|
const optimalHeight = nodeCount * (40 + 10) + 50;
|
|
66
71
|
const finalHeight = Math.min(MAX_HEIGHT, optimalHeight);
|
|
@@ -245,7 +250,6 @@ const SankeyDiagram = ({ data, isLoading = false }: SankeyDiagramProps) => {
|
|
|
245
250
|
|
|
246
251
|
return (
|
|
247
252
|
<div ref={containerRef} className="relative w-full">
|
|
248
|
-
{/* Expand/Compress Controls */}
|
|
249
253
|
<div className="mb-3 flex items-center justify-between">
|
|
250
254
|
<div className="text-sm text-gray-600">
|
|
251
255
|
{data.nodes.length} nodes • {data.links.length} connections
|
|
@@ -292,23 +296,21 @@ const SankeyDiagram = ({ data, isLoading = false }: SankeyDiagramProps) => {
|
|
|
292
296
|
</button>
|
|
293
297
|
</div>
|
|
294
298
|
|
|
295
|
-
{/* Compression Warning */}
|
|
296
299
|
{needsCompression && (
|
|
297
300
|
<div className="mb-2 rounded bg-amber-50 px-3 py-2 text-sm text-amber-800">
|
|
298
301
|
<strong>Compressed view</strong> - click anywhere to expand!
|
|
299
302
|
</div>
|
|
300
303
|
)}
|
|
301
304
|
|
|
302
|
-
{/* SVG Container - Clickable when compressed */}
|
|
303
305
|
<div
|
|
304
|
-
className={`transition-all duration-300 ${
|
|
306
|
+
className={`overflow-x-auto transition-all duration-300 md:overflow-visible ${
|
|
305
307
|
needsCompression
|
|
306
308
|
? 'cursor-pointer hover:bg-gray-50 hover:shadow-md'
|
|
307
309
|
: ''
|
|
308
310
|
}`}
|
|
309
311
|
style={{
|
|
310
312
|
height: `${displayHeight}px`,
|
|
311
|
-
|
|
313
|
+
overflowY: 'hidden',
|
|
312
314
|
}}
|
|
313
315
|
onClick={needsCompression ? handleExpand : undefined}
|
|
314
316
|
role={needsCompression ? 'button' : undefined}
|
|
@@ -333,7 +335,7 @@ const SankeyDiagram = ({ data, isLoading = false }: SankeyDiagramProps) => {
|
|
|
333
335
|
height={dimensions.height}
|
|
334
336
|
style={{
|
|
335
337
|
display: 'block',
|
|
336
|
-
|
|
338
|
+
minWidth: `${dimensions.width}px`, // Ensure SVG itself doesn't shrink
|
|
337
339
|
height: `${dimensions.height}px`,
|
|
338
340
|
transform: needsCompression
|
|
339
341
|
? `scaleY(${displayHeight / dimensions.height})`
|
|
@@ -345,6 +347,10 @@ const SankeyDiagram = ({ data, isLoading = false }: SankeyDiagramProps) => {
|
|
|
345
347
|
></svg>
|
|
346
348
|
</div>
|
|
347
349
|
|
|
350
|
+
<div className="mt-2 text-center text-xs text-gray-500 md:hidden">
|
|
351
|
+
← Scroll to see full journey map →
|
|
352
|
+
</div>
|
|
353
|
+
|
|
348
354
|
{isLoading && (
|
|
349
355
|
<div className="absolute inset-0 flex items-center justify-center rounded bg-black bg-opacity-80">
|
|
350
356
|
<div className="flex items-center space-x-2 text-white">
|
|
@@ -22,15 +22,16 @@ const getWidgetElement = (
|
|
|
22
22
|
classNames: string
|
|
23
23
|
): ReactElement | null => {
|
|
24
24
|
const { hook, value1, value2, value3, nodeId } = props;
|
|
25
|
-
if (!hook
|
|
25
|
+
if (!hook) return null;
|
|
26
26
|
|
|
27
27
|
switch (hook) {
|
|
28
28
|
case 'youtube':
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
if (value1)
|
|
30
|
+
return value2 ? (
|
|
31
|
+
<div className={`${classNames} pointer-events-none`}>
|
|
32
|
+
<YouTubeWrapper embedCode={value1} title={value2} />
|
|
33
|
+
</div>
|
|
34
|
+
) : null;
|
|
34
35
|
|
|
35
36
|
case 'signup':
|
|
36
37
|
return (
|
|
@@ -45,19 +46,21 @@ const getWidgetElement = (
|
|
|
45
46
|
);
|
|
46
47
|
|
|
47
48
|
case 'belief':
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
if (value1)
|
|
50
|
+
return value2 ? (
|
|
51
|
+
<div className={`${classNames} pointer-events-none`}>
|
|
52
|
+
<Belief value={{ slug: value1, scale: value2, extra: value3 }} />
|
|
53
|
+
</div>
|
|
54
|
+
) : null;
|
|
53
55
|
|
|
54
56
|
case 'identifyAs':
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
if (value1)
|
|
58
|
+
return value2 ? (
|
|
59
|
+
<IdentifyAs
|
|
60
|
+
classNames={`${classNames} pointer-events-none`}
|
|
61
|
+
value={{ slug: value1, target: value2, extra: value3 || `` }}
|
|
62
|
+
/>
|
|
63
|
+
) : null;
|
|
61
64
|
|
|
62
65
|
case 'toggle':
|
|
63
66
|
return value2 ? (
|
|
@@ -90,9 +93,16 @@ const getWidgetElement = (
|
|
|
90
93
|
<p className="text-sm font-bold text-gray-700">
|
|
91
94
|
Interactive Disclosure
|
|
92
95
|
</p>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
{value1 ? (
|
|
97
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
98
|
+
Mode: Belief-Driven (<code className="font-bold">{value1}</code>
|
|
99
|
+
)
|
|
100
|
+
</p>
|
|
101
|
+
) : (
|
|
102
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
103
|
+
Mode: Open (Custom Actions)
|
|
104
|
+
</p>
|
|
105
|
+
)}
|
|
96
106
|
</div>
|
|
97
107
|
</div>
|
|
98
108
|
);
|
|
@@ -187,21 +187,27 @@ const StoryFragmentOpenGraphPanel = ({
|
|
|
187
187
|
(item) => item.type === 'Topic' && item.id === 'all-topics'
|
|
188
188
|
);
|
|
189
189
|
|
|
190
|
-
// Convert topic strings to Topic objects with mock IDs
|
|
190
|
+
// Convert topic strings to Topic objects with mock IDs
|
|
191
191
|
const allTopicsArray = topicsContent?.topics || [];
|
|
192
192
|
const topicsWithIds: Topic[] = allTopicsArray.map(
|
|
193
193
|
(topicTitle, index) => ({
|
|
194
|
-
id: index + 1,
|
|
194
|
+
id: index + 1,
|
|
195
195
|
title: topicTitle,
|
|
196
196
|
})
|
|
197
197
|
);
|
|
198
198
|
|
|
199
199
|
setExistingTopics(topicsWithIds);
|
|
200
200
|
|
|
201
|
+
// Prioritize the description from the definitive fullContentMap
|
|
202
|
+
const sfContent = $contentMap.find(
|
|
203
|
+
(item) => item.type === 'StoryFragment' && item.id === nodeId
|
|
204
|
+
);
|
|
205
|
+
const initialDescription = sfContent?.description || '';
|
|
206
|
+
setDraftDetails(initialDescription);
|
|
207
|
+
|
|
201
208
|
let initialTopics: Topic[] = [];
|
|
202
|
-
let initialDescription = '';
|
|
203
209
|
|
|
204
|
-
// Check stored draft data
|
|
210
|
+
// Check stored draft data for topics
|
|
205
211
|
if (storedData) {
|
|
206
212
|
initialTopics = Array.isArray(storedData.topics)
|
|
207
213
|
? storedData.topics.map((t) => ({
|
|
@@ -209,29 +215,18 @@ const StoryFragmentOpenGraphPanel = ({
|
|
|
209
215
|
title: t.title,
|
|
210
216
|
}))
|
|
211
217
|
: [];
|
|
212
|
-
initialDescription = storedData.description || '';
|
|
213
218
|
setDraftTopics(initialTopics);
|
|
214
|
-
|
|
219
|
+
} else if (sfContent && sfContent.topics && sfContent.topics.length > 0) {
|
|
220
|
+
// Fall back to content map data for initial topics if no draft exists
|
|
221
|
+
initialTopics = sfContent.topics.map((topicTitle) => {
|
|
222
|
+
const existingTopic = topicsWithIds.find(
|
|
223
|
+
(t) => t.title.toLowerCase() === topicTitle.toLowerCase()
|
|
224
|
+
);
|
|
225
|
+
return existingTopic || { id: -1, title: topicTitle };
|
|
226
|
+
});
|
|
227
|
+
setDraftTopics(initialTopics);
|
|
215
228
|
} else {
|
|
216
|
-
|
|
217
|
-
const sfContent = $contentMap.find(
|
|
218
|
-
(item) => item.type === 'StoryFragment' && item.id === nodeId
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
if (sfContent && sfContent.topics && sfContent.topics.length > 0) {
|
|
222
|
-
initialTopics = sfContent.topics.map((topicTitle) => {
|
|
223
|
-
const existingTopic = topicsWithIds.find(
|
|
224
|
-
(t) => t.title.toLowerCase() === topicTitle.toLowerCase()
|
|
225
|
-
);
|
|
226
|
-
return existingTopic || { id: -1, title: topicTitle };
|
|
227
|
-
});
|
|
228
|
-
initialDescription = sfContent.description || '';
|
|
229
|
-
setDraftTopics(initialTopics);
|
|
230
|
-
setDraftDetails(initialDescription);
|
|
231
|
-
} else {
|
|
232
|
-
setDraftTopics([]);
|
|
233
|
-
setDraftDetails('');
|
|
234
|
-
}
|
|
229
|
+
setDraftTopics([]);
|
|
235
230
|
}
|
|
236
231
|
|
|
237
232
|
if (initialState.current) {
|
|
@@ -124,7 +124,6 @@ const DisclosureItemEditor = ({
|
|
|
124
124
|
item,
|
|
125
125
|
onUpdate,
|
|
126
126
|
onToggle,
|
|
127
|
-
config,
|
|
128
127
|
onMoveUp,
|
|
129
128
|
onMoveDown,
|
|
130
129
|
isFirst,
|
|
@@ -133,7 +132,6 @@ const DisclosureItemEditor = ({
|
|
|
133
132
|
item: DisclosureItem;
|
|
134
133
|
onUpdate: (updates: Partial<DisclosureItem>) => void;
|
|
135
134
|
onToggle: () => void;
|
|
136
|
-
config: BrandConfig;
|
|
137
135
|
onMoveUp: () => void;
|
|
138
136
|
onMoveDown: () => void;
|
|
139
137
|
isFirst: boolean;
|
|
@@ -258,6 +256,7 @@ export default function InteractiveDisclosureWidget({
|
|
|
258
256
|
onUpdate,
|
|
259
257
|
config,
|
|
260
258
|
}: InteractiveDisclosureWidgetProps) {
|
|
259
|
+
const [mode, setMode] = useState<'belief' | 'open'>('belief');
|
|
261
260
|
const [beliefs, setBeliefs] = useState<BeliefNode[]>([]);
|
|
262
261
|
const [selectedBeliefTag, setSelectedBeliefTag] = useState<string>('');
|
|
263
262
|
const [disclosures, setDisclosures] = useState<DisclosureItem[]>([]);
|
|
@@ -276,14 +275,16 @@ export default function InteractiveDisclosureWidget({
|
|
|
276
275
|
const beliefTag = String(node.codeHookParams?.[0] || '');
|
|
277
276
|
const payloadJson = String(node.codeHookParams?.[1] || '');
|
|
278
277
|
|
|
279
|
-
if (
|
|
280
|
-
|
|
278
|
+
if (beliefTag && beliefTag !== 'BELIEF') {
|
|
279
|
+
setMode('belief');
|
|
280
|
+
} else {
|
|
281
|
+
setMode('open');
|
|
281
282
|
}
|
|
282
283
|
|
|
283
284
|
setSelectedBeliefTag(beliefTag && beliefTag !== 'BELIEF' ? beliefTag : '');
|
|
284
285
|
const currentBelief = beliefs.find((b) => b.slug === beliefTag);
|
|
285
286
|
|
|
286
|
-
if (payloadJson
|
|
287
|
+
if (payloadJson) {
|
|
287
288
|
try {
|
|
288
289
|
const parsed = JSON.parse(payloadJson);
|
|
289
290
|
setWidgetStyles(
|
|
@@ -296,50 +297,62 @@ export default function InteractiveDisclosureWidget({
|
|
|
296
297
|
const loadedDisclosures =
|
|
297
298
|
(parsed.disclosures as StoredDisclosureItem[]) || [];
|
|
298
299
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
300
|
+
if (currentBelief) {
|
|
301
|
+
const scaleKeys =
|
|
302
|
+
currentBelief.scale === 'custom'
|
|
303
|
+
? (currentBelief.customValues || []).map((v) => ({
|
|
304
|
+
slug: v,
|
|
305
|
+
name: v,
|
|
306
|
+
}))
|
|
307
|
+
: heldBeliefsScales[
|
|
308
|
+
currentBelief.scale as keyof typeof heldBeliefsScales
|
|
309
|
+
] || [];
|
|
310
|
+
|
|
311
|
+
const actionCommand =
|
|
312
|
+
currentBelief.scale === 'custom' ? 'identifyAs' : 'declare';
|
|
313
|
+
const finalDisclosures: DisclosureItem[] = loadedDisclosures.map(
|
|
314
|
+
(loadedItem) => {
|
|
315
|
+
const isFromScale = scaleKeys.some(
|
|
316
|
+
(sk) => sk.slug === loadedItem.beliefValue
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
...loadedItem,
|
|
321
|
+
id: generateId(),
|
|
322
|
+
isCustom: !isFromScale,
|
|
323
|
+
actionLisp: isFromScale
|
|
324
|
+
? `(${actionCommand} ${beliefTag} ${quoteIfNecessary(actionCommand, loadedItem.beliefValue)})`
|
|
325
|
+
: loadedItem.actionLisp,
|
|
326
|
+
isDisabled: false,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
);
|
|
330
|
+
scaleKeys.forEach(({ slug, name }) => {
|
|
331
|
+
if (!finalDisclosures.some((d) => d.beliefValue === slug)) {
|
|
332
|
+
finalDisclosures.push({
|
|
333
|
+
id: generateId(),
|
|
334
|
+
beliefValue: slug,
|
|
335
|
+
title: name,
|
|
336
|
+
description: '',
|
|
337
|
+
icon: 'chat-heart-fill',
|
|
338
|
+
actionLisp: `(${actionCommand} ${beliefTag} ${quoteIfNecessary(actionCommand, slug)})`,
|
|
339
|
+
isCustom: false,
|
|
340
|
+
isDisabled: true,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
setDisclosures(finalDisclosures);
|
|
345
|
+
} else {
|
|
346
|
+
const finalDisclosures: DisclosureItem[] = loadedDisclosures.map(
|
|
347
|
+
(loadedItem) => ({
|
|
318
348
|
...loadedItem,
|
|
319
349
|
id: generateId(),
|
|
320
|
-
isCustom:
|
|
321
|
-
actionLisp: isFromScale
|
|
322
|
-
? `(${actionCommand} ${beliefTag} ${quoteIfNecessary(actionCommand, loadedItem.beliefValue)})`
|
|
323
|
-
: loadedItem.actionLisp,
|
|
350
|
+
isCustom: true,
|
|
324
351
|
isDisabled: false,
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (!finalDisclosures.some((d) => d.beliefValue === slug)) {
|
|
330
|
-
finalDisclosures.push({
|
|
331
|
-
id: generateId(),
|
|
332
|
-
beliefValue: slug,
|
|
333
|
-
title: name,
|
|
334
|
-
description: '',
|
|
335
|
-
icon: 'chat-heart-fill',
|
|
336
|
-
actionLisp: `(${actionCommand} ${beliefTag} ${quoteIfNecessary(actionCommand, slug)})`,
|
|
337
|
-
isCustom: false,
|
|
338
|
-
isDisabled: true,
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
});
|
|
342
|
-
setDisclosures(finalDisclosures);
|
|
352
|
+
})
|
|
353
|
+
);
|
|
354
|
+
setDisclosures(finalDisclosures);
|
|
355
|
+
}
|
|
343
356
|
} catch (e) {
|
|
344
357
|
console.error('Error parsing disclosure payload:', e);
|
|
345
358
|
}
|
|
@@ -417,6 +430,25 @@ export default function InteractiveDisclosureWidget({
|
|
|
417
430
|
setDisclosures(newDisclosures);
|
|
418
431
|
};
|
|
419
432
|
|
|
433
|
+
const handleModeChange = (newMode: 'belief' | 'open') => {
|
|
434
|
+
if (mode === newMode) return;
|
|
435
|
+
|
|
436
|
+
setMode(newMode);
|
|
437
|
+
setSelectedBeliefTag('');
|
|
438
|
+
setDisclosures([]);
|
|
439
|
+
setWidgetStyles({
|
|
440
|
+
textColor: '#000000',
|
|
441
|
+
bgColor: '#ffffff',
|
|
442
|
+
bgOpacity: 100,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
if (newMode === 'open') {
|
|
446
|
+
onUpdate(['', '{}']);
|
|
447
|
+
} else {
|
|
448
|
+
onUpdate(['BELIEF', '{}']);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
|
|
420
452
|
const moveDisclosure = (id: string, direction: 'up' | 'down') => {
|
|
421
453
|
const index = disclosures.findIndex((d) => d.id === id);
|
|
422
454
|
if (index === -1) return;
|
|
@@ -477,35 +509,66 @@ export default function InteractiveDisclosureWidget({
|
|
|
477
509
|
|
|
478
510
|
return (
|
|
479
511
|
<div className="space-y-4">
|
|
480
|
-
<div
|
|
481
|
-
<
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
disabled={hasRealSelection}
|
|
486
|
-
>
|
|
487
|
-
<option value="">Select a Belief...</option>
|
|
488
|
-
{beliefs.map((b) => (
|
|
489
|
-
<option key={b.slug} value={b.slug}>
|
|
490
|
-
{b.title} ({b.scale})
|
|
491
|
-
</option>
|
|
492
|
-
))}
|
|
493
|
-
</select>
|
|
494
|
-
{hasRealSelection && (
|
|
512
|
+
<div>
|
|
513
|
+
<label className="block text-xs font-bold text-gray-600">
|
|
514
|
+
Configuration Mode
|
|
515
|
+
</label>
|
|
516
|
+
<div className="isolate mt-1 inline-flex rounded-md shadow-sm">
|
|
495
517
|
<button
|
|
496
518
|
type="button"
|
|
497
|
-
onClick={() =>
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
519
|
+
onClick={() => handleModeChange('belief')}
|
|
520
|
+
className={`relative inline-flex items-center rounded-l-md px-3 py-2 text-sm font-semibold ring-1 ring-inset ring-gray-300 focus:z-10 ${
|
|
521
|
+
mode === 'belief'
|
|
522
|
+
? 'bg-cyan-600 text-white'
|
|
523
|
+
: 'bg-white text-gray-900 hover:bg-gray-50'
|
|
524
|
+
}`}
|
|
503
525
|
>
|
|
504
|
-
|
|
526
|
+
Belief-Driven
|
|
505
527
|
</button>
|
|
506
|
-
|
|
528
|
+
<button
|
|
529
|
+
type="button"
|
|
530
|
+
onClick={() => handleModeChange('open')}
|
|
531
|
+
className={`relative -ml-px inline-flex items-center rounded-r-md px-3 py-2 text-sm font-semibold ring-1 ring-inset ring-gray-300 focus:z-10 ${
|
|
532
|
+
mode === 'open'
|
|
533
|
+
? 'bg-cyan-600 text-white'
|
|
534
|
+
: 'bg-white text-gray-900 hover:bg-gray-50'
|
|
535
|
+
}`}
|
|
536
|
+
>
|
|
537
|
+
Open
|
|
538
|
+
</button>
|
|
539
|
+
</div>
|
|
507
540
|
</div>
|
|
508
|
-
{
|
|
541
|
+
{mode === 'belief' && (
|
|
542
|
+
<div className="flex items-center gap-2">
|
|
543
|
+
<select
|
|
544
|
+
value={selectedBeliefTag}
|
|
545
|
+
onChange={(e) => handleBeliefChange(e.target.value)}
|
|
546
|
+
className="flex-1 rounded-md border-gray-300 shadow-sm"
|
|
547
|
+
disabled={hasRealSelection}
|
|
548
|
+
>
|
|
549
|
+
<option value="">Select a Belief...</option>
|
|
550
|
+
{beliefs.map((b) => (
|
|
551
|
+
<option key={b.slug} value={b.slug}>
|
|
552
|
+
{b.title} ({b.scale})
|
|
553
|
+
</option>
|
|
554
|
+
))}
|
|
555
|
+
</select>
|
|
556
|
+
{hasRealSelection && (
|
|
557
|
+
<button
|
|
558
|
+
type="button"
|
|
559
|
+
onClick={() => {
|
|
560
|
+
setSelectedBeliefTag('');
|
|
561
|
+
setDisclosures([]);
|
|
562
|
+
onUpdate(['BELIEF', '{}']);
|
|
563
|
+
}}
|
|
564
|
+
className="rounded p-1 text-red-600 hover:bg-gray-100"
|
|
565
|
+
>
|
|
566
|
+
<XMarkIcon className="h-5 w-5" />
|
|
567
|
+
</button>
|
|
568
|
+
)}
|
|
569
|
+
</div>
|
|
570
|
+
)}
|
|
571
|
+
{(hasRealSelection || mode === 'open') && (
|
|
509
572
|
<div className="mt-4 border-t border-gray-200 pt-4">
|
|
510
573
|
<button
|
|
511
574
|
type="button"
|
|
@@ -513,8 +576,8 @@ export default function InteractiveDisclosureWidget({
|
|
|
513
576
|
className="flex w-full items-center justify-center rounded-md bg-gray-100 px-3 py-2 text-sm font-bold text-gray-700 hover:bg-gray-200"
|
|
514
577
|
>
|
|
515
578
|
<ChevronDownIcon className="mr-2 h-5 w-5" />
|
|
516
|
-
Configure {disclosures.filter((d) => !d.isDisabled).length}
|
|
517
|
-
|
|
579
|
+
Configure {disclosures.filter((d) => !d.isDisabled).length}{' '}
|
|
580
|
+
Disclosure(s) & Styles
|
|
518
581
|
</button>
|
|
519
582
|
</div>
|
|
520
583
|
)}
|
|
@@ -546,7 +609,8 @@ export default function InteractiveDisclosureWidget({
|
|
|
546
609
|
<div className="flex h-full flex-col">
|
|
547
610
|
<div className="flex-shrink-0 border-b border-gray-200 bg-white px-6 py-3">
|
|
548
611
|
<Dialog.Title className="text-lg font-bold text-gray-900">
|
|
549
|
-
Disclosure Configuration
|
|
612
|
+
Disclosure Configuration
|
|
613
|
+
{selectedBelief && `: ${selectedBelief.title}`}
|
|
550
614
|
</Dialog.Title>
|
|
551
615
|
</div>
|
|
552
616
|
<div className="flex-1 space-y-6 overflow-y-auto p-4">
|
|
@@ -614,7 +678,6 @@ export default function InteractiveDisclosureWidget({
|
|
|
614
678
|
updateDisclosure(item.id, updates)
|
|
615
679
|
}
|
|
616
680
|
onToggle={() => toggleDisclosure(item.id)}
|
|
617
|
-
config={config}
|
|
618
681
|
onMoveUp={() => moveDisclosure(item.id, 'up')}
|
|
619
682
|
onMoveDown={() => moveDisclosure(item.id, 'down')}
|
|
620
683
|
isFirst={index === 0}
|
|
@@ -39,36 +39,25 @@ const FIELD_TYPES = [
|
|
|
39
39
|
{ value: 'image', label: 'Image' },
|
|
40
40
|
];
|
|
41
41
|
|
|
42
|
-
const
|
|
42
|
+
const KnownResourceFormRenderer = ({
|
|
43
43
|
categorySlug,
|
|
44
44
|
contentMap,
|
|
45
45
|
onClose,
|
|
46
|
-
|
|
46
|
+
brandConfig,
|
|
47
|
+
}: KnownResourceFormProps & { brandConfig: BrandConfig }) => {
|
|
47
48
|
const [newFieldName, setNewFieldName] = useState('');
|
|
48
49
|
const [showAddField, setShowAddField] = useState(false);
|
|
49
|
-
const [brandConfig, setBrandConfig] = useState<BrandConfig | null>(null);
|
|
50
|
-
const [loading, setLoading] = useState(false);
|
|
51
|
-
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
if (!brandConfig && !loading) {
|
|
54
|
-
setLoading(true);
|
|
55
|
-
getBrandConfig(window.TRACTSTACK_CONFIG?.tenantId || 'default')
|
|
56
|
-
.then(setBrandConfig)
|
|
57
|
-
.catch(console.error)
|
|
58
|
-
.finally(() => setLoading(false));
|
|
59
|
-
}
|
|
60
|
-
}, [brandConfig, loading]);
|
|
61
50
|
|
|
62
|
-
const knownResources = brandConfig?.KNOWN_RESOURCES || {};
|
|
63
51
|
const isCreate = categorySlug === 'new';
|
|
64
|
-
const
|
|
52
|
+
const knownResources = brandConfig.KNOWN_RESOURCES || {};
|
|
53
|
+
const currentCategory = isCreate ? {} : knownResources[categorySlug];
|
|
65
54
|
|
|
66
55
|
const hasExistingResources =
|
|
67
56
|
!isCreate && contentMap.some((item) => item.categorySlug === categorySlug);
|
|
68
57
|
|
|
69
58
|
const initialState: KnownResourceState = {
|
|
70
59
|
categorySlug: isCreate ? '' : categorySlug,
|
|
71
|
-
fields:
|
|
60
|
+
fields: currentCategory,
|
|
72
61
|
};
|
|
73
62
|
|
|
74
63
|
const validator = (state: KnownResourceState): FieldErrors => {
|
|
@@ -90,8 +79,6 @@ const KnownResourceForm = ({
|
|
|
90
79
|
validator,
|
|
91
80
|
onSave: async (data) => {
|
|
92
81
|
try {
|
|
93
|
-
// Update known resources in brand config
|
|
94
|
-
if (!brandConfig) throw new Error('Brand config not loaded');
|
|
95
82
|
const brandState = convertToLocalState(brandConfig);
|
|
96
83
|
const updatedKnownResources = {
|
|
97
84
|
...brandState.knownResources,
|
|
@@ -108,7 +95,6 @@ const KnownResourceForm = ({
|
|
|
108
95
|
updatedBrandState
|
|
109
96
|
);
|
|
110
97
|
|
|
111
|
-
// Call success callback after save (original pattern)
|
|
112
98
|
setTimeout(() => {
|
|
113
99
|
onClose?.(true);
|
|
114
100
|
}, 1000);
|
|
@@ -179,7 +165,6 @@ const KnownResourceForm = ({
|
|
|
179
165
|
|
|
180
166
|
return (
|
|
181
167
|
<div className="space-y-8">
|
|
182
|
-
{/* Header */}
|
|
183
168
|
<div className="border-b border-gray-200 pb-4">
|
|
184
169
|
<h2 className="text-2xl font-bold text-gray-900">
|
|
185
170
|
{isCreate ? 'Create Resource Category' : `Edit ${categorySlug}`}
|
|
@@ -200,7 +185,6 @@ const KnownResourceForm = ({
|
|
|
200
185
|
</div>
|
|
201
186
|
|
|
202
187
|
<div className="space-y-6">
|
|
203
|
-
{/* Category Name */}
|
|
204
188
|
<StringInput
|
|
205
189
|
label="Category Name"
|
|
206
190
|
value={formState.state.categorySlug}
|
|
@@ -214,7 +198,6 @@ const KnownResourceForm = ({
|
|
|
214
198
|
Must be lowercase with hyphens. Cannot be changed after creation.
|
|
215
199
|
</p>
|
|
216
200
|
|
|
217
|
-
{/* Fields Section */}
|
|
218
201
|
<div className="space-y-6">
|
|
219
202
|
<div className="flex items-center justify-between">
|
|
220
203
|
<h3 className="text-lg font-bold text-gray-900">Fields</h3>
|
|
@@ -228,7 +211,6 @@ const KnownResourceForm = ({
|
|
|
228
211
|
</button>
|
|
229
212
|
</div>
|
|
230
213
|
|
|
231
|
-
{/* Add Field Form */}
|
|
232
214
|
{showAddField && (
|
|
233
215
|
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
|
234
216
|
<h4 className="mb-3 text-sm font-bold text-gray-900">
|
|
@@ -264,7 +246,6 @@ const KnownResourceForm = ({
|
|
|
264
246
|
</div>
|
|
265
247
|
)}
|
|
266
248
|
|
|
267
|
-
{/* Existing Fields */}
|
|
268
249
|
{Object.keys(formState.state.fields).length === 0 ? (
|
|
269
250
|
<div className="py-6 text-center text-gray-500">
|
|
270
251
|
No fields defined yet. Click "Add Field" to create your first
|
|
@@ -275,7 +256,6 @@ const KnownResourceForm = ({
|
|
|
275
256
|
{Object.entries(formState.state.fields).map(
|
|
276
257
|
([fieldName, fieldDef]) => {
|
|
277
258
|
const locked = isFieldLocked(fieldName);
|
|
278
|
-
|
|
279
259
|
return (
|
|
280
260
|
<div
|
|
281
261
|
key={fieldName}
|
|
@@ -304,7 +284,6 @@ const KnownResourceForm = ({
|
|
|
304
284
|
</button>
|
|
305
285
|
)}
|
|
306
286
|
</div>
|
|
307
|
-
|
|
308
287
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
309
288
|
<EnumSelect
|
|
310
289
|
label="Type"
|
|
@@ -315,7 +294,6 @@ const KnownResourceForm = ({
|
|
|
315
294
|
options={FIELD_TYPES}
|
|
316
295
|
disabled={locked}
|
|
317
296
|
/>
|
|
318
|
-
|
|
319
297
|
<BooleanToggle
|
|
320
298
|
label="Optional"
|
|
321
299
|
value={fieldDef.optional || false}
|
|
@@ -324,7 +302,6 @@ const KnownResourceForm = ({
|
|
|
324
302
|
}
|
|
325
303
|
disabled={locked}
|
|
326
304
|
/>
|
|
327
|
-
|
|
328
305
|
{fieldDef.type === 'categoryReference' && (
|
|
329
306
|
<EnumSelect
|
|
330
307
|
label="Reference Category"
|
|
@@ -341,7 +318,6 @@ const KnownResourceForm = ({
|
|
|
341
318
|
disabled={locked}
|
|
342
319
|
/>
|
|
343
320
|
)}
|
|
344
|
-
|
|
345
321
|
{fieldDef.type === 'number' && (
|
|
346
322
|
<>
|
|
347
323
|
<NumberInput
|
|
@@ -372,7 +348,6 @@ const KnownResourceForm = ({
|
|
|
372
348
|
</div>
|
|
373
349
|
</div>
|
|
374
350
|
|
|
375
|
-
{/* Save/Cancel Bar */}
|
|
376
351
|
<UnsavedChangesBar
|
|
377
352
|
formState={formState}
|
|
378
353
|
message="You have unsaved resource category changes"
|
|
@@ -380,7 +355,6 @@ const KnownResourceForm = ({
|
|
|
380
355
|
cancelLabel="Discard Changes"
|
|
381
356
|
/>
|
|
382
357
|
|
|
383
|
-
{/* Cancel Navigation Button */}
|
|
384
358
|
<div className="flex justify-start">
|
|
385
359
|
<button
|
|
386
360
|
type="button"
|
|
@@ -394,4 +368,60 @@ const KnownResourceForm = ({
|
|
|
394
368
|
);
|
|
395
369
|
};
|
|
396
370
|
|
|
371
|
+
const KnownResourceForm = ({
|
|
372
|
+
categorySlug,
|
|
373
|
+
contentMap,
|
|
374
|
+
onClose,
|
|
375
|
+
}: KnownResourceFormProps) => {
|
|
376
|
+
const [brandConfig, setBrandConfig] = useState<BrandConfig | null>(null);
|
|
377
|
+
const [loading, setLoading] = useState(true);
|
|
378
|
+
const [error, setError] = useState<string | null>(null);
|
|
379
|
+
|
|
380
|
+
useEffect(() => {
|
|
381
|
+
getBrandConfig(window.TRACTSTACK_CONFIG?.tenantId || 'default')
|
|
382
|
+
.then(setBrandConfig)
|
|
383
|
+
.catch((err) => {
|
|
384
|
+
console.error('Failed to load brand configuration:', err);
|
|
385
|
+
setError(
|
|
386
|
+
'Could not load resource category configuration. Please try again.'
|
|
387
|
+
);
|
|
388
|
+
})
|
|
389
|
+
.finally(() => setLoading(false));
|
|
390
|
+
}, []);
|
|
391
|
+
|
|
392
|
+
if (loading) {
|
|
393
|
+
return (
|
|
394
|
+
<div className="py-12 text-center text-gray-500">
|
|
395
|
+
Loading configuration...
|
|
396
|
+
</div>
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (error) {
|
|
401
|
+
return (
|
|
402
|
+
<div className="rounded-md bg-red-50 p-4 text-center text-red-700">
|
|
403
|
+
{error}
|
|
404
|
+
</div>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const isCreate = categorySlug === 'new';
|
|
409
|
+
if (!isCreate && !(brandConfig?.KNOWN_RESOURCES || {})[categorySlug]) {
|
|
410
|
+
return (
|
|
411
|
+
<div className="py-12 text-center text-gray-500">
|
|
412
|
+
Resource category "{categorySlug}" not found.
|
|
413
|
+
</div>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return (
|
|
418
|
+
<KnownResourceFormRenderer
|
|
419
|
+
categorySlug={categorySlug}
|
|
420
|
+
contentMap={contentMap}
|
|
421
|
+
onClose={onClose}
|
|
422
|
+
brandConfig={brandConfig!}
|
|
423
|
+
/>
|
|
424
|
+
);
|
|
425
|
+
};
|
|
426
|
+
|
|
397
427
|
export default KnownResourceForm;
|
|
@@ -125,6 +125,9 @@ const ogImage = storyFragment?.socialImagePath
|
|
|
125
125
|
const fullContentMap = await getFullContentMap(
|
|
126
126
|
Astro.locals.tenant?.id || import.meta.env.PUBLIC_TENANTID || 'default'
|
|
127
127
|
);
|
|
128
|
+
const description = fullContentMap.find(
|
|
129
|
+
(item) => item.id === storyFragmentID
|
|
130
|
+
)?.description;
|
|
128
131
|
const urlParams: Record<string, string | boolean> = {};
|
|
129
132
|
for (const [key, value] of Astro.url.searchParams) {
|
|
130
133
|
urlParams[key] = value === '' ? true : value;
|
|
@@ -133,6 +136,7 @@ for (const [key, value] of Astro.url.searchParams) {
|
|
|
133
136
|
|
|
134
137
|
<Layout
|
|
135
138
|
title={title}
|
|
139
|
+
description={description}
|
|
136
140
|
slug={slug}
|
|
137
141
|
canonicalURL={canonicalURL}
|
|
138
142
|
pubDatetime={pubDatetime}
|
|
@@ -219,5 +223,5 @@ for (const [key, value] of Astro.url.searchParams) {
|
|
|
219
223
|
|
|
220
224
|
<script>
|
|
221
225
|
import { setupLayoutObservers } from '@/utils/layout';
|
|
222
|
-
setupLayoutObservers
|
|
226
|
+
document.addEventListener('astro:page-load', setupLayoutObservers);
|
|
223
227
|
</script>
|
|
@@ -99,6 +99,9 @@ if (!fragmentsData) {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
const fullContentMap = await getFullContentMap(tenantId);
|
|
102
|
+
const description = fullContentMap.find(
|
|
103
|
+
(item) => item.id === storyfragmentId
|
|
104
|
+
)?.description;
|
|
102
105
|
const brandConfig = await getBrandConfig(tenantId);
|
|
103
106
|
|
|
104
107
|
if (!brandConfig.SITE_INIT) {
|
|
@@ -118,6 +121,7 @@ paneIds.forEach((paneId: string) => {
|
|
|
118
121
|
|
|
119
122
|
<Layout
|
|
120
123
|
title={storyfragmentTitle}
|
|
124
|
+
description={description}
|
|
121
125
|
slug={lookup || brandConfig.HOME_SLUG}
|
|
122
126
|
ogImage={ogImage}
|
|
123
127
|
menu={storyData.menu || null}
|
|
@@ -8,15 +8,41 @@ import {
|
|
|
8
8
|
import { debounce } from '@/utils/helpers';
|
|
9
9
|
|
|
10
10
|
let hasScrolledForSettingsPanel = false;
|
|
11
|
+
let currentPaneObserver: IntersectionObserver | null = null;
|
|
12
|
+
let settingsPanelSubscription: (() => void) | null = null;
|
|
13
|
+
let debouncedUpdateListener: (() => void) | null = null;
|
|
14
|
+
|
|
15
|
+
function cleanupLayoutObservers() {
|
|
16
|
+
if (currentPaneObserver) {
|
|
17
|
+
currentPaneObserver.disconnect();
|
|
18
|
+
currentPaneObserver = null;
|
|
19
|
+
}
|
|
20
|
+
if (settingsPanelSubscription) {
|
|
21
|
+
settingsPanelSubscription();
|
|
22
|
+
settingsPanelSubscription = null;
|
|
23
|
+
}
|
|
24
|
+
if (debouncedUpdateListener) {
|
|
25
|
+
window.removeEventListener('scroll', debouncedUpdateListener);
|
|
26
|
+
window.removeEventListener('resize', debouncedUpdateListener);
|
|
27
|
+
debouncedUpdateListener = null;
|
|
28
|
+
}
|
|
29
|
+
const storykeepHeader = document.getElementById('storykeepHeader');
|
|
30
|
+
if (storykeepHeader) {
|
|
31
|
+
document.body.style.paddingTop = '';
|
|
32
|
+
storykeepHeader.style.position = '';
|
|
33
|
+
storykeepHeader.style.top = '';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
11
36
|
|
|
12
|
-
// Replace your existing setupPaneObserver with this one.
|
|
13
37
|
function setupPaneObserver() {
|
|
14
|
-
|
|
38
|
+
if (currentPaneObserver) {
|
|
39
|
+
currentPaneObserver.disconnect();
|
|
40
|
+
}
|
|
15
41
|
|
|
16
|
-
settingsPanelStore.subscribe((signalValue) => {
|
|
17
|
-
if (
|
|
18
|
-
|
|
19
|
-
|
|
42
|
+
settingsPanelSubscription = settingsPanelStore.subscribe((signalValue) => {
|
|
43
|
+
if (currentPaneObserver) {
|
|
44
|
+
currentPaneObserver.disconnect();
|
|
45
|
+
currentPaneObserver = null;
|
|
20
46
|
}
|
|
21
47
|
|
|
22
48
|
if (signalValue && signalValue.nodeId) {
|
|
@@ -28,7 +54,7 @@ function setupPaneObserver() {
|
|
|
28
54
|
document.querySelector(`[data-node-id="${nodeId}"]`);
|
|
29
55
|
|
|
30
56
|
if (targetElement) {
|
|
31
|
-
|
|
57
|
+
currentPaneObserver = new IntersectionObserver(
|
|
32
58
|
([entry]) => {
|
|
33
59
|
const signal = settingsPanelStore.get();
|
|
34
60
|
const now = Date.now();
|
|
@@ -41,7 +67,7 @@ function setupPaneObserver() {
|
|
|
41
67
|
},
|
|
42
68
|
{ threshold: 0 }
|
|
43
69
|
);
|
|
44
|
-
|
|
70
|
+
currentPaneObserver.observe(targetElement);
|
|
45
71
|
}
|
|
46
72
|
}, 100);
|
|
47
73
|
}
|
|
@@ -49,6 +75,8 @@ function setupPaneObserver() {
|
|
|
49
75
|
}
|
|
50
76
|
|
|
51
77
|
export function setupLayoutObservers(): void {
|
|
78
|
+
cleanupLayoutObservers();
|
|
79
|
+
|
|
52
80
|
const storykeepHeader = document.getElementById('storykeepHeader');
|
|
53
81
|
const settingsControls = document.getElementById('settingsControls');
|
|
54
82
|
const standardHeader = document.querySelector('header');
|
|
@@ -86,7 +114,7 @@ export function setupLayoutObservers(): void {
|
|
|
86
114
|
}
|
|
87
115
|
};
|
|
88
116
|
|
|
89
|
-
|
|
117
|
+
debouncedUpdateListener = debounce(() => {
|
|
90
118
|
updateStandardHeaderHeight();
|
|
91
119
|
handleScroll();
|
|
92
120
|
updatePanelPosition();
|
|
@@ -98,8 +126,8 @@ export function setupLayoutObservers(): void {
|
|
|
98
126
|
}
|
|
99
127
|
};
|
|
100
128
|
|
|
101
|
-
window.addEventListener('scroll',
|
|
102
|
-
window.addEventListener('resize',
|
|
129
|
+
window.addEventListener('scroll', debouncedUpdateListener, { passive: true });
|
|
130
|
+
window.addEventListener('resize', debouncedUpdateListener);
|
|
103
131
|
settingsPanelOpenStore.subscribe(handleSettingsPanelChange);
|
|
104
132
|
|
|
105
133
|
setupPaneObserver();
|
|
@@ -128,3 +156,5 @@ export function handleSettingsPanelMobile(isOpen: boolean): void {
|
|
|
128
156
|
hasScrolledForSettingsPanel = false;
|
|
129
157
|
}
|
|
130
158
|
}
|
|
159
|
+
|
|
160
|
+
document.addEventListener('astro:before-swap', cleanupLayoutObservers);
|