astro-tractstack 2.0.0-rc.25 → 2.0.0-rc.26

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.0-rc.25",
3
+ "version": "2.0.0-rc.26",
4
4
  "description": "Astro integration for TractStack - redeeming the web from boring experiences",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -312,7 +312,7 @@ const getElement = (
312
312
  case 'impression':
313
313
  return <></>;
314
314
  default:
315
- console.warn(`Node.tsx miss on ${type}`);
315
+ console.warn(`Node.tsx miss on ${type}`, node);
316
316
  return <></>;
317
317
  }
318
318
  };
@@ -1,4 +1,4 @@
1
- import { useEffect, useState, memo, type CSSProperties } from 'react';
1
+ import { useEffect, useState, memo, Fragment, type CSSProperties } from 'react';
2
2
  import { getCtx } from '@/stores/nodes';
3
3
  import { viewportKeyStore } from '@/stores/storykeep';
4
4
  import { RenderChildren } from './RenderChildren';
@@ -27,7 +27,7 @@ const CodeHookContainer = ({
27
27
  {Object.entries(payload.params).map(
28
28
  ([key, value]) =>
29
29
  value && (
30
- <>
30
+ <Fragment key={key}>
31
31
  <span className="min-w-24 font-bold text-gray-600">{key}:</span>
32
32
  <div className="ml-2 flex flex-wrap gap-1">
33
33
  {value.split('|').map((item, index) => (
@@ -39,7 +39,7 @@ const CodeHookContainer = ({
39
39
  </span>
40
40
  ))}
41
41
  </div>
42
- </>
42
+ </Fragment>
43
43
  )
44
44
  )}
45
45
  </div>
@@ -13,6 +13,7 @@ import {
13
13
  viewportModeStore,
14
14
  setViewportMode,
15
15
  settingsPanelStore,
16
+ pendingHomePageSlugStore,
16
17
  } from '@/stores/storykeep';
17
18
  import { getCtx, ROOT_NODE_NAME } from '@/stores/nodes';
18
19
  import SaveModal from '@/components/edit/state/SaveModal';
@@ -24,6 +25,7 @@ interface StoryKeepHeaderProps {
24
25
 
25
26
  const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
26
27
  const viewport = useStore(viewportModeStore);
28
+ const pendingHomePageSlug = useStore(pendingHomePageSlugStore);
27
29
  const ctx = getCtx();
28
30
  const hasTitle = useStore(ctx.hasTitle);
29
31
  const hasPanes = useStore(ctx.hasPanes);
@@ -61,7 +63,8 @@ const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
61
63
  };
62
64
 
63
65
  const handleVisitPage = () => {
64
- if (canUndo) {
66
+ const hasChanges = canUndo || pendingHomePageSlug;
67
+ if (hasChanges) {
65
68
  if (
66
69
  confirm(
67
70
  'You have unsaved changes. Do you want to visit the page anyway?'
@@ -89,6 +92,9 @@ const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
89
92
  { value: 'desktop', Icon: ComputerDesktopIcon, title: 'Desktop Viewport' },
90
93
  ];
91
94
 
95
+ // Show save button if there are undo changes OR pending home page change
96
+ const shouldShowSave = canUndo || pendingHomePageSlug;
97
+
92
98
  if (!hasTitle && !hasPanes) return null;
93
99
 
94
100
  return (
@@ -127,7 +133,7 @@ const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
127
133
  className={`${iconClassName} relative`}
128
134
  >
129
135
  <GlobeAltIcon />
130
- {canUndo && (
136
+ {shouldShowSave && (
131
137
  <ExclamationTriangleIcon className="absolute -right-1 -top-1 h-3 w-3 rounded-full bg-white text-amber-500" />
132
138
  )}
133
139
  </button>
@@ -156,7 +162,7 @@ const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
156
162
  </div>
157
163
  )}
158
164
 
159
- {canUndo && (
165
+ {shouldShowSave && (
160
166
  <div className="flex flex-wrap items-center justify-center gap-2 text-sm">
161
167
  <button
162
168
  onClick={handleSave}
@@ -17,13 +17,13 @@ const SettingsPanel = ({ config, availableCodeHooks }: SettingsPanelProps) => {
17
17
  const ctx = getCtx();
18
18
  const { value: toolModeVal } = useStore(ctx.toolModeValStore);
19
19
 
20
- if (toolModeVal !== `styles` || !signal) {
20
+ if (toolModeVal !== 'styles' || !signal) {
21
21
  return null;
22
22
  }
23
23
 
24
24
  return (
25
25
  <div
26
- className="bg-mydarkgrey max-w-md rounded-xl bg-opacity-20 p-0.5 backdrop-blur-sm"
26
+ className="bg-mydarkgrey flex h-full max-w-sm flex-col rounded-xl bg-opacity-20 p-0.5 backdrop-blur-sm"
27
27
  style={
28
28
  {
29
29
  animation: window.matchMedia(
@@ -37,26 +37,30 @@ const SettingsPanel = ({ config, availableCodeHooks }: SettingsPanelProps) => {
37
37
  }
38
38
  >
39
39
  <style>{`
40
- @keyframes fadeInFromHalf {
41
- 0% { opacity: var(--fade-start, 0.5); }
42
- 100% { opacity: var(--fade-end, 1); }
43
- }
44
- `}</style>
40
+ @keyframes fadeInFromHalf {
41
+ 0% { opacity: var(--fade-start, 0.5); }
42
+ 100% { opacity: var(--fade-end, 1); }
43
+ }
44
+ `}</style>
45
45
  <div
46
- className="w-full rounded-lg border border-gray-200 bg-white p-1.5 shadow-xl md:p-2.5"
46
+ className="flex h-full min-h-0 w-full flex-col rounded-lg border border-gray-200 bg-white bg-opacity-85 shadow-xl"
47
47
  style={{ maxWidth: '90vw' }}
48
48
  >
49
- <div className="mb-4 flex items-center justify-between">
50
- <h3 className="text-myblue text-lg font-bold">{panelTitle}</h3>
51
- <button
52
- onClick={() => settingsPanelStore.set(null)}
53
- className="hover:text-myblue text-gray-500"
54
- >
55
- <XMarkIcon className="h-5 w-5" />
56
- </button>
49
+ {/* Header Section (fixed height) */}
50
+ <div className="flex-shrink-0 p-1.5 md:p-2.5">
51
+ <div className="mb-4 flex items-center justify-between">
52
+ <h3 className="text-myblue text-lg font-bold">{panelTitle}</h3>
53
+ <button
54
+ onClick={() => settingsPanelStore.set(null)}
55
+ className="hover:text-myblue text-gray-500"
56
+ >
57
+ <XMarkIcon className="h-5 w-5" />
58
+ </button>
59
+ </div>
57
60
  </div>
58
61
 
59
- <div className="space-y-4">
62
+ {/* Scrollable Content Section */}
63
+ <div className="min-h-0 flex-1 space-y-4 overflow-y-auto p-1.5 pt-0 md:p-2.5 md:pt-0">
60
64
  <div className="rounded bg-gray-50 p-1.5 md:p-2.5">
61
65
  <PanelSwitch
62
66
  config={config}
@@ -1,4 +1,4 @@
1
- import { useEffect } from 'react';
1
+ import { useEffect, useRef } from 'react';
2
2
  import { useStore } from '@nanostores/react';
3
3
  import PencilSquareIcon from '@heroicons/react/24/outline/PencilSquareIcon';
4
4
  import PaintBrushIcon from '@heroicons/react/24/outline/PaintBrushIcon';
@@ -8,8 +8,11 @@ import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
8
8
  import BugAntIcon from '@heroicons/react/24/outline/BugAntIcon';
9
9
  import { settingsPanelStore } from '@/stores/storykeep';
10
10
  import { getCtx } from '@/stores/nodes';
11
+ import { debounce } from '@/utils/helpers';
11
12
  import type { ToolModeVal } from '@/types/compositorTypes';
12
13
 
14
+ const SHORT_THRESHOLD = 650;
15
+
13
16
  const storykeepToolModes = [
14
17
  {
15
18
  key: 'styles' as const,
@@ -41,12 +44,6 @@ const storykeepToolModes = [
41
44
  title: 'Move',
42
45
  description: 'Keyboard accessible re-order',
43
46
  },
44
- {
45
- key: 'debug' as const,
46
- Icon: BugAntIcon,
47
- title: 'Debug',
48
- description: 'Debug node ids',
49
- },
50
47
  ] as const;
51
48
 
52
49
  interface StoryKeepToolModeProps {
@@ -54,9 +51,10 @@ interface StoryKeepToolModeProps {
54
51
  }
55
52
 
56
53
  const StoryKeepToolMode = ({ isContext }: StoryKeepToolModeProps) => {
57
- //const signal = useStore(settingsPanelStore);
58
54
  const ctx = getCtx();
59
55
  const { value: toolModeVal } = useStore(ctx.toolModeValStore);
56
+ const showGuids = useStore(ctx.showGuids);
57
+ const navRef = useRef<HTMLElement>(null);
60
58
 
61
59
  const hasTitle = useStore(ctx.hasTitle);
62
60
  const hasPanes = useStore(ctx.hasPanes);
@@ -64,6 +62,8 @@ const StoryKeepToolMode = ({ isContext }: StoryKeepToolModeProps) => {
64
62
  const className =
65
63
  'w-8 h-8 py-1 rounded-xl bg-white text-myblue hover:bg-mygreen/20 hover:text-black hover:rotate-3 cursor-pointer transition-all';
66
64
  const classNameActive = 'w-8 h-8 py-1.5 rounded-md bg-myblue text-white';
65
+ const classNameDebugActive =
66
+ 'w-8 h-8 py-1.5 rounded-md bg-orange-500 text-white';
67
67
 
68
68
  const currentToolMode =
69
69
  storykeepToolModes.find((mode) => mode.key === toolModeVal) ??
@@ -72,49 +72,109 @@ const StoryKeepToolMode = ({ isContext }: StoryKeepToolModeProps) => {
72
72
  const handleClick = (mode: ToolModeVal) => {
73
73
  settingsPanelStore.set(null);
74
74
  ctx.toolModeValStore.set({ value: mode });
75
- ctx.showGuids.set(mode === `debug`);
76
75
  ctx.notifyNode('root');
77
76
  };
78
77
 
79
- // Escape key listener
78
+ const handleDebugToggle = () => {
79
+ ctx.showGuids.set(!showGuids);
80
+ ctx.notifyNode('root');
81
+ };
82
+
80
83
  useEffect(() => {
81
84
  const handleEscapeKey = (event: KeyboardEvent) => {
82
85
  if (event.key === 'Escape') {
83
86
  ctx.toolModeValStore.set({ value: 'text' });
84
- console.log('Tool mode reset to text via Escape');
85
87
  }
86
88
  };
89
+
90
+ const toolModeNav = navRef.current;
91
+
92
+ // If the <nav> element hasn't been rendered yet, do nothing.
93
+ // The hook will re-run when hasTitle/hasPanes changes and it does render.
94
+ if (!toolModeNav) {
95
+ return;
96
+ }
97
+
98
+ const updateToolbarLayout = debounce(() => {
99
+ const isWideAndShort =
100
+ window.innerWidth >= 801 && window.innerHeight <= SHORT_THRESHOLD;
101
+ toolModeNav.classList.toggle('is-compact-widget', isWideAndShort);
102
+ }, 50);
103
+
87
104
  document.addEventListener('keydown', handleEscapeKey);
105
+ window.addEventListener('resize', updateToolbarLayout);
106
+
107
+ updateToolbarLayout(); // Initial check
108
+
88
109
  return () => {
89
110
  document.removeEventListener('keydown', handleEscapeKey);
111
+ window.removeEventListener('resize', updateToolbarLayout);
90
112
  };
91
- }, [ctx]);
113
+ // This dependency array is the key. The effect will re-run when the render conditions change.
114
+ }, [ctx, hasTitle, hasPanes, isContext]);
92
115
 
93
- if (!hasTitle || (!hasPanes && !isContext)) return null;
116
+ if (!hasTitle || (!hasPanes && !isContext)) {
117
+ return null;
118
+ }
94
119
 
95
120
  return (
96
- <nav
97
- id="mainNav"
98
- className="z-102 bg-mywhite fixed bottom-0 left-0 right-0 pt-1.5 md:sticky md:bottom-auto md:left-0 md:top-24 md:h-screen md:w-16 md:pt-0"
99
- >
100
- <div className="flex flex-wrap justify-around gap-4 py-3.5 md:mt-0 md:flex-col md:items-center md:gap-8 md:space-x-0 md:space-y-2 md:py-2">
101
- <div className="text-mydarkgrey h-16 text-center text-sm font-bold">
102
- mode:
103
- <div className="font-action text-myblue pt-1.5 text-center text-xs">
104
- {currentToolMode.title}
121
+ <>
122
+ <style>{`
123
+ #mainNav.is-compact-widget {
124
+ position: fixed;
125
+ bottom: 0.25rem;
126
+ left: 0rem;
127
+ top: auto;
128
+ right: auto;
129
+ height: auto;
130
+ width: auto;
131
+ padding: 0.5rem;
132
+ border-radius: 0 0.75rem 0.75rem 0;
133
+ box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
134
+ background-color: rgba(252, 252, 252, 0.7);
135
+ backdrop-filter: blur(4px);
136
+ border: 1px solid rgba(0, 0, 0, 0.05);
137
+ }
138
+ #mainNav.is-compact-widget > div {
139
+ flex-direction: row;
140
+ flex-wrap: nowrap;
141
+ align-items: center;
142
+ gap: 1rem;
143
+ margin: 0;
144
+ padding: 0;
145
+ height: auto;
146
+ }
147
+ `}</style>
148
+ <nav
149
+ id="mainNav"
150
+ ref={navRef}
151
+ className="z-102 bg-mywhite fixed bottom-0 left-0 right-0 pt-1.5 md:sticky md:bottom-auto md:left-0 md:top-24 md:h-screen md:w-16 md:pt-0"
152
+ >
153
+ <div className="flex flex-wrap justify-around gap-4 py-0.5 md:mt-0 md:flex-col md:items-center md:gap-8 md:space-x-0 md:space-y-2 md:py-2">
154
+ <div className="text-mydarkgrey text-center text-sm font-bold">
155
+ mode:
156
+ <div className="font-action text-myblue pt-1.5 text-center text-xs">
157
+ {currentToolMode.title}
158
+ </div>
105
159
  </div>
106
- </div>
107
- {storykeepToolModes.map(({ key, Icon, description }) => (
108
- <div title={description} key={key}>
109
- {key === toolModeVal ? (
110
- <Icon className={classNameActive} />
111
- ) : (
112
- <Icon className={className} onClick={() => handleClick(key)} />
113
- )}
160
+ {storykeepToolModes.map(({ key, Icon, description }) => (
161
+ <div title={description} key={key}>
162
+ {key === toolModeVal ? (
163
+ <Icon className={classNameActive} />
164
+ ) : (
165
+ <Icon className={className} onClick={() => handleClick(key)} />
166
+ )}
167
+ </div>
168
+ ))}
169
+ <div title="Toggle debug node ids" key="debug">
170
+ <BugAntIcon
171
+ className={showGuids ? classNameDebugActive : className}
172
+ onClick={handleDebugToggle}
173
+ />
114
174
  </div>
115
- ))}
116
- </div>
117
- </nav>
175
+ </div>
176
+ </nav>
177
+ </>
118
178
  );
119
179
  };
120
180
 
@@ -11,6 +11,7 @@ import {
11
11
  fullContentMapStore,
12
12
  getPendingImageOperation,
13
13
  clearPendingImageOperation,
14
+ pendingHomePageSlugStore,
14
15
  } from '@/stores/storykeep';
15
16
  import { startLoadingAnimation } from '@/utils/helpers';
16
17
  import type {
@@ -29,6 +30,7 @@ type SaveStage =
29
30
  | 'SAVING_STORY_FRAGMENTS'
30
31
  | 'LINKING_FILES'
31
32
  | 'PROCESSING_STYLES'
33
+ | 'UPDATING_HOME_PAGE'
32
34
  | 'COMPLETED'
33
35
  | 'ERROR';
34
36
 
@@ -61,13 +63,9 @@ export default function SaveModal({
61
63
  const [debugMessages, setDebugMessages] = useState<string[]>([]);
62
64
  const isSaving = useRef(false);
63
65
  const [isNavigating, setIsNavigating] = useState(false);
64
-
65
- // Determine if we're in create mode
66
66
  const isCreateMode = slug === 'create';
67
-
68
67
  const contentMap = fullContentMapStore.get();
69
-
70
- // Get backend URL
68
+ const pendingHomePageSlug = pendingHomePageSlugStore.get();
71
69
  const goBackend =
72
70
  import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
73
71
  const tenantId = import.meta.env.PUBLIC_TENANTID || 'default';
@@ -145,7 +143,8 @@ export default function SaveModal({
145
143
  if (
146
144
  relevantNodeCount === 0 &&
147
145
  nodesWithPendingFiles.length === 0 &&
148
- storyFragmentsWithPendingImages.length === 0
146
+ storyFragmentsWithPendingImages.length === 0 &&
147
+ !pendingHomePageSlug
149
148
  ) {
150
149
  addDebugMessage('No changes to save');
151
150
  setStage('COMPLETED');
@@ -614,6 +613,67 @@ export default function SaveModal({
614
613
  throw new Error(`Failed to process styles: ${errorMsg}`);
615
614
  }
616
615
 
616
+ // Check if we need to update home page
617
+ if (pendingHomePageSlug) {
618
+ setStage('UPDATING_HOME_PAGE');
619
+ setProgress(98);
620
+ addDebugMessage(`Updating home page to: ${pendingHomePageSlug}`);
621
+
622
+ try {
623
+ // First get current brand config
624
+ const response = await fetch(`${goBackend}/api/v1/config/brand`, {
625
+ method: 'GET',
626
+ headers: {
627
+ 'Content-Type': 'application/json',
628
+ 'X-Tenant-ID': tenantId,
629
+ },
630
+ credentials: 'include',
631
+ });
632
+
633
+ if (!response.ok) {
634
+ throw new Error(
635
+ `Failed to get current brand config: ${response.status}`
636
+ );
637
+ }
638
+
639
+ const currentBrandConfig = await response.json();
640
+
641
+ // Update HOME_SLUG
642
+ const updatedBrandConfig = {
643
+ ...currentBrandConfig,
644
+ HOME_SLUG: pendingHomePageSlug,
645
+ };
646
+
647
+ const updateResponse = await fetch(
648
+ `${goBackend}/api/v1/config/brand`,
649
+ {
650
+ method: 'PUT',
651
+ headers: {
652
+ 'Content-Type': 'application/json',
653
+ 'X-Tenant-ID': tenantId,
654
+ },
655
+ credentials: 'include',
656
+ body: JSON.stringify(updatedBrandConfig),
657
+ }
658
+ );
659
+
660
+ if (!updateResponse.ok) {
661
+ throw new Error(
662
+ `Failed to update home page: ${updateResponse.status}`
663
+ );
664
+ }
665
+
666
+ // Clear the pending operation
667
+ pendingHomePageSlugStore.set(null);
668
+ addDebugMessage('Home page updated successfully');
669
+ } catch (error) {
670
+ const errorMsg =
671
+ error instanceof Error ? error.message : 'Unknown error';
672
+ addDebugMessage(`Home page update failed: ${errorMsg}`);
673
+ throw new Error(`Failed to update home page: ${errorMsg}`);
674
+ }
675
+ }
676
+
617
677
  // Success!
618
678
  setStage('COMPLETED');
619
679
  setProgress(100);
@@ -658,6 +718,8 @@ export default function SaveModal({
658
718
  return 'Linking file relationships...';
659
719
  case 'PROCESSING_STYLES':
660
720
  return 'Processing styles...';
721
+ case 'UPDATING_HOME_PAGE':
722
+ return 'Updating home page...';
661
723
  case 'COMPLETED':
662
724
  return `${actionText} ${modeText.toLowerCase()} completed successfully!`;
663
725
  case 'ERROR':
@@ -1,8 +1,11 @@
1
1
  import { useState, useEffect, type ChangeEvent } from 'react';
2
+ import { useStore } from '@nanostores/react';
2
3
  import ExclamationTriangleIcon from '@heroicons/react/24/outline/ExclamationTriangleIcon';
3
4
  import CheckIcon from '@heroicons/react/24/outline/CheckIcon';
4
5
  import LockClosedIcon from '@heroicons/react/24/outline/LockClosedIcon';
6
+ import { Switch } from '@ark-ui/react/switch';
5
7
  import { getCtx } from '@/stores/nodes';
8
+ import { pendingHomePageSlugStore } from '@/stores/storykeep';
6
9
  import { cloneDeep } from '@/utils/helpers';
7
10
  import type { BrandConfig } from '@/types/tractstack';
8
11
  import {
@@ -28,6 +31,8 @@ const StoryFragmentSlugPanel = ({
28
31
  const [validationError, setValidationError] = useState<string | null>(null);
29
32
  const [canSave, setCanSave] = useState(false);
30
33
  const isHomeSlug = slug === config.HOME_SLUG;
34
+ const pendingHomePageSlug = useStore(pendingHomePageSlugStore);
35
+ const isSetAsHomePage = pendingHomePageSlug === slug;
31
36
 
32
37
  const ctx = getCtx();
33
38
  const allNodes = ctx.allNodes.get();
@@ -129,6 +134,14 @@ const StoryFragmentSlugPanel = ({
129
134
  }
130
135
  };
131
136
 
137
+ const handleSetAsHomePageChange = (details: { checked: boolean }) => {
138
+ if (details.checked) {
139
+ pendingHomePageSlugStore.set(slug);
140
+ } else {
141
+ pendingHomePageSlugStore.set(null);
142
+ }
143
+ };
144
+
132
145
  return (
133
146
  <div className="group mb-4 w-full rounded-b-md bg-white px-1.5 py-6">
134
147
  <div className="px-3.5">
@@ -194,72 +207,60 @@ const StoryFragmentSlugPanel = ({
194
207
  </span>
195
208
  </div>
196
209
  </div>
210
+
197
211
  {validationError && (
198
212
  <div className="mt-2 text-sm text-red-600">
199
213
  <ExclamationTriangleIcon className="mr-1 inline h-4 w-4" />
200
214
  {validationError}
201
215
  </div>
202
216
  )}
217
+
203
218
  {isHomeSlug && (
204
- <div className="mt-2 text-sm text-gray-600">
205
- <LockClosedIcon className="mr-1 inline h-4 w-4" />
206
- This is your home page slug and cannot be modified
219
+ <div className="mt-4">
220
+ <div className="inline-flex items-center rounded-full bg-blue-100 px-3 py-1.5 text-sm font-medium text-blue-800">
221
+ <LockClosedIcon className="mr-1.5 h-4 w-4" />
222
+ Home Page
223
+ </div>
224
+ <div className="mt-2 text-sm text-gray-600">
225
+ This is your current home page
226
+ </div>
207
227
  </div>
208
228
  )}
209
- <div className="mt-4 text-lg">
210
- <div className="text-gray-600">
211
- Create a clean, descriptive URL slug that helps users and search
212
- engines understand the page content.
213
- <ul className="ml-4 mt-1">
214
- <li>
215
- <CheckIcon className="inline h-4 w-4" /> Use hyphens to separate
216
- words
217
- </li>
218
- <li>
219
- <CheckIcon className="inline h-4 w-4" /> Keep it short and
220
- descriptive
221
- </li>
222
- <li>
223
- <CheckIcon className="inline h-4 w-4" /> Use only lowercase
224
- letters, numbers, and hyphens
225
- </li>
226
- <li>
227
- <CheckIcon className="inline h-4 w-4" /> Must start and end with
228
- a letter or number
229
- </li>
230
- </ul>
231
- </div>
232
- <div className="py-4">
233
- {!isHomeSlug && (
234
- <>
235
- {charCount < 3 && (
236
- <span className="text-red-500">
237
- Slug must be at least 3 characters
238
- </span>
239
- )}
240
- {charCount >= 3 && charCount < 5 && !validationError && (
241
- <span className="text-gray-500">
242
- Consider adding more characters for better description
243
- </span>
244
- )}
245
- {warning && !validationError && (
246
- <span className="text-yellow-500">
247
- Slug is getting long - consider shortening it
248
- </span>
249
- )}
250
- {isValid && canSave && charCount >= 5 && !validationError && (
251
- <span className="text-green-500">
252
- Good URL length and format!
253
- </span>
254
- )}
255
- {isValid && !canSave && !validationError && (
256
- <span className="text-gray-500">
257
- Valid characters but needs proper formatting to save
258
- </span>
259
- )}
260
- </>
229
+
230
+ {!isHomeSlug && isValid && canSave && (
231
+ <div className="mt-4">
232
+ <div className="flex items-center space-x-3">
233
+ <Switch.Root
234
+ checked={isSetAsHomePage}
235
+ onCheckedChange={handleSetAsHomePageChange}
236
+ className="flex items-center"
237
+ >
238
+ <Switch.Control
239
+ className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
240
+ isSetAsHomePage ? 'bg-cyan-600' : 'bg-gray-200'
241
+ }`}
242
+ >
243
+ <Switch.Thumb
244
+ className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out ${
245
+ isSetAsHomePage ? 'translate-x-5' : 'translate-x-0'
246
+ }`}
247
+ />
248
+ </Switch.Control>
249
+ <Switch.HiddenInput />
250
+ </Switch.Root>
251
+ <span className="text-sm text-gray-700">Set as Home Page</span>
252
+ </div>
253
+ {isSetAsHomePage && (
254
+ <div className="mt-2 text-sm text-cyan-600">
255
+ Will be set as home page when saved
256
+ </div>
261
257
  )}
262
258
  </div>
259
+ )}
260
+
261
+ <div className="mt-4 text-sm text-gray-600">
262
+ Create a clean, descriptive URL slug that helps users and search
263
+ engines understand the page content.
263
264
  </div>
264
265
  </div>
265
266
  </div>
@@ -201,12 +201,19 @@ for (const [key, value] of Astro.url.searchParams) {
201
201
  <!-- Floating Controls (Settings Panel & HUD OR ToolBar) -->
202
202
  <aside
203
203
  id="settingsControls"
204
- class="z-101 pointer-events-none fixed bottom-24 right-0 flex max-h-screen flex-col items-end gap-2 overflow-visible p-4 md:bottom-0"
204
+ class="z-101 pointer-events-none fixed bottom-16 right-2 flex flex-col items-end gap-2 md:bottom-2"
205
205
  >
206
- <div class="pointer-events-auto">
206
+ <div class="pointer-events-none flex-grow"></div>
207
+
208
+ {/* Toolbar's wrapper: Does not grow or shrink */}
209
+ <div class="pointer-events-auto flex-shrink-0">
207
210
  <StoryKeepToolBar client:only="react" />
208
211
  </div>
209
- <div class="pointer-events-auto">
212
+
213
+ {
214
+ /* Settings Panel's wrapper: Grows, shrinks, and will be positioned by our script */
215
+ }
216
+ <div class="pointer-events-auto max-h-full">
210
217
  <SettingsPanel
211
218
  config={brandConfig}
212
219
  availableCodeHooks={Object.keys(codeHookComponents)}
@@ -192,12 +192,19 @@ for (const [key, value] of Astro.url.searchParams) {
192
192
  <!-- Floating Controls (Settings Panel & HUD OR ToolBar) -->
193
193
  <aside
194
194
  id="settingsControls"
195
- class="z-101 pointer-events-none fixed bottom-24 right-0 flex max-h-screen flex-col items-end gap-2 overflow-y-auto p-4 md:bottom-0"
195
+ class="z-101 pointer-events-none fixed bottom-16 right-2 flex flex-col items-end gap-2 md:bottom-2"
196
196
  >
197
- <div class="pointer-events-auto">
197
+ <div class="pointer-events-none flex-grow"></div>
198
+
199
+ {/* Toolbar's wrapper: Does not grow or shrink */}
200
+ <div class="pointer-events-auto flex-shrink-0">
198
201
  <StoryKeepToolBar client:only="react" />
199
202
  </div>
200
- <div class="pointer-events-auto">
203
+
204
+ {
205
+ /* Settings Panel's wrapper: Grows, shrinks, and will be positioned by our script */
206
+ }
207
+ <div class="pointer-events-auto max-h-full">
201
208
  <SettingsPanel
202
209
  config={brandConfig}
203
210
  availableCodeHooks={Object.keys(codeHookComponents)}
@@ -40,11 +40,10 @@ if (initializing) {
40
40
  const title = 'Advanced | StoryKeep';
41
41
 
42
42
  let fullContentMap;
43
- let homeSlug = 'hello';
43
+ const homeSlug = brandConfig.HOME_SLUG || 'hello';
44
44
 
45
45
  try {
46
46
  fullContentMap = await getFullContentMap(tenantId);
47
- homeSlug = fullContentMap.find((item) => item.isHome)?.slug || 'hello';
48
47
  } catch (error) {
49
48
  return Astro.redirect(
50
49
  `/maint?from=${encodeURIComponent(Astro.url.pathname)}`
@@ -29,11 +29,10 @@ const initializing = !brandConfig.SITE_INIT;
29
29
  const title = 'Branding | StoryKeep';
30
30
 
31
31
  let fullContentMap;
32
- let homeSlug = 'hello';
32
+ const homeSlug = brandConfig.HOME_SLUG || 'hello';
33
33
 
34
34
  try {
35
35
  fullContentMap = await getFullContentMap(tenantId);
36
- homeSlug = fullContentMap.find((item) => item.isHome)?.slug || 'hello';
37
36
  } catch (error) {
38
37
  return Astro.redirect(
39
38
  `/maint?from=${encodeURIComponent(Astro.url.pathname)}`
@@ -36,11 +36,10 @@ const title = 'Content | StoryKeep';
36
36
  const createMenu = Astro.url.searchParams.has('create-menu');
37
37
 
38
38
  let fullContentMap;
39
- let homeSlug = 'hello';
39
+ const homeSlug = brandConfig.HOME_SLUG || 'hello';
40
40
 
41
41
  try {
42
42
  fullContentMap = await getFullContentMap(tenantId);
43
- homeSlug = fullContentMap.find((item) => item.isHome)?.slug || 'hello';
44
43
  } catch (error) {
45
44
  return Astro.redirect(
46
45
  `/maint?from=${encodeURIComponent(Astro.url.pathname)}`
@@ -35,14 +35,13 @@ if (initializing) {
35
35
 
36
36
  const title = 'Analytics | StoryKeep';
37
37
 
38
+ const homeSlug = brandConfig.HOME_SLUG || 'hello';
38
39
  let fullContentMap;
39
- let homeSlug = 'hello';
40
40
 
41
41
  try {
42
42
  fullContentMap = await getFullContentMap(
43
43
  Astro.locals.tenant?.id || import.meta.env.PUBLIC_TENANTID || 'default'
44
44
  );
45
- homeSlug = fullContentMap.find((item) => item.isHome)?.slug || 'hello';
46
45
  } catch (error) {
47
46
  return Astro.redirect(
48
47
  `/maint?from=${encodeURIComponent(Astro.url.pathname)}`
@@ -29,6 +29,8 @@ export const preferredThemeStore = atom<Theme>('light');
29
29
  export const hasAssemblyAIStore = atom<boolean>(false);
30
30
  export const codehookMapStore = atom<string[]>([]);
31
31
 
32
+ export const pendingHomePageSlugStore = atom<string | null>(null);
33
+
32
34
  // Tool mode types
33
35
  export type ToolModeVal =
34
36
  | 'styles'
@@ -1,188 +1,140 @@
1
1
  import {
2
2
  settingsPanelOpenStore,
3
+ settingsPanelStore,
3
4
  headerPositionStore,
4
5
  setHeaderPosition,
5
6
  setMobileHeaderFaded,
6
7
  } from '@/stores/storykeep';
8
+ import { debounce } from '@/utils/helpers';
7
9
 
8
- // Track whether initial scroll adjustment has been made for settings panel
9
10
  let hasScrolledForSettingsPanel = false;
10
11
 
11
- /**
12
- * Sets up CSS custom properties for dynamic layout positioning
13
- */
14
12
  export function setupLayoutStyles(): void {
15
- // Calculate and set CSS custom properties for positioning
16
13
  const updateBottomOffset = () => {
17
14
  const mobileNavHeight = window.innerWidth < 801 ? 80 : 0;
18
- const padding = 16;
15
+ const padding = 4;
19
16
  const offset = `${mobileNavHeight + padding}px`;
20
-
21
17
  document.documentElement.style.setProperty(
22
18
  '--bottom-right-controls-bottom-offset',
23
19
  offset
24
20
  );
25
21
  };
26
-
27
- // Set initial values
28
22
  updateBottomOffset();
29
-
30
- // Update on resize
31
23
  window.addEventListener('resize', updateBottomOffset);
32
24
  }
33
25
 
34
- /**
35
- * Sets up scroll observers for header positioning behavior
36
- */
26
+ // Replace your existing setupPaneObserver with this one.
27
+ function setupPaneObserver() {
28
+ let currentObserver: IntersectionObserver | null = null;
29
+
30
+ settingsPanelStore.subscribe((signalValue) => {
31
+ if (currentObserver) {
32
+ currentObserver.disconnect();
33
+ currentObserver = null;
34
+ }
35
+
36
+ if (signalValue && signalValue.nodeId) {
37
+ setTimeout(() => {
38
+ const { nodeId } = signalValue;
39
+
40
+ const targetElement =
41
+ document.getElementById(`pane-${nodeId}`) ||
42
+ document.querySelector(`[data-node-id="${nodeId}"]`);
43
+
44
+ if (targetElement) {
45
+ currentObserver = new IntersectionObserver(
46
+ ([entry]) => {
47
+ if (!entry.isIntersecting) {
48
+ settingsPanelStore.set(null);
49
+ }
50
+ },
51
+ { threshold: 0 }
52
+ );
53
+ currentObserver.observe(targetElement);
54
+ }
55
+ }, 100);
56
+ }
57
+ });
58
+ }
59
+
37
60
  export function setupLayoutObservers(): void {
38
- const header = document.getElementById('storykeepHeader');
61
+ const storykeepHeader = document.getElementById('storykeepHeader');
39
62
  const toolModeNav = document.getElementById('mainNav');
40
- const mainContent = document.getElementById('mainContent');
41
63
  const settingsControls = document.getElementById('settingsControls');
42
64
  const standardHeader = document.querySelector('header');
43
65
 
44
- if (!header) return;
66
+ if (!storykeepHeader || !settingsControls || !standardHeader) return;
45
67
 
46
- let headerHeight = 0;
47
- const updateHeaderHeight = () => {
48
- if (standardHeader) {
49
- headerHeight = standardHeader.offsetHeight;
50
- }
68
+ let standardHeaderHeight = 0;
69
+ const updateStandardHeaderHeight = () => {
70
+ standardHeaderHeight = standardHeader.offsetHeight;
51
71
  };
52
72
 
53
- const updateSettingsMargin = () => {
54
- if (settingsControls && header) {
55
- const storyKeepHeaderHeight = header.offsetHeight;
56
- const viewportHeight = window.innerHeight;
57
- const bottomOffset = window.innerWidth < 801 ? 96 : 16; // Mobile nav + padding
58
-
59
- // Set top margin to avoid StoryKeep header overlap only
60
- settingsControls.style.marginTop = `${storyKeepHeaderHeight + 20}px`;
61
-
62
- // Set max height for inner scroll
63
- const maxHeight =
64
- viewportHeight - storyKeepHeaderHeight - bottomOffset - 20;
65
- settingsControls.style.maxHeight = `${maxHeight}px`;
66
- }
73
+ const updatePanelPosition = () => {
74
+ const headerRect = storykeepHeader.getBoundingClientRect();
75
+ const panelTop = headerRect.bottom;
76
+ settingsControls.style.top = `${panelTop}px`;
67
77
  };
68
78
 
69
79
  const handleScroll = () => {
70
80
  const scrollY = window.scrollY;
71
- const shouldBeSticky = scrollY > headerHeight;
72
-
73
- // Only update header position if it actually needs to change
81
+ const shouldBeSticky = scrollY > standardHeaderHeight;
74
82
  const currentPosition = headerPositionStore.get();
75
83
  const newPosition = shouldBeSticky ? 'sticky' : 'normal';
76
84
 
77
85
  if (currentPosition !== newPosition) {
78
86
  setHeaderPosition(newPosition);
79
-
80
87
  if (shouldBeSticky) {
81
- if (header) {
82
- const headerHeight = header.offsetHeight;
83
- // Add padding to body to prevent layout shift
84
- document.body.style.paddingTop = `${headerHeight}px`;
85
-
86
- header.style.position = 'fixed';
87
- header.style.top = '0';
88
- header.style.left = '0';
89
- header.style.right = '0';
90
- header.style.zIndex = '101';
91
- }
88
+ document.body.style.paddingTop = `${storykeepHeader.offsetHeight}px`;
89
+ storykeepHeader.style.position = 'fixed';
90
+ storykeepHeader.style.top = '0';
92
91
  } else {
93
- if (header) {
94
- // Remove padding when header is not fixed
95
- document.body.style.paddingTop = '';
96
-
97
- header.style.position = '';
98
- header.style.top = '';
99
- header.style.left = '';
100
- header.style.right = '';
101
- header.style.zIndex = '';
102
- }
92
+ document.body.style.paddingTop = '';
93
+ storykeepHeader.style.position = '';
94
+ storykeepHeader.style.top = '';
103
95
  }
104
96
  }
105
97
 
106
- // Update tool mode nav position and main content margin
107
98
  if (toolModeNav && window.innerWidth >= 801) {
108
99
  if (shouldBeSticky) {
109
- // On desktop, make nav fixed when header is sticky
110
100
  toolModeNav.classList.remove('md:static');
111
101
  toolModeNav.classList.add('md:fixed');
112
- toolModeNav.style.top = '60px'; // Below fixed header
102
+ toolModeNav.style.top = '60px';
113
103
  toolModeNav.style.left = '0';
114
-
115
- // Add margin to main content when nav becomes fixed (nav no longer takes flex space)
116
- //if (mainContent) {
117
- // mainContent.classList.add('md:ml-16');
118
- //}
119
104
  } else {
120
- // Normal static positioning when header is visible
121
105
  toolModeNav.classList.remove('md:fixed');
122
106
  toolModeNav.classList.add('md:static');
123
107
  toolModeNav.style.top = '';
124
108
  toolModeNav.style.left = '';
125
-
126
- // Remove margin from main content when nav is static (nav takes flex space naturally)
127
- //if (mainContent) {
128
- // mainContent.classList.remove('md:ml-16');
129
- //}
130
109
  }
131
110
  }
132
111
  };
133
112
 
134
- // Handle resize events
135
- const handleResize = () => {
136
- updateHeaderHeight();
137
- updateSettingsMargin();
138
-
139
- // Handle desktop/mobile breakpoint transitions
140
- const isMobile = window.innerWidth < 801;
141
-
142
- if (isMobile && toolModeNav && mainContent) {
143
- // Force reset to mobile layout
144
- toolModeNav.classList.remove('md:fixed', 'md:static');
145
- toolModeNav.style.top = '';
146
- toolModeNav.style.left = '';
147
-
148
- // Remove desktop margin
149
- mainContent.classList.remove('md:ml-16');
150
- }
151
-
152
- // Re-run scroll logic to handle desktop/mobile transitions
113
+ const debouncedUpdate = debounce(() => {
114
+ updateStandardHeaderHeight();
153
115
  handleScroll();
154
- };
116
+ updatePanelPosition();
117
+ }, 50);
155
118
 
156
- // Listen for settings panel state changes
157
119
  const handleSettingsPanelChange = () => {
158
- const isSettingsOpen = settingsPanelOpenStore.get();
159
-
160
- // Reset scroll flag when panel state changes
161
- if (!isSettingsOpen) {
120
+ if (!settingsPanelOpenStore.get()) {
162
121
  hasScrolledForSettingsPanel = false;
163
122
  }
164
123
  };
165
124
 
166
- // Set up event listeners
167
- window.addEventListener('scroll', handleScroll, { passive: true });
168
- window.addEventListener('resize', handleResize);
169
-
170
- // Subscribe to settings panel state changes
125
+ window.addEventListener('scroll', debouncedUpdate, { passive: true });
126
+ window.addEventListener('resize', debouncedUpdate);
171
127
  settingsPanelOpenStore.subscribe(handleSettingsPanelChange);
172
128
 
173
- // Initial setup
174
- updateHeaderHeight();
175
- updateSettingsMargin();
129
+ setupPaneObserver();
130
+
131
+ updateStandardHeaderHeight();
176
132
  handleScroll();
133
+ updatePanelPosition();
177
134
  }
178
135
 
179
- /**
180
- * Handle settings panel mobile behavior
181
- * This is called when the settings panel is toggled
182
- */
183
136
  export function handleSettingsPanelMobile(isOpen: boolean): void {
184
137
  const isMobile = window.innerWidth < 801;
185
-
186
138
  if (!isMobile) return;
187
139
 
188
140
  if (isOpen) {
@@ -190,19 +142,12 @@ export function handleSettingsPanelMobile(isOpen: boolean): void {
190
142
  const headerHeight = header?.offsetHeight || 0;
191
143
  const currentScrollY = window.scrollY;
192
144
 
193
- // Only scroll if we're near the top and haven't already scrolled
194
145
  if (currentScrollY <= headerHeight && !hasScrolledForSettingsPanel) {
195
- window.scrollTo({
196
- top: headerHeight + 10,
197
- behavior: 'smooth',
198
- });
146
+ window.scrollTo({ top: headerHeight + 10, behavior: 'smooth' });
199
147
  hasScrolledForSettingsPanel = true;
200
148
  }
201
-
202
- // Fade the header
203
149
  setMobileHeaderFaded(true);
204
150
  } else {
205
- // Unfade the header and reset scroll flag
206
151
  setMobileHeaderFaded(false);
207
152
  hasScrolledForSettingsPanel = false;
208
153
  }