astro-tractstack 2.3.2 → 2.3.4

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 (58) hide show
  1. package/bin/create-tractstack.js +7 -4
  2. package/dist/index.js +51 -8
  3. package/package.json +1 -1
  4. package/templates/custom/shopify/Cart.tsx +279 -118
  5. package/templates/custom/shopify/CartIcon.tsx +8 -8
  6. package/templates/custom/shopify/CheckoutModal.tsx +328 -65
  7. package/templates/custom/shopify/ShopifyCartManager.tsx +117 -60
  8. package/templates/custom/shopify/ShopifyCheckout.tsx +2 -26
  9. package/templates/custom/shopify/ShopifyProductGrid.tsx +8 -0
  10. package/templates/custom/shopify/ShopifyServiceList.tsx +25 -37
  11. package/templates/custom/shopify/cart.astro +7 -1
  12. package/templates/src/components/Header.astro +4 -2
  13. package/templates/src/components/compositor/Node.tsx +39 -9
  14. package/templates/src/components/compositor/nodes/CreativePane.tsx +175 -88
  15. package/templates/src/components/edit/pane/AddPanePanel.tsx +2 -2
  16. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +6 -5
  17. package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +9 -15
  18. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
  19. package/templates/src/components/form/advanced/APIConfigSection.tsx +249 -4
  20. package/templates/src/components/form/brand/SiteConfigSection.tsx +9 -0
  21. package/templates/src/components/form/shopify/SchedulingSection.tsx +44 -0
  22. package/templates/src/components/storykeep/Dashboard_Advanced.tsx +66 -21
  23. package/templates/src/components/storykeep/Dashboard_Shopify.tsx +266 -18
  24. package/templates/src/components/storykeep/controls/content/ManageContent.tsx +1 -0
  25. package/templates/src/components/storykeep/controls/content/ProductTable.tsx +38 -24
  26. package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +240 -65
  27. package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +175 -48
  28. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +91 -10
  29. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Sales.tsx +479 -0
  30. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +7 -3
  31. package/templates/src/constants.ts +2 -0
  32. package/templates/src/layouts/Layout.astro +26 -0
  33. package/templates/src/pages/api/auth/logout.ts +35 -2
  34. package/templates/src/pages/api/google/oauth/callback.ts +50 -0
  35. package/templates/src/pages/api/google/oauth/disconnect.ts +32 -0
  36. package/templates/src/pages/api/google/oauth/start.ts +32 -0
  37. package/templates/src/pages/api/google/oauth/status.ts +32 -0
  38. package/templates/src/pages/api/sales/list.ts +66 -0
  39. package/templates/src/pages/api/sales/metrics.ts +60 -0
  40. package/templates/src/pages/context/[...contextSlug].astro +50 -31
  41. package/templates/src/pages/privacy.astro +84 -0
  42. package/templates/src/pages/storykeep/advanced.astro +4 -1
  43. package/templates/src/pages/terms.astro +47 -0
  44. package/templates/src/stores/nodes.ts +8 -0
  45. package/templates/src/stores/shopify.ts +5 -0
  46. package/templates/src/types/tractstack.ts +87 -0
  47. package/templates/src/utils/api/advancedConfig.ts +2 -1
  48. package/templates/src/utils/api/advancedHelpers.ts +20 -0
  49. package/templates/src/utils/api/bookingHelpers.ts +3 -1
  50. package/templates/src/utils/api/brandConfig.ts +2 -0
  51. package/templates/src/utils/api/brandHelpers.ts +14 -1
  52. package/templates/src/utils/api/salesHelpers.ts +21 -0
  53. package/templates/src/utils/booking/appointmentMode.ts +135 -0
  54. package/templates/src/utils/customHelpers.ts +287 -2
  55. package/utils/inject-files.ts +47 -4
  56. package/templates/src/components/codehooks/SandboxAuthWrapper.tsx +0 -101
  57. package/templates/src/utils/actions/actionButton.ts +0 -103
  58. package/templates/src/utils/actions/preParse_Clicked.ts +0 -87
@@ -5,13 +5,15 @@ import {
5
5
  cartStore,
6
6
  modalState,
7
7
  transactionTraceId,
8
- getCartItemKey,
9
8
  } from '@/stores/shopify';
10
9
  import { bookingHelpers } from '@/utils/api/bookingHelpers';
