astro-tractstack 2.0.15 → 2.0.17

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 (28) hide show
  1. package/dist/index.js +33 -13
  2. package/package.json +1 -1
  3. package/templates/custom/with-examples/CodeHook.astro +4 -0
  4. package/templates/custom/with-examples/SandboxLauncher.tsx +67 -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 +6 -1
  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 +61 -46
  16. package/templates/src/components/edit/pane/steps/DirectInjectStep.tsx +96 -0
  17. package/templates/src/components/edit/panels/StyleImagePanel.tsx +10 -8
  18. package/templates/src/components/edit/state/SaveModal.tsx +41 -0
  19. package/templates/src/constants.ts +1 -0
  20. package/templates/src/pages/api/sandbox.ts +86 -0
  21. package/templates/src/pages/sandbox.astro +137 -0
  22. package/templates/src/types/nodeProps.ts +1 -0
  23. package/templates/src/utils/compositor/aiPaneParser.ts +8 -2
  24. package/templates/src/utils/profileStorage.ts +13 -0
  25. package/utils/inject-files.ts +33 -14
  26. package/templates/src/components/edit/pane/AiPaneGenerator.tsx +0 -512
  27. package/templates/src/components/edit/pane/AiPanePreview.tsx +0 -107
  28. 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"
@@ -440,14 +452,6 @@ async function w(t, e, c) {
440
452
  ),
441
453
  dest: "src/components/edit/pane/RestylePaneModal.tsx"
442
454
  },
