astro-tractstack 2.0.3 → 2.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "Astro integration for TractStack - redeeming the web from boring experiences",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,15 +22,16 @@ const getWidgetElement = (
22
22
  classNames: string
23
23
  ): ReactElement | null => {
24
24
  const { hook, value1, value2, value3, nodeId } = props;
25
- if (!hook || !value1) return null;
25
+ if (!hook) return null;
26
26
 
27
27
  switch (hook) {
28
28
  case 'youtube':
29
- return value2 ? (
30
- <div className={`${classNames} pointer-events-none`}>
31
- <YouTubeWrapper embedCode={value1} title={value2} />
32
- </div>
33
- ) : null;
29
+ if (value1)
30
+ return value2 ? (
31
+ <div className={`${classNames} pointer-events-none`}>
32
+ <YouTubeWrapper embedCode={value1} title={value2} />
33
+ </div>
34
+ ) : null;
34
35
 
35
36
  case 'signup':
36
37
  return (
@@ -45,19 +46,21 @@ const getWidgetElement = (
45
46
  );
46
47
 
47
48
  case 'belief':
48
- return value2 ? (
49
- <div className={`${classNames} pointer-events-none`}>
50
- <Belief value={{ slug: value1, scale: value2, extra: value3 }} />
51
- </div>
52
- ) : null;
49
+ if (value1)
50
+ return value2 ? (
51
+ <div className={`${classNames} pointer-events-none`}>
52
+ <Belief value={{ slug: value1, scale: value2, extra: value3 }} />
53
+ </div>
54
+ ) : null;
53
55
 
54
56
  case 'identifyAs':
55
- return value2 ? (
56
- <IdentifyAs
57
- classNames={`${classNames} pointer-events-none`}
58
- value={{ slug: value1, target: value2, extra: value3 || `` }}
59
- />
60
- ) : null;
57
+ if (value1)
58
+ return value2 ? (
59
+ <IdentifyAs
60
+ classNames={`${classNames} pointer-events-none`}
61
+ value={{ slug: value1, target: value2, extra: value3 || `` }}
62
+ />
63
+ ) : null;
61
64
 
62
65
  case 'toggle':
63
66
  return value2 ? (
@@ -90,9 +93,16 @@ const getWidgetElement = (
90
93
  <p className="text-sm font-bold text-gray-700">
91
94
  Interactive Disclosure
92
95
  </p>
93
- <p className="mt-1 text-xs text-gray-500">
94
- Belief Trigger: <code className="font-bold">{value1}</code>
95
- </p>
96
+ {value1 ? (
97
+ <p className="mt-1 text-xs text-gray-500">
98
+ Mode: Belief-Driven (<code className="font-bold">{value1}</code>
99
+ )
100
+ </p>
101
+ ) : (
102
+ <p className="mt-1 text-xs text-gray-500">
103
+ Mode: Open (Custom Actions)
104
+ </p>
105
+ )}
96
106
  </div>
97
107
  </div>
98
108
  );
@@ -187,21 +187,27 @@ const StoryFragmentOpenGraphPanel = ({
187
187
  (item) => item.type === 'Topic' && item.id === 'all-topics'
188
188
  );
189
189
 
190
- // Convert topic strings to Topic objects with mock IDs (since V2 doesn't expose topic IDs in content map)
190
+ // Convert topic strings to Topic objects with mock IDs
191
191
  const allTopicsArray = topicsContent?.topics || [];
192
192
  const topicsWithIds: Topic[] = allTopicsArray.map(
193
193
  (topicTitle, index) => ({
194
- id: index + 1, // Mock ID - in V2 we don't have access to actual topic IDs from content map
194
+ id: index + 1,
195
195
  title: topicTitle,
196
196
  })
197
197
  );
198
198
 
199
199
  setExistingTopics(topicsWithIds);
200
200
 
201
+ // Prioritize the description from the definitive fullContentMap
202
+ const sfContent = $contentMap.find(
203
+ (item) => item.type === 'StoryFragment' && item.id === nodeId
204
+ );
205
+ const initialDescription = sfContent?.description || '';
206
+ setDraftDetails(initialDescription);
207
+
201
208
  let initialTopics: Topic[] = [];
202
- let initialDescription = '';
203
209
 
204
- // Check stored draft data first
210
+ // Check stored draft data for topics
205
211
  if (storedData) {
206
212
  initialTopics = Array.isArray(storedData.topics)
207
213
  ? storedData.topics.map((t) => ({
@@ -209,29 +215,18 @@ const StoryFragmentOpenGraphPanel = ({
209
215
  title: t.title,
210
216
  }))
211
217
  : [];
212
- initialDescription = storedData.description || '';
213
218
  setDraftTopics(initialTopics);
214
- setDraftDetails(initialDescription);
219
+ } else if (sfContent && sfContent.topics && sfContent.topics.length > 0) {
220
+ // Fall back to content map data for initial topics if no draft exists
221
+ initialTopics = sfContent.topics.map((topicTitle) => {
222
+ const existingTopic = topicsWithIds.find(
223
+ (t) => t.title.toLowerCase() === topicTitle.toLowerCase()
224
+ );
225
+ return existingTopic || { id: -1, title: topicTitle };
226
+ });
227
+ setDraftTopics(initialTopics);
215
228
  } else {
216
- // Fall back to content map data
217
- const sfContent = $contentMap.find(
218
- (item) => item.type === 'StoryFragment' && item.id === nodeId
219
- );
220
-
221
- if (sfContent && sfContent.topics && sfContent.topics.length > 0) {
222
- initialTopics = sfContent.topics.map((topicTitle) => {
223
- const existingTopic = topicsWithIds.find(
224
- (t) => t.title.toLowerCase() === topicTitle.toLowerCase()
225
- );
226
- return existingTopic || { id: -1, title: topicTitle };
227
- });
228
- initialDescription = sfContent.description || '';
229
- setDraftTopics(initialTopics);
230
- setDraftDetails(initialDescription);
231
- } else {
232
- setDraftTopics([]);
233
- setDraftDetails('');
234
- }
229
+ setDraftTopics([]);
235
230
  }
236
231
 
237
232
  if (initialState.current) {
@@ -124,7 +124,6 @@ const DisclosureItemEditor = ({
124
124
  item,
125
125
  onUpdate,
126
126
  onToggle,
127
- config,
128
127
  onMoveUp,
129
128
  onMoveDown,
130
129
  isFirst,
@@ -133,7 +132,6 @@ const DisclosureItemEditor = ({
133
132
  item: DisclosureItem;
134
133
  onUpdate: (updates: Partial<DisclosureItem>) => void;
135
134
  onToggle: () => void;
136
- config: BrandConfig;
137
135
  onMoveUp: () => void;
138
136
  onMoveDown: () => void;
139
137
  isFirst: boolean;
@@ -258,6 +256,7 @@ export default function InteractiveDisclosureWidget({
258
256
  onUpdate,
259
257
  config,
260
258
  }: InteractiveDisclosureWidgetProps) {
259
+ const [mode, setMode] = useState<'belief' | 'open'>('belief');
261
260
  const [beliefs, setBeliefs] = useState<BeliefNode[]>([]);
262
261
  const [selectedBeliefTag, setSelectedBeliefTag] = useState<string>('');
263
262
  const [disclosures, setDisclosures] = useState<DisclosureItem[]>([]);
@@ -276,14 +275,16 @@ export default function InteractiveDisclosureWidget({
276
275
  const beliefTag = String(node.codeHookParams?.[0] || '');
277
276
  const payloadJson = String(node.codeHookParams?.[1] || '');
278
277
 
279
- if (beliefs.length === 0 && beliefTag && beliefTag !== 'BELIEF') {
280
- return;
278
+ if (beliefTag && beliefTag !== 'BELIEF') {
279
+ setMode('belief');
280
+ } else {
281
+ setMode('open');
281
282
  }
282
283
 
283
284
  setSelectedBeliefTag(beliefTag && beliefTag !== 'BELIEF' ? beliefTag : '');
284
285
  const currentBelief = beliefs.find((b) => b.slug === beliefTag);
285
286
 
286
- if (payloadJson && currentBelief) {
287
+ if (payloadJson) {
287
288
  try {
288
289
  const parsed = JSON.parse(payloadJson);
289
290
  setWidgetStyles(
@@ -296,50 +297,62 @@ export default function InteractiveDisclosureWidget({
296
297
  const loadedDisclosures =
297
298
  (parsed.disclosures as StoredDisclosureItem[]) || [];
298
299
 
299
- const scaleKeys =
300
- currentBelief.scale === 'custom'
301
- ? (currentBelief.customValues || []).map((v) => ({
302
- slug: v,
303
- name: v,
304
- }))
305
- : heldBeliefsScales[
306
- currentBelief.scale as keyof typeof heldBeliefsScales
307
- ] || [];
308
-
309
- const actionCommand =
310
- currentBelief.scale === 'custom' ? 'identifyAs' : 'declare';
311
- const finalDisclosures: DisclosureItem[] = loadedDisclosures.map(
312
- (loadedItem) => {
313
- const isFromScale = scaleKeys.some(
314
- (sk) => sk.slug === loadedItem.beliefValue
315
- );
316
-
317
- return {
300
+ if (currentBelief) {
301
+ const scaleKeys =
302
+ currentBelief.scale === 'custom'
303
+ ? (currentBelief.customValues || []).map((v) => ({
304
+ slug: v,
305
+ name: v,
306
+ }))
307
+ : heldBeliefsScales[
308
+ currentBelief.scale as keyof typeof heldBeliefsScales
309
+ ] || [];
310
+
311
+ const actionCommand =
312
+ currentBelief.scale === 'custom' ? 'identifyAs' : 'declare';
313
+ const finalDisclosures: DisclosureItem[] = loadedDisclosures.map(
314
+ (loadedItem) => {
315
+ const isFromScale = scaleKeys.some(
316
+ (sk) => sk.slug === loadedItem.beliefValue
317
+ );
318
+
319
+ return {
320
+ ...loadedItem,
321
+ id: generateId(),
322
+ isCustom: !isFromScale,
323
+ actionLisp: isFromScale
324
+ ? `(${actionCommand} ${beliefTag} ${quoteIfNecessary(actionCommand, loadedItem.beliefValue)})`
325
+ : loadedItem.actionLisp,
326
+ isDisabled: false,
327
+ };
328
+ }
329
+ );
330
+ scaleKeys.forEach(({ slug, name }) => {
331
+ if (!finalDisclosures.some((d) => d.beliefValue === slug)) {
332
+ finalDisclosures.push({
333
+ id: generateId(),
334
+ beliefValue: slug,
335
+ title: name,
336
+ description: '',
337
+ icon: 'chat-heart-fill',
338
+ actionLisp: `(${actionCommand} ${beliefTag} ${quoteIfNecessary(actionCommand, slug)})`,
339
+ isCustom: false,
340
+ isDisabled: true,
341
+ });
342
+ }
343
+ });
344
+ setDisclosures(finalDisclosures);
345
+ } else {
346
+ const finalDisclosures: DisclosureItem[] = loadedDisclosures.map(
347
+ (loadedItem) => ({
318
348
  ...loadedItem,
319
349
  id: generateId(),
320
- isCustom: !isFromScale,
321
- actionLisp: isFromScale
322
- ? `(${actionCommand} ${beliefTag} ${quoteIfNecessary(actionCommand, loadedItem.beliefValue)})`
323
- : loadedItem.actionLisp,
350
+ isCustom: true,
324
351
  isDisabled: false,
325
- };
326
- }
327
- );
328
- scaleKeys.forEach(({ slug, name }) => {
329
- if (!finalDisclosures.some((d) => d.beliefValue === slug)) {
330
- finalDisclosures.push({
331
- id: generateId(),
332
- beliefValue: slug,
333
- title: name,
334
- description: '',
335
- icon: 'chat-heart-fill',
336
- actionLisp: `(${actionCommand} ${beliefTag} ${quoteIfNecessary(actionCommand, slug)})`,
337
- isCustom: false,
338
- isDisabled: true,
339
- });
340
- }
341
- });
342
- setDisclosures(finalDisclosures);
352
+ })
353
+ );
354
+ setDisclosures(finalDisclosures);
355
+ }
343
356
  } catch (e) {
344
357
  console.error('Error parsing disclosure payload:', e);
345
358
  }
@@ -417,6 +430,25 @@ export default function InteractiveDisclosureWidget({
417
430
  setDisclosures(newDisclosures);
418
431
  };
419
432
 
433
+ const handleModeChange = (newMode: 'belief' | 'open') => {
434
+ if (mode === newMode) return;
435
+
436
+ setMode(newMode);
437
+ setSelectedBeliefTag('');
438
+ setDisclosures([]);
439
+ setWidgetStyles({
440
+ textColor: '#000000',
441
+ bgColor: '#ffffff',
442
+ bgOpacity: 100,
443
+ });
444
+
445
+ if (newMode === 'open') {
446
+ onUpdate(['', '{}']);
447
+ } else {
448
+ onUpdate(['BELIEF', '{}']);
449
+ }
450
+ };
451
+
420
452
  const moveDisclosure = (id: string, direction: 'up' | 'down') => {
421
453
  const index = disclosures.findIndex((d) => d.id === id);
422
454
  if (index === -1) return;
@@ -477,35 +509,66 @@ export default function InteractiveDisclosureWidget({
477
509
 
478
510
  return (
479
511
  <div className="space-y-4">
480
- <div className="flex items-center gap-2">
481
- <select
482
- value={selectedBeliefTag}
483
- onChange={(e) => handleBeliefChange(e.target.value)}
484
- className="flex-1 rounded-md border-gray-300 shadow-sm"
485
- disabled={hasRealSelection}
486
- >
487
- <option value="">Select a Belief...</option>
488
- {beliefs.map((b) => (
489
- <option key={b.slug} value={b.slug}>
490
- {b.title} ({b.scale})
491
- </option>
492
- ))}
493
- </select>
494
- {hasRealSelection && (
512
+ <div>
513
+ <label className="block text-xs font-bold text-gray-600">
514
+ Configuration Mode
515
+ </label>
516
+ <div className="isolate mt-1 inline-flex rounded-md shadow-sm">
495
517
  <button
496
518
  type="button"
497
- onClick={() => {
498
- setSelectedBeliefTag('');
499
- setDisclosures([]);
500
- onUpdate(['BELIEF', '{}']);
501
- }}
502
- className="rounded p-1 text-red-600 hover:bg-gray-100"
519
+ onClick={() => handleModeChange('belief')}
520
+ className={`relative inline-flex items-center rounded-l-md px-3 py-2 text-sm font-semibold ring-1 ring-inset ring-gray-300 focus:z-10 ${
521
+ mode === 'belief'
522
+ ? 'bg-cyan-600 text-white'
523
+ : 'bg-white text-gray-900 hover:bg-gray-50'
524
+ }`}
503
525
  >
504
- <XMarkIcon className="h-5 w-5" />
526
+ Belief-Driven
505
527
  </button>
506
- )}
528
+ <button
529
+ type="button"
530
+ onClick={() => handleModeChange('open')}
531
+ className={`relative -ml-px inline-flex items-center rounded-r-md px-3 py-2 text-sm font-semibold ring-1 ring-inset ring-gray-300 focus:z-10 ${
532
+ mode === 'open'
533
+ ? 'bg-cyan-600 text-white'
534
+ : 'bg-white text-gray-900 hover:bg-gray-50'
535
+ }`}
536
+ >
537
+ Open
538
+ </button>
539
+ </div>
507
540
  </div>
508
- {hasRealSelection && (
541
+ {mode === 'belief' && (
542
+ <div className="flex items-center gap-2">
543
+ <select
544
+ value={selectedBeliefTag}
545
+ onChange={(e) => handleBeliefChange(e.target.value)}
546
+ className="flex-1 rounded-md border-gray-300 shadow-sm"
547
+ disabled={hasRealSelection}
548
+ >
549
+ <option value="">Select a Belief...</option>
550
+ {beliefs.map((b) => (
551
+ <option key={b.slug} value={b.slug}>
552
+ {b.title} ({b.scale})
553
+ </option>
554
+ ))}
555
+ </select>
556
+ {hasRealSelection && (
557
+ <button
558
+ type="button"
559
+ onClick={() => {
560
+ setSelectedBeliefTag('');
561
+ setDisclosures([]);
562
+ onUpdate(['BELIEF', '{}']);
563
+ }}
564
+ className="rounded p-1 text-red-600 hover:bg-gray-100"
565
+ >
566
+ <XMarkIcon className="h-5 w-5" />
567
+ </button>
568
+ )}
569
+ </div>
570
+ )}
571
+ {(hasRealSelection || mode === 'open') && (
509
572
  <div className="mt-4 border-t border-gray-200 pt-4">
510
573
  <button
511
574
  type="button"
@@ -513,8 +576,8 @@ export default function InteractiveDisclosureWidget({
513
576
  className="flex w-full items-center justify-center rounded-md bg-gray-100 px-3 py-2 text-sm font-bold text-gray-700 hover:bg-gray-200"
514
577
  >
515
578
  <ChevronDownIcon className="mr-2 h-5 w-5" />
516
- Configure {disclosures.filter((d) => !d.isDisabled).length} of{' '}
517
- {disclosures.length} Disclosure(s) & Styles
579
+ Configure {disclosures.filter((d) => !d.isDisabled).length}{' '}
580
+ Disclosure(s) & Styles
518
581
  </button>
519
582
  </div>
520
583
  )}
@@ -546,7 +609,8 @@ export default function InteractiveDisclosureWidget({
546
609
  <div className="flex h-full flex-col">
547
610
  <div className="flex-shrink-0 border-b border-gray-200 bg-white px-6 py-3">
548
611
  <Dialog.Title className="text-lg font-bold text-gray-900">
549
- Disclosure Configuration: {selectedBelief?.title}
612
+ Disclosure Configuration
613
+ {selectedBelief && `: ${selectedBelief.title}`}
550
614
  </Dialog.Title>
551
615
  </div>
552
616
  <div className="flex-1 space-y-6 overflow-y-auto p-4">
@@ -614,7 +678,6 @@ export default function InteractiveDisclosureWidget({
614
678
  updateDisclosure(item.id, updates)
615
679
  }
616
680
  onToggle={() => toggleDisclosure(item.id)}
617
- config={config}
618
681
  onMoveUp={() => moveDisclosure(item.id, 'up')}
619
682
  onMoveDown={() => moveDisclosure(item.id, 'down')}
620
683
  isFirst={index === 0}
@@ -22,6 +22,7 @@ export interface Props {
22
22
  brandConfig?: any;
23
23
  storyfragmentId?: string;
24
24
  sessionId?: string;
25
+ description?: string;
25
26
  impressions?: ImpressionNode[];
26
27
  }
27
28
 
@@ -125,6 +125,9 @@ const ogImage = storyFragment?.socialImagePath
125
125
  const fullContentMap = await getFullContentMap(
126
126
  Astro.locals.tenant?.id || import.meta.env.PUBLIC_TENANTID || 'default'
127
127
  );
128
+ const description = fullContentMap.find(
129
+ (item) => item.id === storyFragmentID
130
+ )?.description;
128
131
  const urlParams: Record<string, string | boolean> = {};
129
132
  for (const [key, value] of Astro.url.searchParams) {
130
133
  urlParams[key] = value === '' ? true : value;
@@ -133,6 +136,7 @@ for (const [key, value] of Astro.url.searchParams) {
133
136
 
134
137
  <Layout
135
138
  title={title}
139
+ description={description}
136
140
  slug={slug}
137
141
  canonicalURL={canonicalURL}
138
142
  pubDatetime={pubDatetime}
@@ -219,5 +223,5 @@ for (const [key, value] of Astro.url.searchParams) {
219
223
 
220
224
  <script>
221
225
  import { setupLayoutObservers } from '@/utils/layout';
222
- setupLayoutObservers();
226
+ document.addEventListener('astro:page-load', setupLayoutObservers);
223
227
  </script>
@@ -99,6 +99,9 @@ if (!fragmentsData) {
99
99
  }
100
100
 
101
101
  const fullContentMap = await getFullContentMap(tenantId);
102
+ const description = fullContentMap.find(
103
+ (item) => item.id === storyfragmentId
104
+ )?.description;
102
105
  const brandConfig = await getBrandConfig(tenantId);
103
106
 
104
107
  if (!brandConfig.SITE_INIT) {
@@ -118,6 +121,7 @@ paneIds.forEach((paneId: string) => {
118
121
 
119
122
  <Layout
120
123
  title={storyfragmentTitle}
124
+ description={description}
121
125
  slug={lookup || brandConfig.HOME_SLUG}
122
126
  ogImage={ogImage}
123
127
  menu={storyData.menu || null}
@@ -8,15 +8,41 @@ import {
8
8
  import { debounce } from '@/utils/helpers';
9
9
 
10
10
  let hasScrolledForSettingsPanel = false;
11
+ let currentPaneObserver: IntersectionObserver | null = null;
12
+ let settingsPanelSubscription: (() => void) | null = null;
13
+ let debouncedUpdateListener: (() => void) | null = null;
14
+
15
+ function cleanupLayoutObservers() {
16
+ if (currentPaneObserver) {
17
+ currentPaneObserver.disconnect();
18
+ currentPaneObserver = null;
19
+ }
20
+ if (settingsPanelSubscription) {
21
+ settingsPanelSubscription();
22
+ settingsPanelSubscription = null;
23
+ }
24
+ if (debouncedUpdateListener) {
25
+ window.removeEventListener('scroll', debouncedUpdateListener);
26
+ window.removeEventListener('resize', debouncedUpdateListener);
27
+ debouncedUpdateListener = null;
28
+ }
29
+ const storykeepHeader = document.getElementById('storykeepHeader');
30
+ if (storykeepHeader) {
31
+ document.body.style.paddingTop = '';
32
+ storykeepHeader.style.position = '';
33
+ storykeepHeader.style.top = '';
34
+ }
35
+ }
11
36
 
12
- // Replace your existing setupPaneObserver with this one.
13
37
  function setupPaneObserver() {
14
- let currentObserver: IntersectionObserver | null = null;
38
+ if (currentPaneObserver) {
39
+ currentPaneObserver.disconnect();
40
+ }
15
41
 
16
- settingsPanelStore.subscribe((signalValue) => {
17
- if (currentObserver) {
18
- currentObserver.disconnect();
19
- currentObserver = null;
42
+ settingsPanelSubscription = settingsPanelStore.subscribe((signalValue) => {
43
+ if (currentPaneObserver) {
44
+ currentPaneObserver.disconnect();
45
+ currentPaneObserver = null;
20
46
  }
21
47
 
22
48
  if (signalValue && signalValue.nodeId) {
@@ -28,7 +54,7 @@ function setupPaneObserver() {
28
54
  document.querySelector(`[data-node-id="${nodeId}"]`);
29
55
 
30
56
  if (targetElement) {
31
- currentObserver = new IntersectionObserver(
57
+ currentPaneObserver = new IntersectionObserver(
32
58
  ([entry]) => {
33
59
  const signal = settingsPanelStore.get();
34
60
  const now = Date.now();
@@ -41,7 +67,7 @@ function setupPaneObserver() {
41
67
  },
42
68
  { threshold: 0 }
43
69
  );
44
- currentObserver.observe(targetElement);
70
+ currentPaneObserver.observe(targetElement);
45
71
  }
46
72
  }, 100);
47
73
  }
@@ -49,6 +75,8 @@ function setupPaneObserver() {
49
75
  }
50
76
 
51
77
  export function setupLayoutObservers(): void {
78
+ cleanupLayoutObservers();
79
+
52
80
  const storykeepHeader = document.getElementById('storykeepHeader');
53
81
  const settingsControls = document.getElementById('settingsControls');
54
82
  const standardHeader = document.querySelector('header');
@@ -86,7 +114,7 @@ export function setupLayoutObservers(): void {
86
114
  }
87
115
  };
88
116
 
89
- const debouncedUpdate = debounce(() => {
117
+ debouncedUpdateListener = debounce(() => {
90
118
  updateStandardHeaderHeight();
91
119
  handleScroll();
92
120
  updatePanelPosition();
@@ -98,8 +126,8 @@ export function setupLayoutObservers(): void {
98
126
  }
99
127
  };
100
128
 
101
- window.addEventListener('scroll', debouncedUpdate, { passive: true });
102
- window.addEventListener('resize', debouncedUpdate);
129
+ window.addEventListener('scroll', debouncedUpdateListener, { passive: true });
130
+ window.addEventListener('resize', debouncedUpdateListener);
103
131
  settingsPanelOpenStore.subscribe(handleSettingsPanelChange);
104
132
 
105
133
  setupPaneObserver();
@@ -128,3 +156,5 @@ export function handleSettingsPanelMobile(isOpen: boolean): void {
128
156
  hasScrolledForSettingsPanel = false;
129
157
  }
130
158
  }
159
+
160
+ document.addEventListener('astro:before-swap', cleanupLayoutObservers);