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
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  useEffect,
3
+ useLayoutEffect,
3
4
  useState,
4
5
  useRef,
5
6
  type FocusEvent,
@@ -23,16 +24,147 @@ const viewportMap = {
23
24
  desktop: 'xl',
24
25
  } as const;
25
26
 
27
+ function removeEditProxies(container: HTMLElement) {
28
+ container.querySelectorAll('[data-proxy-positioned]').forEach((el) => {
29
+ const htmlEl = el as HTMLElement;
30
+ const prev = htmlEl.getAttribute('data-proxy-prev-position');
31
+ htmlEl.style.position = prev ?? '';
32
+ htmlEl.removeAttribute('data-proxy-positioned');
33
+ htmlEl.removeAttribute('data-proxy-prev-position');
34
+ });
35
+
36
+ container
37
+ .querySelectorAll('[data-proxy-for]')
38
+ .forEach((icon) => icon.remove());
39
+
40
+ container.querySelectorAll('[data-ast-id]').forEach((el) => {
41
+ const htmlEl = el as HTMLElement;
42
+ htmlEl.style.outline = '';
43
+ htmlEl.style.outlineOffset = '';
44
+ htmlEl.style.cursor = '';
45
+ });
46
+ }
47
+
48
+ function ensurePositionedAnchor(anchorEl: HTMLElement) {
49
+ if (getComputedStyle(anchorEl).position !== 'static') return;
50
+
51
+ anchorEl.setAttribute('data-proxy-positioned', 'true');
52
+ anchorEl.setAttribute('data-proxy-prev-position', anchorEl.style.position);
53
+ anchorEl.style.position = 'relative';
54
+ }
55
+
56
+ function createProxyIcon(
57
+ htmlEl: HTMLElement,
58
+ astId: string,
59
+ htmlAst: CreativePanePayload,
60
+ paneNodeId: string
61
+ ) {
62
+ const icon = document.createElement('div');
63
+ icon.setAttribute('data-proxy-for', astId);
64
+ icon.className = 'compositor-chrome';
65
+ icon.style.position = 'absolute';
66
+ icon.style.zIndex = '1003';
67
+ icon.style.width = '24px';
68
+ icon.style.height = '24px';
69
+ icon.style.backgroundColor = '#06b6d4';
70
+ icon.style.borderRadius = '9999px';
71
+ icon.style.display = 'flex';
72
+ icon.style.alignItems = 'center';
73
+ icon.style.justifyContent = 'center';
74
+ icon.style.color = 'white';
75
+ icon.style.fontSize = '12px';
76
+ icon.style.boxShadow = '0 10px 15px -3px rgb(0 0 0 / 0.1)';
77
+ icon.style.cursor = 'pointer';
78
+ icon.style.pointerEvents = 'auto';
79
+ icon.innerHTML = '✎';
80
+
81
+ icon.onmouseenter = () => {
82
+ htmlEl.style.outline = '3px solid #06b6d4';
83
+ };
84
+ icon.onmouseleave = () => {
85
+ htmlEl.style.outline = '2px dotted #06b6d4';
86
+ };
87
+ icon.onclick = (e) => {
88
+ e.stopPropagation();
89
+ const meta = htmlAst.editableElements?.[astId];
90
+ if (!meta) return;
91
+
92
+ let action = '';
93
+ if (meta.isCssBackground) {
94
+ action = 'style-creative-bg';
95
+ } else if (meta.tagName === 'img') {
96
+ action = 'style-creative-img';
97
+ } else if (meta.tagName === 'a') {
98
+ action = 'style-creative-link';
99
+ } else if (meta.tagName === 'button') {
100
+ action = 'style-creative-btn';
101
+ }
102
+
103
+ if (action) {
104
+ settingsPanelStore.set({
105
+ action,
106
+ nodeId: paneNodeId,
107
+ childId: astId,
108
+ expanded: true,
109
+ });
110
+ }
111
+ };
112
+
113
+ return icon;
114
+ }
115
+
116
+ function syncEditProxies(
117
+ container: HTMLElement,
118
+ htmlAst: CreativePanePayload,
119
+ paneNodeId: string
120
+ ) {
121
+ removeEditProxies(container);
122
+
123
+ container.querySelectorAll('[data-ast-id]').forEach((el) => {
124
+ const htmlEl = el as HTMLElement;
125
+ if (htmlEl.isContentEditable) return;
126
+
127
+ const astId = htmlEl.getAttribute('data-ast-id');
128
+ if (!astId) return;
129
+
130
+ htmlEl.style.outline = '2px dotted #06b6d4';
131
+ htmlEl.style.outlineOffset = '2px';
132
+
133
+ const icon = createProxyIcon(htmlEl, astId, htmlAst, paneNodeId);
134
+
135
+ if (htmlEl.tagName === 'IMG') {
136
+ const parent = htmlEl.parentElement;
137
+ if (parent instanceof HTMLElement) {
138
+ ensurePositionedAnchor(parent);
139
+ icon.style.top = `${htmlEl.offsetTop - 12}px`;
140
+ icon.style.left = `${htmlEl.offsetLeft - 12}px`;
141
+ parent.appendChild(icon);
142
+ return;
143
+ }
144
+ }
145
+
146
+ ensurePositionedAnchor(htmlEl);
147
+ icon.style.top = '-12px';
148
+ icon.style.left = '-12px';
149
+ htmlEl.appendChild(icon);
150
+ });
151
+ }
152
+
26
153
  export const CreativePane = ({
27
154
  nodeId,
28
155
  htmlAst,
29
156
  isProtected = false,
30
157
  }: CreativePaneProps) => {
158
+ const ctx = getCtx();
31
159
  const previews = useStore(renderedPreviews);
32
160
  const { value: viewportKey } = useStore(viewportKeyStore);
161
+ const { value: toolModeVal } = useStore(ctx.toolModeValStore);
33
162
  const [loading, setLoading] = useState(false);
34
163
  const [error, setError] = useState<string | null>(null);
35
164
  const contentRef = useRef<HTMLDivElement>(null);
165
+ const previewRef = useRef<HTMLDivElement>(null);
166
+ const htmlAstRef = useRef(htmlAst);
167
+ htmlAstRef.current = htmlAst;
36
168
 
37
169
  const activeViewport = viewportMap[viewportKey];
38
170
  const htmlContent = previews[nodeId];
@@ -101,100 +233,56 @@ export const CreativePane = ({
101
233
  };
102
234
  }, [htmlAst?.css, htmlAst?.tree, nodeId]);
