astro-tractstack 2.2.10 → 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 (85) hide show
  1. package/README.md +1 -1
  2. package/bin/create-tractstack.js +2 -2
  3. package/dist/index.js +177 -18
  4. package/package.json +4 -2
  5. package/templates/custom/minimal/CodeHook.astro +22 -5
  6. package/templates/custom/shopify/Cart.tsx +372 -0
  7. package/templates/custom/shopify/CartIcon.tsx +47 -0
  8. package/templates/custom/shopify/CartModal.tsx +63 -0
  9. package/templates/custom/shopify/CheckoutModal.tsx +576 -0
  10. package/templates/custom/shopify/NativeBookingCalendar.tsx +375 -0
  11. package/templates/custom/shopify/ShopifyCartManager.tsx +200 -0
  12. package/templates/custom/shopify/ShopifyCheckout.tsx +167 -0
  13. package/templates/custom/shopify/ShopifyProductGrid.tsx +247 -0
  14. package/templates/custom/shopify/ShopifyServiceList.tsx +135 -0
  15. package/templates/custom/shopify/cart.astro +23 -0
  16. package/templates/custom/with-examples/CodeHook.astro +17 -1
  17. package/templates/custom/with-examples/ProductGrid.astro +1 -1
  18. package/templates/src/client/app.js +4 -2
  19. package/templates/src/components/Footer.astro +4 -4
  20. package/templates/src/components/Header.astro +44 -12
  21. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +3 -3
  22. package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +2 -2
  23. package/templates/src/components/edit/pane/steps/AiCreativeDesignStep.tsx +2 -2
  24. package/templates/src/components/edit/pane/steps/AiLibraryCopyStep.tsx +3 -3
  25. package/templates/src/components/edit/pane/steps/AiRefineDesignStep.tsx +2 -2
  26. package/templates/src/components/edit/pane/steps/AiStandardDesignStep.tsx +7 -7
  27. package/templates/src/components/form/advanced/APIConfigSection.tsx +407 -38
  28. package/templates/src/components/form/shopify/SchedulingSection.tsx +354 -0
  29. package/templates/src/components/storykeep/Dashboard.tsx +18 -4
  30. package/templates/src/components/storykeep/Dashboard_Advanced.tsx +1 -0
  31. package/templates/src/components/storykeep/Dashboard_Content.tsx +5 -96
  32. package/templates/src/components/storykeep/Dashboard_Shopify.tsx +668 -0
  33. package/templates/src/components/storykeep/StoryKeepBackdrop.astro +43 -23
  34. package/templates/src/components/storykeep/controls/content/BeliefTable.tsx +14 -5
  35. package/templates/src/components/storykeep/controls/content/ContentBrowser.tsx +0 -14
  36. package/templates/src/components/storykeep/controls/content/KnownResourceForm.tsx +36 -13
  37. package/templates/src/components/storykeep/controls/content/KnownResourceTable.tsx +5 -2
  38. package/templates/src/components/storykeep/controls/content/ManageContent.tsx +4 -11
  39. package/templates/src/components/storykeep/controls/content/MenuTable.tsx +14 -5
  40. package/templates/src/components/storykeep/controls/content/ProductTable.tsx +333 -0
  41. package/templates/src/components/storykeep/controls/content/ResourceBulkIngest.tsx +9 -5
  42. package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +108 -8
  43. package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +13 -4
  44. package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +14 -5
  45. package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +111 -0
  46. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +393 -0
  47. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Products.tsx +46 -0
  48. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Schedule.tsx +78 -0
  49. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +55 -0
  50. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Services.tsx +47 -0
  51. package/templates/src/lib/resources.ts +11 -21
  52. package/templates/src/pages/api/auth/lookup-lead.ts +72 -0
  53. package/templates/src/pages/api/booking/availability.ts +72 -0
  54. package/templates/src/pages/api/booking/cancel.ts +73 -0
  55. package/templates/src/pages/api/booking/confirm.ts +82 -0
  56. package/templates/src/pages/api/booking/hold.ts +75 -0
  57. package/templates/src/pages/api/booking/list.ts +66 -0
  58. package/templates/src/pages/api/booking/metrics.ts +60 -0
  59. package/templates/src/pages/api/booking/release.ts +76 -0
  60. package/templates/src/pages/api/sandbox.ts +2 -2
  61. package/templates/src/pages/api/shopify/createCart.ts +69 -0
  62. package/templates/src/pages/api/shopify/getProducts.ts +64 -0
  63. package/templates/src/pages/storykeep/login.astro +26 -24
  64. package/templates/src/pages/storykeep/logout.astro +1 -10
  65. package/templates/src/pages/storykeep/manage.astro +69 -0
  66. package/templates/src/pages/storykeep/{content.astro → pages.astro} +4 -8
  67. package/templates/src/pages/storykeep/shopify.astro +101 -0
  68. package/templates/src/stores/navigation.ts +3 -42
  69. package/templates/src/stores/nodes.ts +3 -1
  70. package/templates/src/stores/resources.ts +7 -10
  71. package/templates/src/stores/shopify.ts +266 -0
  72. package/templates/src/types/tractstack.ts +75 -0
  73. package/templates/src/utils/api/advancedConfig.ts +7 -1
  74. package/templates/src/utils/api/advancedHelpers.ts +87 -7
  75. package/templates/src/utils/api/bookingHelpers.ts +125 -0
  76. package/templates/src/utils/api/brandHelpers.ts +14 -0
  77. package/templates/src/utils/api/resourceConfig.ts +13 -5
  78. package/templates/src/utils/auth.ts +29 -9
  79. package/templates/src/utils/compositor/aiGeneration.ts +3 -3
  80. package/templates/src/utils/compositor/aiPaneParser.ts +2 -2
  81. package/templates/src/utils/customHelpers.ts +49 -0
  82. package/templates/src/utils/helpers.ts +59 -0
  83. package/templates/src/utils/profileStorage.ts +5 -0
  84. package/templates/src/utils/tenantResolver.ts +2 -1
  85. package/utils/inject-files.ts +161 -2
