astro-tractstack 2.0.30 → 2.0.31

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.30",
3
+ "version": "2.0.31",
4
4
  "description": "Astro integration for TractStack - redeeming the web from boring experiences",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -16,6 +16,8 @@ import {
16
16
  hasArtpacksStore,
17
17
  settingsPanelStore,
18
18
  brandConfigStore,
19
+ viewportModeStore,
20
+ setViewportMode,
19
21
  } from '@/stores/storykeep';
20
22
  import { getCtx, ROOT_NODE_NAME, type NodesContext } from '@/stores/nodes';
21
23
  import { stopLoadingAnimation } from '@/utils/helpers';
@@ -284,6 +286,12 @@ export const Compositor = (props: CompositorProps) => {
284
286
  selectionOrigin.current = null;
285
287
  };
286
288
 
289
+ useEffect(() => {
290
+ if (viewportModeStore.get() === 'auto') {
291
+ setViewportMode('auto');
292
+ }
293
+ }, []);
294
+
287
295
  useEffect(() => {
288
296
  fullContentMapStore.set(props.fullContentMap);
289
297
  hasAssemblyAIStore.set(props.config?.HAS_AAI || false);
@@ -9,26 +9,53 @@ import { getCtx } from '@/stores/nodes';
9
9
  import { RenderChildren } from './RenderChildren';
10
10
  import { CodeHookContainer } from './Pane';
11
11
  import type { NodeProps } from '@/types/nodeProps';
12
+ import type { BgImageNode, ArtpackImageNode } from '@/types/compositorTypes';
12
13
  import { SaveToLibraryModal } from '@/components/edit/state/SaveToLibraryModal';
13
14
  import { RestylePaneModal } from '@/components/edit/pane/RestylePaneModal';
14
15
  import { selectionStore } from '@/stores/selection';
15
16
  import { copyPaneToClipboard } from '@/utils/compositor/designLibraryHelper';
16
17
 
18
+ function getSizeClasses(
19
+ size: string,
20
+ side: 'image' | 'content',
21
+ viewport: string
22
+ ): string {
23
+ if (viewport === 'mobile') {
24
+ return 'w-full';
25
+ }
26
+ switch (size) {
27
+ case 'narrow':
28
+ return side === 'image' ? 'w-1/3' : 'w-2/3';
29
+ case 'wide':
30
+ return side === 'image' ? 'w-2/3' : 'w-1/3';
31
+ default:
32
+ return 'w-1/2';
33
+ }
34
+ }
35
+
17
36
  export const Pane_DesignLibrary = (props: NodeProps) => {
18
37
  const ctx = getCtx(props);
19
-
20
38
  const { isRestyleModalOpen } = useStore(selectionStore, {
21
39
  keys: ['isRestyleModalOpen'],
22
40
  });
23
-
24
- const wrapperClasses = `grid ${ctx.getNodeClasses(
25
- props.nodeId,
41
+ const [currentViewport, setCurrentViewport] = useState(
26
42
  viewportKeyStore.get().value
27
- )}`;
43
+ );
44
+
45
+ useEffect(() => {
46
+ const unsubscribeViewport = viewportKeyStore.subscribe((newViewport) => {
47
+ setCurrentViewport(newViewport.value);
48
+ });
49
+ return () => unsubscribeViewport();
50
+ }, []);
51
+
52
+ const wrapperClasses = `grid ${ctx.getNodeClasses(props.nodeId, currentViewport)}`;
28
53
  const contentClasses = 'relative w-full h-auto justify-self-start';
29
54
  const contentStyles: CSSProperties = {
30
55
  ...ctx.getNodeCSSPropertiesStyles(props.nodeId),
31
56
  gridArea: '1/1/1/1',
57
+ position: 'relative',
58
+ zIndex: 1,
32
59
  };
33
60
  const codeHookPayload = ctx.getNodeCodeHookPayload(props.nodeId);
34
61
  const [children, setChildren] = useState<string[]>([
@@ -36,11 +63,14 @@ export const Pane_DesignLibrary = (props: NodeProps) => {
36
63
  ]);
37
64
  const [isSaveModalOpen, setIsSaveModalOpen] = useState(false);
38
65
  const [wasCopied, setWasCopied] = useState(false);
66
+ const [renderCount, setRenderCount] = useState(0);
67
+
39
68
  const getPaneId = (): string => `pane-${props.nodeId}`;
40
69
 
41
70
  useEffect(() => {
42
71
  const unsubscribe = ctx.notifications.subscribe(props.nodeId, () => {
43
72
  setChildren([...ctx.getChildNodeIDs(props.nodeId)]);
73
+ setRenderCount((prev) => prev + 1);
44
74
  });
45
75
  return unsubscribe;
46
76
  }, [props.nodeId, ctx.notifications]);
@@ -65,54 +95,145 @@ export const Pane_DesignLibrary = (props: NodeProps) => {
65
95
  }
66
96
  };
67
97
 
98
+ const Buttons = () => (
99
+ <div className="absolute left-2 top-2 z-10 flex flex-row gap-x-2">
100
+ {!props.isSandboxMode && (
101
+ <button
102
+ title="Save Pane to Design Library"
103
+ onClick={handleSaveClick}
104
+ className="flex h-10 w-10 items-center justify-center rounded-full bg-cyan-600 p-1.5 shadow-lg hover:bg-cyan-700"
105
+ >
106
+ <ArchiveBoxArrowDownIcon className="h-7 w-7 text-white" />
107
+ </button>
108
+ )}
109
+ <button
110
+ title="Restyle Pane from Design Library"
111
+ onClick={handleRestyleClick}
112
+ className="flex h-10 w-10 items-center justify-center rounded-full bg-blue-600 p-1.5 shadow-lg hover:bg-blue-700"
113
+ >
114
+ <ArrowPathRoundedSquareIcon className="h-7 w-7 text-white" />
115
+ </button>
116
+ <button
117
+ title="Copy Pane Design to Clipboard"
118
+ onClick={handleCopyToClipboard}
119
+ className={`flex h-10 w-10 items-center justify-center rounded-full p-1.5 shadow-lg transition-colors ${
120
+ wasCopied ? 'bg-green-500' : 'bg-gray-600 hover:bg-gray-700'
121
+ }`}
122
+ >
123
+ {wasCopied ? (
124
+ <CheckIcon className="h-7 w-7 text-white" />
125
+ ) : (
126
+ <ArrowDownTrayIcon className="h-7 w-7 text-white" />
127
+ )}
128
+ </button>
129
+ </div>
130
+ );
131
+
132
+ const allNodes = ctx.allNodes.get();
133
+ const bgNode = children
134
+ .map((id) => allNodes.get(id))
135
+ .find(
136
+ (node) =>
137
+ node?.nodeType === 'BgPane' &&
138
+ 'type' in node &&
139
+ (node.type === 'background-image' || node.type === 'artpack-image')
140
+ ) as (BgImageNode | ArtpackImageNode) | undefined;
141
+
142
+ const useFlexLayout =
143
+ bgNode &&
144
+ (bgNode.position === 'leftBleed' || bgNode.position === 'rightBleed');
145
+ const deferFlexLayout =
146
+ bgNode && (bgNode.position === 'left' || bgNode.position === 'right');
147
+
148
+ const flexDirection =
149
+ currentViewport === 'mobile'
150
+ ? 'flex-col'
151
+ : bgNode?.position === 'rightBleed'
152
+ ? 'flex-row-reverse'
153
+ : 'flex-row';
154
+
68
155
  return (
69
156
  <div id={getPaneId()} className="pane min-h-16">
70
- <div id={ctx.getNodeSlug(props.nodeId)} className={wrapperClasses}>
71
- <div
72
- className={contentClasses}
73
- style={contentStyles}
74
- onClick={(e) => {
75
- ctx.setClickedNodeId(props.nodeId);
76
- e.stopPropagation();
77
- }}
78
- >
79
- <div className="absolute left-2 top-2 z-10 flex flex-row gap-x-2">
80
- {!props.isSandboxMode && (
81
- <button
82
- title="Save Pane to Design Library"
83
- onClick={handleSaveClick}
84
- className="flex h-10 w-10 items-center justify-center rounded-full bg-cyan-600 p-1.5 shadow-lg hover:bg-cyan-700"
85
- >
86
- <ArchiveBoxArrowDownIcon className="h-7 w-7 text-white" />
87
- </button>
88
- )}
89
- <button
90
- title="Restyle Pane from Design Library"
91
- onClick={handleRestyleClick}
92
- className="flex h-10 w-10 items-center justify-center rounded-full bg-blue-600 p-1.5 shadow-lg hover:bg-blue-700"
157
+ <div
158
+ id={ctx.getNodeSlug(props.nodeId)}
159
+ className={useFlexLayout ? '' : wrapperClasses}
160
+ >
161
+ {codeHookPayload ? (
162
+ <div className={contentClasses} style={contentStyles}>
163
+ <Buttons />
164
+ <CodeHookContainer payload={codeHookPayload} />
165
+ </div>
166
+ ) : useFlexLayout ? (
167
+ <div
168
+ className={`flex flex-nowrap ${flexDirection} ${ctx.getNodeClasses(props.nodeId, currentViewport)}`}
169
+ >
170
+ <div
171
+ className={`relative overflow-hidden ${getSizeClasses(bgNode.size || 'equal', 'image', currentViewport)}`}
93
172
  >
94
- <ArrowPathRoundedSquareIcon className="h-7 w-7 text-white" />
95
- </button>
96
- <button
97
- title="Copy Pane Design to Clipboard"
98
- onClick={handleCopyToClipboard}
99
- className={`flex h-10 w-10 items-center justify-center rounded-full p-1.5 shadow-lg transition-colors ${
100
- wasCopied ? 'bg-green-500' : 'bg-gray-600 hover:bg-gray-700'
101
- }`}
173
+ <RenderChildren
174
+ children={children.filter((id) => {
175
+ const node = allNodes.get(id);
176
+ return node?.nodeType === 'BgPane';
177
+ })}
178
+ nodeProps={props}
179
+ key={`bg-children-${props.nodeId}-${renderCount}`}
180
+ />
181
+ </div>
182
+ <div
183
+ className={`${contentClasses} ${getSizeClasses(bgNode.size || 'equal', 'content', currentViewport)}`}
184
+ style={ctx.getNodeCSSPropertiesStyles(props.nodeId)}
185
+ onClick={(e) => {
186
+ ctx.setClickedNodeId(props.nodeId);
187
+ e.stopPropagation();
188
+ }}
102
189
  >
103
- {wasCopied ? (
104
- <CheckIcon className="h-7 w-7 text-white" />
105
- ) : (
106
- <ArrowDownTrayIcon className="h-7 w-7 text-white" />
107
- )}
108
- </button>
190
+ <Buttons />
191
+ <RenderChildren
192
+ children={children.filter((id) => {
193
+ const node = allNodes.get(id);
194
+ return node?.nodeType !== 'BgPane';
195
+ })}
196
+ nodeProps={props}
197
+ key={`content-children-${props.nodeId}-${renderCount}`}
198
+ />
199
+ </div>
109
200
  </div>
110
- {codeHookPayload ? (
111
- <CodeHookContainer payload={codeHookPayload} />
112
- ) : (
113
- <RenderChildren children={children} nodeProps={props} />
114
- )}
115
- </div>
201
+ ) : deferFlexLayout ? (
202
+ <div
203
+ className={contentClasses}
204
+ style={contentStyles}
205
+ onClick={(e) => {
206
+ ctx.setClickedNodeId(props.nodeId);
207
+ e.stopPropagation();
208
+ }}
209
+ >
210
+ <Buttons />
211
+ <RenderChildren
212
+ children={children.filter((id) => {
213
+ const node = allNodes.get(id);
214
+ return node?.nodeType !== 'BgPane';
215
+ })}
216
+ nodeProps={props}
217
+ key={`content-children-${props.nodeId}-${renderCount}`}
218
+ />
219
+ </div>
220
+ ) : (
221
+ <div
222
+ className={contentClasses}
223
+ style={contentStyles}
224
+ onClick={(e) => {
225
+ ctx.setClickedNodeId(props.nodeId);
226
+ e.stopPropagation();
227
+ }}
228
+ >
229
+ <Buttons />
230
+ <RenderChildren
231
+ children={children}
232
+ nodeProps={props}
233
+ key={`render-children-${props.nodeId}-${renderCount}`}
234
+ />
235
+ </div>
236
+ )}
116
237
  </div>
117
238
  {isSaveModalOpen && (
118
239
  <SaveToLibraryModal
@@ -5,64 +5,181 @@ import { getCtx } from '@/stores/nodes';
5
5
  import { RenderChildren } from './RenderChildren';
6
6
  import { CodeHookContainer } from './Pane';
7
7
  import type { NodeProps } from '@/types/nodeProps';
8
+ import type { BgImageNode, ArtpackImageNode } from '@/types/compositorTypes';
9
+
10
+ function getSizeClasses(
11
+ size: string,
12
+ side: 'image' | 'content',
13
+ viewport: string
14
+ ): string {
15
+ if (viewport === 'mobile') {
16
+ return 'w-full';
17
+ }
18
+ switch (size) {
19
+ case 'narrow':
20
+ return side === 'image' ? 'w-1/3' : 'w-2/3';
21
+ case 'wide':
22
+ return side === 'image' ? 'w-2/3' : 'w-1/3';
23
+ default:
24
+ return 'w-1/2';
25
+ }
26
+ }
8
27
 
9
28
  export const PaneEraser = (props: NodeProps) => {
10
- const wrapperClasses = `grid ${getCtx(props).getNodeClasses(props.nodeId, viewportKeyStore.get().value)}`;
29
+ const ctx = getCtx(props);
30
+ const [currentViewport, setCurrentViewport] = useState(
31
+ viewportKeyStore.get().value
32
+ );
33
+
34
+ useEffect(() => {
35
+ const unsubscribeViewport = viewportKeyStore.subscribe((newViewport) => {
36
+ setCurrentViewport(newViewport.value);
37
+ });
38
+ return () => unsubscribeViewport();
39
+ }, []);
40
+
41
+ const wrapperClasses = `grid ${ctx.getNodeClasses(props.nodeId, currentViewport)}`;
11
42
  const contentClasses = 'relative w-full h-auto justify-self-start';
12
43
  const contentStyles: CSSProperties = {
13
- ...getCtx(props).getNodeCSSPropertiesStyles(props.nodeId),
44
+ ...ctx.getNodeCSSPropertiesStyles(props.nodeId),
14
45
  gridArea: '1/1/1/1',
46
+ position: 'relative',
47
+ zIndex: 1,
15
48
  };
16
- const codeHookPayload = getCtx(props).getNodeCodeHookPayload(props.nodeId);
49
+ const codeHookPayload = ctx.getNodeCodeHookPayload(props.nodeId);
17
50
  const [children, setChildren] = useState<string[]>([
18
- ...getCtx(props).getChildNodeIDs(props.nodeId),
51
+ ...ctx.getChildNodeIDs(props.nodeId),
19
52
  ]);
53
+ const [renderCount, setRenderCount] = useState(0);
20
54
 
21
55
  const getPaneId = (): string => `pane-${props.nodeId}`;
22
56
 
23
57
  useEffect(() => {
24
- const unsubscribe = getCtx(props).notifications.subscribe(
25
- props.nodeId,
26
- () => {
27
- console.log(
28
- 'notification received data update for pane node: ' + props.nodeId
29
- );
30
- setChildren([...getCtx(props).getChildNodeIDs(props.nodeId)]);
31
- }
32
- );
58
+ const unsubscribe = ctx.notifications.subscribe(props.nodeId, () => {
59
+ setChildren([...ctx.getChildNodeIDs(props.nodeId)]);
60
+ setRenderCount((prev) => prev + 1);
61
+ });
33
62
  return unsubscribe;
34
- }, []);
63
+ }, [props.nodeId, ctx.notifications]);
64
+
65
+ const DeleteButton = () => (
66
+ <button
67
+ title="Delete Pane"
68
+ onClick={(e) => {
69
+ ctx.setClickedNodeId(props.nodeId);
70
+ e.stopPropagation();
71
+ }}
72
+ className="absolute right-2 top-2 z-10 rounded-full bg-red-700 p-1.5 hover:bg-black"
73
+ >
74
+ <TrashIcon className="h-10 w-10 text-white" />
75
+ </button>
76
+ );
77
+
78
+ const allNodes = ctx.allNodes.get();
79
+ const bgNode = children
80
+ .map((id) => allNodes.get(id))
81
+ .find(
82
+ (node) =>
83
+ node?.nodeType === 'BgPane' &&
84
+ 'type' in node &&
85
+ (node.type === 'background-image' || node.type === 'artpack-image')
86
+ ) as (BgImageNode | ArtpackImageNode) | undefined;
87
+
88
+ const useFlexLayout =
89
+ bgNode &&
90
+ (bgNode.position === 'leftBleed' || bgNode.position === 'rightBleed');
91
+ const deferFlexLayout =
92
+ bgNode && (bgNode.position === 'left' || bgNode.position === 'right');
93
+
94
+ const flexDirection =
95
+ currentViewport === 'mobile'
96
+ ? 'flex-col'
97
+ : bgNode?.position === 'rightBleed'
98
+ ? 'flex-row-reverse'
99
+ : 'flex-row';
35
100
 
36
101
  return (
37
102
  <div id={getPaneId()} className="pane min-h-16">
38
103
  <div
39
- id={getCtx(props).getNodeSlug(props.nodeId)}
40
- className={wrapperClasses}
104
+ id={ctx.getNodeSlug(props.nodeId)}
105
+ className={useFlexLayout ? '' : wrapperClasses}
41
106
  >
42
- <div
43
- className={contentClasses}
44
- style={contentStyles}
45
- onClick={(e) => {
46
- getCtx(props).setClickedNodeId(props.nodeId);
47
- e.stopPropagation();
48
- }}
49
- >
50
- <button
51
- title="Delete Pane"
107
+ {codeHookPayload ? (
108
+ <div className={contentClasses} style={contentStyles}>
109
+ <DeleteButton />
110
+ <CodeHookContainer payload={codeHookPayload} />
111
+ </div>
112
+ ) : useFlexLayout ? (
113
+ <div
114
+ className={`flex flex-nowrap ${flexDirection} ${ctx.getNodeClasses(props.nodeId, currentViewport)}`}
115
+ >
116
+ <div
117
+ className={`relative overflow-hidden ${getSizeClasses(bgNode.size || 'equal', 'image', currentViewport)}`}
118
+ >
119
+ <RenderChildren
120
+ children={children.filter((id) => {
121
+ const node = allNodes.get(id);
122
+ return node?.nodeType === 'BgPane';
123
+ })}
124
+ nodeProps={props}
125
+ key={`bg-children-${props.nodeId}-${renderCount}`}
126
+ />
127
+ </div>
128
+ <div
129
+ className={`${contentClasses} ${getSizeClasses(bgNode.size || 'equal', 'content', currentViewport)}`}
130
+ style={ctx.getNodeCSSPropertiesStyles(props.nodeId)}
131
+ onClick={(e) => {
132
+ ctx.setClickedNodeId(props.nodeId);
133
+ e.stopPropagation();
134
+ }}
135
+ >
136
+ <DeleteButton />
137
+ <RenderChildren
138
+ children={children.filter((id) => {
139
+ const node = allNodes.get(id);
140
+ return node?.nodeType !== 'BgPane';
141
+ })}
142
+ nodeProps={props}
143
+ key={`content-children-${props.nodeId}-${renderCount}`}
144
+ />
145
+ </div>
146
+ </div>
147
+ ) : deferFlexLayout ? (
148
+ <div
149
+ className={contentClasses}
150
+ style={contentStyles}
52
151
  onClick={(e) => {
53
- getCtx(props).setClickedNodeId(props.nodeId);
152
+ ctx.setClickedNodeId(props.nodeId);
54
153
  e.stopPropagation();
55
154
  }}
56
- className="absolute right-2 top-2 z-10 rounded-full bg-red-700 p-1.5 hover:bg-black"
57
155
  >
58
- <TrashIcon className="h-10 w-10 text-white" />
59
- </button>
60
- {codeHookPayload ? (
61
- <CodeHookContainer payload={codeHookPayload} />
62
- ) : (
63
- <RenderChildren children={children} nodeProps={props} />
64
- )}
65
- </div>
156
+ <DeleteButton />
157
+ <RenderChildren
158
+ children={children.filter((id) => {
159
+ const node = allNodes.get(id);
160
+ return node?.nodeType !== 'BgPane';
161
+ })}
162
+ nodeProps={props}
163
+ key={`content-children-${props.nodeId}-${renderCount}`}
164
+ />
165
+ </div>
166
+ ) : (
167
+ <div
168
+ className={contentClasses}
169
+ style={contentStyles}
170
+ onClick={(e) => {
171
+ ctx.setClickedNodeId(props.nodeId);
172
+ e.stopPropagation();
173
+ }}
174
+ >
175
+ <DeleteButton />
176
+ <RenderChildren
177
+ children={children}
178
+ nodeProps={props}
179
+ key={`render-children-${props.nodeId}-${renderCount}`}
180
+ />
181
+ </div>
182
+ )}
66
183
  </div>
67
184
  </div>
68
185
  );
@@ -35,6 +35,7 @@ import type {
35
35
  MenuNode,
36
36
  NodeType,
37
37
  PaneFragmentNode,
38
+ BgImageNode,
38
39
  PaneNode,
39
40
  StoryFragmentNode,
40
41
  Tag,
@@ -3264,6 +3265,15 @@ export class NodesContext {
3264
3265
  breakMobile: visualBreakPane.breakMobile,
3265
3266
  };
3266
3267
  allNodes.push(bgPaneNode);
3268
+ } else if (paneTemplate.bgPane.type === 'background-image') {
3269
+ const bgImagePane = paneTemplate.bgPane as BgImageNode;
3270
+ const bgPaneNode: BgImageNode = {
3271
+ ...bgImagePane,
3272
+ id: bgPaneId,
3273
+ nodeType: 'BgPane',
3274
+ parentId: newPaneId,
3275
+ };
3276
+ allNodes.push(bgPaneNode);
3267
3277
  } else if (paneTemplate.bgPane.type === 'artpack-image') {
3268
3278
  const artpackBgPane = paneTemplate.bgPane as ArtpackImageNode;
3269
3279
  const bgPaneNode: ArtpackImageNode = {
@@ -78,7 +78,8 @@ export function getUserRole(astro: any): 'admin' | 'editor' | null {
78
78
  */
79
79
  export function requireAdmin(astro: any): Response | undefined {
80
80
  if (!isAdmin(astro)) {
81
- return astro.redirect('/storykeep/login');
81
+ const target = encodeURIComponent(astro.url.pathname + astro.url.search);
82
+ return astro.redirect(`/storykeep/login?redirect=${target}`);
82
83
  }
83
84
  }
84
85
 
@@ -88,7 +89,8 @@ export function requireAdmin(astro: any): Response | undefined {
88
89
  */
89
90
  export function requireEditor(astro: any): Response | undefined {
90
91
  if (!isEditor(astro)) {
91
- return astro.redirect('/storykeep/login');
92
+ const target = encodeURIComponent(astro.url.pathname + astro.url.search);
93
+ return astro.redirect(`/storykeep/login?redirect=${target}`);
92
94
  }
93
95
  }
94
96
 
@@ -98,7 +100,8 @@ export function requireEditor(astro: any): Response | undefined {
98
100
  */
99
101
  export function requireAdminOrEditor(astro: any): Response | undefined {
100
102
  if (!isAuthenticated(astro)) {
101
- return astro.redirect('/storykeep/login');
103
+ const target = encodeURIComponent(astro.url.pathname + astro.url.search);
104
+ return astro.redirect(`/storykeep/login?redirect=${target}`);
102
105
  }
103
106
  }
104
107