astro-tractstack 2.3.0 → 2.3.2

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 (95) hide show
  1. package/README.md +1 -1
  2. package/bin/create-tractstack.js +2 -2
  3. package/dist/index.js +130 -19
  4. package/package.json +2 -2
  5. package/templates/custom/minimal/CodeHook.astro +10 -2
  6. package/templates/custom/shopify/Cart.tsx +115 -77
  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 +91 -45
  10. package/templates/custom/shopify/ShopifyCheckout.tsx +4 -33
  11. package/templates/custom/shopify/ShopifyProductGrid.tsx +170 -176
  12. package/templates/custom/shopify/ShopifyServiceList.tsx +112 -51
  13. package/templates/custom/with-examples/CodeHook.astro +10 -2
  14. package/templates/src/components/Footer.astro +6 -6
  15. package/templates/src/components/Header.astro +23 -11
  16. package/templates/src/components/Menu.tsx +157 -135
  17. package/templates/src/components/codehooks/BunnyVideoSetup.tsx +2 -2
  18. package/templates/src/components/codehooks/EpinetDurationSelector.tsx +27 -6
  19. package/templates/src/components/codehooks/EpinetTableView.tsx +153 -112
  20. package/templates/src/components/codehooks/EpinetWrapper.tsx +4 -1
  21. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +8 -1
  22. package/templates/src/components/codehooks/ProductCardSetup.tsx +9 -1
  23. package/templates/src/components/codehooks/ProductGridSetup.tsx +9 -1
  24. package/templates/src/components/compositor/nodes/BgPaneWrapper.tsx +2 -1
  25. package/templates/src/components/compositor/nodes/GhostInsertBlock.tsx +1 -1
  26. package/templates/src/components/edit/ToolBar.tsx +2 -1
  27. package/templates/src/components/edit/context/ContextPaneConfig_slug.tsx +2 -2
  28. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +13 -0
  29. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +3 -3
  30. package/templates/src/components/edit/pane/AddPanePanel_newCustomCopy.tsx +2 -2
  31. package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +2 -2
  32. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
  33. package/templates/src/components/edit/pane/steps/AiCreativeDesignStep.tsx +2 -2
  34. package/templates/src/components/edit/pane/steps/AiLibraryCopyStep.tsx +3 -3
  35. package/templates/src/components/edit/pane/steps/AiRefineDesignStep.tsx +2 -2
  36. package/templates/src/components/edit/pane/steps/AiStandardDesignStep.tsx +7 -7
  37. package/templates/src/components/edit/state/SaveModal.tsx +1 -1
  38. package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +8 -3
  39. package/templates/src/components/form/DateTimeInput.tsx +10 -3
  40. package/templates/src/components/form/FileUpload.tsx +11 -5
  41. package/templates/src/components/form/NumberInput.tsx +2 -2
  42. package/templates/src/components/form/advanced/APIConfigSection.tsx +208 -2
  43. package/templates/src/components/form/brand/SiteConfigSection.tsx +10 -0
  44. package/templates/src/components/form/shopify/SchedulingSection.tsx +354 -0
  45. package/templates/src/components/storykeep/Dashboard.tsx +1 -1
  46. package/templates/src/components/storykeep/Dashboard_Shopify.tsx +252 -110
  47. package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +2 -2
  48. package/templates/src/components/storykeep/controls/content/BeliefTable.tsx +14 -5
  49. package/templates/src/components/storykeep/controls/content/KnownResourceTable.tsx +5 -2
  50. package/templates/src/components/storykeep/controls/content/MenuTable.tsx +14 -5
  51. package/templates/src/components/storykeep/controls/content/ProductTable.tsx +180 -101
  52. package/templates/src/components/storykeep/controls/content/ResourceBulkIngest.tsx +88 -56
  53. package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +14 -4
  54. package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +14 -5
  55. package/templates/src/components/storykeep/email-builder/Blocks.tsx +169 -0
  56. package/templates/src/components/storykeep/email-builder/EmailBuilder.tsx +223 -0
  57. package/templates/src/components/storykeep/email-builder/PreviewModal.tsx +136 -0
  58. package/templates/src/components/storykeep/email-builder/PropertyPanel.tsx +154 -0
  59. package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +104 -0
  60. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +419 -0
  61. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Emails.tsx +105 -0
  62. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Products.tsx +46 -0
  63. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Schedule.tsx +78 -0
  64. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +55 -0
  65. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Services.tsx +47 -0
  66. package/templates/src/layouts/Layout.astro +8 -5
  67. package/templates/src/pages/api/auth/lookup-lead.ts +72 -0
  68. package/templates/src/pages/api/booking/availability.ts +72 -0
  69. package/templates/src/pages/api/booking/cancel.ts +73 -0
  70. package/templates/src/pages/api/booking/confirm.ts +82 -0
  71. package/templates/src/pages/api/booking/hold.ts +75 -0
  72. package/templates/src/pages/api/booking/list.ts +66 -0
  73. package/templates/src/pages/api/booking/metrics.ts +60 -0
  74. package/templates/src/pages/api/booking/release.ts +76 -0
  75. package/templates/src/pages/api/sandbox.ts +2 -2
  76. package/templates/src/pages/api/shopify/createCart.ts +4 -8
  77. package/templates/src/pages/api/shopify/getProducts.ts +15 -15
  78. package/templates/src/pages/storykeep/login.astro +21 -14
  79. package/templates/src/stores/shopify.ts +97 -25
  80. package/templates/src/types/formTypes.ts +4 -2
  81. package/templates/src/types/tractstack.ts +59 -2
  82. package/templates/src/utils/api/advancedConfig.ts +2 -0
  83. package/templates/src/utils/api/advancedHelpers.ts +40 -3
  84. package/templates/src/utils/api/bookingHelpers.ts +125 -0
  85. package/templates/src/utils/api/brandConfig.ts +2 -0
  86. package/templates/src/utils/api/brandHelpers.ts +26 -0
  87. package/templates/src/utils/api/emailHelpers.ts +105 -0
  88. package/templates/src/utils/auth.ts +29 -9
  89. package/templates/src/utils/compositor/aiGeneration.ts +3 -3
  90. package/templates/src/utils/compositor/aiPaneParser.ts +2 -2
  91. package/templates/src/utils/customHelpers.ts +0 -21
  92. package/templates/src/utils/profileStorage.ts +5 -0
  93. package/templates/src/utils/tenantResolver.ts +3 -2
  94. package/utils/inject-files.ts +116 -5
  95. package/templates/custom/shopify/CalDotComBooking.tsx +0 -44
