astro-tractstack 2.3.0 → 2.3.1

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 (63) hide show
  1. package/README.md +1 -1
  2. package/bin/create-tractstack.js +2 -2
  3. package/dist/index.js +94 -16
  4. package/package.json +2 -2
  5. package/templates/custom/minimal/CodeHook.astro +10 -2
  6. package/templates/custom/shopify/Cart.tsx +100 -73
  7. package/templates/custom/shopify/CheckoutModal.tsx +509 -120
  8. package/templates/custom/shopify/NativeBookingCalendar.tsx +375 -0
  9. package/templates/custom/shopify/ShopifyCartManager.tsx +92 -37
  10. package/templates/custom/shopify/ShopifyProductGrid.tsx +139 -173
  11. package/templates/custom/shopify/ShopifyServiceList.tsx +20 -3
  12. package/templates/custom/with-examples/CodeHook.astro +10 -2
  13. package/templates/src/components/Footer.astro +4 -4
  14. package/templates/src/components/Header.astro +9 -3
  15. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +3 -3
  16. package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +2 -2
  17. package/templates/src/components/edit/pane/steps/AiCreativeDesignStep.tsx +2 -2
  18. package/templates/src/components/edit/pane/steps/AiLibraryCopyStep.tsx +3 -3
  19. package/templates/src/components/edit/pane/steps/AiRefineDesignStep.tsx +2 -2
  20. package/templates/src/components/edit/pane/steps/AiStandardDesignStep.tsx +7 -7
  21. package/templates/src/components/form/advanced/APIConfigSection.tsx +244 -2
  22. package/templates/src/components/form/shopify/SchedulingSection.tsx +354 -0
  23. package/templates/src/components/storykeep/Dashboard.tsx +1 -1
  24. package/templates/src/components/storykeep/Dashboard_Shopify.tsx +253 -110
  25. package/templates/src/components/storykeep/controls/content/BeliefTable.tsx +14 -5
  26. package/templates/src/components/storykeep/controls/content/KnownResourceTable.tsx +5 -2
  27. package/templates/src/components/storykeep/controls/content/MenuTable.tsx +14 -5
  28. package/templates/src/components/storykeep/controls/content/ProductTable.tsx +180 -101
  29. package/templates/src/components/storykeep/controls/content/ResourceBulkIngest.tsx +9 -5
  30. package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +13 -4
  31. package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +14 -5
  32. package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +111 -0
  33. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +393 -0
  34. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Products.tsx +46 -0
  35. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Schedule.tsx +78 -0
  36. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +55 -0
  37. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Services.tsx +47 -0
  38. package/templates/src/pages/api/auth/lookup-lead.ts +72 -0
  39. package/templates/src/pages/api/booking/availability.ts +72 -0
  40. package/templates/src/pages/api/booking/cancel.ts +73 -0
  41. package/templates/src/pages/api/booking/confirm.ts +82 -0
  42. package/templates/src/pages/api/booking/hold.ts +75 -0
  43. package/templates/src/pages/api/booking/list.ts +66 -0
  44. package/templates/src/pages/api/booking/metrics.ts +60 -0
  45. package/templates/src/pages/api/booking/release.ts +76 -0
  46. package/templates/src/pages/api/sandbox.ts +2 -2
  47. package/templates/src/pages/api/shopify/createCart.ts +4 -8
  48. package/templates/src/pages/api/shopify/getProducts.ts +15 -15
  49. package/templates/src/pages/storykeep/login.astro +21 -14
  50. package/templates/src/stores/shopify.ts +81 -25
  51. package/templates/src/types/tractstack.ts +54 -0
  52. package/templates/src/utils/api/advancedConfig.ts +2 -0
  53. package/templates/src/utils/api/advancedHelpers.ts +40 -3
  54. package/templates/src/utils/api/bookingHelpers.ts +125 -0
  55. package/templates/src/utils/api/brandHelpers.ts +10 -0
  56. package/templates/src/utils/auth.ts +29 -9
  57. package/templates/src/utils/compositor/aiGeneration.ts +3 -3
  58. package/templates/src/utils/compositor/aiPaneParser.ts +2 -2
  59. package/templates/src/utils/customHelpers.ts +0 -21
  60. package/templates/src/utils/profileStorage.ts +5 -0
  61. package/templates/src/utils/tenantResolver.ts +2 -1
  62. package/utils/inject-files.ts +82 -4
  63. package/templates/custom/shopify/CalDotComBooking.tsx +0 -44
