astro-tractstack 2.0.14 → 2.0.16

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.
Files changed (34) hide show
  1. package/dist/index.js +41 -9
  2. package/package.json +1 -1
  3. package/templates/custom/with-examples/CodeHook.astro +4 -0
  4. package/templates/custom/with-examples/SandboxLauncher.tsx +65 -0
  5. package/templates/env.example +3 -0
  6. package/templates/src/components/codehooks/SandboxAuthWrapper.tsx +75 -0
  7. package/templates/src/components/codehooks/SandboxRegisterForm.tsx +202 -0
  8. package/templates/src/components/compositor/Compositor.tsx +2 -0
  9. package/templates/src/components/compositor/Node.tsx +27 -9
  10. package/templates/src/components/compositor/nodes/Pane_DesignLibrary.tsx +13 -11
  11. package/templates/src/components/compositor/nodes/Pane_layout.tsx +16 -14
  12. package/templates/src/components/edit/Header.tsx +8 -2
  13. package/templates/src/components/edit/PanelSwitch.tsx +4 -4
  14. package/templates/src/components/edit/pane/AddPanePanel.tsx +3 -0
  15. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +463 -561
  16. package/templates/src/components/edit/pane/steps/AiDesignStep.tsx +140 -0
  17. package/templates/src/components/edit/pane/steps/CopyInputStep.tsx +105 -0
  18. package/templates/src/components/edit/pane/steps/DesignLibraryStep.tsx +395 -0
  19. package/templates/src/components/edit/panels/StyleImagePanel.tsx +10 -8
  20. package/templates/src/components/edit/state/SaveModal.tsx +41 -0
  21. package/templates/src/constants/prompts.json +3 -1
  22. package/templates/src/pages/api/sandbox.ts +86 -0
  23. package/templates/src/pages/sandbox.astro +137 -0
  24. package/templates/src/types/nodeProps.ts +1 -0
  25. package/templates/src/utils/compositor/aiPaneParser.ts +32 -84
  26. package/templates/src/utils/compositor/designLibraryHelper.ts +87 -2
  27. package/templates/src/utils/profileStorage.ts +13 -0
  28. package/utils/inject-files.ts +41 -10
  29. package/templates/src/components/edit/pane/AiPaneGenerator.tsx +0 -575
  30. package/templates/src/components/edit/pane/AiPanePreview.tsx +0 -107
  31. package/templates/src/components/edit/pane/PageGen.tsx +0 -485
  32. package/templates/src/components/edit/pane/PageGenSelector.tsx +0 -245
  33. package/templates/src/components/edit/pane/PageGenSpecial.tsx +0 -339
  34. package/templates/src/utils/aai/getTitleSlug.ts +0 -72
package/dist/index.js CHANGED
@@ -138,6 +138,18 @@ async function w(t, e, c) {
138
138
  ),
139
139
  dest: "src/components/compositor/nodes/Pane_layout.tsx"
140
140
  },
