astro-tractstack 2.0.0-rc.9 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +8 -97
- package/README.md +7 -5
- package/bin/create-tractstack.js +31 -8
- package/dist/index.js +106 -29
- package/package.json +10 -5
- package/templates/css/frontend.css +1 -1
- package/templates/custom/minimal/CodeHook.astro +13 -12
- package/templates/custom/minimal/CustomRoutes.astro +25 -31
- package/templates/custom/with-examples/CodeHook.astro +22 -11
- package/templates/custom/with-examples/CustomRoutes.astro +4 -8
- package/templates/custom/with-examples/ProductCard.astro +29 -0
- package/templates/custom/with-examples/ProductCardWrapper.astro +43 -0
- package/templates/custom/with-examples/ProductGrid.astro +64 -0
- package/templates/custom/with-examples/pages/Collections.astro +58 -98
- package/templates/gitignore +42 -0
- package/templates/prettierignore +5 -0
- package/templates/prettierrc +19 -0
- package/templates/src/client/app.js +127 -0
- package/templates/src/client/htmx.min.js +3519 -0
- package/templates/src/client/view.js +429 -0
- package/templates/src/components/Footer.astro +4 -9
- package/templates/src/components/Header.astro +67 -60
- package/templates/src/components/Menu.tsx +188 -52
- package/templates/src/components/codehooks/BunnyVideoSetup.tsx +2 -2
- package/templates/src/components/codehooks/EpinetDurationSelector.tsx +9 -13
- package/templates/src/components/codehooks/EpinetTableView.tsx +11 -7
- package/templates/src/components/codehooks/EpinetWrapper.tsx +1 -0
- package/templates/src/components/codehooks/FeaturedArticle.astro +105 -0
- package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +318 -0
- package/templates/src/components/codehooks/ListContent.astro +32 -162
- package/templates/src/components/codehooks/ListContentSetup.tsx +43 -138
- package/templates/src/components/codehooks/ProductCardSetup.tsx +152 -0
- package/templates/src/components/codehooks/ProductGridSetup.tsx +274 -0
- package/templates/src/components/codehooks/SearchWidget.tsx +453 -0
- package/templates/src/components/compositor/Node.tsx +3 -6
- package/templates/src/components/compositor/PanelVisibilityWrapper.tsx +21 -11
- package/templates/src/components/compositor/elements/BunnyVideo.tsx +21 -20
- package/templates/src/components/compositor/nodes/Pane.tsx +51 -21
- package/templates/src/components/compositor/nodes/RenderChildren.tsx +6 -1
- package/templates/src/components/compositor/nodes/Widget.tsx +16 -2
- package/templates/src/components/compositor/preview/FeaturedArticlePreview.tsx +155 -0
- package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +20 -1
- package/templates/src/components/edit/Header.tsx +10 -4
- package/templates/src/components/edit/PanelSwitch.tsx +11 -7
- package/templates/src/components/edit/SettingsPanel.tsx +29 -18
- package/templates/src/components/edit/ToolBar.tsx +1 -28
- package/templates/src/components/edit/ToolMode.tsx +45 -32
- package/templates/src/components/edit/pane/AddPanePanel_break.tsx +12 -2
- package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +8 -2
- package/templates/src/components/edit/pane/AddPanePanel_newAICopy_modal.tsx +1 -1
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +17 -27
- package/templates/src/components/edit/pane/PageGenSelector.tsx +16 -16
- package/templates/src/components/edit/pane/PageGenSpecial.tsx +26 -49
- package/templates/src/components/edit/pane/PageGen_preview.tsx +17 -2
- package/templates/src/components/edit/pane/PanePanel_path.tsx +2 -4
- package/templates/src/components/edit/pane/PanePanel_title.tsx +243 -76
- package/templates/src/components/edit/panels/StyleBreakPanel.tsx +17 -19
- package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +48 -37
- package/templates/src/components/edit/panels/StyleElementPanel_add.tsx +60 -55
- package/templates/src/components/edit/panels/StyleImagePanel_add.tsx +56 -50
- package/templates/src/components/edit/panels/StyleLiElementPanel_add.tsx +54 -47
- package/templates/src/components/edit/panels/StyleLinkPanel_add.tsx +54 -44
- package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +113 -138
- package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +54 -40
- package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +3 -3
- package/templates/src/components/edit/panels/StyleWidgetPanel_add.tsx +56 -49
- package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +14 -5
- package/templates/src/components/edit/state/SaveModal.tsx +316 -169
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +1 -1
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +56 -55
- package/templates/src/components/edit/widgets/BunnyWidget.tsx +538 -59
- package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +656 -0
- package/templates/src/components/edit/widgets/ToggleWidget.tsx +9 -16
- package/templates/src/components/fields/ArtpackImage.tsx +4 -1
- package/templates/src/components/fields/BackgroundImage.tsx +1 -1
- package/templates/src/components/fields/BackgroundImageWrapper.tsx +127 -35
- package/templates/src/components/fields/ColorPickerCombo.tsx +66 -62
- package/templates/src/components/fields/ImageUpload.tsx +1 -1
- package/templates/src/components/fields/ViewportComboBox.tsx +59 -42
- package/templates/src/components/form/ActionBuilderBeliefSelector.tsx +117 -0
- package/templates/src/components/form/ActionBuilderField.tsx +306 -87
- package/templates/src/components/search/SearchModal.tsx +420 -0
- package/templates/src/components/search/SearchResults.tsx +367 -0
- package/templates/src/components/search/SearchWrapper.tsx +46 -0
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +1 -1
- package/templates/src/components/storykeep/Dashboard_Analytics.tsx +34 -8
- package/templates/src/components/storykeep/Dashboard_Branding.tsx +1 -1
- package/templates/src/components/storykeep/Dashboard_Content.tsx +6 -0
- package/templates/src/components/storykeep/StoryKeepBackdrop.astro +87 -0
- package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +38 -34
- package/templates/src/components/storykeep/controls/content/KnownResourceForm.tsx +1 -1
- package/templates/src/components/storykeep/controls/content/MenuForm.tsx +56 -8
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +18 -3
- package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +5 -8
- package/templates/src/components/storykeep/state/FetchAnalytics.tsx +274 -228
- package/templates/src/components/storykeep/widgets/Wizard.tsx +14 -7
- package/templates/src/components/widgets/ImpressionWrapper.tsx +0 -1
- package/templates/src/constants/shapes.ts +9 -0
- package/templates/src/constants.ts +2121 -16
- package/templates/src/hooks/useSearch.ts +228 -0
- package/templates/src/layouts/Layout.astro +213 -104
- package/templates/src/lib/storyData.ts +4 -1
- package/templates/src/pages/[...slug]/edit.astro +14 -14
- package/templates/src/pages/[...slug].astro +82 -21
- package/templates/src/pages/api/orphan-analysis.ts +0 -1
- package/templates/src/pages/api/tailwind.ts +23 -21
- package/templates/src/pages/context/[...contextSlug]/edit.astro +14 -14
- package/templates/src/pages/context/[...contextSlug].astro +7 -2
- package/templates/src/pages/storykeep/advanced.astro +5 -4
- package/templates/src/pages/storykeep/branding.astro +5 -4
- package/templates/src/pages/storykeep/content.astro +5 -4
- package/templates/src/pages/storykeep/init.astro +40 -1
- package/templates/src/pages/storykeep/login.astro +1 -1
- package/templates/src/pages/storykeep.astro +5 -4
- package/templates/src/stores/nodes.ts +59 -88
- package/templates/src/stores/orphanAnalysis.ts +19 -21
- package/templates/src/stores/storykeep.ts +7 -0
- package/templates/src/types/compositorTypes.ts +6 -0
- package/templates/src/types/tractstack.ts +17 -0
- package/templates/src/utils/actions/lispLexer.ts +2 -2
- package/templates/src/utils/actions/preParse_Action.ts +3 -0
- package/templates/src/utils/api/beliefHelpers.ts +12 -36
- package/templates/src/utils/api/menuHelpers.ts +2 -2
- package/templates/src/utils/api.ts +26 -0
- package/templates/src/utils/compositor/TemplateNodes.ts +7 -0
- package/templates/src/utils/compositor/allowInsert.ts +5 -3
- package/templates/src/utils/compositor/nodesHelper.ts +4 -0
- package/templates/src/utils/compositor/processMarkdown.ts +16 -2
- package/templates/src/utils/compositor/reduceNodesClassNames.ts +4 -0
- package/templates/src/utils/compositor/templateMarkdownStyles.ts +13 -13
- package/templates/src/utils/compositor/typeGuards.ts +1 -0
- package/templates/src/utils/customHelpers.ts +38 -0
- package/templates/src/utils/helpers.ts +2 -2
- package/templates/src/utils/layout.ts +65 -144
- package/utils/inject-files.ts +95 -18
- package/templates/src/client/analytics-events.js +0 -207
- package/templates/src/client/belief-events.js +0 -191
- package/templates/src/client/sse.js +0 -613
- package/templates/src/components/codehooks/FeaturedContent.astro +0 -273
- package/templates/src/components/codehooks/FeaturedContentSetup.tsx +0 -738
- package/templates/src/components/compositor/preview/FeaturedContentPreview.tsx +0 -128
- package/templates/src/components/edit/pane/PanePanel_slug.tsx +0 -219
|
@@ -1,13 +1,23 @@
|
|
|
1
|
-
import { useEffect, useState, memo, type CSSProperties } from 'react';
|
|
1
|
+
import { useEffect, useState, memo, Fragment, type CSSProperties } from 'react';
|
|
2
2
|
import { getCtx } from '@/stores/nodes';
|
|
3
3
|
import { viewportKeyStore } from '@/stores/storykeep';
|
|
4
4
|
import { RenderChildren } from './RenderChildren';
|
|
5
|
-
import
|
|
5
|
+
import FeaturedArticleSetup from '@/components/codehooks/FeaturedArticleSetup';
|
|
6
6
|
import ListContentSetup from '@/components/codehooks/ListContentSetup';
|
|
7
7
|
import BunnyVideoSetup from '@/components/codehooks/BunnyVideoSetup';
|
|
8
|
+
import { ProductCardSetup } from '@/components/codehooks/ProductCardSetup';
|
|
9
|
+
import { ProductGridSetup } from '@/components/codehooks/ProductGridSetup';
|
|
8
10
|
import type { BgImageNode, ArtpackImageNode } from '@/types/compositorTypes';
|
|
9
11
|
import type { NodeProps } from '@/types/nodeProps';
|
|
10
12
|
|
|
13
|
+
const TARGETS = [
|
|
14
|
+
'list-content',
|
|
15
|
+
'featured-article',
|
|
16
|
+
'bunny-video',
|
|
17
|
+
'product-card',
|
|
18
|
+
'product-grid',
|
|
19
|
+
];
|
|
20
|
+
|
|
11
21
|
const CodeHookContainer = ({
|
|
12
22
|
payload,
|
|
13
23
|
}: {
|
|
@@ -27,12 +37,19 @@ const CodeHookContainer = ({
|
|
|
27
37
|
{Object.entries(payload.params).map(
|
|
28
38
|
([key, value]) =>
|
|
29
39
|
value && (
|
|
30
|
-
<
|
|
40
|
+
<Fragment key={key}>
|
|
31
41
|
<span className="min-w-24 font-bold text-gray-600">{key}:</span>
|
|
32
|
-
<
|
|
33
|
-
{
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
<div className="ml-2 flex flex-wrap gap-1">
|
|
43
|
+
{value.split(/,|\|/).map((item, index) => (
|
|
44
|
+
<span
|
|
45
|
+
key={index}
|
|
46
|
+
className="inline-block rounded bg-gray-200 px-2 py-0.5 text-xs text-gray-800"
|
|
47
|
+
>
|
|
48
|
+
{item}
|
|
49
|
+
</span>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
</Fragment>
|
|
36
53
|
)
|
|
37
54
|
)}
|
|
38
55
|
</div>
|
|
@@ -148,15 +165,36 @@ const Pane = memo(
|
|
|
148
165
|
id={getCtx(props).getNodeSlug(props.nodeId)}
|
|
149
166
|
className={useFlexLayout ? '' : wrapperClasses}
|
|
150
167
|
>
|
|
151
|
-
{codeHookPayload && codeHookTarget === '
|
|
152
|
-
<
|
|
168
|
+
{codeHookPayload && codeHookTarget === 'product-card' ? (
|
|
169
|
+
<ProductCardSetup
|
|
170
|
+
nodeId={props.nodeId}
|
|
171
|
+
params={codeHookParams}
|
|
172
|
+
config={props.config!}
|
|
173
|
+
/>
|
|
174
|
+
) : codeHookPayload && codeHookTarget === 'product-grid' ? (
|
|
175
|
+
<ProductGridSetup
|
|
176
|
+
nodeId={props.nodeId}
|
|
177
|
+
params={codeHookParams}
|
|
178
|
+
config={props.config!}
|
|
179
|
+
/>
|
|
180
|
+
) : codeHookPayload && codeHookTarget === 'featured-article' ? (
|
|
181
|
+
<FeaturedArticleSetup
|
|
153
182
|
nodeId={props.nodeId}
|
|
154
183
|
params={codeHookParams}
|
|
184
|
+
config={props.config!}
|
|
155
185
|
/>
|
|
156
186
|
) : codeHookPayload && codeHookTarget === 'list-content' ? (
|
|
157
|
-
<ListContentSetup
|
|
187
|
+
<ListContentSetup
|
|
188
|
+
nodeId={props.nodeId}
|
|
189
|
+
params={codeHookParams}
|
|
190
|
+
config={props.config!}
|
|
191
|
+
/>
|
|
158
192
|
) : codeHookPayload && codeHookTarget === 'bunny-video' ? (
|
|
159
|
-
<BunnyVideoSetup
|
|
193
|
+
<BunnyVideoSetup
|
|
194
|
+
nodeId={props.nodeId}
|
|
195
|
+
params={codeHookParams}
|
|
196
|
+
config={props.config!}
|
|
197
|
+
/>
|
|
160
198
|
) : codeHookPayload && codeHookTarget ? (
|
|
161
199
|
<CodeHookContainer
|
|
162
200
|
payload={{ target: codeHookTarget, params: codeHookParams }}
|
|
@@ -209,11 +247,7 @@ const Pane = memo(
|
|
|
209
247
|
!(
|
|
210
248
|
codeHookPayload &&
|
|
211
249
|
typeof codeHookTarget === 'string' &&
|
|
212
|
-
|
|
213
|
-
'list-content',
|
|
214
|
-
'featured-content',
|
|
215
|
-
'bunny-video',
|
|
216
|
-
].includes(codeHookTarget)
|
|
250
|
+
TARGETS.includes(codeHookTarget)
|
|
217
251
|
)
|
|
218
252
|
)
|
|
219
253
|
getCtx(props).setClickedNodeId(props.nodeId, true);
|
|
@@ -239,11 +273,7 @@ const Pane = memo(
|
|
|
239
273
|
!(
|
|
240
274
|
codeHookPayload &&
|
|
241
275
|
typeof codeHookTarget === 'string' &&
|
|
242
|
-
|
|
243
|
-
'list-content',
|
|
244
|
-
'featured-content',
|
|
245
|
-
'bunny-video',
|
|
246
|
-
].includes(codeHookTarget)
|
|
276
|
+
TARGETS.includes(codeHookTarget)
|
|
247
277
|
)
|
|
248
278
|
)
|
|
249
279
|
getCtx(props).setClickedNodeId(props.nodeId, true);
|
|
@@ -12,7 +12,12 @@ export const RenderChildren = (props: RenderChildrenProps) => {
|
|
|
12
12
|
return (
|
|
13
13
|
<>
|
|
14
14
|
{children.map((id: string) => (
|
|
15
|
-
<Node
|
|
15
|
+
<Node
|
|
16
|
+
nodeId={id}
|
|
17
|
+
key={id}
|
|
18
|
+
ctx={nodeProps.ctx}
|
|
19
|
+
config={nodeProps.config}
|
|
20
|
+
/>
|
|
16
21
|
))}
|
|
17
22
|
</>
|
|
18
23
|
);
|
|
@@ -79,12 +79,26 @@ const getWidgetElement = (
|
|
|
79
79
|
case 'bunny':
|
|
80
80
|
return value1 ? (
|
|
81
81
|
<div className={`${classNames} pointer-events-none`}>
|
|
82
|
-
<BunnyVideo
|
|
82
|
+
<BunnyVideo videoId={value1} title={value2 || 'Bunny Video'} />
|
|
83
83
|
</div>
|
|
84
84
|
) : null;
|
|
85
85
|
|
|
86
|
+
case 'interactiveDisclosure':
|
|
87
|
+
return (
|
|
88
|
+
<div className={`${classNames} pointer-events-none`}>
|
|
89
|
+
<div className="rounded-md border-2 border-dashed border-gray-300 bg-gray-50 p-4 text-center">
|
|
90
|
+
<p className="text-sm font-bold text-gray-700">
|
|
91
|
+
Interactive Disclosure
|
|
92
|
+
</p>
|
|
93
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
94
|
+
Belief Trigger: <code className="font-bold">{value1}</code>
|
|
95
|
+
</p>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
|
|
86
100
|
default:
|
|
87
|
-
return
|
|
101
|
+
return <div>Widget {hook} not found.</div>;
|
|
88
102
|
}
|
|
89
103
|
};
|
|
90
104
|
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import type { FullContentMapItem } from '@/types/tractstack';
|
|
3
|
+
import {
|
|
4
|
+
PaneSnapshotGenerator,
|
|
5
|
+
type SnapshotData,
|
|
6
|
+
} from '@/components/compositor/preview/PaneSnapshotGenerator';
|
|
7
|
+
|
|
8
|
+
interface FeaturedArticlePreviewProps {
|
|
9
|
+
story: FullContentMapItem;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const PREVIEW_TIMEOUT = 10000; // 10 seconds
|
|
13
|
+
|
|
14
|
+
const FeaturedArticlePreview = ({ story }: FeaturedArticlePreviewProps) => {
|
|
15
|
+
const [htmlFragment, setHtmlFragment] = useState<string | null>(null);
|
|
16
|
+
const [snapshot, setSnapshot] = useState<SnapshotData | null>(null);
|
|
17
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
let isCancelled = false;
|
|
21
|
+
setSnapshot(null);
|
|
22
|
+
setIsLoading(true);
|
|
23
|
+
setHtmlFragment(null); // Reset HTML fragment as well
|
|
24
|
+
console.log(
|
|
25
|
+
`[Preview DEBUG] Starting generation for story: "${story.title}"`
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const timeoutId = setTimeout(() => {
|
|
29
|
+
if (!isCancelled) {
|
|
30
|
+
console.error('[Preview DEBUG] Generation timed out after 10 seconds.');
|
|
31
|
+
setIsLoading(false);
|
|
32
|
+
}
|
|
33
|
+
}, PREVIEW_TIMEOUT);
|
|
34
|
+
|
|
35
|
+
const fetchHtml = async () => {
|
|
36
|
+
try {
|
|
37
|
+
if (!story.panes || story.panes.length === 0) {
|
|
38
|
+
throw new Error('Story has no panes to generate a preview from.');
|
|
39
|
+
}
|
|
40
|
+
const paneIdsToFetch = story.panes.slice(0, 2);
|
|
41
|
+
console.log(
|
|
42
|
+
`[Preview DEBUG] 1. Fetching HTML for panes:`,
|
|
43
|
+
paneIdsToFetch
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const goBackend =
|
|
47
|
+
import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
48
|
+
const response = await fetch(`${goBackend}/api/v1/fragments/panes`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: {
|
|
51
|
+
'Content-Type': 'application/json',
|
|
52
|
+
'X-Tenant-ID': window.TRACTSTACK_CONFIG?.tenantId || 'default',
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify({ paneIds: paneIdsToFetch }),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (isCancelled) return;
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
throw new Error(`API fetch failed with status: ${response.status}`);
|
|
60
|
+
}
|
|
61
|
+
const data = await response.json();
|
|
62
|
+
const combinedHtml = paneIdsToFetch
|
|
63
|
+
.map((id) => data.fragments?.[id] || '')
|
|
64
|
+
.join('');
|
|
65
|
+
if (!combinedHtml.trim()) {
|
|
66
|
+
throw new Error('API returned no HTML content for the panes.');
|
|
67
|
+
}
|
|
68
|
+
console.log(
|
|
69
|
+
`[Preview DEBUG] 2. HTML received (length: ${combinedHtml.length}). Will now render snapshot generator.`
|
|
70
|
+
);
|
|
71
|
+
if (!isCancelled) {
|
|
72
|
+
setHtmlFragment(combinedHtml);
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
if (!isCancelled) {
|
|
76
|
+
const errorMessage = e instanceof Error ? e.message : 'Unknown error';
|
|
77
|
+
console.error(
|
|
78
|
+
'[Preview DEBUG] An error occurred while fetching HTML:',
|
|
79
|
+
errorMessage
|
|
80
|
+
);
|
|
81
|
+
setIsLoading(false);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
fetchHtml();
|
|
87
|
+
|
|
88
|
+
return () => {
|
|
89
|
+
isCancelled = true;
|
|
90
|
+
clearTimeout(timeoutId);
|
|
91
|
+
};
|
|
92
|
+
}, [story.id]);
|
|
93
|
+
|
|
94
|
+
const handleSnapshotComplete = (id: string, data: SnapshotData) => {
|
|
95
|
+
console.log('[Preview DEBUG] 3. Snapshot generation successful.');
|
|
96
|
+
setSnapshot(data);
|
|
97
|
+
setIsLoading(false);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleSnapshotError = (id: string, err: string) => {
|
|
101
|
+
console.error(
|
|
102
|
+
'[Preview DEBUG] An error occurred during snapshot generation:',
|
|
103
|
+
err
|
|
104
|
+
);
|
|
105
|
+
setIsLoading(false);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<>
|
|
110
|
+
{/* This is the invisible worker component. It only renders when its input is ready. */}
|
|
111
|
+
{htmlFragment && isLoading && (
|
|
112
|
+
<PaneSnapshotGenerator
|
|
113
|
+
id={`live-preview-${story.id}`}
|
|
114
|
+
htmlString={htmlFragment}
|
|
115
|
+
outputWidth={600}
|
|
116
|
+
onComplete={handleSnapshotComplete}
|
|
117
|
+
onError={handleSnapshotError}
|
|
118
|
+
/>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
{/* This is the visible UI that reacts to state changes. */}
|
|
122
|
+
{isLoading && (
|
|
123
|
+
<div className="flex aspect-[4/3] w-full items-center justify-center rounded-lg bg-gray-200">
|
|
124
|
+
<div className="h-12 w-12 animate-spin rounded-full border-4 border-solid border-cyan-600 border-t-transparent"></div>
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
{!isLoading && snapshot && (
|
|
129
|
+
<img
|
|
130
|
+
src={snapshot.imageData}
|
|
131
|
+
alt={`Live preview of ${story.title}`}
|
|
132
|
+
className="h-auto w-full rounded-lg object-cover shadow-lg"
|
|
133
|
+
/>
|
|
134
|
+
)}
|
|
135
|
+
|
|
136
|
+
{!isLoading && !snapshot && (
|
|
137
|
+
<>
|
|
138
|
+
{story.thumbSrc ? (
|
|
139
|
+
<img
|
|
140
|
+
src={story.thumbSrc}
|
|
141
|
+
alt={`Preview of ${story.title}`}
|
|
142
|
+
className="h-auto w-full rounded-lg object-cover shadow-lg"
|
|
143
|
+
/>
|
|
144
|
+
) : (
|
|
145
|
+
<div className="flex aspect-[4/3] w-full items-center justify-center rounded-lg bg-red-50 p-4 text-center text-sm text-red-700">
|
|
146
|
+
Could not display preview.
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</>
|
|
150
|
+
)}
|
|
151
|
+
</>
|
|
152
|
+
);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export default FeaturedArticlePreview;
|
|
@@ -18,6 +18,16 @@ export interface PaneSnapshotGeneratorProps {
|
|
|
18
18
|
|
|
19
19
|
const snapshotCache = new Map<string, SnapshotData>();
|
|
20
20
|
|
|
21
|
+
function hashString(str: string): string {
|
|
22
|
+
let hash = 0;
|
|
23
|
+
for (let i = 0; i < str.length; i++) {
|
|
24
|
+
const char = str.charCodeAt(i);
|
|
25
|
+
hash = (hash << 5) - hash + char;
|
|
26
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
27
|
+
}
|
|
28
|
+
return hash.toString(36);
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
export const PaneSnapshotGenerator = ({
|
|
22
32
|
id,
|
|
23
33
|
htmlString,
|
|
@@ -31,7 +41,7 @@ export const PaneSnapshotGenerator = ({
|
|
|
31
41
|
useEffect(() => {
|
|
32
42
|
if (!htmlString || isGenerating) return;
|
|
33
43
|
|
|
34
|
-
const cacheKey = `${id}-${htmlString
|
|
44
|
+
const cacheKey = `${id}-${hashString(htmlString)}-${outputWidth}`;
|
|
35
45
|
if (snapshotCache.has(cacheKey)) {
|
|
36
46
|
const cached = snapshotCache.get(cacheKey)!;
|
|
37
47
|
onComplete(id, cached);
|
|
@@ -195,5 +205,14 @@ export const PaneSnapshotGenerator = ({
|
|
|
195
205
|
generateSnapshot();
|
|
196
206
|
}, [id, htmlString, isGenerating, onComplete, onError, config, outputWidth]);
|
|
197
207
|
|
|
208
|
+
// Show spinner while generating
|
|
209
|
+
if (isGenerating) {
|
|
210
|
+
return (
|
|
211
|
+
<div className="flex h-24 items-center justify-center">
|
|
212
|
+
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-cyan-600"></div>
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
198
217
|
return null;
|
|
199
218
|
};
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
viewportModeStore,
|
|
14
14
|
setViewportMode,
|
|
15
15
|
settingsPanelStore,
|
|
16
|
+
pendingHomePageSlugStore,
|
|
16
17
|
} from '@/stores/storykeep';
|
|
17
18
|
import { getCtx, ROOT_NODE_NAME } from '@/stores/nodes';
|
|
18
19
|
import SaveModal from '@/components/edit/state/SaveModal';
|
|
@@ -24,6 +25,7 @@ interface StoryKeepHeaderProps {
|
|
|
24
25
|
|
|
25
26
|
const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
|
|
26
27
|
const viewport = useStore(viewportModeStore);
|
|
28
|
+
const pendingHomePageSlug = useStore(pendingHomePageSlugStore);
|
|
27
29
|
const ctx = getCtx();
|
|
28
30
|
const hasTitle = useStore(ctx.hasTitle);
|
|
29
31
|
const hasPanes = useStore(ctx.hasPanes);
|
|
@@ -61,7 +63,8 @@ const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
|
|
|
61
63
|
};
|
|
62
64
|
|
|
63
65
|
const handleVisitPage = () => {
|
|
64
|
-
|
|
66
|
+
const hasChanges = canUndo || pendingHomePageSlug;
|
|
67
|
+
if (hasChanges) {
|
|
65
68
|
if (
|
|
66
69
|
confirm(
|
|
67
70
|
'You have unsaved changes. Do you want to visit the page anyway?'
|
|
@@ -89,6 +92,9 @@ const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
|
|
|
89
92
|
{ value: 'desktop', Icon: ComputerDesktopIcon, title: 'Desktop Viewport' },
|
|
90
93
|
];
|
|
91
94
|
|
|
95
|
+
// Show save button if there are undo changes OR pending home page change
|
|
96
|
+
const shouldShowSave = canUndo || pendingHomePageSlug;
|
|
97
|
+
|
|
92
98
|
if (!hasTitle && !hasPanes) return null;
|
|
93
99
|
|
|
94
100
|
return (
|
|
@@ -96,7 +102,7 @@ const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
|
|
|
96
102
|
<div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-2 p-2">
|
|
97
103
|
{/* Viewport Section with stacked label */}
|
|
98
104
|
<div className="flex flex-col items-center">
|
|
99
|
-
<span className="text-xs font-
|
|
105
|
+
<span className="text-xs font-bold text-gray-600">Viewport:</span>
|
|
100
106
|
<span className="text-xs text-gray-700">{viewport}</span>
|
|
101
107
|
</div>
|
|
102
108
|
|
|
@@ -127,7 +133,7 @@ const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
|
|
|
127
133
|
className={`${iconClassName} relative`}
|
|
128
134
|
>
|
|
129
135
|
<GlobeAltIcon />
|
|
130
|
-
{
|
|
136
|
+
{shouldShowSave && (
|
|
131
137
|
<ExclamationTriangleIcon className="absolute -right-1 -top-1 h-3 w-3 rounded-full bg-white text-amber-500" />
|
|
132
138
|
)}
|
|
133
139
|
</button>
|
|
@@ -156,7 +162,7 @@ const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
|
|
|
156
162
|
</div>
|
|
157
163
|
)}
|
|
158
164
|
|
|
159
|
-
{
|
|
165
|
+
{shouldShowSave && (
|
|
160
166
|
<div className="flex flex-wrap items-center justify-center gap-2 text-sm">
|
|
161
167
|
<button
|
|
162
168
|
onClick={handleSave}
|
|
@@ -173,10 +173,12 @@ const PanelSwitch = ({
|
|
|
173
173
|
case 'style-link-remove':
|
|
174
174
|
case 'style-link-remove-hover':
|
|
175
175
|
if (clickedNode && signal.className)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
176
|
+
return (
|
|
177
|
+
<StyleLinkRemovePanel
|
|
178
|
+
node={clickedNode}
|
|
179
|
+
className={signal.className}
|
|
180
|
+
/>
|
|
181
|
+
);
|
|
180
182
|
break;
|
|
181
183
|
|
|
182
184
|
case 'style-link-config':
|
|
@@ -311,17 +313,19 @@ const PanelSwitch = ({
|
|
|
311
313
|
break;
|
|
312
314
|
|
|
313
315
|
case 'style-code-config':
|
|
314
|
-
if (clickedNode)
|
|
316
|
+
if (clickedNode)
|
|
317
|
+
return <StyleWidgetConfigPanel node={clickedNode} config={config} />;
|
|
318
|
+
break;
|
|
315
319
|
|
|
316
320
|
case 'style-code-add':
|
|
317
321
|
case 'style-code-container-add':
|
|
318
322
|
case 'style-code-outer-add':
|
|
319
|
-
if (clickedNode && markdownNode
|
|
323
|
+
if (clickedNode && markdownNode)
|
|
320
324
|
return (
|
|
321
325
|
<StyleWidgetAddPanel
|
|
322
326
|
node={clickedNode}
|
|
323
327
|
parentNode={markdownNode}
|
|
324
|
-
childId={signal
|
|
328
|
+
childId={signal?.childId}
|
|
325
329
|
/>
|
|
326
330
|
);
|
|
327
331
|
break;
|
|
@@ -17,19 +17,23 @@ const SettingsPanel = ({ config, availableCodeHooks }: SettingsPanelProps) => {
|
|
|
17
17
|
const ctx = getCtx();
|
|
18
18
|
const { value: toolModeVal } = useStore(ctx.toolModeValStore);
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
const isLinkInsertSignal = signal?.action === 'style-link';
|
|
21
|
+
const shouldShow =
|
|
22
|
+
toolModeVal === 'styles' || (toolModeVal === 'text' && isLinkInsertSignal);
|
|
23
|
+
|
|
24
|
+
if (!shouldShow || !signal) {
|
|
21
25
|
return null;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
return (
|
|
25
29
|
<div
|
|
26
|
-
className="bg-mydarkgrey rounded-xl bg-opacity-20 p-0.5 backdrop-blur-sm"
|
|
30
|
+
className="bg-mydarkgrey min-w-xs flex h-full max-w-sm flex-col rounded-xl bg-opacity-20 p-0.5 backdrop-blur-sm"
|
|
27
31
|
style={
|
|
28
32
|
{
|
|
29
33
|
animation: window.matchMedia(
|
|
30
34
|
'(prefers-reduced-motion: no-preference)'
|
|
31
35
|
).matches
|
|
32
|
-
? 'fadeInFromHalf
|
|
36
|
+
? 'fadeInFromHalf 450ms ease-in'
|
|
33
37
|
: 'none',
|
|
34
38
|
'--fade-start': '0.5',
|
|
35
39
|
'--fade-end': '1',
|
|
@@ -37,23 +41,30 @@ const SettingsPanel = ({ config, availableCodeHooks }: SettingsPanelProps) => {
|
|
|
37
41
|
}
|
|
38
42
|
>
|
|
39
43
|
<style>{`
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<div
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
>
|
|
52
|
-
<
|
|
53
|
-
|
|
44
|
+
@keyframes fadeInFromHalf {
|
|
45
|
+
0% { opacity: var(--fade-start, 0.5); }
|
|
46
|
+
100% { opacity: var(--fade-end, 1); }
|
|
47
|
+
}
|
|
48
|
+
`}</style>
|
|
49
|
+
<div
|
|
50
|
+
className="flex h-full min-h-0 w-full flex-col rounded-lg border border-gray-200 bg-white bg-opacity-85 shadow-xl"
|
|
51
|
+
style={{ maxWidth: '90vw' }}
|
|
52
|
+
>
|
|
53
|
+
{/* Header Section (fixed height) */}
|
|
54
|
+
<div className="flex-shrink-0 p-1.5 md:p-2.5">
|
|
55
|
+
<div className="mb-4 flex items-center justify-between">
|
|
56
|
+
<h3 className="text-myblue text-lg font-bold">{panelTitle}</h3>
|
|
57
|
+
<button
|
|
58
|
+
onClick={() => settingsPanelStore.set(null)}
|
|
59
|
+
className="hover:text-myblue text-gray-500"
|
|
60
|
+
>
|
|
61
|
+
<XMarkIcon className="h-5 w-5" />
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
54
64
|
</div>
|
|
55
65
|
|
|
56
|
-
|
|
66
|
+
{/* Scrollable Content Section */}
|
|
67
|
+
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto p-1.5 pt-0 md:p-2.5 md:pt-0">
|
|
57
68
|
<div className="rounded bg-gray-50 p-1.5 md:p-2.5">
|
|
58
69
|
<PanelSwitch
|
|
59
70
|
config={config}
|
|
@@ -2,37 +2,10 @@ import { useStore } from '@nanostores/react';
|
|
|
2
2
|
import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
|
|
3
3
|
import { getCtx } from '@/stores/nodes';
|
|
4
4
|
import { toggleSettingsPanel } from '@/stores/storykeep';
|
|
5
|
+
import { toolAddModeTitles, toolAddModes } from '@/constants';
|
|
5
6
|
|
|
6
7
|
import type { ToolAddMode } from '@/types/compositorTypes';
|
|
7
8
|
|
|
8
|
-
const toolAddModeTitles: Record<ToolAddMode, string> = {
|
|
9
|
-
p: 'Paragraph',
|
|
10
|
-
h2: 'Heading 2',
|
|
11
|
-
h3: 'Heading 3',
|
|
12
|
-
h4: 'Heading 4',
|
|
13
|
-
img: 'Image',
|
|
14
|
-
signup: 'Email Sign-up Widget',
|
|
15
|
-
yt: 'YouTube Video',
|
|
16
|
-
bunny: 'Bunny Video',
|
|
17
|
-
belief: 'Belief Select',
|
|
18
|
-
identify: 'Identity As',
|
|
19
|
-
toggle: 'Toggle Belief',
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const toolAddModes: ToolAddMode[] = [
|
|
23
|
-
'p',
|
|
24
|
-
'h2',
|
|
25
|
-
'h3',
|
|
26
|
-
'h4',
|
|
27
|
-
'img',
|
|
28
|
-
'signup',
|
|
29
|
-
'yt',
|
|
30
|
-
'bunny',
|
|
31
|
-
'belief',
|
|
32
|
-
'identify',
|
|
33
|
-
'toggle',
|
|
34
|
-
];
|
|
35
|
-
|
|
36
9
|
const AddElementsPanel = ({
|
|
37
10
|
currentToolAddMode,
|
|
38
11
|
}: {
|