astro-tractstack 2.0.0-rc.8 → 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 (141) hide show
  1. package/LICENSE +8 -97
  2. package/README.md +7 -5
  3. package/bin/create-tractstack.js +35 -11
  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_Content.tsx +6 -0
  88. package/templates/src/components/storykeep/StoryKeepBackdrop.astro +87 -0
  89. package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +37 -33
  90. package/templates/src/components/storykeep/controls/content/MenuForm.tsx +55 -7
  91. package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +17 -2
  92. package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +5 -8
  93. package/templates/src/components/storykeep/state/FetchAnalytics.tsx +274 -228
  94. package/templates/src/components/storykeep/widgets/Wizard.tsx +14 -7
  95. package/templates/src/components/tenant/RegistrationForm.tsx +1 -1
  96. package/templates/src/components/widgets/ImpressionWrapper.tsx +0 -1
  97. package/templates/src/constants/shapes.ts +9 -0
  98. package/templates/src/constants.ts +2121 -16
  99. package/templates/src/hooks/useSearch.ts +228 -0
  100. package/templates/src/layouts/Layout.astro +213 -104
  101. package/templates/src/lib/storyData.ts +4 -1
  102. package/templates/src/pages/[...slug]/edit.astro +14 -14
  103. package/templates/src/pages/[...slug].astro +82 -21
  104. package/templates/src/pages/api/orphan-analysis.ts +0 -1
  105. package/templates/src/pages/api/tailwind.ts +23 -21
  106. package/templates/src/pages/context/[...contextSlug]/edit.astro +14 -14
  107. package/templates/src/pages/context/[...contextSlug].astro +7 -2
  108. package/templates/src/pages/storykeep/advanced.astro +5 -4
  109. package/templates/src/pages/storykeep/branding.astro +5 -4
  110. package/templates/src/pages/storykeep/content.astro +5 -4
  111. package/templates/src/pages/storykeep/init.astro +40 -1
  112. package/templates/src/pages/storykeep/login.astro +1 -1
  113. package/templates/src/pages/storykeep.astro +5 -4
  114. package/templates/src/stores/nodes.ts +59 -88
  115. package/templates/src/stores/orphanAnalysis.ts +19 -21
  116. package/templates/src/stores/storykeep.ts +7 -0
  117. package/templates/src/types/compositorTypes.ts +6 -0
  118. package/templates/src/types/tractstack.ts +17 -0
  119. package/templates/src/utils/actions/lispLexer.ts +2 -2
  120. package/templates/src/utils/actions/preParse_Action.ts +3 -0
  121. package/templates/src/utils/api/beliefHelpers.ts +12 -36
  122. package/templates/src/utils/api/menuHelpers.ts +2 -2
  123. package/templates/src/utils/api.ts +26 -0
  124. package/templates/src/utils/compositor/TemplateNodes.ts +7 -0
  125. package/templates/src/utils/compositor/allowInsert.ts +5 -3
  126. package/templates/src/utils/compositor/nodesHelper.ts +4 -0
  127. package/templates/src/utils/compositor/processMarkdown.ts +16 -2
  128. package/templates/src/utils/compositor/reduceNodesClassNames.ts +4 -0
  129. package/templates/src/utils/compositor/templateMarkdownStyles.ts +13 -13
  130. package/templates/src/utils/compositor/typeGuards.ts +1 -0
  131. package/templates/src/utils/customHelpers.ts +38 -0
  132. package/templates/src/utils/helpers.ts +2 -2
  133. package/templates/src/utils/layout.ts +65 -144
  134. package/utils/inject-files.ts +95 -18
  135. package/templates/src/client/analytics-events.js +0 -207
  136. package/templates/src/client/belief-events.js +0 -191
  137. package/templates/src/client/sse.js +0 -613
  138. package/templates/src/components/codehooks/FeaturedContent.astro +0 -273
  139. package/templates/src/components/codehooks/FeaturedContentSetup.tsx +0 -738
  140. package/templates/src/components/compositor/preview/FeaturedContentPreview.tsx +0 -128
  141. package/templates/src/components/edit/pane/PanePanel_slug.tsx +0 -219
