astro-tractstack 2.0.0-rc.64 → 2.0.0-rc.66
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/dist/index.js +4 -8
- package/package.json +1 -1
- package/templates/src/client/app.js +127 -0
- package/templates/src/client/view.js +423 -0
- package/templates/src/components/edit/SettingsPanel.tsx +1 -1
- package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +76 -33
- package/templates/src/layouts/Layout.astro +24 -65
- package/templates/src/pages/[...slug]/edit.astro +1 -1
- package/templates/src/pages/[...slug].astro +1 -1
- package/templates/src/pages/context/[...contextSlug]/edit.astro +2 -8
- package/templates/src/pages/context/[...contextSlug].astro +1 -1
- package/utils/inject-files.ts +4 -8
- package/templates/src/client/analytics-events.js +0 -207
- package/templates/src/client/belief-events.js +0 -210
- package/templates/src/client/sse.js +0 -683
package/dist/index.js
CHANGED
|
@@ -1292,16 +1292,12 @@ async function w(t, e, c) {
|
|
|
1292
1292
|
dest: "public/client/htmx.min.js"
|
|
1293
1293
|
},
|
|
1294
1294
|
{
|
|
1295
|
-
src: t("../templates/src/client/
|
|
1296
|
-
dest: "public/client/
|
|
1295
|
+
src: t("../templates/src/client/view.js"),
|
|
1296
|
+
dest: "public/client/view.js"
|
|
1297
1297
|
},
|
|
1298
1298
|
{
|
|
1299
|
-
src: t("../templates/src/client/
|
|
1300
|
-
dest: "public/client/
|
|
1301
|
-
},
|
|
1302
|
-
{
|
|
1303
|
-
src: t("../templates/src/client/analytics-events.js"),
|
|
1304
|
-
dest: "public/client/analytics-events.js"
|
|
1299
|
+
src: t("../templates/src/client/app.js"),
|
|
1300
|
+
dest: "public/client/app.js"
|
|
1305
1301
|
},
|
|
1306
1302
|
// StoryKeep Editor (add new section)
|
|
1307
1303
|
{
|
package/package.json
CHANGED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TractStack Singleton Application Manager
|
|
3
|
+
*
|
|
4
|
+
* This script is loaded with `is:persist` and runs only once per session.
|
|
5
|
+
* Its purpose is to manage long-lived application state and services,
|
|
6
|
+
* primarily the Server-Sent Events (SSE) connection. It provides a stable
|
|
7
|
+
* API on `window.TractStackApp` for transient view scripts to interact with.
|
|
8
|
+
* It is completely decoupled from the DOM and HTMX.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const VERBOSE = false;
|
|
12
|
+
|
|
13
|
+
function log(message, ...args) {
|
|
14
|
+
if (VERBOSE) console.log('✅ SINGLETON [app.js]:', message, ...args);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function logError(message, ...args) {
|
|
18
|
+
if (VERBOSE) console.error('❌ SINGLETON [app.js]:', message, ...args);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!window.TractStackApp) {
|
|
22
|
+
log('INITIALIZING SINGLETON for the first time.');
|
|
23
|
+
|
|
24
|
+
const TractStackApp = {
|
|
25
|
+
config: {},
|
|
26
|
+
eventSource: null,
|
|
27
|
+
|
|
28
|
+
initialize(config) {
|
|
29
|
+
log('Initializing with config from first page load.', config);
|
|
30
|
+
this.config = config;
|
|
31
|
+
if (config.sessionId && !this.eventSource) {
|
|
32
|
+
this.startSSE();
|
|
33
|
+
} else {
|
|
34
|
+
log(
|
|
35
|
+
'SSE connection not started: missing sessionId or already connected.'
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
updateConfig(newConfig) {
|
|
41
|
+
const oldStoryfragmentId = this.config.storyfragmentId;
|
|
42
|
+
this.config = { ...this.config, ...newConfig };
|
|
43
|
+
log('Configuration updated due to page navigation.', {
|
|
44
|
+
newConfig,
|
|
45
|
+
storyfragmentIdChanged:
|
|
46
|
+
oldStoryfragmentId !== newConfig.storyfragmentId,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (this.config.sessionId && !this.eventSource) {
|
|
50
|
+
log(
|
|
51
|
+
'Session ID became available after navigation. Starting SSE connection.'
|
|
52
|
+
);
|
|
53
|
+
this.startSSE();
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
getConfig() {
|
|
58
|
+
return this.config;
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
startSSE() {
|
|
62
|
+
if (this.eventSource) {
|
|
63
|
+
log('Closing existing SSE connection before starting a new one.');
|
|
64
|
+
this.eventSource.close();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { backendUrl, sessionId, storyfragmentId, tenantId } = this.config;
|
|
68
|
+
if (!sessionId || !tenantId) {
|
|
69
|
+
logError('Cannot start SSE connection: missing sessionId or tenantId.');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const sseUrl = `${backendUrl}/api/v1/auth/sse?sessionId=${sessionId}&storyfragmentId=${storyfragmentId}&tenantId=${tenantId}`;
|
|
74
|
+
log('Attempting to establish SSE connection...', { url: sseUrl });
|
|
75
|
+
|
|
76
|
+
this.eventSource = new EventSource(sseUrl);
|
|
77
|
+
|
|
78
|
+
this.eventSource.onopen = () => {
|
|
79
|
+
log('SSE Connection opened successfully.');
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
this.eventSource.onerror = (error) => {
|
|
83
|
+
logError('SSE Connection error occurred.', error);
|
|
84
|
+
this.eventSource.close();
|
|
85
|
+
this.eventSource = null;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
this.eventSource.addEventListener('panes_updated', (event) => {
|
|
89
|
+
try {
|
|
90
|
+
const data = JSON.parse(event.data);
|
|
91
|
+
log('Received `panes_updated` event from server.', data);
|
|
92
|
+
|
|
93
|
+
log(
|
|
94
|
+
'Dispatching `tractstack:panes-updated` CustomEvent to the window.'
|
|
95
|
+
);
|
|
96
|
+
window.dispatchEvent(
|
|
97
|
+
new CustomEvent('tractstack:panes-updated', { detail: data })
|
|
98
|
+
);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
logError('Failed to parse `panes_updated` event data.', {
|
|
101
|
+
error,
|
|
102
|
+
rawData: event.data,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
window.TractStackApp = TractStackApp;
|
|
110
|
+
|
|
111
|
+
if (window.TRACTSTACK_CONFIG) {
|
|
112
|
+
window.TractStackApp.initialize(window.TRACTSTACK_CONFIG);
|
|
113
|
+
} else {
|
|
114
|
+
logError('Initial config not found at singleton creation time.');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
document.addEventListener('astro:page-load', () => {
|
|
118
|
+
log('`astro:page-load` detected. Updating internal config.');
|
|
119
|
+
if (window.TRACTSTACK_CONFIG) {
|
|
120
|
+
window.TractStackApp.updateConfig(window.TRACTSTACK_CONFIG);
|
|
121
|
+
} else {
|
|
122
|
+
logError(
|
|
123
|
+
'`astro:page-load` fired, but `window.TRACTSTACK_CONFIG` was not found!'
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -0,0 +1,423 @@
|
|
|
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
|
+
}, 150);
|
|
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
|
+
isPageInitialized = false;
|
|
365
|
+
paneViewTimes.clear();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (!window.tractstackViewLifecycleListenersAttached) {
|
|
369
|
+
log(
|
|
370
|
+
'Attaching one-time lifecycle listeners that persist across navigations.'
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
document.addEventListener('astro:before-preparation', resetViewState);
|
|
374
|
+
|
|
375
|
+
document.addEventListener('change', function (event) {
|
|
376
|
+
const target = event.target;
|
|
377
|
+
if (
|
|
378
|
+
target.matches &&
|
|
379
|
+
(target.matches('select[data-belief-id]') ||
|
|
380
|
+
target.matches('input[type="checkbox"][data-belief-id]'))
|
|
381
|
+
) {
|
|
382
|
+
const beliefId = target.getAttribute('data-belief-id');
|
|
383
|
+
const beliefType = target.getAttribute('data-belief-type');
|
|
384
|
+
const paneId = target.getAttribute('data-pane-id');
|
|
385
|
+
|
|
386
|
+
let beliefValue;
|
|
387
|
+
if (target.type === 'checkbox') {
|
|
388
|
+
// TEMPORARY HARDCODING: Use YES/NO for all toggles.
|
|
389
|
+
beliefValue = target.checked ? 'BELIEVES_YES' : 'BELIEVES_NO';
|
|
390
|
+
} else {
|
|
391
|
+
beliefValue = target.value;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
sendStateUpdate({
|
|
395
|
+
beliefId,
|
|
396
|
+
beliefType,
|
|
397
|
+
beliefValue,
|
|
398
|
+
paneId: paneId || '',
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
window.addEventListener('tractstack:panes-updated', (event) => {
|
|
404
|
+
log('Received `tractstack:panes-updated` event from Singleton.');
|
|
405
|
+
if (!window.TractStackApp) return;
|
|
406
|
+
const data = event.detail;
|
|
407
|
+
const currentConfig = window.TractStackApp.getConfig();
|
|
408
|
+
if (data.updates) {
|
|
409
|
+
data.updates.forEach((update) =>
|
|
410
|
+
processStoryfragmentUpdate(update, currentConfig)
|
|
411
|
+
);
|
|
412
|
+
} else {
|
|
413
|
+
processStoryfragmentUpdate(data, currentConfig);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
window.addEventListener('beforeunload', flushPendingPaneEvents);
|
|
418
|
+
|
|
419
|
+
window.tractstackViewLifecycleListenersAttached = true;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
initializeCurrentView();
|
|
423
|
+
document.addEventListener('astro:page-load', initializeCurrentView);
|
|
@@ -27,7 +27,7 @@ const SettingsPanel = ({ config, availableCodeHooks }: SettingsPanelProps) => {
|
|
|
27
27
|
|
|
28
28
|
return (
|
|
29
29
|
<div
|
|
30
|
-
className="bg-mydarkgrey flex h-full max-w-sm flex-col rounded-xl bg-opacity-20 p-0.5 backdrop-blur-sm"
|
|
30
|
+
className="bg-mydarkgrey min-w-xs flex h-full max-w-sm flex-col rounded-xl bg-opacity-20 p-0.5 backdrop-blur-sm"
|
|
31
31
|
style={
|
|
32
32
|
{
|
|
33
33
|
animation: window.matchMedia(
|