@@ -4,16 +4,15 @@ import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
4
4
  import ClipboardDocumentIcon from '@heroicons/react/24/outline/ClipboardDocumentIcon';
5
5
  import CheckIcon from '@heroicons/react/24/outline/CheckIcon';
6
6
  import {
7
- shopifyData,
8
7
  shopifyStatus,
9
- fetchShopifyProducts,
8
+ shopifyActiveTabStore,
10
9
  type ShopifyProduct,
11
10
  } from '@/stores/shopify';
12
- import ProductTable from './controls/content/ProductTable';
13
11
  import ResourceForm from './controls/content/ResourceForm';
14
12
  import { saveBrandConfigWithStateUpdate } from '@/utils/api/brandConfig';
15
13
  import {
16
14
  deleteResource,
15
+ getResource,
17
16
  getResourcesByCategory,
18
17
  } from '@/utils/api/resourceConfig';
19
18
  import { convertToLocalState } from '@/utils/api/brandHelpers';
@@ -21,6 +20,13 @@ import BooleanToggle from '@/components/form/BooleanToggle';
21
20
  import type { BrandConfig, BrandConfigState } from '@/types/tractstack';
22
21
  import type { ResourceNode } from '@/types/compositorTypes';
23
22
  import type { ResourceConfig } from '@/types/tractstack';
23
+ import ShopifyDashboard from './shopify/ShopifyDashboard';
24
+ import ShopifyDashboard_Products from './shopify/ShopifyDashboard_Products';
25
+ import ShopifyDashboard_Services from './shopify/ShopifyDashboard_Services';
26
+ import ShopifyDashboard_Schedule from './shopify/ShopifyDashboard_Schedule';
27
+ import ShopifyDashboard_Search from './shopify/ShopifyDashboard_Search';
28
+ import ShopifyDashboard_Bookings from './shopify/ShopifyDashboard_Bookings';
29
+ import ShopifyDashboard_Emails from './shopify/ShopifyDashboard_Emails';
24
30
 