11
10
  import {
12
11
  RESTRICTION_MESSAGES,
13
12
  calculateCartDuration,
13
+ getCartItemKey,
14
+ isSharedFeeService,
14
15
  } from '@/utils/customHelpers';
16
+ import { wouldCartHaveImpossibleRemoteMix } from '@/utils/booking/appointmentMode';
15
17
  import type { ResourceNode } from '@/types/compositorTypes';
16
18
  import type { CartItemState } from '@/stores/shopify';
17
19
  import type { BrandConfigState } from '@/types/tractstack';
@@ -26,6 +28,9 @@ export default function ShopifyCartManager({
26
28
  brandConfig,
27
29
  }: ShopifyCartManagerProps) {
28
30
  const queue = useStore(addQueue);
31
+ const productResources = resources.filter(
32
+ (r) => r.categorySlug === 'product'
33
+ );
29
34
 
30
35
  useEffect(() => {
31
36
  if (queue.length > 0) {
@@ -38,19 +43,37 @@ export default function ShopifyCartManager({
38
43
  return;
39
44
  }
40
45
 
41
- const key = getCartItemKey(actionItem);
46
+ const key = getCartItemKey(actionItem, resource, productResources);
42
47
  const currentCart = cartStore.get();
43
48
  const currentItem = currentCart[key];
44
- const currentQty = currentItem?.quantity || 0;
49
+ const isSharedFeeResource = isSharedFeeService(
50
+ resource,
51
+ productResources
52
+ );
53
+ const legacySharedKeys = isSharedFeeResource
54
+ ? Object.keys(currentCart).filter(
55
+ (cartKey) =>
56
+ cartKey !== key &&
57
+ currentCart[cartKey]?.resourceId === actionItem.resourceId
58
+ )
59
+ : [];
60
+ const legacySharedItems = legacySharedKeys
61
+ .map((cartKey) => currentCart[cartKey])
62
+ .filter((item): item is CartItemState => !!item);
63
+ const mergedCurrentItem = currentItem || legacySharedItems[0];
64
+ const currentQty = isSharedFeeResource
65
+ ? currentItem?.quantity ||
66
+ legacySharedItems.reduce((total, item) => total + item.quantity, 0)
67
+ : currentItem?.quantity || 0;
45
68
  const nextCart = { ...currentCart };
46
69
 
47
70
  if (actionItem.action === 'remove') {
48
- const newQty = Math.max(0, currentQty - 1);
71
+ const newQty = isSharedFeeResource ? 0 : Math.max(0, currentQty - 1);
49
72
 
50
73
  if (newQty === 0) {
51
74
  if (
52
75
  resource?.optionsPayload?.needsBooking ||
53
- currentItem?.boundResourceId
76
+ mergedCurrentItem?.boundResourceId
54
77
  ) {
55
78
  const traceId = transactionTraceId.get();
56
79
  if (traceId) {
@@ -58,23 +81,34 @@ export default function ShopifyCartManager({
58
81
  .releaseHold(traceId)
59
82
  .catch((err) =>
60
83
  console.error('Failed to release hold on cart removal:', err)
61
- );
84
+ )
85
+ .finally(() => {
86
+ transactionTraceId.set('');
87
+ });
62
88
  }
63
89
  }
64
90
  delete nextCart[key];
91
+ legacySharedKeys.forEach((legacyKey) => {
92
+ delete nextCart[legacyKey];
93
+ });
65
94
  } else {
66
95
  nextCart[key] = {
67
- ...currentItem,
96
+ ...mergedCurrentItem,
68
97
  resourceId: actionItem.resourceId,
69
98
  quantity: newQty,
70
99
  };
71
100
  }
72
101
 
73
- if (currentItem?.boundResourceId || actionItem.boundResourceId) {
102
+ if (mergedCurrentItem?.boundResourceId || actionItem.boundResourceId) {
74
103
  const boundId =
75
- currentItem?.boundResourceId || actionItem.boundResourceId;
104
+ mergedCurrentItem?.boundResourceId || actionItem.boundResourceId;
76
105
  if (boundId) {
77
- const serviceKey = getCartItemKey({ resourceId: boundId });
106
+ const boundResource = resources.find((r) => r.id === boundId);
107
+ const serviceKey = getCartItemKey(
108
+ { resourceId: boundId },
109
+ boundResource,
110
+ productResources
111
+ );
78
112
  const serviceItem = nextCart[serviceKey];
79
113
  if (serviceItem) {
80
114
  const newServiceQty = Math.max(0, serviceItem.quantity - 1);
@@ -94,27 +128,39 @@ export default function ShopifyCartManager({
94
128
  addQueue.set(remaining);
95
129
  } else if (actionItem.action === 'add') {
96
130
  transactionTraceId.set('');
97
- const newQty = currentQty + 1;
131
+ const newQty = isSharedFeeResource ? 1 : currentQty + 1;
98
132
 
99
133
  const newItem: CartItemState = {
100
134
  resourceId: actionItem.resourceId,
101
135
  quantity: newQty,
102
- gid: actionItem.gid || currentItem?.gid,
103
- variantId: actionItem.variantId || currentItem?.variantId,
136
+ gid: actionItem.gid || mergedCurrentItem?.gid,
137
+ variantId: isSharedFeeResource
138
+ ? undefined
139
+ : actionItem.variantId || mergedCurrentItem?.variantId,
104
140
  variantIdShipped:
105
- actionItem.variantIdShipped || currentItem?.variantIdShipped,
141
+ actionItem.variantIdShipped || mergedCurrentItem?.variantIdShipped,
106
142
  variantIdPickup:
107
- actionItem.variantIdPickup || currentItem?.variantIdPickup,
143
+ actionItem.variantIdPickup || mergedCurrentItem?.variantIdPickup,
108
144
  boundResourceId:
109
- actionItem.boundResourceId || currentItem?.boundResourceId,
145
+ actionItem.boundResourceId || mergedCurrentItem?.boundResourceId,
110
146
  };
111
147
 
148
+ legacySharedKeys.forEach((legacyKey) => {
149
+ delete nextCart[legacyKey];
150
+ });
112
151
  nextCart[key] = newItem;
113
152
 
114
153
  if (newItem.boundResourceId) {
115
- const serviceKey = getCartItemKey({
116
- resourceId: newItem.boundResourceId,
117
- });
154
+ const boundResource = resources.find(
155
+ (r) => r.id === newItem.boundResourceId
156
+ );
157
+ const serviceKey = getCartItemKey(
158
+ {
159
+ resourceId: newItem.boundResourceId,
160
+ },
161
+ boundResource,
162
+ productResources
163
+ );
118
164
  const serviceItem = nextCart[serviceKey];
119
165
 
120
166
  if (serviceItem) {
@@ -130,54 +176,65 @@ export default function ShopifyCartManager({
130
176
  }
131
177
  }
132
178
 
133
- const rawDuration = calculateCartDuration(nextCart, resources);
134
-
135
- const interval = 15;
136
- const snappedDuration = Math.ceil(rawDuration / interval) * interval;
137
-
138
- const dynamicMax = brandConfig?.scheduling?.maxLengthMinutes || 180;
139
- if (snappedDuration > dynamicMax) {
179
+ if (wouldCartHaveImpossibleRemoteMix(nextCart, resources)) {
140
180
  modalState.set({
141
181
  isOpen: true,
142
182
  type: 'restriction',
143
- title: 'Appointment Length Limit Reached',
144
- message: RESTRICTION_MESSAGES.MAX_DURATION(dynamicMax),
183
+ title: 'Incompatible Booking Modes',
184
+ message: RESTRICTION_MESSAGES.INCOMPATIBLE_REMOTE,
145
185
  });
146
186
  } else {
147
- cartStore.set(nextCart);
148
-
149
- if (!actionItem.suppressModal) {
150
- let targetResource = resource;
151
- if (newItem.boundResourceId) {
152
- const bound = resources.find(
153
- (r) => r.id === newItem.boundResourceId
154
- );
155
- if (bound) {
156
- targetResource = bound;
187
+ const rawDuration = calculateCartDuration(nextCart, resources);
188
+
189
+ const interval = 15;
190
+ const snappedDuration = Math.ceil(rawDuration / interval) * interval;
191
+
192
+ const dynamicMax = brandConfig?.scheduling?.maxLengthMinutes || 180;
193
+ if (snappedDuration > dynamicMax) {
194
+ modalState.set({
195
+ isOpen: true,
196
+ type: 'restriction',
197
+ title: 'Appointment Length Limit Reached',
198
+ message: RESTRICTION_MESSAGES.MAX_DURATION(dynamicMax),
199
+ });
200
+ } else {
201
+ cartStore.set(nextCart);
202
+
203
+ if (!actionItem.suppressModal) {
204
+ let targetResource = resource;
205
+ if (newItem.boundResourceId) {
206
+ const bound = resources.find(
207
+ (r) => r.id === newItem.boundResourceId
208
+ );
209
+ if (bound) {
210
+ targetResource = bound;
211
+ }
157
212
  }
158
- }
159
213
 
160
- if (
161
- targetResource.categorySlug === 'service' ||
162
- targetResource.optionsPayload?.needsBooking
163
- ) {
164
- modalState.set({
165
- isOpen: true,
166
- type: 'success',
167
- title: 'Booking Required',
168
- message: RESTRICTION_MESSAGES.BOOKING(
169
- (
170
- targetResource.optionsPayload?.bookingLengthMinutes || 0
171
- ).toString()
172
- ),
173
- });
174
- } else {
175
- modalState.set({
176
- isOpen: true,
177
- type: 'success',
178
- title: 'Added to Cart',
179
- message: RESTRICTION_MESSAGES.DEFAULT_ADD(targetResource.title),
180
- });
214
+ if (
215
+ targetResource.categorySlug === 'service' ||
216
+ targetResource.optionsPayload?.needsBooking
217
+ ) {
218
+ modalState.set({
219
+ isOpen: true,
220
+ type: 'success',
221
+ title: 'Booking Required',
222
+ message: RESTRICTION_MESSAGES.BOOKING(
223
+ (
224
+ targetResource.optionsPayload?.bookingLengthMinutes || 0
225
+ ).toString()
226
+ ),
227
+ });
228
+ } else {
229
+ modalState.set({
230
+ isOpen: true,
231
+ type: 'success',
232
+ title: 'Added to Cart',
233
+ message: RESTRICTION_MESSAGES.DEFAULT_ADD(
234
+ targetResource.title
235
+ ),
236
+ });
237
+ }
181
238
  }
182
239
  }
183
240
  }
@@ -6,6 +6,7 @@ import {
6
6
  CART_STATES,
7
7
  isShopifyHandoff,
8
8
  } from '@/stores/shopify';
9
+ import { buildShopifyCheckoutLines } from '@/utils/customHelpers';
9
10
  import type { ResourceNode } from '@/types/compositorTypes';
10
11
 
11
12
  interface ShopifyCheckoutProps {
@@ -33,32 +34,7 @@ export default function ShopifyCheckout({
33
34
  setStatus('PROCESSING');
34
35
 
35
36
  try {
36
- const cartItems = Object.values(cart);
37
-
38
- const lines = cartItems
39
- .map((item) => {
40
- // Resolve the ResourceNode for this item
41
- const resource = resources.find((r) => r.id === item.resourceId);
42
-
43
- if (!resource) {
44
- return null;
45
- }
46
-
47
- // Use the explicitly sanitized variant ID from the cart state
48
- const merchandiseId = item.variantId;
49
-
50
- // If we have no ID, we cannot add this item to the Shopify cart.
51
- if (!merchandiseId) return null;
52
-
53
- return {
54
- merchandiseId,
55
- quantity: item.quantity,
56
- };
57
- })
58
- .filter((line) => line !== null) as Array<{
59
- merchandiseId: string;
60
- quantity: number;
61
- }>;
37
+ const lines = buildShopifyCheckoutLines(cart, resources);
62
38
 
63
39
  if (lines.length === 0) {
64
40
  throw new Error(
@@ -1,6 +1,7 @@
1
1
  import { useState } from 'react';
2
2
  import { useStore } from '@nanostores/react';
3
3
  import { cartStore, addQueue, type CartAction } from '@/stores/shopify';
4
+ import { collectServiceGids } from '@/utils/customHelpers';
4
5
  import { getShopifyImage } from '@/utils/helpers';
5
6
  import type { ResourceNode } from '@/types/compositorTypes';
6
7
 
@@ -240,6 +241,13 @@ export default function ShopifyProductGrid({ resources = {}, options }: Props) {
240
241
  products = products.filter((p) => p.optionsPayload?.group === group);
241
242
  }
242
243
 
244
+ const serviceGids = collectServiceGids(services);
245
+ products = products.filter((p) => {
246
+ const gid =
247
+ typeof p.optionsPayload?.gid === 'string' ? p.optionsPayload.gid : '';
248
+ return gid === '' || !serviceGids.has(gid);
249
+ });
250
+
243
251
  if (products.length === 0) return null;
244
252
 
245
253
  return (
@@ -1,10 +1,10 @@
1
1
  import { useStore } from '@nanostores/react';
2
+ import { cartStore, addQueue, type CartAction } from '@/stores/shopify';
2
3
  import {
3
- cartStore,
4
- addQueue,
5
4
  getCartItemKey,
6
- type CartAction,
7
- } from '@/stores/shopify';
5
+ getServiceVariantIdFromCanonicalProduct,
6
+ isSharedFeeService,
7
+ } from '@/utils/customHelpers';
8
8
  import type { ResourceNode } from '@/types/compositorTypes';
9
9
 
10
10
  interface Props {
@@ -55,33 +55,17 @@ export default function ShopifyServiceList({ resources = {}, options }: Props) {
55
55
  (s) => !boundServiceSlugs.has(s.slug)
56
56
  );
57
57
 
58
- const getServiceVariantId = (resource: ResourceNode): string | undefined => {
59
- try {
60
- if (resource.optionsPayload?.shopifyData) {
61
- const data = JSON.parse(resource.optionsPayload.shopifyData);
62
- // Handle both raw product data and simplified product objects
63
- const product = data.products?.[0] || data;
64
- return product?.variants?.[0]?.id;
65
- }
66
- } catch (e) {
67
- return undefined;
68
- }
69
- return undefined;
70
- };
71
-
72
58
  const handleToggle = (resource: ResourceNode, currentQuantity: number) => {
73
59
  const actionType = currentQuantity > 0 ? 'remove' : 'add';
60
+ const gid =
61
+ typeof resource.optionsPayload?.gid === 'string'
62
+ ? resource.optionsPayload.gid
63
+ : undefined;
74
64
 
75
- const variantId = getServiceVariantId(resource);
76
- let gid: string | undefined;
77
-
78
- try {
79
- if (resource.optionsPayload?.shopifyData) {
80
- const data = JSON.parse(resource.optionsPayload.shopifyData);
81
- const product = data.products?.[0] || data;
82
- gid = product?.id;
83
- }
84
- } catch (e) {}
65
+ const sharedFee = isSharedFeeService(resource, products);
66
+ const variantId = sharedFee
67
+ ? undefined
68
+ : getServiceVariantIdFromCanonicalProduct(resource, products);
85
69
 
86
70
  const newAction: CartAction = {
87
71
  resourceId: resource.id,
@@ -115,14 +99,20 @@ export default function ShopifyServiceList({ resources = {}, options }: Props) {
115
99
  <section className="w-full">
116
100
  <div className="space-y-4">
117
101
  {displayServices.map((resource) => {
118
- const variantId = getServiceVariantId(resource);
119
- const key = getCartItemKey({
120
- resourceId: resource.id,
121
- variantId,
122
- });
102
+ const key = getCartItemKey(
103
+ { resourceId: resource.id },
104
+ resource,
105
+ products
106
+ );
123
107
 
124
108
  const cartItem = cart[key];
125
- const isSelected = (cartItem?.quantity || 0) > 0;
109
+ const legacyCartItem = Object.values(cart).find(
110
+ (item) =>
111
+ item.resourceId === resource.id && (item.quantity || 0) > 0
112
+ );
113
+ const selectedQuantity =
114
+ cartItem?.quantity || legacyCartItem?.quantity || 0;
115
+ const isSelected = selectedQuantity > 0;
126
116
  const duration = resource.optionsPayload?.bookingLengthMinutes;
127
117
 
128
118
  return (
@@ -152,9 +142,7 @@ export default function ShopifyServiceList({ resources = {}, options }: Props) {
152
142
 
153
143
  <div className="ml-4 flex-shrink-0">
154
144
  <button
155
- onClick={() =>
156
- handleToggle(resource, cartItem?.quantity || 0)
157
- }
145
+ onClick={() => handleToggle(resource, selectedQuantity)}
158
146
  className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
159
147
  isSelected ? 'bg-black' : 'bg-gray-200'
160
148
  }`}
@@ -18,6 +18,12 @@ const resources = await getHeaderResources(tenantId, resourceCategories);
18
18
 
19
19
  <Layout title="Your Cart" slug="cart">
20
20
  <main class="mx-auto max-w-7xl px-4 py-16 md:px-6 xl:px-8">
21
- <Cart resources={resources} embedded={true} client:only="react" />
21
+ <Cart
22
+ resources={resources}
23
+ allowRemote={brandConfig?.SCHEDULING?.allowRemote || false}
24
+ remoteOnly={brandConfig?.SCHEDULING?.remoteOnly || false}
25
+ embedded={true}
26
+ client:only="react"
27
+ />
22
28
  </main>
23
29
  </Layout>
@@ -76,13 +76,15 @@ if (hasShopify) {
76
76
  <>
77
77
  {slug !== `cart` ? (
78
78
  <div class="flex w-full justify-end px-4 py-2 md:px-8">
79
- <CartIcon client:only="react" />
79
+ <CartIcon client:only="react" resources={shopifyResources} />
80
80
  </div>
81
81
  ) : (
82
82
  <CheckoutModal
83
83
  client:only="react"
84
84
  resources={shopifyResources}
85
- maxLength={brandConfig?.scheduling?.maxLengthMinutes || 180}
85
+ maxLength={brandConfig?.SCHEDULING?.maxLengthMinutes || 180}
86
+ allowRemote={brandConfig?.SCHEDULING?.allowRemote || false}
87
+ remoteOnly={brandConfig?.SCHEDULING?.remoteOnly || false}
86
88
  />
87
89
  )}
88
90
  </>
@@ -8,6 +8,7 @@ import {
8
8
  isTopLevelBlockNode,
9
9
  parseCodeHook,
10
10
  } from '@/utils/compositor/nodesHelper';
11
+ import { isStoryFragmentNode } from '@/utils/compositor/typeGuards';
11
12
  import { PaneAddMode } from '@/types/compositorTypes';
12
13
  import { NodeOverlay } from './tools/NodeOverlay';
13
14
  import PanelVisibilityWrapper from '@/components/compositor/PanelVisibilityWrapper';
@@ -108,14 +109,6 @@ export const Node = memo((props: NodeProps) => {
108
109
  case 'Pane':
109
110
  {
110
111
  const paneNode = node as PaneNode;
111
- const storyfragmentNodeId = ctx.getClosestNodeTypeFromId(
112
- node.id,
113
- 'StoryFragment'
114
- );
115
- const storyfragmentNode = ctx.allNodes
116
- .get()
117
- .get(storyfragmentNodeId) as StoryFragmentNode;
118
- const first = paneNode.id === storyfragmentNode.paneIds?.[0];
119
112
  const isHtmlAstPane = !!paneNode.htmlAst;
120
113
  const paneNodes = ctx.getChildNodeIDs(node.id);
121
114
 
@@ -140,7 +133,7 @@ export const Node = memo((props: NodeProps) => {
140
133
  );
141
134
  }
142
135
 
143
- if (!isPreview && !paneNodes.length) {
136
+ if (!isPreview && !paneNodes.length && !isHtmlAstPane) {
144
137
  return (
145
138
  <>
146
139
  <ContextPanePanel nodeId={node.id} />
@@ -160,8 +153,45 @@ export const Node = memo((props: NodeProps) => {
160
153
  </>
161
154
  );
162
155
  }
156
+
157
+ const contextContent = isHtmlAstPane ? (
158
+ <CreativePane nodeId={props.nodeId} htmlAst={paneNode.htmlAst!} />
159
+ ) : (
160
+ <Pane {...props} />
161
+ );
162
+
163
+ element = (
164
+ <>
165
+ <div className="py-0.5">
166
+ <ConfigPanePanel
167
+ nodeId={props.nodeId}
168
+ isHtmlAstPane={isHtmlAstPane}
169
+ isSandboxMode={props.isSandboxMode || false}
170
+ />
171
+ <PanelVisibilityWrapper
172
+ nodeId={props.nodeId}
173
+ panelType="settings"
174
+ ctx={ctx}
175
+ >
176
+ {contextContent}
177
+ </PanelVisibilityWrapper>
178
+ </div>
179
+ </>
180
+ );
181
+ break;
163
182
  }
164
183
 
184
+ const storyfragmentNodeId = ctx.getClosestNodeTypeFromId(
185
+ node.id,
186
+ 'StoryFragment'
187
+ );
188
+ const storyfragmentNode = storyfragmentNodeId
189
+ ? (ctx.allNodes.get().get(storyfragmentNodeId) ?? null)
190
+ : null;
191
+ const first =
192
+ isStoryFragmentNode(storyfragmentNode) &&
193
+ paneNode.id === storyfragmentNode.paneIds?.[0];
194
+
165
195
  const content = isHtmlAstPane ? (
166
196
  <CreativePane nodeId={props.nodeId} htmlAst={paneNode.htmlAst!} />
167
197
  ) : (