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
@@ -0,0 +1,228 @@
1
+ import { useState, useCallback, useRef, useMemo } from 'react';
2
+ import { TractStackAPI } from '@/utils/api';
3
+ import type {
4
+ DiscoverySuggestion,
5
+ CategorizedResults,
6
+ } from '@/types/tractstack';
7
+
8
+ interface UseSearchReturn {
9
+ // Discovery phase
10
+ suggestions: DiscoverySuggestion[];
11
+ isDiscovering: boolean;
12
+ discoverError: string | null;
13
+
14
+ // Retrieve phase
15
+ searchResults: CategorizedResults | null;
16
+ isRetrieving: boolean;
17
+ retrieveError: string | null;
18
+
19
+ // Actions
20
+ discoverTerms: (query: string) => void;
21
+ selectSuggestion: (suggestion: DiscoverySuggestion) => void;
22
+ selectExactMatch: (term: string) => void;
23
+ clearAll: () => void;
24
+ }
25
+
26
+ const DEBOUNCE_MS = 150;
27
+ const BACKEND_THROTTLE_MS = 1200;
28
+
29
+ export function useSearch(): UseSearchReturn {
30
+ // Discovery state
31
+ const [suggestions, setSuggestions] = useState<DiscoverySuggestion[]>([]);
32
+ const [isDiscovering, setIsDiscovering] = useState(false);
33
+ const [discoverError, setDiscoverError] = useState<string | null>(null);
34
+
35
+ // Retrieve state
36
+ const [searchResults, setSearchResults] = useState<CategorizedResults | null>(
37
+ null
38
+ );
39
+ const [isRetrieving, setIsRetrieving] = useState(false);
40
+ const [retrieveError, setRetrieveError] = useState<string | null>(null);
41
+
42
+ // --- REVISED STATE FOR SEARCH LOGIC ---
43
+ const searchTimerRef = useRef<NodeJS.Timeout>();
44
+ const lastExecutionTimeRef = useRef<number>(0);
45
+ const pendingQueryRef = useRef<string | null>(null);
46
+ const inflightQueryRef = useRef<string | null>(null);
47
+ const api = useMemo(() => new TractStackAPI(), []);
48
+
49
+ const performDiscovery = useCallback(
50
+ async (query: string) => {
51
+ if (!query.trim()) {
52
+ setSuggestions([]);
53
+ return;
54
+ }
55
+
56
+ if (inflightQueryRef.current === query.trim()) {
57
+ return;
58
+ }
59
+
60
+ inflightQueryRef.current = query.trim();
61
+ setIsDiscovering(true);
62
+ setDiscoverError(null);
63
+
64
+ try {
65
+ const response = await api.discover(query.trim());
66
+
67
+ if (inflightQueryRef.current === query.trim()) {
68
+ if (response.success && response.data) {
69
+ setSuggestions(response.data.suggestions);
70
+ } else {
71
+ setDiscoverError(response.error || 'Discovery failed');
72
+ setSuggestions([]);
73
+ }
74
+ }
75
+ } catch (err) {
76
+ if (inflightQueryRef.current === query.trim()) {
77
+ setDiscoverError(
78
+ err instanceof Error ? err.message : 'Discovery failed'
79
+ );
80
+ setSuggestions([]);
81
+ }
82
+ } finally {
83
+ if (inflightQueryRef.current === query.trim()) {
84
+ inflightQueryRef.current = null;
85
+ setIsDiscovering(false);
86
+ }
87
+ }
88
+ },
89
+ [api]
90
+ );
91
+
92
+ const performRetrieve = useCallback(
93
+ async (term: string, isTopic: boolean = false) => {
94
+ setIsRetrieving(true);
95
+ setRetrieveError(null);
96
+
97
+ try {
98
+ const response = await api.retrieve(term, isTopic);
99
+
100
+ if (response.success && response.data) {
101
+ setSearchResults(response.data);
102
+ } else {
103
+ setRetrieveError(response.error || 'Retrieval failed');
104
+ setSearchResults(null);
105
+ }
106
+ } catch (err) {
107
+ setRetrieveError(
108
+ err instanceof Error ? err.message : 'Retrieval failed'
109
+ );
110
+ setSearchResults(null);
111
+ } finally {
112
+ setIsRetrieving(false);
113
+ }
114
+ },
115
+ [api]
116
+ );
117
+
118
+ const discoverTerms = useCallback(
119
+ (query: string) => {
120
+ // Clear any existing timer.
121
+ if (searchTimerRef.current) {
122
+ clearTimeout(searchTimerRef.current);
123
+ }
124
+
125
+ // Clear results when starting new discovery
126
+ setSearchResults(null);
127
+ setRetrieveError(null);
128
+
129
+ // Handle empty queries immediately
130
+ if (!query.trim()) {
131
+ setSuggestions([]);
132
+ setDiscoverError(null);
133
+ setIsDiscovering(false);
134
+ pendingQueryRef.current = null;
135
+ inflightQueryRef.current = null;
136
+ return;
137
+ }
138
+
139
+ // Always store the latest query for the next execution
140
+ pendingQueryRef.current = query;
141
+
142
+ const now = Date.now();
143
+ const timeSinceLastSearch = now - lastExecutionTimeRef.current;
144
+
145
+ // Start with the basic debounce delay
146
+ let delay = DEBOUNCE_MS;
147
+
148
+ // If we are inside the throttle window, we must wait longer
149
+ if (timeSinceLastSearch < BACKEND_THROTTLE_MS) {
150
+ const remainingThrottle = BACKEND_THROTTLE_MS - timeSinceLastSearch;
151
+ delay = Math.max(delay, remainingThrottle);
152
+ }
153
+
154
+ searchTimerRef.current = setTimeout(() => {
155
+ // Double check there's a query to run
156
+ if (pendingQueryRef.current !== null) {
157
+ const queryToExecute = pendingQueryRef.current;
158
+
159
+ // Update execution time as soon as the search is initiated
160
+ lastExecutionTimeRef.current = Date.now();
161
+ performDiscovery(queryToExecute);
162
+ }
163
+ }, delay);
164
+ },
165
+ [performDiscovery]
166
+ );
167
+
168
+ const selectSuggestion = useCallback(
169
+ (suggestion: DiscoverySuggestion) => {
170
+ // Clear suggestions
171
+ setSuggestions([]);
172
+ setDiscoverError(null);
173
+
174
+ // Perform retrieve based on suggestion type
175
+ const isTopic = suggestion.type === 'TOPIC';
176
+ performRetrieve(suggestion.term, isTopic);
177
+ },
178
+ [performRetrieve]
179
+ );
180
+
181
+ const selectExactMatch = useCallback(
182
+ (term: string) => {
183
+ // Clear suggestions
184
+ setSuggestions([]);
185
+ setDiscoverError(null);
186
+
187
+ // Check if term exists in current suggestions to determine if it's a topic
188
+ const matchingSuggestion = suggestions.find(
189
+ (s) => s.term.toLowerCase() === term.toLowerCase()
190
+ );
191
+ const isTopic = matchingSuggestion?.type === 'TOPIC';
192
+
193
+ performRetrieve(term, isTopic);
194
+ },
195
+ [suggestions, performRetrieve]
196
+ );
197
+
198
+ const clearAll = useCallback(() => {
199
+ // Clear the main search timer
200
+ if (searchTimerRef.current) {
201
+ clearTimeout(searchTimerRef.current);
202
+ }
203
+
204
+ // Reset all state
205
+ setSuggestions([]);
206
+ setIsDiscovering(false);
207
+ setDiscoverError(null);
208
+ setSearchResults(null);
209
+ setIsRetrieving(false);
210
+ setRetrieveError(null);
211
+ pendingQueryRef.current = null;
212
+ inflightQueryRef.current = null;
213
+ lastExecutionTimeRef.current = 0; // Reset throttle timer
214
+ }, []);
215
+
216
+ return {
217
+ suggestions,
218
+ isDiscovering,
219
+ discoverError,
220
+ searchResults,
221
+ isRetrieving,
222
+ retrieveError,
223
+ discoverTerms,
224
+ selectSuggestion,
225
+ selectExactMatch,
226
+ clearAll,
227
+ };
228
+ }
@@ -45,48 +45,42 @@ const {
45
45
  impressions = [],
46
46
  } = Astro.props;