25
31
  interface DashboardShopifyProps {
26
32
  brandConfig: BrandConfig;
@@ -33,8 +39,9 @@ export default function StoryKeepDashboard_Shopify({
33
39
  brandConfig,
34
40
  existingResources,
35
41
  }: DashboardShopifyProps) {
36
- const data = useStore(shopifyData);
37
42
  const status = useStore(shopifyStatus);
43
+ const activeTab = useStore(shopifyActiveTabStore);
44
+
38
45
  const [selectedProduct, setSelectedProduct] = useState<ShopifyProduct | null>(
39
46
  null
40
47
  );
@@ -51,6 +58,12 @@ export default function StoryKeepDashboard_Shopify({
51
58
  );
52
59
  const [showTypeSelector, setShowTypeSelector] = useState(false);
53
60
 
61
+ const [showSmartCartWarning, setShowSmartCartWarning] = useState(false);
62
+ const [pendingImport, setPendingImport] = useState<{
63
+ category: string;
64
+ product: ShopifyProduct;
65
+ } | null>(null);
66
+
54
67
  const [machineState, setMachineState] = useState<MachineState>('INIT');
55
68
  const [internalBrandConfig, setInternalBrandConfig] =
56
69
  useState<BrandConfigState | null>(null);
@@ -59,6 +72,17 @@ export default function StoryKeepDashboard_Shopify({
59
72
  const [wantService, setWantService] = useState(true);
60
73
  const [isSaving, setIsSaving] = useState(false);
61
74
 
75
+ // Tab definitions
76
+ const tabs = [
77
+ { id: 'dashboards', name: 'Dashboard' },
78
+ { id: 'bookings', name: 'Bookings' },
79
+ { id: 'products', name: 'Products' },
80
+ { id: 'services', name: 'Services' },
81
+ { id: 'schedule', name: 'Schedule' },
82
+ { id: 'search', name: 'Import Products' },
83
+ { id: 'emails', name: 'Emails' },
84
+ ];
85
+
62
86
  useEffect(() => {
63
87
  if (brandConfig) {
64
88
  const localState = convertToLocalState(brandConfig);
@@ -73,14 +97,6 @@ export default function StoryKeepDashboard_Shopify({
73
97
  }
74
98
  }, [brandConfig]);
75
99
 
76
- // Operational Effect: Fetch products only when READY
77
- useEffect(() => {
78
- if (machineState === 'READY') {
79
- fetchShopifyProducts();
80
- }
81
- }, [machineState]);
82
-
83
- // Memoize the lookup map for performance (gid -> ResourceNode)
84
100
  const linkedResourceMap = useMemo(() => {
85
101
  const map = new Map<string, ResourceNode>();
86
102
  resources.forEach((r) => {
@@ -91,14 +107,9 @@ export default function StoryKeepDashboard_Shopify({
91
107
  return map;
92
108
  }, [resources]);
93
109
 
94
- const handleRefresh = () => {
95
- fetchShopifyProducts();
96
- };
97
-
98
110
  const refreshResources = async () => {
99
111
  const tenantId = window.TRACTSTACK_CONFIG?.tenantId || 'default';
100
112
  try {
101
- // Fetch both categories if configured, then merge unique
102
113
  const promises = [];
103
114
  if (internalBrandConfig?.knownResources['product']) {
104
115
  promises.push(getResourcesByCategory(tenantId, 'product'));
@@ -129,18 +140,48 @@ export default function StoryKeepDashboard_Shopify({
129
140
  setTargetProduct(product);
130
141
  setShowTypeSelector(true);
131
142
  } else if (hasServiceSchema) {
132
- startCreateFlow('service', product);
143
+ executePreFlightCheck('service', product);
133
144
  } else {
134
- startCreateFlow('product', product);
145
+ executePreFlightCheck('product', product);
135
146
  }
136
147
  };
137
148
 
138
- const handleEdit = (_product: ShopifyProduct, resource: ResourceNode) => {
149
+ // Handler for ProductTable (External Catalog)
150
+ const handleEditFromCatalog = (
151
+ _product: ShopifyProduct,
152
+ resource: ResourceNode
153
+ ) => {
139
154
  setDraftResource(resource as any);
140
155
  setIsCreateMode(false);
141
156
  setShowResourceModal(true);
142
157
  };
143
158
 
159
+ // Handler for ResourceTable (Local Management)
160
+ const handleEditResource = async (resourceId: string) => {
161
+ try {
162
+ const resource = await getResource(
163
+ window.TRACTSTACK_CONFIG?.tenantId || 'default',
164
+ resourceId
165
+ );
166
+ setDraftResource(resource as any);
167
+ setIsCreateMode(false);
168
+ setShowResourceModal(true);
169
+ } catch (error) {
170
+ console.error('Failed to load resource for editing:', error);
171
+ }
172
+ };
173
+
174
+ const executePreFlightCheck = (category: string, product: ShopifyProduct) => {
175
+ const hasMode = product.options.some((opt) => opt.name === 'Mode');
176
+ if (category === 'service' || hasMode) {
177
+ startCreateFlow(category, product);
178
+ } else {
179
+ setPendingImport({ category, product });
180
+ setShowSmartCartWarning(true);
181
+ setShowTypeSelector(false);
182
+ }
183
+ };
184
+
144
185
  const startCreateFlow = (category: string, product: ShopifyProduct) => {
145
186
  const schema = internalBrandConfig?.knownResources[category] || {};
146
187
  const mergedOptions: Record<string, any> = {
@@ -148,7 +189,6 @@ export default function StoryKeepDashboard_Shopify({
148
189
  shopifyData: JSON.stringify(product),
149
190
  };
150
191
 
151
- // Apply schema defaults for missing fields
152
192
  Object.entries(schema).forEach(([key, def]) => {
153
193
  if (mergedOptions[key] === undefined) {
154
194
  if (def.type === 'number') {
@@ -190,7 +230,6 @@ export default function StoryKeepDashboard_Shopify({
190
230
  window.TRACTSTACK_CONFIG?.tenantId || 'default',
191
231
  resourceId
192
232
  );
193
- // Optimistic update
194
233
  setResources((prev) => prev.filter((r) => r.id !== resourceId));
195
234
  } catch (error) {
196
235
  console.error('Unlink failed', error);
@@ -214,11 +253,11 @@ export default function StoryKeepDashboard_Shopify({
214
253
  try {
215
254
  const updatedKnownResources = { ...internalBrandConfig.knownResources };
216
255
 
217
- // 1. Product Schema
218
256
  if (wantProduct) {
219
257
  updatedKnownResources['product'] = {
220
258
  gid: { type: 'string', optional: false },
221
259
  allowMultiple: { type: 'boolean', optional: false },
260
+ group: { type: 'string', optional: true },
222
261
  shopifyData: { type: 'string', optional: false },
223
262
  shopifyImage: { type: 'string', optional: true, defaultValue: '{}' },
224
263
  ...(wantService
@@ -233,10 +272,10 @@ export default function StoryKeepDashboard_Shopify({
233
272
  };
234
273
  }
235
274
 
236
- // 2. Service Schema
237
275
  if (wantService) {
238
276
  updatedKnownResources['service'] = {
239
277
  gid: { type: 'string', optional: true },
278
+ group: { type: 'string', optional: true },
240
279
  shopifyData: { type: 'string', optional: true },
241
280
  shopifyImage: { type: 'string', optional: true, defaultValue: '{}' },
242
281
  bookingLengthMinutes: {
@@ -269,11 +308,25 @@ export default function StoryKeepDashboard_Shopify({
269
308
  }
270
309
  };
271
310
 
272
- if (machineState === 'INIT') {
273
- return null;
274
- }
311
+ const handleDismissHelper = async () => {
312
+ if (!internalBrandConfig) return;
313
+ try {
314
+ const updatedState = {
315
+ ...internalBrandConfig,
316
+ showShopifyHelper: false,
317
+ };
318
+ await saveBrandConfigWithStateUpdate(
319
+ window.TRACTSTACK_CONFIG?.tenantId || 'default',
320
+ updatedState
321
+ );
322
+ setInternalBrandConfig(updatedState);
323
+ } catch (error) {
324
+ console.error('Failed to dismiss Shopify helper:', error);
325
+ }
326
+ };
327
+
328
+ if (machineState === 'INIT') return null;
275
329
 
276
- // CONFIG State
277
330
  if (machineState === 'CONFIG' || machineState === 'UPDATE') {
278
331
  return (
279
332
  <div className="mt-8 rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
@@ -336,7 +389,6 @@ export default function StoryKeepDashboard_Shopify({
336
389
  );
337
390
  }
338
391
 
339
- // READY State
340
392
  return (
341
393
  <div className="mt-8 rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
342
394
  <div className="mb-6 border-b border-gray-100 pb-4">
@@ -346,12 +398,66 @@ export default function StoryKeepDashboard_Shopify({
346
398
  </p>
347
399
  </div>
348
400
 
349
- {status.error && (
350
- <div className="mb-6 rounded-md bg-red-50 p-4">
351
- <div className="flex">
352
- <div className="flex-shrink-0">
353
- <span className="text-red-400">⚠️</span>
354
- </div>
401
+ {/* Persistent Onboarding Helper Banner */}
402
+ {internalBrandConfig?.showShopifyHelper && (
403
+ <div className="relative mb-8 rounded-lg border border-cyan-200 bg-cyan-50 p-6 pr-12 shadow-sm">
404
+ <button
405
+ onClick={handleDismissHelper}
406
+ className="absolute right-4 top-4 text-cyan-600 hover:text-cyan-800"
407
+ title="Dismiss instructions"
408
+ >
409
+ <XMarkIcon className="h-6 w-6" />
410
+ </button>
411
+ <h3 className="text-lg font-bold text-cyan-900">
412
+ Smart Cart Architecture: "Mode" Options
413
+ </h3>
414
+ <div className="mt-2 space-y-3 text-sm text-cyan-800">
415
+ <p>
416
+ To enable automatic shipping fee bypass for local pickup, your
417
+ Shopify products must be configured with a specific architectural
418
+ pattern:
419
+ </p>
420
+ <ul className="list-inside list-disc space-y-1 font-bold">
421
+ <li>
422
+ Option Name: Must be exactly{' '}
423
+ <code className="rounded bg-cyan-100 px-1">Mode</code>
424
+ </li>
425
+ <li>
426
+ Option Values: Must include{' '}
427
+ <code className="rounded bg-cyan-100 px-1">Shipped</code> and{' '}
428
+ <code className="rounded bg-cyan-100 px-1">Pickup</code>
429
+ </li>
430
+ </ul>
431
+ <p>
432
+ Without this "Mode" option, items will always be treated as
433
+ standard shipped products.
434
+ </p>
435
+ </div>
436
+ </div>
437
+ )}
438
+
439
+ {/* Tab Navigation Shell */}
440
+ <div className="mb-8 border-b border-gray-200">
441
+ <nav className="-mb-px flex flex-wrap gap-x-8" aria-label="Tabs">
442
+ {tabs.map((tab) => (
443
+ <button
444
+ key={tab.id}
445
+ onClick={() => shopifyActiveTabStore.set(tab.id)}
446
+ className={`whitespace-nowrap border-b-2 px-1 py-4 text-sm font-bold ${
447
+ activeTab === tab.id
448
+ ? 'border-cyan-500 text-cyan-600'
449
+ : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'
450
+ } `}
451
+ >
452
+ {tab.name}
453
+ </button>
454
+ ))}
455
+ </nav>
456
+ </div>
457
+
458
+ <div className="space-y-6">
459
+ {status.error && (
460
+ <div className="mb-6 rounded-md bg-red-50 p-4">
355
461
  <div className="ml-3">
356
462
  <h3 className="text-sm font-bold text-red-800">Error</h3>
357
463
  <div className="mt-2 text-sm text-red-700">
@@ -359,32 +465,50 @@ export default function StoryKeepDashboard_Shopify({
359
465
  </div>
360
466
  </div>
361
467
  </div>
362
- </div>
363
- )}
364
-
365
- {status.isLoading && data.products.length === 0 ? (
366
- <div className="flex h-64 items-center justify-center">
367
- <div className="text-center">
368
- <div className="mx-auto h-8 w-8 animate-spin rounded-full border-2 border-gray-300 border-t-cyan-600"></div>
369
- <p className="mt-2 text-sm text-gray-500">
370
- Loading products from Shopify...
371
- </p>
372
- </div>
373
- </div>
374
- ) : (
375
- <ProductTable
376
- products={data.products}
377
- linkedResourceMap={linkedResourceMap}
378
- onRefresh={handleRefresh}
379
- isRefreshing={status.isLoading}
380
- onSelectProduct={setSelectedProduct}
381
- onLink={handleLink}
382
- onUnlink={handleUnlink}
383
- onEdit={handleEdit}
384
- />
385
- )}
468
+ )}
469
+
470
+ {activeTab === 'dashboards' && <ShopifyDashboard />}
471
+
472
+ {activeTab === 'bookings' && (
473
+ <ShopifyDashboard_Bookings existingResources={resources} />
474
+ )}
475
+
476
+ {activeTab === 'products' && (
477
+ <ShopifyDashboard_Products
478
+ resources={resources}
479
+ onEdit={handleEditResource}
480
+ onCreate={() => shopifyActiveTabStore.set('search')}
481
+ onRefresh={refreshResources}
482
+ />
483
+ )}
484
+
485
+ {activeTab === 'services' && (
486
+ <ShopifyDashboard_Services
487
+ resources={resources}
488
+ onEdit={handleEditResource}
489
+ onCreate={() => shopifyActiveTabStore.set('search')}
490
+ onRefresh={refreshResources}
491
+ />
492
+ )}
493
+
494
+ {activeTab === 'schedule' && (
495
+ <ShopifyDashboard_Schedule brandConfig={brandConfig} />
496
+ )}
497
+
498
+ {activeTab === 'search' && (
499
+ <ShopifyDashboard_Search
500
+ linkedResourceMap={linkedResourceMap}
501
+ onSelectProduct={setSelectedProduct}
502
+ onLink={handleLink}
503
+ onUnlink={handleUnlink}
504
+ onEdit={handleEditFromCatalog}
505
+ />
506
+ )}
507
+
508
+ {activeTab === 'emails' && <ShopifyDashboard_Emails />}
509
+ </div>
386
510
 
387
- {/* Product Inspector Modal */}
511
+ {/* Shared Modals */}
388
512
  {selectedProduct && (
389
513
  <div className="relative z-50" aria-modal="true">
390
514
  <div className="fixed inset-0 bg-black bg-opacity-75" />
@@ -399,59 +523,41 @@ export default function StoryKeepDashboard_Shopify({
399
523
  </h3>
400
524
  <button
401
525
  type="button"
402
- className="rounded-md bg-white text-gray-400 hover:text-gray-500"
526
+ className="rounded-md bg-white text-gray-400 hover:text-gray-50"
403
527
  onClick={() => setSelectedProduct(null)}
404
528
  >
405
529
  <XMarkIcon className="h-6 w-6" />
406
530
  </button>
407
531
  </div>
408
532
  <div className="overflow-y-auto p-6">
409
- <div className="mb-4">
410
- <h4 className="mb-2 text-sm font-bold text-gray-900">
411
- Product Details
533
+ <div className="mb-2 flex items-center justify-between">
534
+ <h4 className="text-sm font-bold text-gray-900">
535
+ Raw API Data
412
536
  </h4>
413
- <dl className="grid grid-cols-1 gap-x-4 gap-y-4 md:grid-cols-2">
414
- <div className="md:col-span-1">
415
- <dt className="text-sm font-bold text-gray-500">
416
- Handle
417
- </dt>
418
- <dd className="mt-1 text-sm text-gray-900">
419
- {selectedProduct.handle}
420
- </dd>
421
- </div>
422
- </dl>
423
- </div>
424
- <div>
425
- <div className="mb-2 flex items-center justify-between">
426
- <h4 className="text-sm font-bold text-gray-900">
427
- Raw API Data
428
- </h4>
429
- <button
430
- onClick={handleCopy}
431
- className="flex items-center gap-1 text-xs text-cyan-600 hover:text-cyan-800"
432
- >
433
- {copied ? (
434
- <CheckIcon className="h-4 w-4" />
435
- ) : (
436
- <ClipboardDocumentIcon className="h-4 w-4" />
437
- )}
438
- {copied ? 'Copied' : 'Copy JSON'}
439
- </button>
440
- </div>
441
- <pre
442
- className="overflow-auto rounded-md bg-gray-50 p-4 text-xs text-gray-800"
443
- style={{ maxHeight: '40vh' }}
537
+ <button
538
+ onClick={handleCopy}
539
+ className="flex items-center gap-1 text-xs text-cyan-600 hover:text-cyan-800"
444
540
  >
445
- {JSON.stringify(selectedProduct, null, 2)}
446
- </pre>
541
+ {copied ? (
542
+ <CheckIcon className="h-4 w-4" />
543
+ ) : (
544
+ <ClipboardDocumentIcon className="h-4 w-4" />
545
+ )}
546
+ {copied ? 'Copied' : 'Copy JSON'}
547
+ </button>
447
548
  </div>
549
+ <pre
550
+ className="overflow-auto rounded-md bg-gray-50 p-4 text-xs text-gray-800"
551
+ style={{ maxHeight: '40vh' }}
552
+ >
553
+ {JSON.stringify(selectedProduct, null, 2)}
554
+ </pre>
448
555
  </div>
449
556
  </div>
450
557
  </div>
451
558
  </div>
452
559
  )}
453
560
 
454
- {/* Type Selector Modal */}
455
561
  {showTypeSelector && targetProduct && (
456
562
  <div className="relative z-50" aria-modal="true">
457
563
  <div className="fixed inset-0 bg-black bg-opacity-50" />
@@ -462,27 +568,66 @@ export default function StoryKeepDashboard_Shopify({
462
568
  Import as...
463
569
  </h3>
464
570
  <p className="mt-2 text-sm text-gray-500">
465
- Should "{targetProduct.title}" be imported as a Product or
466
- Service?
571
+ Should "{targetProduct.title}" be a Product or Service?
467
572
  </p>
468
573
  <div className="mt-6 flex flex-col gap-3">
469
574
  <button
470
- onClick={() => startCreateFlow('product', targetProduct)}
575
+ onClick={() =>
576
+ executePreFlightCheck('product', targetProduct)
577
+ }
471
578
  className="flex w-full items-center justify-center rounded-md bg-cyan-600 px-4 py-2 text-sm font-bold text-white shadow-sm hover:bg-cyan-500"
472
579
  >
473
580
  Product
474
581
  </button>
475
582
  <button
476
- onClick={() => startCreateFlow('service', targetProduct)}
583
+ onClick={() =>
584
+ executePreFlightCheck('service', targetProduct)
585
+ }
477
586
  className="flex w-full items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-bold text-gray-700 shadow-sm hover:bg-gray-50"
478
587
  >
479
588
  Service (Bookable)
480
589
  </button>
481
590
  </div>
482
- <div className="mt-4 border-t pt-4">
591
+ </div>
592
+ </div>
593
+ </div>
594
+ </div>
595
+ )}
596
+
597
+ {showSmartCartWarning && pendingImport && (
598
+ <div className="relative z-50" aria-modal="true">
599
+ <div className="fixed inset-0 bg-black bg-opacity-50" />
600
+ <div className="fixed inset-0 flex items-center justify-center p-4">
601
+ <div className="w-full max-w-md overflow-hidden rounded-lg bg-white shadow-xl">
602
+ <div className="px-6 py-4">
603
+ <h3 className="text-lg font-bold text-gray-900">
604
+ Missing Smart Cart Architecture
605
+ </h3>
606
+ <p className="mt-2 text-sm text-gray-500">
607
+ To enable Smart Cart pickup, the product must have a "Mode"
608
+ option containing "Shipped" and "Pickup". If you import now,
609
+ it will be standard shipping only.
610
+ </p>
611
+ <div className="mt-6 flex flex-col gap-3">
483
612
  <button
484
- onClick={() => setShowTypeSelector(false)}
485
- className="w-full text-center text-sm text-gray-500 hover:text-gray-700"
613
+ onClick={() => {
614
+ setShowSmartCartWarning(false);
615
+ startCreateFlow(
616
+ pendingImport.category,
617
+ pendingImport.product
618
+ );
619
+ setPendingImport(null);
620
+ }}
621
+ className="flex w-full items-center justify-center rounded-md bg-cyan-600 px-4 py-2 text-sm font-bold text-white shadow-sm hover:bg-cyan-500"
622
+ >
623
+ Acknowledge & Import
624
+ </button>
625
+ <button
626
+ onClick={() => {
627
+ setShowSmartCartWarning(false);
628
+ setPendingImport(null);
629
+ }}
630
+ className="flex w-full items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-bold text-gray-700 shadow-sm hover:bg-gray-50"
486
631
  >
487
632
  Cancel
488
633
  </button>
@@ -493,14 +638,13 @@ export default function StoryKeepDashboard_Shopify({
493
638
  </div>
494
639
  )}
495
640
 
496
- {/* Resource Form Modal */}
497
641
  {showResourceModal && draftResource && (
498
642
  <div className="fixed inset-0 z-50 overflow-y-auto bg-gray-900 bg-opacity-50 backdrop-blur-sm">
499
643
  <div className="flex min-h-full items-center justify-center p-4">
500
644
  <div className="w-full max-w-2xl rounded-lg bg-white p-6 shadow-xl">
501
645
  <ResourceForm
502
646
  resourceData={draftResource as any}
503
- fullContentMap={resources as any} // Use local resources for slug uniqueness check
647
+ fullContentMap={resources as any}
504
648
  categorySlug={draftResource.categorySlug || ''}
505
649
  categorySchema={
506
650
  internalBrandConfig?.knownResources[
@@ -511,9 +655,7 @@ export default function StoryKeepDashboard_Shopify({
511
655
  onClose={(saved) => {
512
656
  setShowResourceModal(false);
513
657
  setIsCreateMode(true);
514
- if (saved) {
515
- refreshResources();
516
- }
658
+ if (saved) refreshResources();
517
659
  }}
518
660
  />
519
661
  </div>
@@ -1,4 +1,4 @@
1
- import { useState, useEffect } from 'react';
1
+ import { useState, useEffect, type KeyboardEvent } from 'react';
2
2
  import { useStore } from '@nanostores/react';
3
3
  import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
4
4
  import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
@@ -125,7 +125,7 @@ export default function BeliefForm({
125
125
  }
126
126
  };
127
127
 
128
- const handleKeyDown = (e: React.KeyboardEvent) => {
128
+ const handleKeyDown = (e: KeyboardEvent) => {
129
129
  if (e.key === 'Enter') {
130
130
  e.preventDefault();
131
131
  handleAddCustomValue();
@@ -170,7 +170,7 @@ export default function BeliefTable({
170
170
  </div>
171
171
 
172
172
  {/* Table Container */}
173
- <div className="overflow-hidden rounded-lg border border-gray-200 bg-white shadow">
173
+ <div className="overflow-x-auto rounded-lg border border-gray-200 bg-white shadow">
174
174
  {filteredBeliefs.length === 0 ? (
175
175
  <div className="px-6 py-12 text-center">
176
176
  <svg
@@ -204,7 +204,7 @@ export default function BeliefTable({
204
204
  )}
205
205
  </div>
206
206
  ) : (
207
- <div className="overflow-hidden">
207
+ <div className="inline-block min-w-full align-middle">
208
208
  <table className="min-w-full divide-y divide-gray-200">
209
209
  <thead className="bg-gray-50">
210
210
  <tr>
@@ -237,15 +237,24 @@ export default function BeliefTable({
237
237
  <tr key={belief.id} className="hover:bg-gray-50">
238
238
  <td className="px-3 py-4 md:px-6">
239
239
  <div className="flex flex-col">
240
- <div className="text-sm font-bold text-gray-900">
240
+ <div
241
+ className="max-w-xs truncate text-sm font-bold text-gray-900"
242
+ title={belief.title}
243
+ >
241
244
  {belief.title}
242
245
  </div>
243
- <div className="text-sm text-gray-500 md:hidden">
246
+ <div
247
+ className="max-w-xs truncate text-sm text-gray-500 md:hidden"
248
+ title={belief.slug}
249
+ >
244
250
  {belief.slug}
245
251
  </div>
246
252
  </div>
247
253
  </td>
248
- <td className="hidden whitespace-nowrap px-3 py-4 text-sm text-gray-500 md:table-cell md:px-6">
254
+ <td
255
+ className="hidden max-w-xs truncate whitespace-nowrap px-3 py-4 text-sm text-gray-500 md:table-cell md:px-6"
256
+ title={belief.slug}
257
+ >
249
258
  {belief.slug}
250
259
  </td>
251
260
  <td className="hidden whitespace-nowrap px-3 py-4 text-sm md:table-cell md:px-6">
@@ -142,7 +142,7 @@ const KnownResourceTable = ({
142
142
  </div>
143
143
  </div>
144
144
 
145
- <div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
145
+ <div className="overflow-x-auto shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
146
146
  <table className="min-w-full divide-y divide-gray-300">
147
147
  <thead className="bg-gray-50">
148
148
  <tr>
@@ -185,7 +185,10 @@ const KnownResourceTable = ({
185
185
  className="cursor-pointer hover:bg-gray-50"
186
186
  onClick={() => onEdit(categorySlug)}
187
187
  >
188
- <td className="whitespace-nowrap px-6 py-4 text-sm font-bold text-gray-900">
188
+ <td
189
+ className="max-w-xs truncate whitespace-nowrap px-6 py-4 text-sm font-bold text-gray-900"
190
+ title={categorySlug}
191
+ >
189
192
  {categorySlug}
190
193
  </td>
191
194
  <td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">