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.
- package/LICENSE +8 -97
- package/README.md +7 -5
- package/bin/create-tractstack.js +35 -11
- package/dist/index.js +106 -29
- package/package.json +10 -5
- package/templates/css/frontend.css +1 -1
- package/templates/custom/minimal/CodeHook.astro +13 -12
- package/templates/custom/minimal/CustomRoutes.astro +25 -31
- package/templates/custom/with-examples/CodeHook.astro +22 -11
- package/templates/custom/with-examples/CustomRoutes.astro +4 -8
- package/templates/custom/with-examples/ProductCard.astro +29 -0
- package/templates/custom/with-examples/ProductCardWrapper.astro +43 -0
- package/templates/custom/with-examples/ProductGrid.astro +64 -0
- package/templates/custom/with-examples/pages/Collections.astro +58 -98
- package/templates/gitignore +42 -0
- package/templates/prettierignore +5 -0
- package/templates/prettierrc +19 -0
- package/templates/src/client/app.js +127 -0
- package/templates/src/client/htmx.min.js +3519 -0
- package/templates/src/client/view.js +429 -0
- package/templates/src/components/Footer.astro +4 -9
- package/templates/src/components/Header.astro +67 -60
- package/templates/src/components/Menu.tsx +188 -52
- package/templates/src/components/codehooks/BunnyVideoSetup.tsx +2 -2
- package/templates/src/components/codehooks/EpinetDurationSelector.tsx +9 -13
- package/templates/src/components/codehooks/EpinetTableView.tsx +11 -7
- package/templates/src/components/codehooks/EpinetWrapper.tsx +1 -0
- package/templates/src/components/codehooks/FeaturedArticle.astro +105 -0
- package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +318 -0
- package/templates/src/components/codehooks/ListContent.astro +32 -162
- package/templates/src/components/codehooks/ListContentSetup.tsx +43 -138
- package/templates/src/components/codehooks/ProductCardSetup.tsx +152 -0
- package/templates/src/components/codehooks/ProductGridSetup.tsx +274 -0
- package/templates/src/components/codehooks/SearchWidget.tsx +453 -0
- package/templates/src/components/compositor/Node.tsx +3 -6
- package/templates/src/components/compositor/PanelVisibilityWrapper.tsx +21 -11
- package/templates/src/components/compositor/elements/BunnyVideo.tsx +21 -20
- package/templates/src/components/compositor/nodes/Pane.tsx +51 -21
- package/templates/src/components/compositor/nodes/RenderChildren.tsx +6 -1
- package/templates/src/components/compositor/nodes/Widget.tsx +16 -2
- package/templates/src/components/compositor/preview/FeaturedArticlePreview.tsx +155 -0
- package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +20 -1
- package/templates/src/components/edit/Header.tsx +10 -4
- package/templates/src/components/edit/PanelSwitch.tsx +11 -7
- package/templates/src/components/edit/SettingsPanel.tsx +29 -18
- package/templates/src/components/edit/ToolBar.tsx +1 -28
- package/templates/src/components/edit/ToolMode.tsx +45 -32
- package/templates/src/components/edit/pane/AddPanePanel_break.tsx +12 -2
- package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +8 -2
- package/templates/src/components/edit/pane/AddPanePanel_newAICopy_modal.tsx +1 -1
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +17 -27
- package/templates/src/components/edit/pane/PageGenSelector.tsx +16 -16
- package/templates/src/components/edit/pane/PageGenSpecial.tsx +26 -49
- package/templates/src/components/edit/pane/PageGen_preview.tsx +17 -2
- package/templates/src/components/edit/pane/PanePanel_path.tsx +2 -4
- package/templates/src/components/edit/pane/PanePanel_title.tsx +243 -76
- package/templates/src/components/edit/panels/StyleBreakPanel.tsx +17 -19
- package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +48 -37
- package/templates/src/components/edit/panels/StyleElementPanel_add.tsx +60 -55
- package/templates/src/components/edit/panels/StyleImagePanel_add.tsx +56 -50
- package/templates/src/components/edit/panels/StyleLiElementPanel_add.tsx +54 -47
- package/templates/src/components/edit/panels/StyleLinkPanel_add.tsx +54 -44
- package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +113 -138
- package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +54 -40
- package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +3 -3
- package/templates/src/components/edit/panels/StyleWidgetPanel_add.tsx +56 -49
- package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +14 -5
- package/templates/src/components/edit/state/SaveModal.tsx +316 -169
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +1 -1
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +56 -55
- package/templates/src/components/edit/widgets/BunnyWidget.tsx +538 -59
- package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +656 -0
- package/templates/src/components/edit/widgets/ToggleWidget.tsx +9 -16
- package/templates/src/components/fields/ArtpackImage.tsx +4 -1
- package/templates/src/components/fields/BackgroundImage.tsx +1 -1
- package/templates/src/components/fields/BackgroundImageWrapper.tsx +127 -35
- package/templates/src/components/fields/ColorPickerCombo.tsx +66 -62
- package/templates/src/components/fields/ImageUpload.tsx +1 -1
- package/templates/src/components/fields/ViewportComboBox.tsx +59 -42
- package/templates/src/components/form/ActionBuilderBeliefSelector.tsx +117 -0
- package/templates/src/components/form/ActionBuilderField.tsx +306 -87
- package/templates/src/components/search/SearchModal.tsx +420 -0
- package/templates/src/components/search/SearchResults.tsx +367 -0
- package/templates/src/components/search/SearchWrapper.tsx +46 -0
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +1 -1
- package/templates/src/components/storykeep/Dashboard_Analytics.tsx +34 -8
- package/templates/src/components/storykeep/Dashboard_Content.tsx +6 -0
- package/templates/src/components/storykeep/StoryKeepBackdrop.astro +87 -0
- package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +37 -33
- package/templates/src/components/storykeep/controls/content/MenuForm.tsx +55 -7
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +17 -2
- package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +5 -8
- package/templates/src/components/storykeep/state/FetchAnalytics.tsx +274 -228
- package/templates/src/components/storykeep/widgets/Wizard.tsx +14 -7
- package/templates/src/components/tenant/RegistrationForm.tsx +1 -1
- package/templates/src/components/widgets/ImpressionWrapper.tsx +0 -1
- package/templates/src/constants/shapes.ts +9 -0
- package/templates/src/constants.ts +2121 -16
- package/templates/src/hooks/useSearch.ts +228 -0
- package/templates/src/layouts/Layout.astro +213 -104
- package/templates/src/lib/storyData.ts +4 -1
- package/templates/src/pages/[...slug]/edit.astro +14 -14
- package/templates/src/pages/[...slug].astro +82 -21
- package/templates/src/pages/api/orphan-analysis.ts +0 -1
- package/templates/src/pages/api/tailwind.ts +23 -21
- package/templates/src/pages/context/[...contextSlug]/edit.astro +14 -14
- package/templates/src/pages/context/[...contextSlug].astro +7 -2
- package/templates/src/pages/storykeep/advanced.astro +5 -4
- package/templates/src/pages/storykeep/branding.astro +5 -4
- package/templates/src/pages/storykeep/content.astro +5 -4
- package/templates/src/pages/storykeep/init.astro +40 -1
- package/templates/src/pages/storykeep/login.astro +1 -1
- package/templates/src/pages/storykeep.astro +5 -4
- package/templates/src/stores/nodes.ts +59 -88
- package/templates/src/stores/orphanAnalysis.ts +19 -21
- package/templates/src/stores/storykeep.ts +7 -0
- package/templates/src/types/compositorTypes.ts +6 -0
- package/templates/src/types/tractstack.ts +17 -0
- package/templates/src/utils/actions/lispLexer.ts +2 -2
- package/templates/src/utils/actions/preParse_Action.ts +3 -0
- package/templates/src/utils/api/beliefHelpers.ts +12 -36
- package/templates/src/utils/api/menuHelpers.ts +2 -2
- package/templates/src/utils/api.ts +26 -0
- package/templates/src/utils/compositor/TemplateNodes.ts +7 -0
- package/templates/src/utils/compositor/allowInsert.ts +5 -3
- package/templates/src/utils/compositor/nodesHelper.ts +4 -0
- package/templates/src/utils/compositor/processMarkdown.ts +16 -2
- package/templates/src/utils/compositor/reduceNodesClassNames.ts +4 -0
- package/templates/src/utils/compositor/templateMarkdownStyles.ts +13 -13
- package/templates/src/utils/compositor/typeGuards.ts +1 -0
- package/templates/src/utils/customHelpers.ts +38 -0
- package/templates/src/utils/helpers.ts +2 -2
- package/templates/src/utils/layout.ts +65 -144
- package/utils/inject-files.ts +95 -18
- package/templates/src/client/analytics-events.js +0 -207
- package/templates/src/client/belief-events.js +0 -191
- package/templates/src/client/sse.js +0 -613
- package/templates/src/components/codehooks/FeaturedContent.astro +0 -273
- package/templates/src/components/codehooks/FeaturedContentSetup.tsx +0 -738
- package/templates/src/components/compositor/preview/FeaturedContentPreview.tsx +0 -128
- package/templates/src/components/edit/pane/PanePanel_slug.tsx +0 -219
|
@@ -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-
|
|
151
|
+
<div class="px-4 text-center text-2xl md:px-12">
|
|
157
152
|
Copyright © {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
|
|
162
|
+
class="text-myblue my-2 flex flex-row items-center justify-center md:flex-col"
|
|
168
163
|
>
|
|
169
|
-
<div class="px-
|
|
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
|
|
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
|
-
{
|
|
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.
|
|
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 -
|
|
398
|
-
|
|
399
|
-
|
|
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.
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
});
|
|
430
|
+
if (e.target && (e.target as Element).id === 'admin-modal') {
|
|
431
|
+
closeModal();
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
});
|
|
429
435
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
//});
|
|
445
|
+
function closeModal() {
|
|
446
|
+
const modal = document.getElementById('admin-modal');
|
|
447
|
+
const heartBtn = document.getElementById('admin-heart-btn');
|
|
452
448
|
|
|
453
|
-
|
|
454
|
-
|
|
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>
|