@@ -0,0 +1,429 @@
1
+ /**
2
+ * TractStack View Initializer
3
+ *
4
+ * This script is transient and runs on every page load and client-side navigation.
5
+ * It is responsible for initializing all logic related to the current document,
6
+ * including HTMX configuration, analytics, and DOM event listeners. It gets its
7
+ * configuration state from the persistent `app.js` singleton.
8
+ */
9
+
10
+ const VERBOSE = false;
11
+
12
+ // ============================================================================
13
+ // LOGGING UTILITIES
14
+ // ============================================================================
15
+ function log(message, ...args) {
16
+ if (VERBOSE) console.log('📄 VIEW [view.js]:', message, ...args);
17
+ }
18
+
19
+ function logError(message, ...args) {
20
+ if (VERBOSE) console.error('❌ VIEW [view.js]:', message, ...args);
21
+ }
22
+
23
+ // ============================================================================
24
+ // STATE & CONFIGURATION
25
+ // ============================================================================
26
+
27
+ let paneViewTimes = new Map();
28
+ let globalObserver = null;
29
+ let isPageInitialized = false;
30
+
31
+ // ============================================================================
32
+ // CORE LOGIC FUNCTIONS
33
+ // ============================================================================
34
+
35
+ /**
36
+ * A generic utility to send state updates to the backend API.
37
+ * @param {object} data - The payload for the state update.
38
+ */
39
+ async function sendStateUpdate(data) {
40
+ if (!window.TractStackApp) {
41
+ logError('Singleton not found, cannot send state update.');
42
+ return;
43
+ }
44
+ const config = window.TractStackApp.getConfig();
45
+ const url = `${config.backendUrl}/api/v1/state`;
46
+ const body = { paneId: '', duration: 0, ...data };
47
+ log('Sending state update to backend.', { url, body });
48
+
49
+ try {
50
+ const response = await fetch(url, {
51
+ method: 'POST',
52
+ headers: {
53
+ 'Content-Type': 'application/x-www-form-urlencoded',
54
+ 'X-Tenant-ID': config.tenantId,
55
+ 'X-TractStack-Session-ID': config.sessionId,
56
+ 'X-StoryFragment-ID': config.storyfragmentId,
57
+ },
58
+ body: new URLSearchParams(body),
59
+ });
60
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
61
+ } catch (error) {
62
+ logError('Failed to send state update.', { error, data });
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Initializes all analytics tracking for the current page view.
68
+ * @param {object} config - The configuration object for the current page.
69
+ */
70
+ function initAnalyticsTracking(config) {
71
+ const { storyfragmentId } = config;
72
+
73
+ log(`Initializing analytics tracking for storyfragment: ${storyfragmentId}`);
74
+
75
+ if (globalObserver) {
76
+ globalObserver.disconnect();
77
+ }
78
+
79
+ globalObserver = new IntersectionObserver(
80
+ (entries) => {
81
+ entries.forEach((entry) => {
82
+ const paneId = entry.target.getAttribute('data-pane-id');
83
+ if (!paneId) return;
84
+ if (entry.isIntersecting) {
85
+ if (!paneViewTimes.has(paneId)) {
86
+ paneViewTimes.set(paneId, Date.now());
87
+ }
88
+ } else {
89
+ const startTime = paneViewTimes.get(paneId);
90
+ if (startTime) {
91
+ const duration = Date.now() - startTime;
92
+ paneViewTimes.delete(paneId);
93
+ const THRESHOLD_GLOSSED = 7000;
94
+ const THRESHOLD_READ = 42000;
95
+ let eventVerb = null;
96
+ if (duration >= THRESHOLD_READ) eventVerb = 'READ';
97
+ else if (duration >= THRESHOLD_GLOSSED) eventVerb = 'GLOSSED';
98
+ if (eventVerb) {
99
+ sendStateUpdate({
100
+ beliefId: paneId,
101
+ beliefType: 'Pane',
102
+ beliefValue: eventVerb,
103
+ duration,
104
+ });
105
+ }
106
+ }
107
+ }
108
+ });
109
+ },
110
+ { threshold: 0.1, rootMargin: '0px' }
111
+ );
112
+
113
+ const panes = document.querySelectorAll('[data-pane-id]');
114
+ log(`Observing ${panes.length} panes for visibility tracking.`);
115
+ panes.forEach((pane) => globalObserver.observe(pane));
116
+
117
+ const hasTrackedEntered =
118
+ localStorage.getItem('tractstack_entered_tracked') === 'true';
119
+ if (!hasTrackedEntered && storyfragmentId) {
120
+ log('Tracking first-ever "ENTERED" event.');
121
+ sendStateUpdate({
122
+ beliefId: storyfragmentId,
123
+ beliefType: 'StoryFragment',
124
+ beliefValue: 'ENTERED',
125
+ });
126
+ localStorage.setItem('tractstack_entered_tracked', 'true');
127
+ }
128
+
129
+ if (storyfragmentId) {
130
+ log(`Tracking "PAGEVIEWED" event for storyfragment: ${storyfragmentId}`);
131
+ sendStateUpdate({
132
+ beliefId: storyfragmentId,
133
+ beliefType: 'StoryFragment',
134
+ beliefValue: 'PAGEVIEWED',
135
+ });
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Flushes any pending analytics events, typically called before the page unloads.
141
+ */
142
+ function flushPendingPaneEvents() {
143
+ if (paneViewTimes.size === 0) return;
144
+ log('Flushing pending pane view events before page unload.');
145
+ const flushTime = Date.now();
146
+ paneViewTimes.forEach((startTime, paneId) => {
147
+ const duration = flushTime - startTime;
148
+ const THRESHOLD_GLOSSED = 7000;
149
+ const THRESHOLD_READ = 42000;
150
+ let eventVerb = null;
151
+ if (duration >= THRESHOLD_READ) eventVerb = 'READ';
152
+ else if (duration >= THRESHOLD_GLOSSED) eventVerb = 'GLOSSED';
153
+ if (eventVerb) {
154
+ sendStateUpdate({
155
+ beliefId: paneId,
156
+ beliefType: 'Pane',
157
+ beliefValue: eventVerb,
158
+ duration,
159
+ });
160
+ }
161
+ });
162
+ paneViewTimes.clear();
163
+ }
164
+
165
+ /**
166
+ * Configures the fresh HTMX instance on each page load.
167
+ */
168
+ function configureHtmxForPage() {
169
+ if (!window.htmx) {
170
+ logError('Cannot configure HTMX: window.htmx is not defined.');
171
+ return;
172
+ }
173
+
174
+ htmx.config.selfRequestsOnly = false;
175
+
176
+ log('Configuring HTMX listeners for new page view.', {
177
+ selfRequestsOnly: htmx.config.selfRequestsOnly,
178
+ });
179
+
180
+ htmx.on('htmx:configRequest', function (evt) {
181
+ if (!window.TractStackApp) return;
182
+ const config = window.TractStackApp.getConfig();
183
+ log('Intercepting HTMX request with `htmx:configRequest`.', {
184
+ originalPath: evt.detail.path,
185
+ });
186
+ evt.detail.headers['X-Tenant-ID'] = config.tenantId;
187
+ evt.detail.headers['X-StoryFragment-ID'] = config.storyfragmentId;
188
+ evt.detail.headers['X-TractStack-Session-ID'] = config.sessionId;
189
+
190
+ if (evt.detail.path && evt.detail.path.startsWith('/api/v1/')) {
191
+ evt.detail.path = config.backendUrl + evt.detail.path;
192
+ log('Request path rewritten.', { newPath: evt.detail.path });
193
+ }
194
+ });
195
+
196
+ htmx.on('htmx:beforeRequest', async function (evt) {
197
+ const params = evt.detail.requestConfig.parameters;
198
+ if (params && params.beliefVerb === 'IDENTIFY_AS') {
199
+ log('Intercepting IDENTIFY_AS action to perform pre-unset.');
200
+ evt.preventDefault();
201
+
202
+ const originalPayload = { ...params };
203
+ const unsetPayload = {
204
+ unsetBeliefIds: originalPayload.beliefId,
205
+ paneId: originalPayload.paneId || '',
206
+ gotoPaneID: originalPayload.gotoPaneID || '',
207
+ };
208
+
209
+ log('Step 1: Sending UNSET request.', unsetPayload);
210
+ await sendStateUpdate(unsetPayload);
211
+
212
+ log('Step 2: Sending original IDENTIFY_AS request.', originalPayload);
213
+ await sendStateUpdate(originalPayload);
214
+ }
215
+ });
216
+
217
+ log('Processing the document body with htmx.process().');
218
+ htmx.process(document.body);
219
+ }
220
+
221
+ /**
222
+ * Processes a `panes_updated` event received from the singleton.
223
+ * @param {object} update - The update payload from the SSE event.
224
+ * @param {object} config - The current page's configuration.
225
+ */
226
+ function processStoryfragmentUpdate(update, config) {
227
+ if (update.storyfragmentId !== config.storyfragmentId) {
228
+ log('Ignoring update for a different storyfragment.', {
229
+ eventStoryfragment: update.storyfragmentId,
230
+ currentStoryfragment: config.storyfragmentId,
231
+ });
232
+ return;
233
+ }
234
+
235
+ log('Processing storyfragment update from Singleton.', { update });
236
+
237
+ const uniquePaneIds = [...new Set(update.affectedPanes)];
238
+ const codeHookPaneIds = [];
239
+ const regularPaneIds = [];
240
+
241
+ uniquePaneIds.forEach((paneId) => {
242
+ if (
243
+ update.CodeHookVisibility &&
244
+ update.CodeHookVisibility.hasOwnProperty(paneId)
245
+ ) {
246
+ codeHookPaneIds.push(paneId);
247
+ } else {
248
+ regularPaneIds.push(paneId);
249
+ }
250
+ });
251
+
252
+ log('Split panes for processing.', { codeHookPaneIds, regularPaneIds });
253
+
254
+ codeHookPaneIds.forEach((paneId) => {
255
+ const element = document.querySelector(`#pane-${paneId}`);
256
+ if (!element) {
257
+ logError(`Code hook element not found: #pane-${paneId}`);
258
+ return;
259
+ }
260
+ const visibilityValue = update.CodeHookVisibility[paneId];
261
+ log(`Handling Code Hook pane: ${paneId}`, { visibilityValue });
262
+ element.style.display = visibilityValue === false ? 'none' : 'block';
263
+
264
+ const unsetDiv = document.querySelector(`#pane-${paneId}-unset`);
265
+ if (unsetDiv) {
266
+ if (Array.isArray(visibilityValue)) {
267
+ log(
268
+ `Generating "unset" button for pane ${paneId} with beliefs:`,
269
+ visibilityValue
270
+ );
271
+ const hxValsObject = {
272
+ unsetBeliefIds: visibilityValue.join(','),
273
+ paneId: paneId,
274
+ gotoPaneID: update.gotoPaneId || '',
275
+ };
276
+ unsetDiv.innerHTML = `
277
+ <button
278
+ type="button"
279
+ class="text-mydarkgrey absolute right-2 top-2 z-10 rounded-full bg-white p-1.5 hover:bg-black hover:text-white"
280
+ title="Go Back"
281
+ hx-post="/api/v1/state"
282
+ hx-trigger="click"
283
+ hx-swap="none"
284
+ hx-vals='${JSON.stringify(hxValsObject)}'
285
+ hx-preserve="true"
286
+ >
287
+ <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
288
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3" />
289
+ </svg>
290
+ </button>`;
291
+ htmx.process(unsetDiv);
292
+ } else {
293
+ log(`Clearing "unset" button for pane ${paneId}`);
294
+ unsetDiv.innerHTML = '';
295
+ }
296
+ }
297
+ });
298
+
299
+ regularPaneIds.forEach((paneId) => {
300
+ const element = document.querySelector(`[data-pane-id="${paneId}"]`);
301
+ if (element && window.htmx) {
302
+ log(`Triggering 'refresh' on regular pane element: ${paneId}`);
303
+ htmx.trigger(element, 'refresh');
304
+ } else {
305
+ logError(`Could not find regular pane element to refresh: ${paneId}`);
306
+ }
307
+ });
308
+
309
+ if (update.gotoPaneId) {
310
+ setTimeout(() => {
311
+ const targetElement = document.getElementById(
312
+ `pane-${update.gotoPaneId}`
313
+ );
314
+ if (targetElement) {
315
+ log(`Smart scrolling to target pane: ${update.gotoPaneId}`);
316
+ const elementRect = targetElement.getBoundingClientRect();
317
+ const viewportHeight = window.innerHeight;
318
+ const scrollBlock =
319
+ elementRect.height > viewportHeight ? 'start' : 'center';
320
+ log(`Using scroll behavior: block: '${scrollBlock}'`);
321
+ targetElement.scrollIntoView({
322
+ behavior: 'smooth',
323
+ block: scrollBlock,
324
+ });
325
+ } else {
326
+ logError(
327
+ `Target pane element for scrolling not found: #pane-${update.gotoPaneId}`
328
+ );
329
+ }
330
+ }, 350);
331
+ }
332
+ }
333
+
334
+ // ============================================================================
335
+ // MAIN EXECUTION & LIFECYCLE MANAGEMENT
336
+ // ============================================================================
337
+
338
+ function initializeCurrentView() {
339
+ if (isPageInitialized) {
340
+ log('View already initialized for this page. Skipping redundant setup.');
341
+ return;
342
+ }
343
+
344
+ log('INITIALIZING VIEW for new page.');
345
+
346
+ if (!window.TractStackApp) {
347
+ logError('Singleton `TractStackApp` not found! Cannot initialize view.');
348
+ return;
349
+ }
350
+ const config = window.TractStackApp.getConfig();
351
+ if (!config.configured) {
352
+ logError('Singleton config not ready. Aborting view initialization.');
353
+ return;
354
+ }
355
+
356
+ configureHtmxForPage();
357
+ initAnalyticsTracking(config);
358
+
359
+ isPageInitialized = true;
360
+ }
361
+
362
+ function resetViewState() {
363
+ log('Resetting view state before new page preparation.');
364
+ flushPendingPaneEvents();
365
+ isPageInitialized = false;
366
+ paneViewTimes.clear();
367
+ }
368
+
369
+ if (!window.tractstackViewLifecycleListenersAttached) {
370
+ log(
371
+ 'Attaching one-time lifecycle listeners that persist across navigations.'
372
+ );
373
+
374
+ document.addEventListener('astro:before-preparation', resetViewState);
375
+
376
+ document.addEventListener('change', function (event) {
377
+ const target = event.target;
378
+ if (
379
+ target.matches &&
380
+ (target.matches('select[data-belief-id]') ||
381
+ target.matches('input[type="checkbox"][data-belief-id]'))
382
+ ) {
383
+ const beliefId = target.getAttribute('data-belief-id');
384
+ const beliefType = target.getAttribute('data-belief-type');
385
+ const paneId = target.getAttribute('data-pane-id');
386
+
387
+ let beliefValue;
388
+ if (target.type === 'checkbox') {
389
+ const onVerb = target.getAttribute('data-verb');
390
+ const offVerb = target.getAttribute('data-off-verb');
391
+ if (onVerb && offVerb) {
392
+ beliefValue = target.checked ? onVerb : offVerb;
393
+ } else {
394
+ beliefValue = target.checked ? 'BELIEVES_YES' : 'BELIEVES_NO';
395
+ }
396
+ } else {
397
+ beliefValue = target.value;
398
+ }
399
+
400
+ sendStateUpdate({
401
+ beliefId,
402
+ beliefType,
403
+ beliefValue,
404
+ paneId: paneId || '',
405
+ });
406
+ }
407
+ });
408
+
409
+ window.addEventListener('tractstack:panes-updated', (event) => {
410
+ log('Received `tractstack:panes-updated` event from Singleton.');
411
+ if (!window.TractStackApp) return;
412
+ const data = event.detail;
413
+ const currentConfig = window.TractStackApp.getConfig();
414
+ if (data.updates) {
415
+ data.updates.forEach((update) =>
416
+ processStoryfragmentUpdate(update, currentConfig)
417
+ );
418
+ } else {
419
+ processStoryfragmentUpdate(data, currentConfig);
420
+ }
421
+ });
422
+
423
+ window.addEventListener('beforeunload', flushPendingPaneEvents);
424
+
425
+ window.tractstackViewLifecycleListenersAttached = true;
426
+ }
427
+
428
+ initializeCurrentView();
429
+ document.addEventListener('astro:page-load', initializeCurrentView);
@@ -106,12 +106,7 @@ const createdDate = created ? new Date(created) : new Date();
106
106
  {allLinks.map((item: any) => (
107
107
  <a
108
108
  href={item.to}
109
- class={
110
- 'z-10 whitespace-nowrap rounded px-3.5 py-1.5 text-lg shadow-sm transition-colors ' +
111
- (item.featured
112
- ? 'bg-brand-7 hover:bg-myblack focus:bg-brand-7 text-white hover:text-white focus:text-white'
113
- : 'text-brand-7 hover:bg-myblack focus:bg-brand-7 bg-white hover:text-white focus:text-white')
114
- }
109
+ class="bg-brand-7 hover:bg-myblack focus:bg-brand-7 z-10 whitespace-nowrap rounded px-3.5 py-1.5 text-lg text-white shadow-sm transition-colors hover:text-white focus:text-white"
115
110
  title={item.description}
116
111
  >
117
112
  <span class="font-bold">{item.name}</span>
@@ -153,7 +148,7 @@ const createdDate = created ? new Date(created) : new Date();
153
148
  <div
154
149
  class="text-myblue xs:flex-row my-2 flex flex-col items-center justify-center"
155
150
  >
156
- <div class="px-12 text-center text-2xl">
151
+ <div class="px-4 text-center text-2xl md:px-12">
157
152
  Copyright &#169; {createdDate.getFullYear()}{
158
153
  createdDate.getFullYear() !== new Date().getFullYear()
159
154
  ? `-${new Date().getFullYear()}`
@@ -164,9 +159,9 @@ const createdDate = created ? new Date(created) : new Date();
164
159
  </div>
165
160
  </div>
166
161
  <div
167
- class="text-myblue xs:flex-row my-2 flex flex-col items-center justify-center"
162
+ class="text-myblue my-2 flex flex-row items-center justify-center md:flex-col"
168
163
  >
169
- <div class="px-12 text-center text-lg">
164
+ <div class="px-4 text-center text-lg md:px-12">
170
165
  pressed with
171
166
  <a
172
167
  href="https://tractstack.com/?utm_source=tractstack&utm_medium=www&utm_campaign=community"
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  import Menu from './Menu';
3
+ import SearchWrapper from '@/components/search/SearchWrapper';
4
+ import { getFullContentMap } from '@/stores/analytics';
3
5
  import { isAuthenticated, isAdmin, getUserRole } from '@/utils/auth';
4
6
  import ImpressionWrapper from '@/components/widgets/ImpressionWrapper';
5
7
  import type { MenuNode } from '@/types/tractstack';
@@ -33,6 +35,10 @@ const {
33
35
 
34
36
  const isHome = slug === brandConfig?.HOME_SLUG;
35
37
 
38
+ const tenantId =
39
+ Astro.locals.tenant?.id || import.meta.env.PUBLIC_TENANTID || 'default';
40
+ const fullContentMap = await getFullContentMap(tenantId);
41
+
36
42
  const getAssetPath = (configPath: string, fallback: string) => {
37
43
  // Always prioritize brandConfig values when they exist
38
44
  if (configPath && configPath !== '') {
@@ -40,11 +46,8 @@ const getAssetPath = (configPath: string, fallback: string) => {
40
46
  }
41
47
  return fallback;
42
48
  };
43
-
44
49
  const logo = getAssetPath(brandConfig?.LOGO, '/brand/logo.svg');
45
50
  const wordmark = getAssetPath(brandConfig?.WORDMARK, '/brand/wordmark.svg');
46
-
47
- // Handle empty WORDMARK_MODE by defaulting to "default"
48
51
  const wordmarkMode =
49
52
  brandConfig?.WORDMARK_MODE && brandConfig.WORDMARK_MODE !== ''
50
53
  ? brandConfig.WORDMARK_MODE
@@ -108,7 +111,6 @@ const authStatus = {
108
111
  >
109
112
  <h1 class="text-mydarkgrey truncate text-xl">{title}</h1>
110
113
  <div class="flex flex-row flex-nowrap items-center gap-x-2">
111
- {/* Home Icon */}
112
114
  {
113
115
  !isHome ? (
114
116
  <a
@@ -133,7 +135,6 @@ const authStatus = {
133
135
  ) : null
134
136
  }
135
137
 
136
- {/* StoryKeep Dashboard Icon */}
137
138
  {
138
139
  authStatus.isAdmin || authStatus.isAuthenticated ? (
139
140
  <a
@@ -158,14 +159,14 @@ const authStatus = {
158
159
  ) : null
159
160
  }
160
161
 
161
- {/* Edit Icon */}
162
162
  {
163
- isEditable ? (
163
+ isEditable &&
164
+ (authStatus.isAdmin || authStatus.userRole === 'editor') ? (
164
165
  <a
165
166
  data-astro-reload
166
167
  href={!isContext ? `/${slug}/edit` : `/context/${slug}/edit`}
167
168
  class="text-myblue/80 hover:text-myblue hover:rotate-6"
168
- title={!isContext ? 'Edit this story' : 'Edit this pane'}
169
+ title={!isContext ? 'Edit this story' : 'Edit this page'}
169
170
  >
170
171
  <svg
171
172
  class="h-6 w-6"
@@ -184,7 +185,12 @@ const authStatus = {
184
185
  ) : null
185
186
  }
186
187
 
187
- {/* Remember Me Icon */}
188
+ {
189
+ !isStoryKeep && (
190
+ <SearchWrapper contentMap={fullContentMap} client:load />
191
+ )
192
+ }
193
+
188
194
  <script is:inline define:vars={{ sessionId }}>
189
195
  function initRememberMe() {
190
196
  const consent = localStorage.getItem('tractstack_consent') === '1';
@@ -246,7 +252,6 @@ const authStatus = {
246
252
  ) : null
247
253
  }
248
254
 
249
- {/* Logout Icon - Admin/Editor Only */}
250
255
  {
251
256
  authStatus.isAdmin || authStatus.userRole === 'editor' ? (
252
257
  <a
@@ -335,7 +340,13 @@ const authStatus = {
335
340
  }
336
341
 
337
342
  <script>
338
- document.addEventListener('DOMContentLoaded', function () {
343
+ if (document.readyState === 'loading') {
344
+ document.addEventListener('DOMContentLoaded', setupAdminModal);
345
+ } else {
346
+ setupAdminModal();
347
+ }
348
+
349
+ function setupAdminModal() {
339
350
  const logoutBtn = document.getElementById(
340
351
  'logout-btn'
341
352
  ) as HTMLButtonElement | null;
@@ -394,65 +405,61 @@ const authStatus = {
394
405
  });
395
406
  }
396
407
 
397
- // Admin Modal JavaScript - Only runs when admin elements exist
398
- const heartBtn = document.getElementById('admin-heart-btn');
399
- const modal = document.getElementById('admin-modal');
400
- const closeBtn = document.getElementById('admin-modal-close');
401
- const iframe = document.getElementById('admin-sysop-iframe');
402
-
403
- if (heartBtn && modal && closeBtn && iframe) {
404
- // Open modal when heart icon is clicked
405
- heartBtn.addEventListener('click', function (e) {
408
+ // Admin Modal JavaScript - Use event delegation
409
+ document.addEventListener('click', function (e) {
410
+ if (e.target && (e.target as Element).closest('#admin-heart-btn')) {
406
411
  e.preventDefault();
407
- modal.classList.remove('hidden');
408
- modal.setAttribute('aria-hidden', 'false');
409
-
410
- // Focus management for accessibility
411
- closeBtn.focus();
412
-
413
- // Prevent body scroll when modal is open
414
- document.body.style.overflow = 'hidden';
415
- });
412
+ const modal = document.getElementById('admin-modal');
413
+ const closeBtn = document.getElementById('admin-modal-close');
414
+
415
+ if (modal && closeBtn) {
416
+ modal.classList.remove('hidden');
417
+ modal.setAttribute('aria-hidden', 'false');
418
+ closeBtn.focus();
419
+ document.body.style.overflow = 'hidden';
420
+ }
421
+ return;
422
+ }
416
423
 
417
- // Close modal when X button is clicked
418
- closeBtn.addEventListener('click', function (e) {
424
+ if (e.target && (e.target as Element).closest('#admin-modal-close')) {
419
425
  e.preventDefault();
420
426
  closeModal();
421
- });
427
+ return;
428
+ }
422
429
 
423
- // Close modal when clicking outside the iframe
424
- modal.addEventListener('click', function (e) {
425
- if (e.target === modal) {
426
- closeModal();
427
- }
428
- });
430
+ if (e.target && (e.target as Element).id === 'admin-modal') {
431
+ closeModal();
432
+ return;
433
+ }
434
+ });
429
435
 
430
- // Close modal with Escape key
431
- document.addEventListener('keydown', function (e) {
432
- if (e.key === 'Escape' && !modal.classList.contains('hidden')) {
436
+ document.addEventListener('keydown', function (e) {
437
+ if (e.key === 'Escape') {
438
+ const modal = document.getElementById('admin-modal');
439
+ if (modal && !modal.classList.contains('hidden')) {
433
440
  closeModal();
434
441
  }
435
- });
436
-
437
- function closeModal() {
438
- modal?.classList.add('hidden');
439
- modal?.setAttribute('aria-hidden', 'true');
440
-
441
- // Restore body scroll
442
- document.body.style.overflow = '';
443
-
444
- // Return focus to heart button for accessibility
445
- heartBtn?.focus();
446
442
  }
443
+ });
447
444
 
448
- // Handle iframe load events for better UX
449
- //iframe.addEventListener('load', function () {
450
- // console.log('Admin panel loaded successfully');
451
- //});
445
+ function closeModal() {
446
+ const modal = document.getElementById('admin-modal');
447
+ const heartBtn = document.getElementById('admin-heart-btn');
452
448
 
453
- iframe.addEventListener('error', function () {
454
- console.error('Failed to load admin panel');
455
- });
449
+ modal?.classList.add('hidden');
450
+ modal?.setAttribute('aria-hidden', 'true');
451
+ document.body.style.overflow = '';
452
+ heartBtn?.focus();
456
453
  }
457
- });
454
+
455
+ // iframe error handling
456
+ document.addEventListener('DOMContentLoaded', function () {
457
+ const iframe = document.getElementById('admin-sysop-iframe');
458
+ if (iframe) {
459
+ iframe.addEventListener('error', function () {
460
+ console.error('Failed to load admin panel');
461
+ });
462
+ }
463
+ });
464
+ }
458
465
  </script>