47
47
 
48
- const isDev = import.meta.env.DEV;
49
-
50
- // Get site status from the store
51
48
  const isInitialized = !freshInstallStore.get().needsSetup;
52
-
53
- // Conditionally set asset paths based on initialization status
49
+ const goBackend = import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
50
+ const tenantId =
51
+ Astro.locals.tenant?.id || import.meta.env.PUBLIC_TENANTID || 'default';
52
+ const brandConfig = propBrandConfig || (await getBrandConfig(tenantId));
54
53
  const cssBasePath = isInitialized ? '/media/css' : '/styles';
55
54
  const fontBasePath = isInitialized ? '/media/fonts' : '/fonts';
56
- const mainStylesUrl =
57
- isStoryKeep || isDev
55
+ const mainStylesUrl = (() => {
56
+ const baseUrl = isStoryKeep
58
57
  ? `${cssBasePath}/storykeep.css`
59
58
  : `${cssBasePath}/frontend.css`;
60
- const goBackend = import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
61
- const tenantId =
62
- Astro.locals.tenant?.id || import.meta.env.PUBLIC_TENANTID || 'default';
59
+ if (!isStoryKeep && brandConfig?.STYLES_VER) {
60
+ return `${baseUrl}?v=${brandConfig.STYLES_VER}`;
61
+ }
62
+ return baseUrl;
63
+ })();
63
64
 
