astro-tractstack 2.0.0-rc.24 → 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 +120 -6
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +56 -55
- package/templates/src/layouts/Layout.astro +4 -7
- package/templates/src/pages/[...slug]/edit.astro +10 -3
- package/templates/src/pages/api/tailwind.ts +23 -30
- 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 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
|
-
|
|
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,12 +11,15 @@ 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 {
|
|
18
|
+
FlatNode,
|
|
17
19
|
BaseNode,
|
|
18
20
|
PaneNode,
|
|
19
21
|
StoryFragmentNode,
|
|
22
|
+
MarkdownPaneFragmentNode,
|
|
20
23
|
} from '@/types/compositorTypes';
|
|
21
24
|
|
|
22
25
|
type SaveStage =
|
|
@@ -27,6 +30,7 @@ type SaveStage =
|
|
|
27
30
|
| 'SAVING_STORY_FRAGMENTS'
|
|
28
31
|
| 'LINKING_FILES'
|
|
29
32
|
| 'PROCESSING_STYLES'
|
|
33
|
+
| 'UPDATING_HOME_PAGE'
|
|
30
34
|
| 'COMPLETED'
|
|
31
35
|
| 'ERROR';
|
|
32
36
|
|
|
@@ -59,13 +63,9 @@ export default function SaveModal({
|
|
|
59
63
|
const [debugMessages, setDebugMessages] = useState<string[]>([]);
|
|
60
64
|
const isSaving = useRef(false);
|
|
61
65
|
const [isNavigating, setIsNavigating] = useState(false);
|
|
62
|
-
|
|
63
|
-
// Determine if we're in create mode
|
|
64
66
|
const isCreateMode = slug === 'create';
|
|
65
|
-
|
|
66
67
|
const contentMap = fullContentMapStore.get();
|
|
67
|
-
|
|
68
|
-
// Get backend URL
|
|
68
|
+
const pendingHomePageSlug = pendingHomePageSlugStore.get();
|
|
69
69
|
const goBackend =
|
|
70
70
|
import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
71
71
|
const tenantId = import.meta.env.PUBLIC_TENANTID || 'default';
|
|
@@ -143,7 +143,8 @@ export default function SaveModal({
|
|
|
143
143
|
if (
|
|
144
144
|
relevantNodeCount === 0 &&
|
|
145
145
|
nodesWithPendingFiles.length === 0 &&
|
|
146
|
-
storyFragmentsWithPendingImages.length === 0
|
|
146
|
+
storyFragmentsWithPendingImages.length === 0 &&
|
|
147
|
+
!pendingHomePageSlug
|
|
147
148
|
) {
|
|
148
149
|
addDebugMessage('No changes to save');
|
|
149
150
|
setStage('COMPLETED');
|
|
@@ -317,6 +318,56 @@ export default function SaveModal({
|
|
|
317
318
|
isContext
|
|
318
319
|
);
|
|
319
320
|
|
|
321
|
+
// This ensures css generation in the next phase uses fresh values
|
|
322
|
+
payload.optionsPayload.nodes.forEach((transformedNode) => {
|
|
323
|
+
const liveNode = ctx.allNodes.get().get(transformedNode.id);
|
|
324
|
+
if (!liveNode) return;
|
|
325
|
+
|
|
326
|
+
let needsUpdate = false;
|
|
327
|
+
let updatedNode: BaseNode = { ...liveNode };
|
|
328
|
+
|
|
329
|
+
// Update elementCss for TagElement nodes (FlatNode)
|
|
330
|
+
if (
|
|
331
|
+
transformedNode.nodeType === 'TagElement' &&
|
|
332
|
+
transformedNode.elementCss
|
|
333
|
+
) {
|
|
334
|
+
const flatNode = liveNode as FlatNode;
|
|
335
|
+
if (flatNode.elementCss !== transformedNode.elementCss) {
|
|
336
|
+
(updatedNode as FlatNode).elementCss =
|
|
337
|
+
transformedNode.elementCss;
|
|
338
|
+
needsUpdate = true;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Update parentCss for Markdown nodes (MarkdownPaneFragmentNode)
|
|
343
|
+
if (
|
|
344
|
+
transformedNode.nodeType === 'Markdown' &&
|
|
345
|
+
transformedNode.parentCss
|
|
346
|
+
) {
|
|
347
|
+
const markdownNode = liveNode as MarkdownPaneFragmentNode;
|
|
348
|
+
const currentParentCss = markdownNode.parentCss;
|
|
349
|
+
const newParentCss = transformedNode.parentCss as string[];
|
|
350
|
+
|
|
351
|
+
const isDifferent =
|
|
352
|
+
!currentParentCss ||
|
|
353
|
+
currentParentCss.length !== newParentCss.length ||
|
|
354
|
+
currentParentCss.some(
|
|
355
|
+
(css, index) => css !== newParentCss[index]
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
if (isDifferent) {
|
|
359
|
+
(updatedNode as MarkdownPaneFragmentNode).parentCss =
|
|
360
|
+
newParentCss;
|
|
361
|
+
needsUpdate = true;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Only update the live node if there are actual changes
|
|
366
|
+
if (needsUpdate) {
|
|
367
|
+
ctx.allNodes.get().set(transformedNode.id, updatedNode);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
320
371
|
// Check if this pane exists or is new
|
|
321
372
|
const paneExistsInBackend = contentMap.some(
|
|
322
373
|
(item) => item.type === 'Pane' && item.id === paneNode.id
|
|
@@ -562,6 +613,67 @@ export default function SaveModal({
|
|
|
562
613
|
throw new Error(`Failed to process styles: ${errorMsg}`);
|
|
563
614
|
}
|
|
564
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
|
+
|
|
565
677
|
// Success!
|
|
566
678
|
setStage('COMPLETED');
|
|
567
679
|
setProgress(100);
|
|
@@ -606,6 +718,8 @@ export default function SaveModal({
|
|
|
606
718
|
return 'Linking file relationships...';
|
|
607
719
|
case 'PROCESSING_STYLES':
|
|
608
720
|
return 'Processing styles...';
|
|
721
|
+
case 'UPDATING_HOME_PAGE':
|
|
722
|
+
return 'Updating home page...';
|
|
609
723
|
case 'COMPLETED':
|
|
610
724
|
return `${actionText} ${modeText.toLowerCase()} completed successfully!`;
|
|
611
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>
|
|
@@ -45,8 +45,6 @@ const {
|
|
|
45
45
|
impressions = [],
|
|
46
46
|
} = Astro.props;
|
|
47
47
|
|
|
48
|
-
const isDev = import.meta.env.DEV;
|
|
49
|
-
|
|
50
48
|
// Get site status from the store
|
|
51
49
|
const isInitialized = !freshInstallStore.get().needsSetup;
|
|
52
50
|
|
|
@@ -60,13 +58,12 @@ const brandConfig = propBrandConfig || (await getBrandConfig(tenantId));
|
|
|
60
58
|
const cssBasePath = isInitialized ? '/media/css' : '/styles';
|
|
61
59
|
const fontBasePath = isInitialized ? '/media/fonts' : '/fonts';
|
|
62
60
|
const mainStylesUrl = (() => {
|
|
63
|
-
const baseUrl =
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
: `${cssBasePath}/frontend.css`;
|
|
61
|
+
const baseUrl = isStoryKeep // || isDev
|
|
62
|
+
? `${cssBasePath}/storykeep.css`
|
|
63
|
+
: `${cssBasePath}/frontend.css`;
|
|
67
64
|
|
|
68
65
|
// Only add version for frontend.css (the dynamic one)
|
|
69
|
-
if (!isStoryKeep &&
|
|
66
|
+
if (!isStoryKeep && brandConfig?.STYLES_VER) {
|
|
70
67
|
return `${baseUrl}?v=${brandConfig.STYLES_VER}`;
|
|
71
68
|
}
|
|
72
69
|
|
|
@@ -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)}
|
|
@@ -11,24 +11,6 @@ export const POST: APIRoute = async ({ request }) => {
|
|
|
11
11
|
request.headers.get('X-Tenant-ID') ||
|
|
12
12
|
import.meta.env.PUBLIC_TENANTID ||
|
|
13
13
|
'default';
|
|
14
|
-
const isMultiTenant =
|
|
15
|
-
import.meta.env.PUBLIC_ENABLE_MULTI_TENANT === 'true' &&
|
|
16
|
-
tenantId !== 'default';
|
|
17
|
-
|
|
18
|
-
if (isMultiTenant) {
|
|
19
|
-
return new Response('CSS generation disabled in multi-tenant mode', {
|
|
20
|
-
status: 403,
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Read tailwind config from project root
|
|
25
|
-
const configPath = path.join(process.cwd(), 'tailwind.config.cjs');
|
|
26
|
-
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
27
|
-
const tailwindConfig = new Function(
|
|
28
|
-
'module',
|
|
29
|
-
'exports',
|
|
30
|
-
configContent + '; return module.exports;'
|
|
31
|
-
)({ exports: {} }, {});
|
|
32
14
|
|
|
33
15
|
const goBackend =
|
|
34
16
|
import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
@@ -63,18 +45,29 @@ export const POST: APIRoute = async ({ request }) => {
|
|
|
63
45
|
...new Set([...(cleanClasses || []), ...(dirtyClasses || [])]),
|
|
64
46
|
];
|
|
65
47
|
|
|
66
|
-
//
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
48
|
+
// Read base tailwind config from project root
|
|
49
|
+
const configPath = path.join(process.cwd(), 'tailwind.config.cjs');
|
|
50
|
+
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
51
|
+
const baseTailwindConfig = new Function(
|
|
52
|
+
'module',
|
|
53
|
+
'exports',
|
|
54
|
+
configContent + '; return module.exports;'
|
|
55
|
+
)({ exports: {} }, {});
|
|
56
|
+
|
|
57
|
+
// Create config with safelist
|
|
58
|
+
const tailwindConfigWithSafelist = {
|
|
59
|
+
...baseTailwindConfig,
|
|
60
|
+
safelist: allClasses,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Generate CSS using JIT with safelist
|
|
64
|
+
const tailwindCss = createTailwindcss({
|
|
65
|
+
tailwindConfig: tailwindConfigWithSafelist,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Use simple HTML content since safelist should handle class generation
|
|
69
|
+
const htmlContent = ['<div>Using the safelist</div>'];
|
|
70
|
+
|
|
78
71
|
const generatedCss = await tailwindCss.generateStylesFromContent(
|
|
79
72
|
`@tailwind base; @tailwind utilities;`,
|
|
80
73
|
htmlContent
|
|
@@ -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
|
}
|