141
+ {
142
+ src: t(
143
+ "../templates/src/components/codehooks/SandboxAuthWrapper.tsx"
144
+ ),
145
+ dest: "src/components/codehooks/SandboxAuthWrapper.tsx"
146
+ },
147
+ {
148
+ src: t(
149
+ "../templates/src/components/codehooks/SandboxRegisterForm.tsx"
150
+ ),
151
+ dest: "src/components/codehooks/SandboxRegisterForm.tsx"
152
+ },
141
153
  {
142
154
  src: t("../templates/src/components/compositor/nodes/Markdown.tsx"),
143
155
  dest: "src/components/compositor/nodes/Markdown.tsx"
@@ -441,12 +453,22 @@ async function w(t, e, c) {
441
453
  dest: "src/components/edit/pane/RestylePaneModal.tsx"
442
454
  },
443
455
  {
444
- src: t("../templates/src/components/edit/pane/AiPaneGenerator.tsx"),
445
- dest: "src/components/edit/pane/AiPaneGenerator.tsx"
456
+ src: t(
457
+ "../templates/src/components/edit/pane/steps/CopyInputStep.tsx"
458
+ ),
459
+ dest: "src/components/edit/pane/steps/CopyInputStep.tsx"
446
460
  },
447
461
  {
448
- src: t("../templates/src/components/edit/pane/AiPanePreview.tsx"),
449
- dest: "src/components/edit/pane/AiPanePreview.tsx"
462
+ src: t(
463
+ "../templates/src/components/edit/pane/steps/DesignLibraryStep.tsx"
464
+ ),
465
+ dest: "src/components/edit/pane/steps/DesignLibraryStep.tsx"
466
+ },
467
+ {
468
+ src: t(
469
+ "../templates/src/components/edit/pane/steps/AiDesignStep.tsx"
470
+ ),
471
+ dest: "src/components/edit/pane/steps/AiDesignStep.tsx"
450
472
  },
451
473
  {
452
474
  src: t(
@@ -581,11 +603,6 @@ async function w(t, e, c) {
581
603
  src: t("../templates/src/stores/selection.ts"),
582
604
  dest: "src/stores/selection.ts"
583
605
  },
584
- // AAI utils
585
- {
586
- src: t("../templates/src/utils/aai/getTitleSlug.ts"),
587
- dest: "src/utils/aai/getTitleSlug.ts"
588
- },
589
606
  // Compositor utils - etl
590
607
  {
591
608
  src: t("../templates/src/utils/etl/index.ts"),
@@ -816,6 +833,10 @@ async function w(t, e, c) {
816
833
  ),
817
834
  dest: "src/pages/context/[...contextSlug]/edit.astro"
818
835
  },
836
+ {
837
+ src: t("../templates/src/pages/sandbox.astro"),
838
+ dest: "src/pages/sandbox.astro"
839
+ },
819
840
  {
820
841
  src: t("../templates/src/pages/storykeep.astro"),
821
842
  dest: "src/pages/storykeep.astro"
@@ -860,6 +881,10 @@ async function w(t, e, c) {
860
881
  src: t("../templates/src/pages/api/tailwind.ts"),
861
882
  dest: "src/pages/api/tailwind.ts"
862
883
  },
884
+ {
885
+ src: t("../templates/src/pages/api/sandbox.ts"),
886
+ dest: "src/pages/api/sandbox.ts"
887
+ },
863
888
  // Authentication Pages
864
889
  {
865
890
  src: t("../templates/src/pages/storykeep/login.astro"),
@@ -2092,6 +2117,13 @@ async function w(t, e, c) {
2092
2117
  },
2093
2118
  // Example Components (Conditional)
2094
2119
  ...c?.includeExamples ? [
2120
+ {
2121
+ src: t(
2122
+ "../templates/custom/with-examples/SandboxLauncher.tsx"
2123
+ ),
2124
+ dest: "src/custom/SandboxLauncher.tsx",
2125
+ protected: !0
2126
+ },
2095
2127
  {
2096
2128
  src: t("../templates/custom/with-examples/CustomHero.astro"),
2097
2129
  dest: "src/custom/CustomHero.astro",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.0.14",
3
+ "version": "2.0.16",
4
4
  "description": "Astro integration for TractStack - redeeming the web from boring experiences",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -7,6 +7,7 @@ import BunnyVideoWrapper from '@/components/codehooks/BunnyVideoWrapper.astro';
7
7
  import EpinetWrapper from '@/components/codehooks/EpinetWrapper';
8
8
  import ProductCardWrapper from './ProductCardWrapper.astro';
9
9
  import ProductGrid from './ProductGrid.astro';
10
+ import SandboxLauncher from './SandboxLauncher';
10
11
  import type { FullContentMapItem } from '@/types/tractstack';
11
12
  import type { ResourceNode } from '@/types/compositorTypes';
12
13
 
@@ -30,6 +31,7 @@ export const components = {
30
31
  'search-widget': true,
31
32
  'product-card': true,
32
33
  'product-grid': true,
34
+ 'get-crafting': true,
33
35
  'bunny-video': import.meta.env.PUBLIC_ENABLE_BUNNY === 'true',
34
36
  epinet: true,
35
37
  };
@@ -48,6 +50,8 @@ export const components = {
48
50
  <SearchWidget fullContentMap={fullContentMap} client:load />
49
51
  ) : target === 'bunny-video' && import.meta.env.PUBLIC_ENABLE_BUNNY ? (
50
52
  <BunnyVideoWrapper options={options} />
53
+ ) : target === 'get-crafting' ? (
54
+ <SandboxLauncher client:only="react" />
51
55
  ) : target === 'custom-hero' ? (
52
56
  <CustomHero />
53
57
  ) : target === 'epinet' ? (
@@ -0,0 +1,65 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { ProfileStorage } from '@/utils/profileStorage';
3
+ import SandboxRegisterForm from '@/components/codehooks/SandboxRegisterForm';
4
+
5
+ export default function SandboxLauncher() {
6
+ const [profileExists, setProfileExists] = useState<boolean | null>(null);
7
+
8
+ useEffect(() => {
9
+ // This check must run on the client to access localStorage
10
+ setProfileExists(ProfileStorage.hasProfile());
11
+ }, []);
12
+
13
+ const handleRegistrationSuccess = () => {
14
+ setProfileExists(true);
15
+ };
16
+
17
+ // Avoid a flash of the wrong state during server-side rendering
18
+ if (profileExists === null) {
19
+ return null;
20
+ }
21
+
22
+ return (
23
+ <div className="mx-auto my-16 max-w-6xl px-4 md:my-24">
24
+ <div className="flex flex-col items-center gap-12 md:flex-row md:gap-16">
25
+ {/* Column 1: The Pitch (Always Visible) */}
26
+ <div className="px-6 text-right md:w-1/2">
27
+ <h2 className="text-4xl font-bold text-gray-900 md:text-5xl">
28
+ Press <span className="italic text-blue-600">your own</span> Tract
29
+ Stack
30
+ </h2>
31
+ <p className="mt-4 text-lg text-gray-600">
32
+ Create an interactive webpage in a sandbox! No credit card required.
33
+ </p>
34
+ <p className="mt-8 text-sm text-gray-500">
35
+ Already connected?{' '}
36
+ <a
37
+ href="/storykeep/profile"
38
+ className="font-bold text-blue-600 underline hover:text-blue-500"
39
+ >
40
+ Unlock your profile
41
+ </a>
42
+ </p>
43
+ </div>
44
+
45
+ {/* Column 2: The Action (Switches between Form and Button) */}
46
+ <div className="w-full max-w-xl md:w-1/2">
47
+ {!profileExists ? (
48
+ <SandboxRegisterForm onSuccess={handleRegistrationSuccess} />
49
+ ) : (
50
+ <div className="flex justify-center md:justify-start">
51
+ <div className="flex justify-center md:justify-start">
52
+ <a
53
+ className="transform rounded-xl bg-blue-600 px-6 py-6 text-3xl font-bold text-white shadow-xl transition-transform hover:scale-105 md:text-5xl"
54
+ href="/sandbox"
55
+ >
56
+ Get Crafting
57
+ </a>
58
+ </div>
59
+ </div>
60
+ )}
61
+ </div>
62
+ </div>
63
+ </div>
64
+ );
65
+ }
@@ -6,3 +6,6 @@ PUBLIC_GO_BACKEND=http://localhost:8080
6
6
  PUBLIC_TENANTID=default
7
7
  # OPTIONAL: Multi-Tenant Configuration
8
8
  PUBLIC_ENABLE_MULTI_TENANT=false
9
+ # OPTIONAL: Enable Sandbox DEMO mode
10
+ # ***SPECIAL*** has security implications
11
+ #PRIVATE_SANDBOX_SECRET=strong-secret-password
@@ -0,0 +1,75 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { Dialog } from '@ark-ui/react/dialog';
3
+ import { Portal } from '@ark-ui/react/portal';
4
+ import XMarkIcon from '@heroicons/react/24/solid/XMarkIcon';
5
+ import { ProfileStorage } from '@/utils/profileStorage';
6
+ import SandboxRegisterForm from '@/components/codehooks/SandboxRegisterForm';
7
+
8
+ export default function SandboxAuthWrapper() {
9
+ const [profileExists, setProfileExists] = useState<boolean | null>(null);
10
+
11
+ useEffect(() => {
12
+ setProfileExists(ProfileStorage.hasProfile());
13
+ }, []);
14
+
15
+ const handleRegistrationSuccess = () => {
16
+ setProfileExists(true);
17
+ };
18
+
19
+ const handleClose = () => {
20
+ window.location.href = '/';
21
+ };
22
+
23
+ if (profileExists === null || profileExists === true) {
24
+ return null;
25
+ }
26
+
27
+ return (
28
+ <Dialog.Root open={true} modal={true} trapFocus={false}>
29
+ <Portal>
30
+ <Dialog.Backdrop
31
+ className="fixed inset-0 bg-black bg-opacity-75"
32
+ style={{ zIndex: 9005 }}
33
+ />
34
+ <Dialog.Positioner
35
+ className="fixed inset-0 flex items-center justify-center p-4"
36
+ style={{ zIndex: 9005 }}
37
+ >
38
+ <Dialog.Content className="relative grid w-full max-w-6xl grid-cols-1 overflow-hidden rounded-lg bg-white shadow-2xl md:grid-cols-2">
39
+ <button
40
+ onClick={handleClose}
41
+ className="absolute right-4 top-4 z-10 rounded-full bg-gray-100 p-2 text-gray-600 shadow-sm transition-colors hover:bg-gray-200"
42
+ title="Close and exit Sandbox"
43
+ >
44
+ <XMarkIcon className="h-5 w-5" />
45
+ </button>
46
+
47
+ <div className="flex flex-col justify-center bg-gray-50 p-8 text-right">
48
+ <h2 className="text-4xl font-bold text-gray-900 md:text-5xl">
49
+ Press <span className="italic text-blue-600">your own</span>{' '}
50
+ Tract Stack
51
+ </h2>
52
+ <p className="mt-4 text-lg text-gray-600">
53
+ Create an interactive webpage in a sandbox! No credit card
54
+ required.
55
+ </p>
56
+ <p className="mt-8 text-sm text-gray-500">
57
+ Already connected?{' '}
58
+ <a
59
+ href="/storykeep/profile"
60
+ className="font-bold text-blue-600 underline hover:text-blue-500"
61
+ >
62
+ Unlock your profile
63
+ </a>
64
+ </p>
65
+ </div>
66
+
67
+ <div className="flex flex-col justify-center p-8">
68
+ <SandboxRegisterForm onSuccess={handleRegistrationSuccess} />
69
+ </div>
70
+ </Dialog.Content>
71
+ </Dialog.Positioner>
72
+ </Portal>
73
+ </Dialog.Root>
74
+ );
75
+ }
@@ -0,0 +1,202 @@
1
+ import { useState } from 'react';
2
+ import { ProfileStorage } from '@/utils/profileStorage';
3
+ import type { FormEvent } from 'react';
4
+
5
+ interface SandboxRegisterFormProps {
6
+ onSuccess: () => void;
7
+ }
8
+
9
+ async function createProfile(payload: {
10
+ firstname: string;
11
+ email: string;
12
+ codeword: string;
13
+ persona: 'major' | 'none';
14
+ }) {
15
+ try {
16
+ const sessionData = ProfileStorage.prepareHandshakeData();
17
+ const currentConsent = payload.persona === 'major' ? '1' : '0';
18
+
19
+ const response = await fetch('/api/auth/profile', {
20
+ method: 'POST',
21
+ headers: { 'Content-Type': 'application/json' },
22
+ body: JSON.stringify({
23
+ firstname: payload.firstname,
24
+ email: payload.email,
25
+ codeword: payload.codeword,
26
+ contactPersona: payload.persona,
27
+ shortBio: '',
28
+ sessionId: sessionData.sessionId,
29
+ consent: currentConsent,
30
+ isUpdate: false,
31
+ }),
32
+ });
33
+
34
+ const result = await response.json();
35
+
36
+ if (!result.success) {
37
+ return {
38
+ success: false,
39
+ error: result.error || 'Profile creation failed',
40
+ };
41
+ }
42
+
43
+ if (result.profile) {
44
+ ProfileStorage.setProfileData({
45
+ firstname: result.profile.firstname,
46
+ contactPersona: result.profile.contactPersona,
47
+ email: result.profile.email,
48
+ shortBio: result.profile.shortBio,
49
+ });
50
+ }
51
+ if (result.token) {
52
+ ProfileStorage.storeProfileToken(result.token);
53
+ }
54
+ if (result.encryptedEmail && result.encryptedCode) {
55
+ ProfileStorage.storeEncryptedCredentials(
56
+ result.encryptedEmail,
57
+ result.encryptedCode
58
+ );
59
+ }
60
+ if (result.consent) {
61
+ ProfileStorage.storeConsent(result.consent);
62
+ }
63
+
64
+ return { success: true };
65
+ } catch (e) {
66
+ console.error('Sandbox profile creation error:', e);
67
+ return { success: false, error: 'A network error occurred.' };
68
+ }
69
+ }
70
+
71
+ export default function SandboxRegisterForm({
72
+ onSuccess,
73
+ }: SandboxRegisterFormProps) {
74
+ const [firstname, setFirstname] = useState('');
75
+ const [email, setEmail] = useState('');
76
+ const [codeword, setCodeword] = useState('');
77
+ const [consent, setConsent] = useState(true);
78
+ const [isLoading, setIsLoading] = useState(false);
79
+ const [error, setError] = useState<string | null>(null);
80
+
81
+ const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
82
+ e.preventDefault();
83
+ if (!email || !codeword || !firstname) {
84
+ setError('Please fill out all required fields.');
85
+ return;
86
+ }
87
+
88
+ setIsLoading(true);
89
+ setError(null);
90
+
91
+ const persona = consent ? 'major' : 'none';
92
+
93
+ const result = await createProfile({
94
+ firstname,
95
+ email,
96
+ codeword,
97
+ persona,
98
+ });
99
+
100
+ if (result.success) {
101
+ onSuccess();
102
+ } else {
103
+ setError(result.error || 'An unknown error occurred. Please try again.');
104
+ setIsLoading(false);
105
+ }
106
+ };
107
+
108
+ return (
109
+ <form
110
+ onSubmit={handleSubmit}
111
+ className="rounded-lg border bg-white p-6 shadow-sm"
112
+ >
113
+ <div className="space-y-4">
114
+ <div>
115
+ <label htmlFor="firstname" className="block text-sm text-gray-700">
116
+ First Name
117
+ </label>
118
+ <input
119
+ type="text"
120
+ name="firstname"
121
+ id="firstname"
122
+ autoComplete="given-name"
123
+ value={firstname}
124
+ onChange={(e) => setFirstname(e.target.value)}
125
+ disabled={isLoading}
126
+ className="mt-1 block w-full rounded-md border-gray-300 px-3 py-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
127
+ required
128
+ />
129
+ </div>
130
+
131
+ <div>
132
+ <label htmlFor="email" className="block text-sm text-gray-700">
133
+ Email address
134
+ </label>
135
+ <input
136
+ type="email"
137
+ name="email"
138
+ id="email"
139
+ autoComplete="email"
140
+ value={email}
141
+ onChange={(e) => setEmail(e.target.value)}
142
+ disabled={isLoading}
143
+ className="mt-1 block w-full rounded-md border-gray-300 px-3 py-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
144
+ required
145
+ />
146
+ </div>
147
+
148
+ <div>
149
+ <label htmlFor="codeword" className="block text-sm text-gray-700">
150
+ Create a Codeword (to unlock your account)
151
+ </label>
152
+ <input
153
+ type="password"
154
+ name="codeword"
155
+ id="codeword"
156
+ autoComplete="new-password"
157
+ value={codeword}
158
+ onChange={(e) => setCodeword(e.target.value)}
159
+ disabled={isLoading}
160
+ className="mt-1 block w-full rounded-md border-gray-300 px-3 py-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
161
+ required
162
+ />
163
+ </div>
164
+
165
+ <div className="flex items-start">
166
+ <div className="flex h-5 items-center">
167
+ <input
168
+ id="consent"
169
+ name="consent"
170
+ type="checkbox"
171
+ checked={consent}
172
+ onChange={(e) => setConsent(e.target.checked)}
173
+ disabled={isLoading}
174
+ className="h-4 w-4 rounded border-gray-300 text-cyan-600 focus:ring-cyan-500"
175
+ />
176
+ </div>
177
+ <div className="ml-3 text-sm">
178
+ <label htmlFor="consent" className="text-gray-700">
179
+ Keep me in touch with major updates
180
+ </label>
181
+ </div>
182
+ </div>
183
+
184
+ {error && (
185
+ <p className="text-sm text-red-600" role="alert">
186
+ {error}
187
+ </p>
188
+ )}
189
+
190
+ <div>
191
+ <button
192
+ type="submit"
193
+ disabled={isLoading}
194
+ className="flex w-full justify-center rounded-md border border-transparent bg-black px-4 py-2 text-sm font-bold text-white shadow-sm transition-colors hover:bg-orange-500 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:bg-gray-400"
195
+ >
196
+ {isLoading ? 'Creating...' : 'Start Crafting'}
197
+ </button>
198
+ </div>
199
+ </div>
200
+ </form>
201
+ );
202
+ }
@@ -50,6 +50,7 @@ export type CompositorProps = {
50
50
  availableCodeHooks: string[];
51
51
  urlParams: Record<string, string | boolean>;
52
52
  fullCanonicalURL: string;
53
+ isSandboxMode?: boolean;
53
54
  };
54
55
 
55
56
  const VERBOSE = false;
@@ -464,6 +465,7 @@ export const Compositor = (props: CompositorProps) => {
464
465
  key={`${props.id}-${updateCounter}`}
465
466
  ctx={props.ctx}
466
467
  config={props.config}
468
+ isSandboxMode={props.isSandboxMode}
467
469
  onDragStart={handleDragStart}
468
470
  />
469
471
  )}
@@ -32,7 +32,6 @@ import { NodeBasicTagEraser } from './nodes/tagElements/NodeBasicTag_eraser';
32
32
  import { NodeBasicTagSettings } from './nodes/tagElements/NodeBasicTag_settings';
33
33
  import { Pane_DesignLibrary } from './nodes/Pane_DesignLibrary';
34
34
  import AddPanePanel from '@/components/edit/pane/AddPanePanel';
35
- import PageCreationSelector from '@/components/edit/pane/PageGenSelector';
36
35
  import ConfigPanePanel from '@/components/edit/pane/ConfigPanePanel';
37
36
  import StoryFragmentConfigPanel from '@/components/edit/storyfragment/StoryFragmentConfigPanel';
38
37
  import StoryFragmentTitlePanel from '@/components/edit/storyfragment/StoryFragmentPanel_title';
@@ -46,6 +45,7 @@ import type {
46
45
  BaseNode,
47
46
  FlatNode,
48
47
  } from '@/types/compositorTypes';
48
+ import { PaneAddMode } from '@/types/compositorTypes';
49
49
  import { handleClickEventDefault } from '@/utils/compositor/handleClickEvent';
50
50
  import { selectionStore } from '@/stores/selection';
51
51
  import type { NodeProps, SelectionOrigin } from '@/types/nodeProps';
@@ -89,6 +89,26 @@ function parseCodeHook(node: BaseNode | FlatNode) {
89
89
  return null;
90
90
  }
91
91
 
92
+ // Helper component to safely set the panel mode for an empty page
93
+ const EmptyPageHandler = (props: NodeProps) => {
94
+ const ctx = getCtx(props);
95
+ useEffect(() => {
96
+ ctx.setPaneAddMode(props.nodeId, PaneAddMode.NEW);
97
+ }, []);
98
+
99
+ // Now that the mode is set, render the panel which will read it.
100
+ return (
101
+ <AddPanePanel
102
+ nodeId={props.nodeId}
103
+ first={true}
104
+ ctx={ctx}
105
+ isStoryFragment={true}
106
+ config={props.config!}
107
+ isSandboxMode={props.isSandboxMode}
108
+ />
109
+ );
110
+ };
111
+
92
112
  const getElement = (
93
113
  node: BaseNode | FlatNode,
94
114
  props: NodeProps
@@ -96,8 +116,11 @@ const getElement = (
96
116
  if (node === undefined) return <></>;
97
117
  const isPreview = getCtx(props).rootNodeId.get() === `tmp`;
98
118
  const hasPanes = useStore(getCtx(props).hasPanes);
99
- const isTemplate = useStore(getCtx(props).isTemplate);
100
- const sharedProps = { ...props, nodeId: node.id };
119
+ const sharedProps = {
120
+ ...props,
121
+ nodeId: node.id,
122
+ isSandboxMode: props.isSandboxMode,
123
+ };
101
124
  const type = getType(node);
102
125
 
103
126
  switch (type) {
@@ -152,12 +175,7 @@ const getElement = (
152
175
  </div>
153
176
  </div>
154
177
  ) : !hasPanes && sf.slug && sf.title && !isPreview ? (
155
- <PageCreationSelector
156
- nodeId={props.nodeId}
157
- ctx={getCtx(props)}
158
- isTemplate={isTemplate}
159
- config={props.config!}
160
- />
178
+ <EmptyPageHandler {...sharedProps} />
161
179
  ) : (
162
180
  <>
163
181
  <PanelVisibilityWrapper
@@ -14,10 +14,6 @@ import { selectionStore } from '@/stores/selection';
14
14
  export const Pane_DesignLibrary = (props: NodeProps) => {
15
15
  const ctx = getCtx(props);
16
16
 
17
- if (!props.config || !props.config.TENANT_ID) {
18
- return <></>;
19
- }
20
-
21
17
  const { isRestyleModalOpen } = useStore(selectionStore, {
22
18
  keys: ['isRestyleModalOpen'],
23
19
  });
@@ -56,6 +52,10 @@ export const Pane_DesignLibrary = (props: NodeProps) => {
56
52
  setIsSaveModalOpen(true);
57
53
  };
58
54
 
55
+ if (!props.config || !props.config.TENANT_ID) {
56
+ return <></>;
57
+ }
58
+
59
59
  return (
60
60
  <div id={getPaneId()} className="pane min-h-16">
61
61
  <div id={ctx.getNodeSlug(props.nodeId)} className={wrapperClasses}>
@@ -68,13 +68,15 @@ export const Pane_DesignLibrary = (props: NodeProps) => {
68
68
  }}
69
69
  >
70
70
  <div className="absolute left-2 top-2 z-10 flex flex-col gap-y-2">
71
- <button
72
- title="Save Pane to Design Library"
73
- onClick={handleSaveClick}
74
- className="flex h-10 w-10 items-center justify-center rounded-full bg-cyan-600 p-1.5 shadow-lg hover:bg-cyan-700"
75
- >
76
- <ArchiveBoxArrowDownIcon className="h-7 w-7 text-white" />
77
- </button>
71
+ {props.isSandboxMode && (
72
+ <button
73
+ title="Save Pane to Design Library"
74
+ onClick={handleSaveClick}
75
+ className="flex h-10 w-10 items-center justify-center rounded-full bg-cyan-600 p-1.5 shadow-lg hover:bg-cyan-700"
76
+ >
77
+ <ArchiveBoxArrowDownIcon className="h-7 w-7 text-white" />
78
+ </button>
79
+ )}
78
80
  <button
79
81
  title="Restyle Pane from Design Library"
80
82
  onClick={handleRestyleClick}
@@ -51,20 +51,22 @@ export const PaneLayout = (props: NodeProps) => {
51
51
  e.stopPropagation();
52
52
  }}
53
53
  >
54
- <button
55
- title="Apply New Layout"
56
- onClick={(e) => {
57
- getCtx(props).setClickedNodeId(props.nodeId);
58
- e.stopPropagation();
59
- }}
60
- onDoubleClick={(e) => {
61
- getCtx(props).setClickedNodeId(props.nodeId, true);
62
- e.stopPropagation();
63
- }}
64
- className="absolute right-2 top-2 z-10 rounded-full bg-cyan-700 p-1.5 hover:bg-black"
65
- >
66
- <PuzzlePieceIcon className="h-10 w-10 text-white" />
67
- </button>
54
+ {!props.isSandboxMode && (
55
+ <button
56
+ title="Apply New Layout"
57
+ onClick={(e) => {
58
+ getCtx(props).setClickedNodeId(props.nodeId);
59
+ e.stopPropagation();
60
+ }}
61
+ onDoubleClick={(e) => {
62
+ getCtx(props).setClickedNodeId(props.nodeId, true);
63
+ e.stopPropagation();
64
+ }}
65
+ className="absolute right-2 top-2 z-10 rounded-full bg-cyan-700 p-1.5 hover:bg-black"
66
+ >
67
+ <PuzzlePieceIcon className="h-10 w-10 text-white" />
68
+ </button>
69
+ )}
68
70
  {codeHookPayload ? (
69
71
  <CodeHookContainer payload={codeHookPayload} />
70
72
  ) : (
@@ -21,9 +21,14 @@ import SaveModal from '@/components/edit/state/SaveModal';
21
21
  interface StoryKeepHeaderProps {
22
22
  slug: string;
23
23
  isContext: boolean;
24
+ isSandboxMode?: boolean;
24
25
  }
25
26
 
26
- const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
27
+ const StoryKeepHeader = ({
28
+ slug,
29
+ isContext = false,
30
+ isSandboxMode = false,
31
+ }: StoryKeepHeaderProps) => {
27
32
  const viewport = useStore(viewportModeStore);
28
33
  const pendingHomePageSlug = useStore(pendingHomePageSlugStore);
29
34
  const ctx = getCtx();
@@ -162,7 +167,7 @@ const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
162
167
  </div>
163
168
  )}
164
169
 
165
- {shouldShowSave && (
170
+ {shouldShowSave && !isSandboxMode && (
166
171
  <div className="flex flex-wrap items-center justify-center gap-2 text-sm">
167
172
  <button
168
173
  onClick={handleSave}
@@ -179,6 +184,7 @@ const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
179
184
  isContext={isContext}
180
185
  show={showSaveModal}
181
186
  onClose={handleCloseSaveModal}
187
+ isSandboxMode={isSandboxMode}
182
188
  />
183
189
  </>
184
190
  );