@@ -6,6 +6,11 @@ import type { ResourceNode } from '@/types/compositorTypes';
6
6
 
7
7
  interface Props {
8
8
  resources: Record<string, ResourceNode[]>;
9
+ options?: {
10
+ params?: {
11
+ options?: string;
12
+ };
13
+ };
9
14
  }
10
15
 
11
16
  interface ShopifyOption {
@@ -17,6 +22,7 @@ interface ShopifyVariant {
17
22
  id: string;
18
23
  title: string;
19
24
  price: { amount: string; currencyCode: string };
25
+ compareAtPrice?: { amount: string; currencyCode: string };
20
26
  selectedOptions: { name: string; value: string }[];
21
27
  }
22
28
 
@@ -31,7 +37,6 @@ function ProductCard({ resource, allServices }: ProductCardProps) {
31
37
  const serviceBoundSlug = resource.optionsPayload?.serviceBound as
32
38
  | string
33
39
  | undefined;
34
-
35
40
  const boundServiceResource = serviceBoundSlug
36
41
  ? allServices.find((r) => r.slug === serviceBoundSlug)
37
42
  : undefined;
@@ -47,15 +52,12 @@ function ProductCard({ resource, allServices }: ProductCardProps) {
47
52
 
48
53
  const options: ShopifyOption[] = product?.options || [];
49
54
  const variants: ShopifyVariant[] = product?.variants || [];
50
-
51
- const isUnconfigured = options.some((o) => o.name === 'Title');
52
-
53
- if (isUnconfigured) {
54
- return null;
55
- }
55
+ const vendor: string = product?.vendor || '';
56
56
 
57
57
  const hasModeOption = options.some((o) => o.name === 'Mode');
58
- const visibleOptions = options.filter((o) => o.name !== 'Mode');
58
+ const visibleOptions = options.filter(
59
+ (o) => o.name !== 'Mode' && o.name !== 'Title'
60
+ );
59
61
 
60
62
  const [selections, setSelections] = useState<Record<string, string>>(() => {
61
63
  const initial: Record<string, string> = {};
@@ -66,216 +68,180 @@ function ProductCard({ resource, allServices }: ProductCardProps) {
66
68
  });
67
69
 
68
70
  const getVariant = (targetMode: 'Shipped' | 'Pickup' | null) => {
71
+ if (targetMode && !hasModeOption) return undefined;
69
72
  const found = variants.find((v) => {
70
73
  const optionsMatch = visibleOptions.every((opt) => {
71
74
  const variantOpt = v.selectedOptions.find((o) => o.name === opt.name);
72
75
  return variantOpt?.value === selections[opt.name];
73
76
  });
74
-
75
77
  if (!optionsMatch) return false;
76
-
77
78
  if (hasModeOption && targetMode) {
78
79
  const modeOpt = v.selectedOptions.find((o) => o.name === 'Mode');
79
80
  return modeOpt?.value === targetMode;
80
81
  }
81
-
82
- return true;
82
+ return !(hasModeOption && !targetMode);
83
83
  });
84
-
85
84
  return found;
86
85
  };
87
86
 
88
- const variantShipped = getVariant(hasModeOption ? 'Shipped' : null);
89
- const variantPickup = getVariant(hasModeOption ? 'Pickup' : null);
90
- const cartKey = `${resource.id}_${variantShipped?.id || 'null'}_${
91
- variantPickup?.id || 'null'
92
- }`;
93
- const cartItem = cart[cartKey];
94
- const quantity = cartItem?.quantity || 0;
87
+ const variantShipped = hasModeOption
88
+ ? getVariant('Shipped')
89
+ : getVariant(null);
90
+ const variantPickup = hasModeOption ? getVariant('Pickup') : undefined;
95
91
 
96
- const currentDisplayVariant =
97
- getVariant('Shipped') || getVariant('Pickup') || variants[0];
92
+ const currentDisplayVariant = variantShipped || variantPickup || variants[0];
98
93
  const price = currentDisplayVariant?.price?.amount;
94
+ const compareAtPrice = currentDisplayVariant?.compareAtPrice?.amount;
99
95
  const currency = currentDisplayVariant?.price?.currencyCode || 'USD';
96
+
97
+ // High contrast rose badge calculation
98
+ let discountPercent = 0;
99
+ if (price && compareAtPrice) {
100
+ const p = parseFloat(price);
101
+ const cap = parseFloat(compareAtPrice);
102
+ if (cap > p) {
103
+ discountPercent = Math.round(((cap - p) / cap) * 100);
104
+ }
105
+ }
106
+
100
107
  const { src, srcSet } = getShopifyImage(
101
108
  resource,
102
109
  '600',
103
110
  currentDisplayVariant?.id
104
111
  );
105
112
 
106
- const handleAction = (action: 'add' | 'remove') => {
107
- if (action === 'remove') {
108
- const queueUpdates: CartAction[] = [];
109
-
110
- queueUpdates.push({
113
+ const handleAction = () => {
114
+ const queueUpdates: CartAction[] = [
115
+ {
111
116
  resourceId: resource.id,
117
+ gid: product?.id,
112
118
  variantIdShipped: variantShipped?.id,
113
119
  variantIdPickup: variantPickup?.id,
114
- action: 'remove',
115
- });
116
-
117
- if (boundServiceResource) {
118
- queueUpdates.push({
119
- resourceId: boundServiceResource.id,
120
- variantId: boundServiceResource.optionsPayload?.shopifyData
121
- ? JSON.parse(boundServiceResource.optionsPayload.shopifyData)
122
- .variants?.[0]?.id
123
- : undefined,
124
- action: 'remove',
125
- });
126
- }
127
-
128
- addQueue.set([...addQueue.get(), ...queueUpdates]);
129
- return;
130
- }
131
-
132
- const queueUpdates: CartAction[] = [];
133
-
134
- const productAction: CartAction & { boundResourceId?: string } = {
135
- resourceId: resource.id,
136
- gid: product?.id,
137
- variantIdShipped: variantShipped?.id,
138
- variantIdPickup: variantPickup?.id,
139
- action: 'add',
140
- boundResourceId: boundServiceResource?.id,
141
- };
142
- queueUpdates.push(productAction);
143
-
144
- if (boundServiceResource) {
145
- let serviceVariantId = undefined;
146
- try {
147
- if (boundServiceResource.optionsPayload?.shopifyData) {
148
- const serviceData = JSON.parse(
149
- boundServiceResource.optionsPayload.shopifyData
150
- );
151
- serviceVariantId = serviceData.variants?.[0]?.id;
152
- }
153
- } catch (e) {}
154
-
155
- queueUpdates.push({
156
- resourceId: boundServiceResource.id,
157
- variantId: serviceVariantId,
158
120
  action: 'add',
159
- });
160
- } else if (serviceBoundSlug) {
161
- console.warn(
162
- `[Shopify] Service bound to slug '${serviceBoundSlug}' was not found in provided resources.`
163
- );
164
- }
165
-
121
+ boundResourceId: boundServiceResource?.id,
122
+ },
123
+ ];
166
124
  addQueue.set([...addQueue.get(), ...queueUpdates]);
167
125
  };
168
126
 
169
127
  return (
170
- <div className="flex flex-col overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm">
171
- <div className="aspect-square w-full overflow-hidden bg-gray-100">
172
- <img
173
- src={src}
174
- srcSet={srcSet}
175
- alt={resource.title}
176
- className="h-full w-full object-cover object-center"
177
- loading="lazy"
178
- />
179
- </div>
180
-
181
- <div className="flex flex-1 flex-col p-6">
182
- <h3 className="text-lg font-bold text-gray-900">{resource.title}</h3>
183
- <p className="mt-2 flex-grow text-sm text-gray-500">
184
- {resource.oneliner}
185
- </p>
186
-
187
- {boundServiceResource && (
188
- <div className="mt-2 w-fit rounded bg-blue-50 p-3 text-xs font-bold text-blue-700">
189
- Includes {boundServiceResource.title}
190
- </div>
191
- )}
128
+ <div className="group flex flex-col text-left font-main">
129
+ {/* Clickable Area: Image and Title */}
130
+ <button
131
+ onClick={handleAction}
132
+ className="text-left focus:outline-none"
133
+ aria-label={`Add ${resource.title} to cart`}
134
+ >
135
+ {/* Rounded-2xl Frame with Top-Left Badge */}
136
+ <div className="relative aspect-square w-full overflow-hidden rounded-2xl bg-brand-8 transition-opacity group-hover:opacity-90">
137
+ <img
138
+ src={src}
139
+ srcSet={srcSet}
140
+ alt={resource.title}
141
+ className="h-full w-full object-cover object-center"
142
+ loading="lazy"
143
+ />
144
+ {discountPercent > 0 && (
145
+ <div className="absolute left-4 top-4 flex h-12 w-12 items-center justify-center rounded-md bg-rose-600 text-xs font-bold text-white shadow-sm">
146
+ -{discountPercent}%
147
+ </div>
148
+ )}
149
+ </div>
192
150
 
193
- {visibleOptions.length > 0 && (
194
- <div className="mt-4 w-fit space-y-3 p-1">
195
- {visibleOptions.map((opt) => (
196
- <div key={opt.name}>
197
- <label className="mb-1 block text-xs font-bold text-gray-700">
198
- {opt.name}
199
- </label>
200
- <select
201
- value={selections[opt.name]}
202
- onChange={(e) => {
203
- const newVal = e.target.value;
204
- setSelections((prev) => ({
205
- ...prev,
206
- [opt.name]: newVal,
207
- }));
208
- }}
209
- className="block w-full rounded-md border-gray-300 p-2 text-sm shadow-sm focus:border-black focus:ring-black"
210
- >
211
- {opt.values.map((val) => (
212
- <option key={val} value={val}>
213
- {val}
214
- </option>
215
- ))}
216
- </select>
217
- </div>
218
- ))}
151
+ <div className="mt-6 flex flex-col">
152
+ {/* Vendor Label */}
153
+ {vendor && (
154
+ <span className="text-xs font-bold uppercase tracking-widest text-brand-6">
155
+ {vendor}
156
+ </span>
157
+ )}
158
+
159
+ <h3 className="mt-1 text-2xl font-bold text-brand-1">
160
+ {resource.title}
161
+ </h3>
162
+
163
+ {/* Combined Price Baseline */}
164
+ <div className="mt-1 flex items-baseline space-x-2">
165
+ <span className="text-lg font-bold text-brand-1">
166
+ {price} {currency}
167
+ </span>
168
+ {discountPercent > 0 && (
169
+ <span className="text-sm text-brand-6 line-through">
170
+ {compareAtPrice} {currency}
171
+ </span>
172
+ )}
219
173
  </div>
220
- )}
221
174
 
222
- <div className="mt-4 flex items-center justify-between">
223
- <span className="text-base font-bold text-gray-900">
224
- {price ? `${price} ${currency}` : ''}
225
- </span>
226
- <div className="flex items-center space-x-3">
227
- {quantity > 0 ? (
228
- <>
229
- <button
230
- onClick={() => handleAction('remove')}
231
- className="flex h-8 w-8 items-center justify-center rounded-full border border-gray-300 text-gray-600 hover:bg-gray-100"
232
- aria-label="Remove one"
233
- >
234
- -
235
- </button>
236
- <span className="text-sm font-bold text-gray-900">
237
- {quantity}
238
- </span>
239
- <button
240
- onClick={() => handleAction('add')}
241
- className="flex h-8 w-8 items-center justify-center rounded-full border border-gray-300 text-gray-600 hover:bg-gray-100"
242
- aria-label="Add one"
243
- >
244
- +
245
- </button>
246
- </>
247
- ) : (
248
- <button
249
- onClick={() => handleAction('add')}
250
- className="rounded-md bg-black px-4 py-2 text-sm font-bold text-white hover:bg-gray-800"
175
+ <p className="mt-3 text-sm text-brand-7">{resource.oneliner}</p>
176
+ </div>
177
+ </button>
178
+
179
+ {/* Interactive Variant Selectors */}
180
+ {visibleOptions.length > 0 && (
181
+ <div className="mt-4 space-y-4">
182
+ {visibleOptions.map((opt) => (
183
+ <div key={opt.name} onClick={(e) => e.stopPropagation()}>
184
+ <label className="mb-1 block text-xs font-bold uppercase text-brand-7">
185
+ {opt.name}
186
+ </label>
187
+ <select
188
+ value={selections[opt.name]}
189
+ onChange={(e) =>
190
+ setSelections((prev) => ({
191
+ ...prev,
192
+ [opt.name]: e.target.value,
193
+ }))
194
+ }
195
+ className="block w-full border-b border-brand-8 bg-transparent py-2 text-sm text-brand-1 focus:border-brand-1 focus:outline-none"
251
196
  >
252
- Add to Cart
253
- </button>
254
- )}
255
- </div>
197
+ {opt.values.map((val) => (
198
+ <option key={val} value={val}>
199
+ {val}
200
+ </option>
201
+ ))}
202
+ </select>
203
+ </div>
204
+ ))}
256
205
  </div>
257
- </div>
206
+ )}
207
+
208
+ {boundServiceResource && (
209
+ <div className="bg-brand-4/10 mt-4 w-fit rounded px-2 py-1 text-xs font-bold text-brand-4">
210
+ INCLUDES {boundServiceResource.title.toUpperCase()}
211
+ </div>
212
+ )}
258
213
  </div>
259
214
  );
260
215
  }
261
216
 
262
- export default function ShopifyProductGrid({ resources = {} }: Props) {
263
- const products = resources['product'] || [];
217
+ export default function ShopifyProductGrid({ resources = {}, options }: Props) {
218
+ let products = resources['product'] || [];
264
219
  const services = resources['service'] || [];
265
220
 
266
- if (products.length === 0) {
267
- return null;
221
+ let group = '';
222
+ try {
223
+ const parsedOptions = JSON.parse(options?.params?.options || '{}');
224
+ group = parsedOptions.group || '';
225
+ } catch (e) {}
226
+
227
+ if (group) {
228
+ products = products.filter((p) => p.optionsPayload?.group === group);
268
229
  }
269
230
 
231
+ if (products.length === 0) return null;
232
+
233
+ // Grid with increased gaps matching the design
270
234
  return (
271
- <div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
272
- {products.map((resource) => (
273
- <ProductCard
274
- key={resource.id}
275
- resource={resource}
276
- allServices={services}
277
- />
278
- ))}
235
+ <div className="mx-auto max-w-7xl px-4 md:px-8">
236
+ <div className="grid grid-cols-2 gap-x-8 gap-y-16 md:gap-x-12 xl:grid-cols-3">
237
+ {products.map((resource) => (
238
+ <ProductCard
239
+ key={resource.id}
240
+ resource={resource}
241
+ allServices={services}
242
+ />
243
+ ))}
244
+ </div>
279
245
  </div>
280
246
  );
281
247
  }
@@ -4,13 +4,30 @@ import type { ResourceNode } from '@/types/compositorTypes';
4
4
 
5
5
  interface Props {
6
6
  resources: Record<string, ResourceNode[]>;
7
+ options?: {
8
+ params?: {
9
+ options?: string;
10
+ };
11
+ };
7
12
  }
8
13
 
9
- export default function ShopifyServiceList({ resources = {} }: Props) {
14
+ export default function ShopifyServiceList({ resources = {}, options }: Props) {
10
15
  const cart = useStore(cartStore);
11
16
 
12
17
  const products = resources['product'] || [];
13
- const services = resources['service'] || [];
18
+ let services = resources['service'] || [];
19
+
20
+ let group = '';
21
+ try {
22
+ const parsedOptions = JSON.parse(options?.params?.options || '{}');
23
+ group = parsedOptions.group || '';
24
+ } catch (e) {
25
+ // Ignore JSON parse errors
26
+ }
27
+
28
+ if (group) {
29
+ services = services.filter((s) => s.optionsPayload?.group === group);
30
+ }
14
31
 
15
32
  const boundServiceSlugs = new Set(
16
33
  products
@@ -86,7 +103,7 @@ export default function ShopifyServiceList({ resources = {} }: Props) {
86
103
  <div className="flex items-center gap-2">
87
104
  <h3 className="font-bold text-gray-900">{resource.title}</h3>
88
105
  {duration && (
89
- <span className="inline-flex items-center rounded-full bg-blue-50 px-2 py-0.5 text-xs font-bold text-blue-700">
106
+ <span className="inline-flex items-center rounded-sm bg-blue-50 px-2 py-0.5 text-xs font-bold text-blue-700">
90
107
  {duration} mins
91
108
  </span>
92
109
  )}
@@ -62,9 +62,17 @@ export const components = {
62
62
  ) : target === 'epinet' ? (
63
63
  <EpinetWrapper fullContentMap={fullContentMap} client:only="react" />
64
64
  ) : target === 'shopify-product-grid' ? (
65
- <ShopifyProductGrid resources={resourcesPayload} client:only="react" />
65
+ <ShopifyProductGrid
66
+ options={options}
67
+ resources={resourcesPayload}
68
+ client:only="react"
69
+ />
66
70
  ) : target === 'shopify-service-list' ? (
67
- <ShopifyServiceList resources={resourcesPayload} client:only="react" />
71
+ <ShopifyServiceList
72
+ options={options}
73
+ resources={resourcesPayload}
74
+ client:only="react"
75
+ />
68
76
  ) : (
69
77
  <div class="rounded-lg bg-gray-50 p-8 text-center">
70
78
  <p class="text-gray-600">CodeHook target "{target}" not found</p>
@@ -161,8 +161,8 @@ const createdDate = created ? new Date(created) : new Date();
161
161
  <div
162
162
  class="my-2 flex flex-row items-center justify-center text-myblue md:flex-col"
163
163
  >
164
- <div class="px-4 text-center text-lg md:px-12">
165
- pressed with
164
+ <div class="px-4 text-center text-sm italic md:px-12">
165
+ This website has been pressed with
166
166
  <a
167
167
  href="https://tractstack.com/?utm_source=tractstack&utm_medium=www&utm_campaign=community"
168
168
  class="font-bold underline hover:text-black"
@@ -171,12 +171,12 @@ const createdDate = created ? new Date(created) : new Date();
171
171
  >
172
172
  Tract Stack</a
173
173
  >
174
- &ndash; the &#8220;free web&#8221; press by{` `}
174
+ by{` `}
175
175
  <a
176
176
  href="https://atriskmedia.com/?utm_source=tractstack&utm_medium=www&utm_campaign=community"
177
177
  class="font-bold underline hover:text-black"
178
178
  target="_blank">At Risk Media</a
179
- >
179
+ >.
180
180
  </div>
181
181
  <br /><br /><br />
182
182
  </div>
@@ -10,13 +10,13 @@ import ShopifyCartManager from '@/custom/shopify/ShopifyCartManager';
10
10
  import CartIcon from '@/custom/shopify/CartIcon';
11
11
  import CartModal from '@/custom/shopify/CartModal';
12
12
  import CheckoutModal from '@/custom/shopify/CheckoutModal';
13
- import type { MenuNode } from '@/types/tractstack';
13
+ import type { MenuNode, BrandConfig } from '@/types/tractstack';
14
14
  import type { ResourceNode } from '@/types/compositorTypes';
15
15
 
16
16
  export interface Props {
17
17
  title: string;
18
18
  slug: string;
19
- brandConfig: any;
19
+ brandConfig: BrandConfig;
20
20
  isContext?: boolean;
21
21
  isStoryKeep?: boolean;
22
22
  isEditable?: boolean;
@@ -79,7 +79,11 @@ if (hasShopify) {
79
79
  <CartIcon client:only="react" />
80
80
  </div>
81
81
  ) : (
82
- <CheckoutModal client:only="react" resources={shopifyResources} />
82
+ <CheckoutModal
83
+ client:only="react"
84
+ resources={shopifyResources}
85
+ maxLength={brandConfig?.scheduling?.maxLengthMinutes || 180}
86
+ />
83
87
  )}
84
88
  </>
85
89
  ) : null
@@ -94,6 +98,7 @@ if (hasShopify) {
94
98
  [`default`, `logo`].includes(wordmarkMode) ? (
95
99
  <>
96
100
  <img
101
+ id="t8k-logo"
97
102
  src={logo}
98
103
  alt="Logo"
99
104
  class="pointer-events-none h-8 w-auto"
@@ -106,6 +111,7 @@ if (hasShopify) {
106
111
  {
107
112
  [`default`, `wordmark`].includes(wordmarkMode) ? (
108
113
  <img
114
+ id="t8k-wordmark"
109
115
  src={wordmark}
110
116
  alt="Wordmark"
111
117
  class="pointer-events-none h-14 w-auto max-w-48 md:max-w-72"
@@ -276,9 +276,9 @@ const AddPaneNewPanel = ({
276
276
  {!hasAssemblyAI && (
277
277
  <div className="rounded-lg border-l-4 border-blue-400 bg-blue-50 p-4 shadow-sm">
278
278
  <p className="text-sm text-blue-800">
279
- Tract Stack uses AssemblyAI AskLemur service to generate designs,
280
- describe content, and streamline the management of your site. We
281
- strongly recommend enabling these features. See{' '}
279
+ Tract Stack uses AssemblyAI to generate designs, describe content,
280
+ and streamline the management of your site. We strongly recommend
281
+ enabling these features. See{' '}
282
282
  <a
283
283
  href="https://freewebpress.org"
284
284
  target="_blank"
@@ -11,7 +11,7 @@ import { AiDesignStep, type AiDesignConfig } from './steps/AiDesignStep';
11
11
  import { AiRefineDesignStep } from './steps/AiRefineDesignStep';
12
12
  import prompts from '@/constants/prompts.json';
13
13
  import { parseAiPane } from '@/utils/compositor/aiPaneParser';
14
- import { callAskLemurAPI } from '@/utils/compositor/aiGeneration';
14
+ import { callAaiAPI } from '@/utils/compositor/aiGeneration';
15
15
  import type { PaneNode, TemplatePane } from '@/types/compositorTypes';
16
16
 
17
17
  interface AiRestylePaneModalProps {
@@ -87,7 +87,7 @@ export const AiRestylePaneModal = ({
87
87
  .replace('{{COPY_INPUT}}', 'A generic content section')
88
88
  .replace('{{LAYOUT_TYPE}}', 'Text Only');
89
89
 
90
- const resultStr = await callAskLemurAPI({
90
+ const resultStr = await callAaiAPI({
91
91
  prompt: formattedPrompt,
92
92
  context: shellPromptDetails.system || '',
93
93
  expectJson: true,
@@ -6,7 +6,7 @@ import ArrowPathRoundedSquareIcon from '@heroicons/react/24/outline/ArrowPathRou
6
6
  import prompts from '@/constants/prompts.json';
7
7
  import { htmlToHtmlAst } from '@/utils/compositor/htmlAst';
8
8
  import type { TemplatePane } from '@/types/compositorTypes';
9
- import { callAskLemurAPI } from '@/utils/compositor/aiGeneration';
9
+ import { callAaiAPI } from '@/utils/compositor/aiGeneration';
10
10
  import BooleanToggle from '@/components/form/BooleanToggle';
11
11
  import { AiDesignStep, type AiDesignConfig } from './AiDesignStep';
12
12
 
@@ -86,7 +86,7 @@ export const AiCreativeDesignStep = ({
86
86
  userPrompt = userPrompt.replace('{{DESIGN_NOTES}}', combinedNotes);
87
87
 
88
88
  // Use shared infrastructure utility
89
- const rawHtml = await callAskLemurAPI({
89
+ const rawHtml = await callAaiAPI({
90
90
  prompt: userPrompt,
91
91
  context: systemPrompt,
92
92
  expectJson: false,
@@ -6,7 +6,7 @@ import {
6
6
  convertTemplateToAIShell,
7
7
  } from '@/utils/compositor/designLibraryHelper';
8
8
  import { parseAiPane, parseAiCopyHtml } from '@/utils/compositor/aiPaneParser';
9
- import { callAskLemurAPI } from '@/utils/compositor/aiGeneration';
9
+ import { callAaiAPI } from '@/utils/compositor/aiGeneration';
10
10
  import { CopyInputStep, type CopyMode } from './CopyInputStep';
11
11
  import type { DesignLibraryEntry } from '@/types/tractstack';
12
12
  import type { TemplatePane } from '@/types/compositorTypes';
@@ -159,7 +159,7 @@ export const AiLibraryCopyStep = ({
159
159
  .replace('{{LAYOUT_TYPE}}', layoutType)
160
160
  .replace('{{COLUMN_EXAMPLE}}', columnPreset.example);
161
161
 
162
- const copyResult = await callAskLemurAPI({
162
+ const copyResult = await callAaiAPI({
163
163
  prompt: formattedCopyPrompt,
164
164
  context: copyPromptDetails.system || '',
165
165
  expectJson: false,
@@ -201,7 +201,7 @@ export const AiLibraryCopyStep = ({
201
201
  .replace('{{LAYOUT_TYPE}}', layoutType)
202
202
  .replace('{{SHELL_JSON}}', shellResult);
203
203
 
204
- const copyResult = await callAskLemurAPI({
204
+ const copyResult = await callAaiAPI({
205
205
  prompt: formattedCopyPrompt,
206
206
  context: copyPromptDetails.system || '',
207
207
  expectJson: false,
@@ -5,7 +5,7 @@ import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
5
5
  import ArrowPathRoundedSquareIcon from '@heroicons/react/24/outline/ArrowPathRoundedSquareIcon';
6
6
  import prompts from '@/constants/prompts.json';
7
7
  import { htmlToHtmlAst, cleanHtml } from '@/utils/compositor/htmlAst';
8
- import { callAskLemurAPI } from '@/utils/compositor/aiGeneration';
8
+ import { callAaiAPI } from '@/utils/compositor/aiGeneration';
9
9
  import type { TemplatePane } from '@/types/compositorTypes';
10
10
 
11
11
  interface AiRefineDesignStepProps {
@@ -54,7 +54,7 @@ export const AiRefineDesignStep = ({
54
54
  userPrompt = userPrompt.replace('{{HTML_INPUT}}', cleanHtml(initialHtml));
55
55
 
56
56
  // 1. Get RAW output from AI
57
- const resultHtml = await callAskLemurAPI({
57
+ const resultHtml = await callAaiAPI({
58
58
  prompt: userPrompt,
59
59
  context: systemPrompt,
60
60
  expectJson: false,