64
- // Social media and SEO setup
65
- const brandConfig = propBrandConfig || (await getBrandConfig(tenantId));
66
65
  const defaultFavIcon = brandConfig.FAVICON || `/brand/favicon.ico`;
67
66
  const defaultSocialImageURL = ogImage || brandConfig.OG || `/brand/og.png`;
68
67
  const defaultSocialLogoURL = brandConfig.OGLOGO || `/brand/oglogo.png`;
69
68
  const defaultSocialTitle =
70
- typeof title === `string` && title
71
- ? title
72
- : typeof brandConfig.OGTITLE === `string`
73
- ? brandConfig.OGTITLE
74
- : `TractStack dynamic website`;
69
+ title || brandConfig.OGTITLE || `TractStack dynamic website`;
75
70
  const defaultSocialAuthor = brandConfig.OGAUTHOR || `TractStack`;
76
71
  const defaultSocialDesc =
77
72
  description ||
78
73
  brandConfig.OGDESC ||
79
74
  `No-code website builder and content marketing platform`;
80
- const buildVersion = (() => {
81
- if (modDatetime) return modDatetime.getTime();
82
- if (pubDatetime) return pubDatetime.getTime();
83
- return Date.now();
84
- })();
85
- const socialImageWithVersion = `${defaultSocialImageURL}?v=${buildVersion}`;
86
- const socialLogoWithVersion = `${defaultSocialLogoURL}?v=${buildVersion}`;
75
+ const socialImageFullURL = brandConfig?.SITE_URL
76
+ ? `${brandConfig.SITE_URL.replace(/\/$/, '')}${defaultSocialImageURL}`
77
+ : `${defaultSocialImageURL}`;
78
+ const socialLogoFullURL = brandConfig?.SITE_URL
79
+ ? `${brandConfig.SITE_URL.replace(/\/$/, '')}${defaultSocialLogoURL}`
80
+ : `${defaultSocialLogoURL}`;
87
81
  const gtagId = brandConfig?.GTAG || false;
88
82
  const gtagUrl =
89
- typeof gtagId === `string` && gtagId.length > 1
83
+ gtagId && typeof gtagId === `string` && gtagId.length > 1
90
84
  ? `https://www.googletagmanager.com/gtag/js?id=${gtagId}`
91
85
  : null;
92
86
  const fullCanonicalUrl = brandConfig?.SITE_URL
@@ -94,6 +88,8 @@ const fullCanonicalUrl = brandConfig?.SITE_URL
94
88
  ? `${brandConfig.SITE_URL.replace(/\/$/, '')}${canonicalURL}`
95
89
  : `${brandConfig.SITE_URL.replace(/\/$/, '')}/${canonicalURL}`
96
90
  : canonicalURL;
91
+
92
+ const enableBunny = import.meta.env.PUBLIC_ENABLE_BUNNY === 'true';
97
93
  ---
98
94
 
99
95
  <!doctype html>
@@ -114,6 +110,11 @@ const fullCanonicalUrl = brandConfig?.SITE_URL
114
110
  <link rel="stylesheet" href={mainStylesUrl} />