103
235
 
104
- useEffect(() => {
105
- const ctx = getCtx();
106
- const unsubscribe = ctx.toolModeValStore.subscribe((state) => {
107
- const container = contentRef.current;
108
- if (!container) return;
236
+ useLayoutEffect(() => {
237
+ const el = previewRef.current;
238
+ if (!el || htmlContent == null) return;
239
+ if (el.innerHTML !== htmlContent) {
240
+ el.innerHTML = htmlContent;
241
+ }
242
+ }, [htmlContent]);
109
243
 
110
- const editables = container.querySelectorAll('[data-ast-id]');
244
+ useLayoutEffect(() => {
245
+ const container = contentRef.current;
246
+ if (!container || !htmlContent) return;
111
247
 
112
- if (state.value === 'text') {
113
- editables.forEach((el) => {
114
- const htmlEl = el as HTMLElement;
115
- if (htmlEl.isContentEditable) return;
116
- htmlEl.style.outline = '2px dotted #06b6d4';
117
- htmlEl.style.outlineOffset = '2px';
248
+ if (toolModeVal === 'text') {
249
+ syncEditProxies(container, htmlAstRef.current, nodeId);
250
+ } else {
251
+ removeEditProxies(container);
252
+ }
253
+ });
118
254
 
119
- const astId = htmlEl.getAttribute('data-ast-id');
120
- if (!astId) return;
255
+ useEffect(() => {
256
+ const container = contentRef.current;
257
+ if (!container || !htmlContent) return;
121
258
 
122
- const existingIcon = container.querySelector(
123
- `[data-proxy-for="${astId}"]`
124
- );
125
- if (existingIcon) return;
126
-
127
- const icon = document.createElement('div');
128
- icon.setAttribute('data-proxy-for', astId);
129
- icon.style.position = 'absolute';
130
- icon.style.zIndex = '1003';
131
- icon.style.width = '24px';
132
- icon.style.height = '24px';
133
- icon.style.backgroundColor = '#06b6d4';
134
- icon.style.borderRadius = '9999px';
135
- icon.style.display = 'flex';
136
- icon.style.alignItems = 'center';
137
- icon.style.justifyContent = 'center';
138
- icon.style.color = 'white';
139
- icon.style.fontSize = '12px';
140
- icon.style.boxShadow = '0 10px 15px -3px rgb(0 0 0 / 0.1)';
141
- icon.style.cursor = 'pointer';
142
- icon.innerHTML = '✎';
143
-
144
- const rect = htmlEl.getBoundingClientRect();
145
- const containerRect = container.getBoundingClientRect();
146
- icon.style.top = `${rect.top - containerRect.top - 12}px`;
147
- icon.style.left = `${rect.left - containerRect.left - 12}px`;
148
-
149
- icon.onmouseenter = () => {
150
- htmlEl.style.outline = '3px solid #06b6d4';
151
- };
152
- icon.onmouseleave = () => {
153
- htmlEl.style.outline = '2px dotted #06b6d4';
154
- };
155
- icon.onclick = () => {
156
- const meta = htmlAst.editableElements?.[astId];
157
- if (meta) {
158
- let action = '';
159
- if (meta.isCssBackground) {
160
- action = 'style-creative-bg';
161
- } else if (meta.tagName === 'img') {
162
- action = 'style-creative-img';
163
- } else if (meta.tagName === 'a') {
164
- action = 'style-creative-link';
165
- } else if (meta.tagName === 'button') {
166
- action = 'style-creative-btn';
167
- }
168
-
169
- if (action) {
170
- settingsPanelStore.set({
171
- action,
172
- nodeId,
173
- childId: astId,
174
- expanded: true,
175
- });
176
- }
177
- }
178
- };
179
-
180
- container.appendChild(icon);
181
- });
182
- } else {
183
- editables.forEach((el) => {
184
- (el as HTMLElement).style.outline = '';
185
- (el as HTMLElement).style.outlineOffset = '';
186
- (el as HTMLElement).style.cursor = '';
187
- });
188
- const icons = container.querySelectorAll('[data-proxy-for]');
189
- icons.forEach((icon) => icon.remove());
190
- }
259
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
260
+
261
+ const resizeObserver = new ResizeObserver(() => {
262
+ if (ctx.toolModeValStore.get().value !== 'text') return;
263
+ if (debounceTimer) clearTimeout(debounceTimer);
264
+ debounceTimer = setTimeout(() => {
265
+ if (ctx.toolModeValStore.get().value === 'text') {
266
+ syncEditProxies(container, htmlAstRef.current, nodeId);
267
+ }
268
+ }, 100);
191
269
  });
270
+ resizeObserver.observe(container);
192
271
 
193
- return () => unsubscribe();
194
- }, [htmlContent]);
272
+ return () => {
273
+ resizeObserver.disconnect();
274
+ if (debounceTimer) clearTimeout(debounceTimer);
275
+ };
276
+ }, [htmlContent, nodeId, toolModeVal, ctx]);
277
+
278
+ useEffect(() => {
279
+ return () => {
280
+ const container = contentRef.current;
281
+ if (container) removeEditProxies(container);
282
+ };
283
+ }, []);
195
284
 
196
285
  const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
197
- const ctx = getCtx();
198
286
  const mode = ctx.toolModeValStore.get().value;
199
287
  if (mode !== 'text' || isProtected) return;
200
288
 
@@ -208,7 +296,6 @@ export const CreativePane = ({
208
296
  };
209
297
 
210
298
  const handleBlur = (e: FocusEvent<HTMLDivElement>) => {
211
- const ctx = getCtx();
212
299
  const mode = ctx.toolModeValStore.get().value;
213
300
  if (mode !== 'text' || isProtected) return;
214
301
 
@@ -255,7 +342,7 @@ export const CreativePane = ({
255
342
  {isProtected && (
256
343
  <div className="absolute inset-0 z-50 cursor-crosshair bg-transparent" />
257
344
  )}
258
- <div dangerouslySetInnerHTML={{ __html: htmlContent }} />
345
+ <div ref={previewRef} />
259
346
  </div>
260
347
  </>
261
348
  );
@@ -94,7 +94,7 @@ const AddPanePanel = ({
94
94
  />
95
95
  ) : mode === PaneAddMode.REUSE && !isContextPane ? (
96
96
  <AddPaneReUsePanel nodeId={nodeId} first={first} setMode={setMode} />
97
- ) : mode === PaneAddMode.CODEHOOK ? (
97
+ ) : mode === PaneAddMode.CODEHOOK && !isContextPane ? (
98
98
  <AddPaneCodeHookPanel
99
99
  nodeId={nodeId}
100
100
  first={first}
@@ -139,7 +139,7 @@ const AddPanePanel = ({
139
139
  )}
140
140
  </>
141
141
  )}
142
- {!isTemplate && (
142
+ {!isTemplate && !isContextPane && (
143
143
  <button
144
144
  onClick={() => setMode(PaneAddMode.CODEHOOK)}
145
145
  className="rounded bg-white px-2 py-1 text-sm text-cyan-700 shadow-sm transition-colors hover:bg-cyan-700 hover:text-white"
@@ -294,13 +294,14 @@ const AddPaneNewPanel = ({
294
294
 
295
295
  <div className="mx-auto mb-6 max-w-xl text-center">
296
296
  <h3 className="text-lg font-bold text-gray-800">
297
- Don't publish a page. Stack your story.
297
+ {isContextPane
298
+ ? 'One focused page for in-context links.'
299
+ : "Don't publish a page. Stack your story."}
298
300
  </h3>
299
301
  <p className="text-sm text-gray-500">
300
- A static page is a wall of text. A Smart Tract is a series of beats.
301
- Build one Pane at a time and give each one a single job. Every Pane
302
- acts as a listening device—telling you exactly when your audience
303
- leans in, and when they drift away.
302
+ {isContextPane
303
+ ? 'Readers open this URL from links on your Smart Tract—definitions, offers, FAQs, or anything that needs its own address without breaking the flow. Design this single pane for that job; they close the page and return to where they came from.'
304
+ : 'A static page is a wall of text. A Smart Tract is a series of beats. Build one Pane at a time and give each one a single job. Every Pane acts as a listening device—telling you exactly when your audience leans in, and when they drift away.'}
304
305
  </p>
305
306
  </div>
306
307
 
@@ -174,24 +174,13 @@ const AddPaneReUsePanel = ({
174
174
 
175
175
  templateData.paneNode.parentId = storyfragmentId;
176
176
 
177
- let specificIdx = -1;
178
- let elIdx = -1;
179
177
  const location = first ? 'before' : 'after';
180
-
181
- specificIdx = storyFragmentNode.paneIds.indexOf(nodeId);
182
- elIdx = specificIdx;
183
- if (elIdx === -1) {
184
- storyFragmentNode.paneIds.push(templateData.paneNode.id);
185
- } else {
178
+ const elIdx = storyFragmentNode.paneIds.indexOf(nodeId);
179
+ let specificIdx = elIdx;
180
+ if (elIdx !== -1) {
186
181
  if (location === 'before') {
187
- storyFragmentNode.paneIds.splice(elIdx, 0, templateData.paneNode.id);
188
182
  specificIdx = Math.max(0, specificIdx - 1);
189
183
  } else {
190
- storyFragmentNode.paneIds.splice(
191
- elIdx + 1,
192
- 0,
193
- templateData.paneNode.id
194
- );
195
184
  specificIdx = Math.min(
196
185
  specificIdx + 1,
197
186
  storyFragmentNode.paneIds.length
@@ -206,7 +195,12 @@ const AddPaneReUsePanel = ({
206
195
  specificIdx
207
196
  );
208
197
  ctx.addNodes(templateData.childNodes);
209
- ctx.notifyNode(storyfragmentId);
198
+ ctx.insertPaneId(
199
+ storyfragmentId,
200
+ templateData.paneNode.id,
201
+ nodeId,
202
+ location
203
+ );
210
204
 
211
205
  setMode(PaneAddMode.DEFAULT);
212
206
  } catch (error) {
@@ -260,7 +260,7 @@ const ConfigPanePanel = ({
260
260
  )}
261
261
  </>
262
262
  )}
263
- {isCodeHook && (
263
+ {isCodeHook && !isContextPane && (
264
264
  <button
265
265
  onClick={handleCodeHookConfig}
266
266
  className={buttonClass}