@@ -0,0 +1,372 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { ulid } from 'ulid';
3
+ import { useStore } from '@nanostores/react';
4
+ import {
5
+ addQueue,
6
+ cartStore,
7
+ cartState,
8
+ CART_STATES,
9
+ isShopifyHandoff,
10
+ transactionTraceId,
11
+ type CartItemState,
12
+ } from '@/stores/shopify';
13
+ import { getShopifyImage } from '@/utils/helpers';
14
+ import type { ResourceNode } from '@/types/compositorTypes';
15
+
16
+ interface CartProps {
17
+ resources: ResourceNode[];
18
+ }
19
+
20
+ const getCleanVariantTitle = (variant: any) => {
21
+ if (variant?.selectedOptions) {
22
+ const filtered = variant.selectedOptions
23
+ .filter(
24
+ (o: any) =>
25
+ o.name !== 'Mode' && o.name !== 'Title' && o.value !== 'Default Title'
26
+ )
27
+ .map((o: any) => o.value)
28
+ .join(' / ');
29
+
30
+ return filtered === 'Default Title' ? '' : filtered;
31
+ }
32
+
33
+ const title = variant?.title || '';
34
+ return title === 'Default Title' ? '' : title;
35
+ };
36
+
37
+ export default function Cart({ resources = [] }: CartProps) {
38
+ const cart = useStore(cartStore);
39
+ const isHandoff = useStore(isShopifyHandoff);
40
+ const [pickupEnabled, setPickupEnabled] = useState(false);
41
+
42
+ const cartValues = Object.values(cart);
43
+
44
+ const boundServiceIds = new Set(
45
+ cartValues
46
+ .map((item) => item.boundResourceId)
47
+ .filter((id) => !!id) as string[]
48
+ );
49
+
50
+ const displayableItems = cartValues.filter(
51
+ (item) => !boundServiceIds.has(item.resourceId)
52
+ );
53
+
54
+ const groupedItems = displayableItems.reduce(
55
+ (acc, item) => {
56
+ if (!acc[item.resourceId]) {
57
+ acc[item.resourceId] = [];
58
+ }
59
+ acc[item.resourceId].push(item);
60
+ return acc;
61
+ },
62
+ {} as Record<string, CartItemState[]>
63
+ );
64
+
65
+ const hasService = cartValues.some((item) => {
66
+ const resource = resources.find((r) => r.id === item.resourceId);
67
+ return !!resource?.optionsPayload?.bookingLengthMinutes;
68
+ });
69
+
70
+ const hasPhysicalProductWithPickup = cartValues.some(
71
+ (item) =>
72
+ item.variantIdPickup && item.variantIdPickup !== item.variantIdShipped
73
+ );
74
+
75
+ const canPickup = hasService && hasPhysicalProductWithPickup;
76
+
77
+ useEffect(() => {
78
+ if (canPickup) {
79
+ setPickupEnabled(true);
80
+ } else {
81
+ setPickupEnabled(false);
82
+ }
83
+ }, [canPickup]);
84
+
85
+ const isPickupMode = canPickup && pickupEnabled;
86
+
87
+ const dispatchAction = (item: CartItemState, action: 'add' | 'remove') => {
88
+ addQueue.set([
89
+ ...addQueue.get(),
90
+ {
91
+ resourceId: item.resourceId,
92
+ action,
93
+ variantId: item.variantId,
94
+ variantIdShipped: item.variantIdShipped,
95
+ variantIdPickup: item.variantIdPickup,
96
+ boundResourceId: item.boundResourceId,
97
+ suppressModal: action === 'add' ? true : undefined,
98
+ },
99
+ ]);
100
+ };
101
+
102
+ if (isHandoff) {
103
+ return (
104
+ <div
105
+ className="fixed inset-0 flex flex-col items-center justify-center bg-black bg-opacity-75 backdrop-blur-md"
106
+ style={{ zIndex: 9005 }}
107
+ >
108
+ <div className="h-12 w-12 animate-spin rounded-full border-4 border-gray-200 border-t-white"></div>
109
+ <h3 className="mt-4 text-lg font-bold text-white">
110
+ Finalizing Handoff...
111
+ </h3>
112
+ <p className="mt-2 text-sm text-gray-300">
113
+ Redirecting to Shopify secured payment
114
+ </p>
115
+ </div>
116
+ );
117
+ }
118
+
119
+ if (cartValues.length === 0) {
120
+ return (
121
+ <div className="relative">
122
+ <div className="rounded-lg border bg-gray-50 p-8 text-center">
123
+ <h2 className="text-xl font-bold">Your cart is empty</h2>
124
+ <p className="mt-2 text-gray-600">Add some items to get started.</p>
125
+ </div>
126
+ </div>
127
+ );
128
+ }
129
+
130
+ return (
131
+ <div className="rounded-lg bg-white shadow">
132
+ <div className="flex items-center justify-between border-b border-gray-200 px-6 py-4">
133
+ <h2 className="text-xl font-bold text-gray-800">Shopping Cart</h2>
134
+ {canPickup && (
135
+ <label className="flex items-center space-x-2 text-sm font-bold text-gray-900">
136
+ <input
137
+ type="checkbox"
138
+ checked={pickupEnabled}
139
+ onChange={(e) => setPickupEnabled(e.target.checked)}
140
+ className="h-4 w-4 rounded border-gray-300 text-black focus:ring-black"
141
+ />
142
+ <span>Pick up at Store</span>
143
+ </label>
144
+ )}
145
+ </div>
146
+
147
+ <ul className="divide-y divide-gray-200">
148
+ {Object.keys(groupedItems).map((resourceId) => {
149
+ const items = groupedItems[resourceId];
150
+ const resource = resources.find((r) => r.id === resourceId);
151
+ if (!resource || items.length === 0) return null;
152
+
153
+ const isService = !!resource.optionsPayload?.bookingLengthMinutes;
154
+ const serviceDuration = resource.optionsPayload?.bookingLengthMinutes;
155
+
156
+ const firstItem = items[0];
157
+ const boundServiceId = firstItem.boundResourceId;
158
+ const boundServiceResource = boundServiceId
159
+ ? resources.find((r) => r.id === boundServiceId)
160
+ : null;
161
+
162
+ const activeVariantIdFirst = isPickupMode
163
+ ? firstItem.variantIdPickup
164
+ : firstItem.variantIdShipped;
165
+ const displayIdFirst =
166
+ activeVariantIdFirst ||
167
+ firstItem.variantIdPickup ||
168
+ firstItem.variantId;
169
+
170
+ const { src, srcSet } = getShopifyImage(
171
+ resource,
172
+ '600',
173
+ displayIdFirst
174
+ );
175
+
176
+ let productData: any = {};
177
+ try {
178
+ if (resource.optionsPayload?.shopifyData) {
179
+ productData = JSON.parse(resource.optionsPayload.shopifyData);
180
+ }
181
+ } catch (e) {
182
+ console.error('Failed to parse Shopify data', resource.id);
183
+ }
184
+ const variants = productData?.variants || [];
185
+
186
+ return (
187
+ <li key={resourceId} className="p-6">
188
+ <div className="flex items-start">
189
+ {!isService && (
190
+ <div className="h-16 w-16 flex-shrink-0 overflow-hidden rounded-md border border-gray-200">
191
+ <img
192
+ src={src}
193
+ srcSet={srcSet}
194
+ alt={resource.title}
195
+ className="aspect-square h-full w-full object-cover object-center"
196
+ loading="lazy"
197
+ />
198
+ </div>
199
+ )}
200
+ <div className="ml-4 flex-1">
201
+ <div className="flex justify-between">
202
+ <div>
203
+ <div className="flex items-center gap-2">
204
+ <h3 className="text-base font-bold text-gray-900">
205
+ {resource.title}
206
+ </h3>
207
+ {isService && (
208
+ <span className="inline-flex items-center rounded-sm bg-blue-50 px-2 py-0.5 text-xs font-bold text-blue-700">
209
+ {serviceDuration} mins
210
+ </span>
211
+ )}
212
+ </div>
213
+
214
+ {boundServiceResource && (
215
+ <div className="mt-2 flex items-center gap-2 rounded-md bg-blue-50 px-3 py-2">
216
+ <span className="inline-block h-2 w-2 rounded-full bg-blue-500"></span>
217
+ <div>
218
+ <p className="text-sm font-bold text-blue-900">
219
+ Includes Booking: {boundServiceResource.title}
220
+ </p>
221
+ <p className="text-xs text-blue-700">
222
+ Duration:{' '}
223
+ {boundServiceResource.optionsPayload
224
+ ?.bookingLengthMinutes || 0}{' '}
225
+ mins
226
+ </p>
227
+ </div>
228
+ </div>
229
+ )}
230
+ <p className="mt-1 text-sm text-gray-500">
231
+ {resource.oneliner}
232
+ </p>
233
+ </div>
234
+ </div>
235
+
236
+ <div className="mt-4 space-y-4 border-t border-gray-100 pt-4">
237
+ {items.map((item, idx) => {
238
+ const activeVariantId = isPickupMode
239
+ ? item.variantIdPickup
240
+ : item.variantIdShipped;
241
+
242
+ const displayId =
243
+ activeVariantId ||
244
+ item.variantIdPickup ||
245
+ item.variantId;
246
+
247
+ let price = '0.00';
248
+ let currency = 'USD';
249
+ let variantTitle = '';
250
+
251
+ const variant = variants.find(
252
+ (v: any) => v.id === displayId
253
+ );
254
+
255
+ if (variant) {
256
+ price = variant.price?.amount || '0.00';
257
+ currency = variant.price?.currencyCode || 'USD';
258
+ variantTitle = getCleanVariantTitle(variant);
259
+ }
260
+
261
+ return (
262
+ <div
263
+ key={`${item.resourceId}_${displayId}_${idx}`}
264
+ className="flex items-center justify-between"
265
+ >
266
+ <div className="flex items-center gap-2">
267
+ {variantTitle && (
268
+ <div className="text-sm font-bold text-gray-700">
269
+ <span>{variantTitle}</span>
270
+ </div>
271
+ )}
272
+ {isPickupMode &&
273
+ !isService &&
274
+ (item.variantIdPickup &&
275
+ item.variantIdPickup !== item.variantIdShipped ? (
276
+ <span className="inline-flex items-center rounded bg-gray-100 px-2 py-0.5 text-xs font-bold text-gray-800">
277
+ Store Pickup
278
+ </span>
279
+ ) : (
280
+ <span className="inline-flex items-center rounded bg-red-50 px-2 py-0.5 text-xs font-bold text-red-700">
281
+ Not available for pickup
282
+ </span>
283
+ ))}
284
+ </div>
285
+
286
+ <div className="flex items-center">
287
+ <div className="mr-6 text-right">
288
+ <p className="text-sm font-bold text-gray-900">
289
+ {price && parseFloat(price) > 0
290
+ ? `${(parseFloat(price) * item.quantity).toFixed(2)} ${currency}`
291
+ : 'No Charge'}
292
+ </p>
293
+ </div>
294
+
295
+ {isService ? (
296
+ <button
297
+ onClick={() =>
298
+ addQueue.set([
299
+ ...addQueue.get(),
300
+ {
301
+ resourceId: item.resourceId,
302
+ action: 'remove',
303
+ variantId: item.variantId,
304
+ },
305
+ ])
306
+ }
307
+ className="rounded-md border border-gray-300 px-3 py-1 text-sm font-bold text-gray-600 hover:bg-gray-100"
308
+ >
309
+ Remove
310
+ </button>
311
+ ) : (
312
+ <div className="flex items-center rounded-md border border-gray-300">
313
+ <button
314
+ onClick={() => dispatchAction(item, 'remove')}
315
+ className="px-3 py-1 text-gray-600 hover:bg-gray-100"
316
+ >
317
+ -
318
+ </button>
319
+ <span className="border-l border-r border-gray-300 px-3 py-1 text-gray-900">
320
+ {item.quantity}
321
+ </span>
322
+ <button
323
+ onClick={() => dispatchAction(item, 'add')}
324
+ className="px-3 py-1 text-gray-600 hover:bg-gray-100"
325
+ >
326
+ +
327
+ </button>
328
+ </div>
329
+ )}
330
+ </div>
331
+ </div>
332
+ );
333
+ })}
334
+ </div>
335
+ </div>
336
+ </div>
337
+ </li>
338
+ );
339
+ })}
340
+ </ul>
341
+
342
+ <div className="rounded-b-lg border-t border-gray-200 bg-gray-50 px-6 py-6">
343
+ <div className="flex justify-end">
344
+ <button
345
+ className="rounded-lg bg-black px-6 py-3 font-bold text-white transition-colors hover:bg-gray-800"
346
+ onClick={() => {
347
+ const currentCart = cartStore.get();
348
+ const sanitizedCart = { ...currentCart };
349
+
350
+ Object.keys(sanitizedCart).forEach((key) => {
351
+ const item = sanitizedCart[key];
352
+
353
+ if (isPickupMode && item.variantIdPickup) {
354
+ item.variantId = item.variantIdPickup;
355
+ } else if (!isPickupMode && item.variantIdShipped) {
356
+ item.variantId = item.variantIdShipped;
357
+ }
358
+ });
359
+
360
+ cartStore.set(sanitizedCart);
361
+ transactionTraceId.set(ulid());
362
+
363
+ cartState.set(CART_STATES.CHECKOUT);
364
+ }}
365
+ >
366
+ Proceed to Checkout
367
+ </button>
368
+ </div>
369
+ </div>
370
+ </div>
371
+ );
372
+ }
@@ -0,0 +1,47 @@
1
+ import { useStore } from '@nanostores/react';
2
+ import { cartStore } from '@/stores/shopify';
3
+
4
+ export default function CartIcon() {
5
+ const cart = useStore(cartStore);
6
+ const cartValues = Object.values(cart);
7
+ const boundServiceIds = new Set(
8
+ cartValues.map((item) => item.boundResourceId).filter(Boolean)
9
+ );
10
+ const totalQuantity = cartValues
11
+ .filter((item) => !boundServiceIds.has(item.resourceId))
12
+ .reduce((total, item) => total + item.quantity, 0);
13
+
14
+ const handleOpenCart = () => {
15
+ window.location.href = '/cart';
16
+ };
17
+
18
+ if (totalQuantity === 0) {
19
+ return null;
20
+ }
21
+
22
+ return (
23
+ <button
24
+ onClick={handleOpenCart}
25
+ className="relative flex items-center justify-center rounded-full p-2 text-gray-700 transition-colors hover:bg-gray-100"
26
+ aria-label="Open Cart"
27
+ >
28
+ <svg
29
+ xmlns="http://www.w3.org/2000/svg"
30
+ fill="none"
31
+ viewBox="0 0 24 24"
32
+ strokeWidth={1.5}
33
+ stroke="currentColor"
34
+ className="h-6 w-6"
35
+ >
36
+ <path
37
+ strokeLinecap="round"
38
+ strokeLinejoin="round"
39
+ d="M15.75 10.5V6a3.75 3.75 0 10-7.5 0v4.5m11.356-1.993l1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 01-1.12-1.243l1.264-12A1.125 1.125 0 015.513 7.5h12.974c.576 0 1.059.435 1.119 1.007zM8.625 10.5a.375.375 0 11-.75 0 .375.375 0 01.75 0zm7.5 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
40
+ />
41
+ </svg>
42
+ <span className="absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full bg-black text-xs font-bold text-white ring-2 ring-white">
43
+ {totalQuantity}
44
+ </span>
45
+ </button>
46
+ );
47
+ }
@@ -0,0 +1,63 @@
1
+ import { useStore } from '@nanostores/react';
2
+ import { Dialog } from '@ark-ui/react/dialog';
3
+ import { Portal } from '@ark-ui/react/portal';
4
+ import { modalState } from '@/stores/shopify';
5
+
6
+ export default function CartModal() {
7
+ const state = useStore(modalState);
8
+
9
+ const handleClose = () => {
10
+ modalState.set({ ...state, isOpen: false });
11
+ };
12
+
13
+ const handleAccept = () => {
14
+ modalState.set({ ...state, isOpen: false });
15
+ window.location.href = '/cart';
16
+ };
17
+
18
+ if (!state.isOpen) return null;
19
+
20
+ const isCartPage =
21
+ typeof window !== 'undefined' && window.location.pathname === '/cart';
22
+
23
+ return (
24
+ <Dialog.Root
25
+ open={state.isOpen}
26
+ onOpenChange={(e) => !e.open && handleClose()}
27
+ >
28
+ <Portal>
29
+ <Dialog.Backdrop className="fixed inset-0 z-50 bg-black bg-opacity-75 backdrop-blur-sm" />
30
+ <Dialog.Positioner className="fixed inset-0 z-50 flex items-center justify-center p-4">
31
+ <Dialog.Content className="w-full max-w-md overflow-hidden rounded-lg bg-white shadow-xl">
32
+ <div className="p-6">
33
+ <Dialog.Title className="text-xl font-bold text-gray-900">
34
+ {state.title}
35
+ </Dialog.Title>
36
+
37
+ <div className="mt-4 text-gray-600">
38
+ <p>{state.message}</p>
39
+ </div>
40
+
41
+ <div className="mt-6 flex justify-end gap-3">
42
+ <button
43
+ onClick={handleClose}
44
+ className="rounded-md bg-gray-200 px-4 py-2 text-sm font-bold text-gray-800 hover:bg-gray-300"
45
+ >
46
+ {isCartPage ? 'Close' : 'Continue Shopping'}
47
+ </button>
48
+ {!isCartPage && (
49
+ <button
50
+ onClick={handleAccept}
51
+ className="rounded-md bg-black px-4 py-2 text-sm font-bold text-white hover:bg-gray-800"
52
+ >
53
+ View Cart
54
+ </button>
55
+ )}
56
+ </div>
57
+ </div>
58
+ </Dialog.Content>
59
+ </Dialog.Positioner>
60
+ </Portal>
61
+ </Dialog.Root>
62
+ );
63
+ }