115
111
  <link rel="sitemap" href="/sitemap.xml" />
116
112
 
113
+ <link
114
+ rel="stylesheet"
115
+ href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css"
116
+ />
117
+
117
118
  <meta name="storyfragment-id" content={storyfragmentId} />
118
119
  <meta name="session-id" content={sessionId} />
119
120
 
@@ -134,14 +135,14 @@ const fullCanonicalUrl = brandConfig?.SITE_URL
134
135
  <meta property="og:type" content="website" />
135
136
  <meta property="og:description" content={defaultSocialDesc} />
136
137
  <meta property="og:url" content={fullCanonicalUrl} />
137
- <meta property="og:image" content={socialImageWithVersion} />
138
- <meta property="og:logo" content={socialLogoWithVersion} />
138
+ <meta property="og:image" content={socialImageFullURL} />
139
+ <meta property="og:logo" content={socialLogoFullURL} />
139
140
 
140
141
  <meta property="twitter:card" content="summary_large_image" />
141
142
  <meta property="twitter:url" content={fullCanonicalUrl} />
142
143
  <meta property="twitter:title" content={defaultSocialTitle} />
143
144
  <meta property="twitter:description" content={defaultSocialDesc} />
144
- <meta property="twitter:image" content={socialImageWithVersion} />
145
+ <meta property="twitter:image" content={socialImageFullURL} />
145
146
 
