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.
Files changed (142) hide show
  1. package/LICENSE +8 -97
  2. package/README.md +7 -5
  3. package/bin/create-tractstack.js +31 -8
  4. package/dist/index.js +106 -29
  5. package/package.json +10 -5
  6. package/templates/css/frontend.css +1 -1
  7. package/templates/custom/minimal/CodeHook.astro +13 -12
  8. package/templates/custom/minimal/CustomRoutes.astro +25 -31
  9. package/templates/custom/with-examples/CodeHook.astro +22 -11
  10. package/templates/custom/with-examples/CustomRoutes.astro +4 -8
  11. package/templates/custom/with-examples/ProductCard.astro +29 -0
  12. package/templates/custom/with-examples/ProductCardWrapper.astro +43 -0
  13. package/templates/custom/with-examples/ProductGrid.astro +64 -0
  14. package/templates/custom/with-examples/pages/Collections.astro +58 -98
  15. package/templates/gitignore +42 -0
  16. package/templates/prettierignore +5 -0
  17. package/templates/prettierrc +19 -0
  18. package/templates/src/client/app.js +127 -0
  19. package/templates/src/client/htmx.min.js +3519 -0
  20. package/templates/src/client/view.js +429 -0
  21. package/templates/src/components/Footer.astro +4 -9
  22. package/templates/src/components/Header.astro +67 -60
  23. package/templates/src/components/Menu.tsx +188 -52
  24. package/templates/src/components/codehooks/BunnyVideoSetup.tsx +2 -2
  25. package/templates/src/components/codehooks/EpinetDurationSelector.tsx +9 -13
  26. package/templates/src/components/codehooks/EpinetTableView.tsx +11 -7
  27. package/templates/src/components/codehooks/EpinetWrapper.tsx +1 -0
  28. package/templates/src/components/codehooks/FeaturedArticle.astro +105 -0
  29. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +318 -0
  30. package/templates/src/components/codehooks/ListContent.astro +32 -162
  31. package/templates/src/components/codehooks/ListContentSetup.tsx +43 -138
  32. package/templates/src/components/codehooks/ProductCardSetup.tsx +152 -0
  33. package/templates/src/components/codehooks/ProductGridSetup.tsx +274 -0
  34. package/templates/src/components/codehooks/SearchWidget.tsx +453 -0
  35. package/templates/src/components/compositor/Node.tsx +3 -6
  36. package/templates/src/components/compositor/PanelVisibilityWrapper.tsx +21 -11
  37. package/templates/src/components/compositor/elements/BunnyVideo.tsx +21 -20
  38. package/templates/src/components/compositor/nodes/Pane.tsx +51 -21
  39. package/templates/src/components/compositor/nodes/RenderChildren.tsx +6 -1
  40. package/templates/src/components/compositor/nodes/Widget.tsx +16 -2
  41. package/templates/src/components/compositor/preview/FeaturedArticlePreview.tsx +155 -0
  42. package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +20 -1
  43. package/templates/src/components/edit/Header.tsx +10 -4
  44. package/templates/src/components/edit/PanelSwitch.tsx +11 -7
  45. package/templates/src/components/edit/SettingsPanel.tsx +29 -18
  46. package/templates/src/components/edit/ToolBar.tsx +1 -28
  47. package/templates/src/components/edit/ToolMode.tsx +45 -32
  48. package/templates/src/components/edit/pane/AddPanePanel_break.tsx +12 -2
  49. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +8 -2
  50. package/templates/src/components/edit/pane/AddPanePanel_newAICopy_modal.tsx +1 -1
  51. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +17 -27
  52. package/templates/src/components/edit/pane/PageGenSelector.tsx +16 -16
  53. package/templates/src/components/edit/pane/PageGenSpecial.tsx +26 -49
  54. package/templates/src/components/edit/pane/PageGen_preview.tsx +17 -2
  55. package/templates/src/components/edit/pane/PanePanel_path.tsx +2 -4
  56. package/templates/src/components/edit/pane/PanePanel_title.tsx +243 -76
  57. package/templates/src/components/edit/panels/StyleBreakPanel.tsx +17 -19
  58. package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +48 -37
  59. package/templates/src/components/edit/panels/StyleElementPanel_add.tsx +60 -55
  60. package/templates/src/components/edit/panels/StyleImagePanel_add.tsx +56 -50
  61. package/templates/src/components/edit/panels/StyleLiElementPanel_add.tsx +54 -47
  62. package/templates/src/components/edit/panels/StyleLinkPanel_add.tsx +54 -44
  63. package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +113 -138
  64. package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +54 -40
  65. package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +3 -3
  66. package/templates/src/components/edit/panels/StyleWidgetPanel_add.tsx +56 -49
  67. package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +14 -5
  68. package/templates/src/components/edit/state/SaveModal.tsx +316 -169
  69. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +1 -1
  70. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +56 -55
  71. package/templates/src/components/edit/widgets/BunnyWidget.tsx +538 -59
  72. package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +656 -0
  73. package/templates/src/components/edit/widgets/ToggleWidget.tsx +9 -16
  74. package/templates/src/components/fields/ArtpackImage.tsx +4 -1
  75. package/templates/src/components/fields/BackgroundImage.tsx +1 -1
  76. package/templates/src/components/fields/BackgroundImageWrapper.tsx +127 -35
  77. package/templates/src/components/fields/ColorPickerCombo.tsx +66 -62
  78. package/templates/src/components/fields/ImageUpload.tsx +1 -1
  79. package/templates/src/components/fields/ViewportComboBox.tsx +59 -42
  80. package/templates/src/components/form/ActionBuilderBeliefSelector.tsx +117 -0
  81. package/templates/src/components/form/ActionBuilderField.tsx +306 -87
  82. package/templates/src/components/search/SearchModal.tsx +420 -0
  83. package/templates/src/components/search/SearchResults.tsx +367 -0
  84. package/templates/src/components/search/SearchWrapper.tsx +46 -0
  85. package/templates/src/components/storykeep/Dashboard_Advanced.tsx +1 -1
  86. package/templates/src/components/storykeep/Dashboard_Analytics.tsx +34 -8
  87. package/templates/src/components/storykeep/Dashboard_Branding.tsx +1 -1
  88. package/templates/src/components/storykeep/Dashboard_Content.tsx +6 -0
  89. package/templates/src/components/storykeep/StoryKeepBackdrop.astro +87 -0
  90. package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +38 -34
  91. package/templates/src/components/storykeep/controls/content/KnownResourceForm.tsx +1 -1
  92. package/templates/src/components/storykeep/controls/content/MenuForm.tsx +56 -8
  93. package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +18 -3
  94. package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +5 -8
  95. package/templates/src/components/storykeep/state/FetchAnalytics.tsx +274 -228
  96. package/templates/src/components/storykeep/widgets/Wizard.tsx +14 -7
  97. package/templates/src/components/widgets/ImpressionWrapper.tsx +0 -1
  98. package/templates/src/constants/shapes.ts +9 -0
  99. package/templates/src/constants.ts +2121 -16
  100. package/templates/src/hooks/useSearch.ts +228 -0
  101. package/templates/src/layouts/Layout.astro +213 -104
  102. package/templates/src/lib/storyData.ts +4 -1
  103. package/templates/src/pages/[...slug]/edit.astro +14 -14
  104. package/templates/src/pages/[...slug].astro +82 -21
  105. package/templates/src/pages/api/orphan-analysis.ts +0 -1
  106. package/templates/src/pages/api/tailwind.ts +23 -21
  107. package/templates/src/pages/context/[...contextSlug]/edit.astro +14 -14
  108. package/templates/src/pages/context/[...contextSlug].astro +7 -2
  109. package/templates/src/pages/storykeep/advanced.astro +5 -4
  110. package/templates/src/pages/storykeep/branding.astro +5 -4
  111. package/templates/src/pages/storykeep/content.astro +5 -4
  112. package/templates/src/pages/storykeep/init.astro +40 -1
  113. package/templates/src/pages/storykeep/login.astro +1 -1
  114. package/templates/src/pages/storykeep.astro +5 -4
  115. package/templates/src/stores/nodes.ts +59 -88
  116. package/templates/src/stores/orphanAnalysis.ts +19 -21
  117. package/templates/src/stores/storykeep.ts +7 -0
  118. package/templates/src/types/compositorTypes.ts +6 -0
  119. package/templates/src/types/tractstack.ts +17 -0
  120. package/templates/src/utils/actions/lispLexer.ts +2 -2
  121. package/templates/src/utils/actions/preParse_Action.ts +3 -0
  122. package/templates/src/utils/api/beliefHelpers.ts +12 -36
  123. package/templates/src/utils/api/menuHelpers.ts +2 -2
  124. package/templates/src/utils/api.ts +26 -0
  125. package/templates/src/utils/compositor/TemplateNodes.ts +7 -0
  126. package/templates/src/utils/compositor/allowInsert.ts +5 -3
  127. package/templates/src/utils/compositor/nodesHelper.ts +4 -0
  128. package/templates/src/utils/compositor/processMarkdown.ts +16 -2
  129. package/templates/src/utils/compositor/reduceNodesClassNames.ts +4 -0
  130. package/templates/src/utils/compositor/templateMarkdownStyles.ts +13 -13
  131. package/templates/src/utils/compositor/typeGuards.ts +1 -0
  132. package/templates/src/utils/customHelpers.ts +38 -0
  133. package/templates/src/utils/helpers.ts +2 -2
  134. package/templates/src/utils/layout.ts +65 -144
  135. package/utils/inject-files.ts +95 -18
  136. package/templates/src/client/analytics-events.js +0 -207
  137. package/templates/src/client/belief-events.js +0 -191
  138. package/templates/src/client/sse.js +0 -613
  139. package/templates/src/components/codehooks/FeaturedContent.astro +0 -273
  140. package/templates/src/components/codehooks/FeaturedContentSetup.tsx +0 -738
  141. package/templates/src/components/compositor/preview/FeaturedContentPreview.tsx +0 -128
  142. 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 FeaturedContentSetup from '@/components/codehooks/FeaturedContentSetup';
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
- <div key={key} className="flex items-start">
40
+ <Fragment key={key}>
31
41
  <span className="min-w-24 font-bold text-gray-600">{key}:</span>