443
- {
444
- src: t("../templates/src/components/edit/pane/AiPaneGenerator.tsx"),
445
- dest: "src/components/edit/pane/AiPaneGenerator.tsx"
446
- },
447
- {
448
- src: t("../templates/src/components/edit/pane/AiPanePreview.tsx"),
449
- dest: "src/components/edit/pane/AiPanePreview.tsx"
450
- },
451
455
  {
452
456
  src: t(
453
457
  "../templates/src/components/edit/pane/steps/CopyInputStep.tsx"
@@ -466,6 +470,12 @@ async function w(t, e, c) {
466
470
  ),
467
471
  dest: "src/components/edit/pane/steps/AiDesignStep.tsx"
468
472
  },
473
+ {
474
+ src: t(
475
+ "../templates/src/components/edit/pane/steps/DirectInjectStep.tsx"
476
+ ),
477
+ dest: "src/components/edit/pane/steps/DirectInjectStep.tsx"
478
+ },
469
479
  {
470
480
  src: t(
471
481
  "../templates/src/components/edit/pane/AddPanePanel_newCustomCopy.tsx"
@@ -599,11 +609,6 @@ async function w(t, e, c) {
599
609
  src: t("../templates/src/stores/selection.ts"),
600
610
  dest: "src/stores/selection.ts"
601
611
  },
602
- // AAI utils
603
- {
604
- src: t("../templates/src/utils/aai/getTitleSlug.ts"),
605
- dest: "src/utils/aai/getTitleSlug.ts"
606
- },
607
612
  // Compositor utils - etl
608
613
  {
609
614
  src: t("../templates/src/utils/etl/index.ts"),
@@ -834,6 +839,10 @@ async function w(t, e, c) {
834
839
  ),
835
840
  dest: "src/pages/context/[...contextSlug]/edit.astro"
836
841
  },
842
+ {
843
+ src: t("../templates/src/pages/sandbox.astro"),
844
+ dest: "src/pages/sandbox.astro"
845
+ },
837
846
  {
838
847
  src: t("../templates/src/pages/storykeep.astro"),
839
848
  dest: "src/pages/storykeep.astro"
@@ -878,6 +887,10 @@ async function w(t, e, c) {
878
887
  src: t("../templates/src/pages/api/tailwind.ts"),
879
888
  dest: "src/pages/api/tailwind.ts"
880
889
  },
890
+ {
891
+ src: t("../templates/src/pages/api/sandbox.ts"),
892
+ dest: "src/pages/api/sandbox.ts"
893
+ },
881
894
  // Authentication Pages
882
895
  {
883
896
  src: t("../templates/src/pages/storykeep/login.astro"),
@@ -2110,6 +2123,13 @@ async function w(t, e, c) {
2110
2123
  },
2111
2124
  // Example Components (Conditional)
2112
2125
  ...c?.includeExamples ? [
2126
+ {
2127
+ src: t(
2128
+ "../templates/custom/with-examples/SandboxLauncher.tsx"
2129
+ ),
2130
+ dest: "src/custom/SandboxLauncher.tsx",
2131
+ protected: !0
2132
+ },
2113
2133
  {
2114
2134
  src: t("../templates/custom/with-examples/CustomHero.astro"),
2115
2135
  dest: "src/custom/CustomHero.astro",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.0.15",
3
+ "version": "2.0.17",
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,67 @@
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
+ {!profileExists && (
35
+ <p className="mt-8 text-sm text-gray-500">
36
+ Already connected?{' '}
37
+ <a
38
+ href="/storykeep/profile"
39
+ className="font-bold text-blue-600 underline hover:text-blue-500"
40
+ >
41
+ Unlock your profile
42
+ </a>
43
+ </p>
44
+ )}
45
+ </div>
46
+
47
+ {/* Column 2: The Action (Switches between Form and Button) */}
48
+ <div className="w-full max-w-xl md:w-1/2">
49
+ {!profileExists ? (
50
+ <SandboxRegisterForm onSuccess={handleRegistrationSuccess} />
51
+ ) : (
52
+ <div className="flex justify-center md:justify-start">
53
+ <div className="flex justify-center md:justify-start">
54
+ <a
55
+ 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"
56
+ href="/sandbox"
57
+ >
58
+ Get Crafting
59
+ </a>
60
+ </div>
61
+ </div>
62
+ )}
63
+ </div>
64
+ </div>
65
+ </div>
66
+ );
67
+ }
@@ -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
  )}
@@ -104,6 +104,7 @@ const EmptyPageHandler = (props: NodeProps) => {
104
104
  ctx={ctx}
105
105
  isStoryFragment={true}
106
106
  config={props.config!}
107
+ isSandboxMode={props.isSandboxMode}
107
108
  />
108
109
  );
109
110
  };
@@ -115,7 +116,11 @@ const getElement = (
115
116
  if (node === undefined) return <></>;
116
117
  const isPreview = getCtx(props).rootNodeId.get() === `tmp`;
117
118
  const hasPanes = useStore(getCtx(props).hasPanes);
118
- const sharedProps = { ...props, nodeId: node.id };
119
+ const sharedProps = {
120
+ ...props,
121
+ nodeId: node.id,
122
+ isSandboxMode: props.isSandboxMode,
123
+ };
119
124
  const type = getType(node);
120
125
 
121
126
  switch (type) {
@@ -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
  );
@@ -52,10 +52,6 @@ const PanelSwitch = ({
52
52
  }: SettingsPanelProps) => {
53
53
  const signal = useStore(settingsPanelStore);
54
54
 
55
- if (!signal) {
56
- return null;
57
- }
58
-
59
55
  useEffect(() => {
60
56
  if (signal?.action && onTitleChange) {
61
57
  const title = getSettingsPanelTitle(signal.action);
@@ -63,6 +59,10 @@ const PanelSwitch = ({
63
59
  }
64
60
  }, [signal?.action, onTitleChange]);
65
61
 
62
+ if (!signal) {
63
+ return null;
64
+ }
65
+
66
66
  const ctx = getCtx();
67
67
  const allNodes = ctx.allNodes.get();
68
68
  const clickedNode = allNodes.get(signal.nodeId) as FlatNode | undefined;
@@ -16,6 +16,7 @@ interface AddPanePanelProps {
16
16
  isStoryFragment?: boolean;
17
17
  isContextPane?: boolean;
18
18
  config?: BrandConfig;
19
+ isSandboxMode?: boolean;
19
20
  }
20
21
 
21
22
  const AddPanePanel = ({
@@ -25,6 +26,7 @@ const AddPanePanel = ({
25
26
  isStoryFragment = false,
26
27
  isContextPane = false,
27
28
  config,
29
+ isSandboxMode = false,
28
30
  }: AddPanePanelProps) => {
29
31
  const [reset, setReset] = useState(false);
30
32
  const lookup = first ? `${nodeId}-0` : nodeId;
@@ -66,6 +68,7 @@ const AddPanePanel = ({
66
68
  isStoryFragment={isStoryFragment}
67
69
  isContextPane={isContextPane}
68
70
  config={config!}
71
+ isSandboxMode={isSandboxMode}
69
72
  />
70
73
  ) : mode === PaneAddMode.BREAK && !isContextPane ? (
71
74
  <AddPaneBreakPanel