146
147
  {
147
148
  pubDatetime && (
@@ -183,16 +184,17 @@ const fullCanonicalUrl = brandConfig?.SITE_URL
183
184
 
184
185
  <ClientRouter />
185
186
 
186
- <script
187
- is:inline
188
- type="text/javascript"
189
- src="//assets.mediadelivery.net/playerjs/player-0.1.0.min.js"></script>
187
+ {
188
+ enableBunny && (
189
+ <script
190
+ is:inline
191
+ type="text/javascript"
192
+ src="//assets.mediadelivery.net/playerjs/player-0.1.0.min.js"
193
+ />
194
+ )
195
+ }
190
196
 
191
- <script
192
- src="https://unpkg.com/htmx.org@2.0.4"
193
- crossorigin="anonymous"
194
- is:inline
195
- is:persist></script>
197
+ <script src="/client/htmx.min.js" is:inline></script>
196
198
 
197
199
  <script
198
200
  define:vars={{
@@ -204,87 +206,194 @@ const fullCanonicalUrl = brandConfig?.SITE_URL
204
206
  }}
205
207
  is:inline
206
208
  >
207
- function createTractstackConfig() {
208
- window.TRACTSTACK_CONFIG = {
209
- configured: true,
210
- backendUrl: goBackend,
211
- tenantId: tenantId,
212
- fontBasePath: fontBasePath,
213
- storyfragmentId: storyfragmentId,
214
- sessionId: sessionId,
215
- };
216
- }
209
+ // Capture the initial, server-rendered values from define:vars into top-level constants.
210
+ // This makes the scope explicit and resolves the linter error.
211
+ const initialStoryfragmentId = storyfragmentId;
212
+ const initialSessionId = sessionId;
217
213
 
218
214
  function updateTractstackConfig() {
219
- // Get current values from meta tags
220
215
  const storyfragmentMeta = document.querySelector(
221
216
  'meta[name="storyfragment-id"]'
222
217
  );
223
218
  const sessionMeta = document.querySelector('meta[name="session-id"]');
224
219
 
225
- const currentStoryfragmentId = storyfragmentMeta
226
- ? storyfragmentMeta.content
227
- : null;
228
- const currentSessionId = sessionMeta ? sessionMeta.content : sessionId;
229
- if (window.TRACTSTACK_CONFIG && currentStoryfragmentId) {
230
- window.TRACTSTACK_CONFIG.storyfragmentId = currentStoryfragmentId;
231
- window.TRACTSTACK_CONFIG.sessionId = currentSessionId;
232
- } else {
233
- createTractstackConfig();
234
- }
235
- }
236
-
237
- // Check if config exists.
238
- // If not, create it. This handles the very first load.
239
- if (!window.TRACTSTACK_CONFIG) {
240
- createTractstackConfig();
220
+ window.TRACTSTACK_CONFIG = {
221
+ configured: true,
222
+ backendUrl: goBackend,
223
+ tenantId: tenantId,
224
+ fontBasePath: fontBasePath,
225
+ // Use the meta tag if it exists (for subsequent client-side loads),
226
+ // otherwise fall back to the initial server-rendered value.
227
+ storyfragmentId: storyfragmentMeta
228
+ ? storyfragmentMeta.content
229
+ : initialStoryfragmentId,
230
+ sessionId: sessionMeta ? sessionMeta.content : initialSessionId,
231
+ };
241
232
  }
242
233
 
243
- // Update on subsequent loads, or set for the first time if script runs after initial setup
234
+ updateTractstackConfig();
244
235
  document.addEventListener('astro:page-load', updateTractstackConfig);
245
236
  </script>
246
237
  </head>
247
238
  <body class="font-main w-full">
248
- {
249
- !isEditor && (
250
- <Header
251
- title={title}
252
- slug={slug}
253
- brandConfig={brandConfig}
254
- isContext={isContext}
255
- isStoryKeep={isStoryKeep}
256
- isEditable={isEditable}
257
- sessionId={sessionId}
258
- menu={menu}
259
- storyfragmentId={storyfragmentId}
260
- impressions={impressions}
261
- />
262
- )
263
- }
264
- <div
265
- id="loading-indicator"
266
- class="bg-myorange fixed left-0 top-0 z-50 h-1 w-full scale-x-0 transform transition-transform duration-300 ease-out"
267
- >
268
- </div>
269
- <div id="content" class="transition-opacity duration-300">
270
- <slot />
239
+ <div class="overflow-hidden">
240
+ {
241
+ !isEditor && (
242
+ <Header
243
+ title={title}
244
+ slug={slug}
245
+ brandConfig={brandConfig}
246
+ isContext={isContext}
247
+ isStoryKeep={isStoryKeep}
248
+ isEditable={isEditable}
249
+ sessionId={sessionId}
250
+ menu={menu}
251
+ storyfragmentId={storyfragmentId}
252
+ impressions={impressions}
253
+ />
254
+ )
255
+ }
256
+ <div
257
+ id="loading-backdrop"
258
+ class="pointer-events-none fixed inset-0"
259
+ style="opacity: 0; z-index: 10069; display: none; background-color: rgba(255, 255, 255, 0.3); backdrop-filter: blur(4px); transition: opacity 300ms;"
260
+ >
261
+ </div>
262
+ <div
263
+ id="loading-indicator"
264
+ class="bg-myorange fixed left-0 top-0 h-1 w-full scale-x-0 transform transition-transform duration-300 ease-out"
265
+ style="opacity: 0.5; filter: blur(0.5px); z-index: 99999;"
266
+ >
267
+ </div>
268
+
269
+ <div id="content" class="transition-opacity duration-300">
270
+ <slot />
271
+ </div>
272
+ {
273
+ !isEditor && (
274
+ <Footer
275
+ slug={slug}
276
+ brandConfig={brandConfig}
277
+ isContext={isContext}
278
+ menu={menu}
279
+ created={created}
280
+ backToTop={true}
281
+ />
282
+ )
283
+ }
271
284
  </div>
272
- {
273
- !isEditor && (
274
- <Footer
275
- slug={slug}
276
- brandConfig={brandConfig}
277
- isContext={isContext}
278
- menu={menu}
279
- created={created}
280
- backToTop={true}
281
- />
282
- )
283
- }
284
- <script type="module" is:inline is:persist is:raw>
285
- import '/client/sse.js';
286
- import '/client/belief-events.js';
287
- import '/client/analytics-events.js';
285
+
286
+ <script type="module" src="/client/app.js" is:inline is:persist></script>
287
+
288
+ <script type="module" src="/client/view.js" is:inline></script>
289
+
290
+ <script is:inline is:persist>
291
+ let navProgressInterval = null;
292
+ let navSafetyTimeout = null;
293
+
294
+ function startNavLoadingAnimation() {
295
+ const loadingIndicator = document.getElementById('loading-indicator');
296
+ const loadingBackdrop = document.getElementById('loading-backdrop');
297
+ const content = document.getElementById('content');
298
+
299
+ if (
300
+ window.matchMedia('(prefers-reduced-motion: no-preference)')
301
+ .matches &&
302
+ loadingIndicator &&
303
+ loadingBackdrop
304
+ ) {
305
+ if (navSafetyTimeout !== null) {
306
+ clearTimeout(navSafetyTimeout);
307
+ navSafetyTimeout = null;
308
+ }
309
+
310
+ loadingBackdrop.style.display = 'block';
311
+ void loadingBackdrop.offsetHeight;
312
+ loadingBackdrop.style.opacity = '1';
313
+
314
+ if (content) {
315
+ content.style.opacity = '0.7';
316
+ }
317
+
318
+ loadingIndicator.style.transform = 'scaleX(0)';
319
+ loadingIndicator.style.display = 'block';
320
+
321
+ let progress = 0;
322
+ navProgressInterval = setInterval(() => {
323
+ progress += 2;
324
+ if (progress > 90) {
325
+ if (navProgressInterval !== null) {
326
+ clearInterval(navProgressInterval);
327
+ }
328
+ return;
329
+ }
330
+ loadingIndicator.style.transform = `scaleX(${progress / 100})`;
331
+ }, 20);
332
+
333
+ navSafetyTimeout = setTimeout(() => {
334
+ stopNavLoadingAnimation();
335
+ }, 10000);
336
+ }
337
+ }
338
+
339
+ function stopNavLoadingAnimation() {
340
+ const loadingIndicator = document.getElementById('loading-indicator');
341
+ const loadingBackdrop = document.getElementById('loading-backdrop');
342
+ const content = document.getElementById('content');
343
+
344
+ if (navSafetyTimeout !== null) {
345
+ clearTimeout(navSafetyTimeout);
346
+ navSafetyTimeout = null;
347
+ }
348
+
349
+ if (
350
+ window.matchMedia('(prefers-reduced-motion: no-preference)')
351
+ .matches &&
352
+ loadingIndicator &&
353
+ loadingBackdrop
354
+ ) {
355
+ if (navProgressInterval !== null) {
356
+ clearInterval(navProgressInterval);
357
+ navProgressInterval = null;
358
+ }
359
+
360
+ loadingIndicator.style.transform = 'scaleX(1)';
361
+
362
+ if (content) {
363
+ content.style.opacity = '1';
364
+ }
365
+
366
+ loadingBackdrop.style.opacity = '0';
367
+
368
+ setTimeout(() => {
369
+ loadingIndicator.style.display = 'none';
370
+ loadingIndicator.style.transform = 'scaleX(0)';
371
+ loadingBackdrop.style.display = 'none';
372
+ }, 300);
373
+ }
374
+ }
375
+
376
+ function setupNavigationLoading() {
377
+ document.addEventListener('astro:before-preparation', () => {
378
+ startNavLoadingAnimation();
379
+ });
380
+
381
+ document.addEventListener('astro:page-load', () => {
382
+ stopNavLoadingAnimation();
383
+ });
384
+
385
+ document.addEventListener('astro:after-swap', () => {
386
+ stopNavLoadingAnimation();
387
+ });
388
+ }
389
+
390
+ if (document.readyState === 'loading') {
391
+ document.addEventListener('DOMContentLoaded', setupNavigationLoading);
392
+ } else {
393
+ setupNavigationLoading();
394
+ }
395
+
396
+ document.addEventListener('astro:page-load', setupNavigationLoading);
288
397
  </script>
289
398
  </body>
290
399
  </html>
@@ -1,5 +1,5 @@
1
1
  import { handleFailedResponse } from '@/utils/backend';
2
- import type { ImpressionNode } from '@/types/compositorTypes';
2
+ import type { ImpressionNode, ResourceNode } from '@/types/compositorTypes';
3
3
 
4
4
  export interface StoryData {
5
5
  id: string;
@@ -7,11 +7,14 @@ export interface StoryData {
7
7
  slug: string;
8
8
  paneIds: string[];
9
9
  codeHookTargets: Record<string, string>;
10
+ codeHookVisibility: Record<string, boolean | string[]>;
11
+ resourcesPayload: Record<string, ResourceNode[]>;
10
12
  impressions: ImpressionNode[];
11
13
  fragments: Record<string, string>;
12
14
  menu: any;
13
15
  isHome: boolean;
14
16
  created: string;
17
+ socialImagePath?: string | null;
15
18
  }
16
19
 
17
20
  export async function getStoryData(