32
- <span className="ml-2 text-gray-800">
33
- {JSON.stringify(value)}
34
- </span>
35
- </div>
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 === 'featured-content' ? (
152
- <FeaturedContentSetup
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 nodeId={props.nodeId} params={codeHookParams} />
187
+ <ListContentSetup
188
+ nodeId={props.nodeId}
189
+ params={codeHookParams}
190
+ config={props.config!}
191
+ />
158
192
  ) : codeHookPayload && codeHookTarget === 'bunny-video' ? (
159
- <BunnyVideoSetup nodeId={props.nodeId} params={codeHookParams} />
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 nodeId={id} key={id} ctx={nodeProps.ctx} />
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 embedUrl={value1} title={value2 || 'Bunny Video'} />
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 null;
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.length}-${outputWidth}`;
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
- if (canUndo) {
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-medium text-gray-600">Viewport:</span>
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
- {canUndo && (
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
- {canUndo && (
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
- <StyleLinkRemovePanel
177
- node={clickedNode}
178
- className={signal.className}
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) return <StyleWidgetConfigPanel node={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 && signal.childId)
323
+ if (clickedNode && markdownNode)
320
324
  return (
321
325
  <StyleWidgetAddPanel
322
326
  node={clickedNode}
323
327
  parentNode={markdownNode}
324
- childId={signal.childId}
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
- if (toolModeVal !== `styles` || !signal) {
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 150ms ease-in'
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
- @keyframes fadeInFromHalf {
41
- 0% { opacity: var(--fade-start, 0.5); }
42
- 100% { opacity: var(--fade-end, 1); }
43
- }
44
- `}</style>
45
- <div className="w-full max-w-lg rounded-lg border border-gray-200 bg-white p-1.5 shadow-xl md:p-2.5">
46
- <div className="mb-4 flex items-center justify-between">
47
- <h3 className="text-myblue text-lg font-bold">{panelTitle}</h3>
48
- <button
49
- onClick={() => settingsPanelStore.set(null)}
50
- className="hover:text-myblue text-gray-500"
51
- >
52
- <XMarkIcon className="h-5 w-5" />
53
- </button>
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
- <div className="space-y-4">
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
  }: {