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 +1 -1
- package/templates/src/components/compositor/Node.tsx +1 -1
- package/templates/src/components/compositor/nodes/Pane.tsx +3 -3
- package/templates/src/components/edit/Header.tsx +9 -3
- package/templates/src/components/edit/SettingsPanel.tsx +21 -17
- package/templates/src/components/edit/ToolMode.tsx +93 -33
- package/templates/src/components/edit/state/SaveModal.tsx +68 -6
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +56 -55
- package/templates/src/pages/[...slug]/edit.astro +10 -3
- package/templates/src/pages/context/[...contextSlug]/edit.astro +10 -3
- package/templates/src/pages/storykeep/advanced.astro +1 -2
- package/templates/src/pages/storykeep/branding.astro +1 -2
- package/templates/src/pages/storykeep/content.astro +1 -2
- package/templates/src/pages/storykeep.astro +1 -2
- package/templates/src/stores/storykeep.ts +2 -0
- package/templates/src/utils/layout.ts +66 -121
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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 !==
|
|
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-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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))
|
|
116
|
+
if (!hasTitle || (!hasPanes && !isContext)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
94
119
|
|
|
95
120
|
return (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
</
|
|
117
|
-
|
|
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-
|
|
205
|
-
<
|
|
206
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
{
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
66
|
+
if (!storykeepHeader || !settingsControls || !standardHeader) return;
|
|
45
67
|
|
|
46
|
-
let
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
headerHeight = standardHeader.offsetHeight;
|
|
50
|
-
}
|
|
68
|
+
let standardHeaderHeight = 0;
|
|
69
|
+
const updateStandardHeaderHeight = () => {
|
|
70
|
+
standardHeaderHeight = standardHeader.offsetHeight;
|
|
51
71
|
};
|
|
52
72
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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 >
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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';
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
window.addEventListener('
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
}
|