@tuturuuu/ui 0.7.0 → 0.8.0

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 (67) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/package.json +8 -8
  3. package/src/components/ui/currency-input.test.tsx +43 -0
  4. package/src/components/ui/currency-input.tsx +1 -1
  5. package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +60 -35
  6. package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +176 -167
  7. package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +16 -10
  8. package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +75 -36
  9. package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +39 -42
  10. package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +1 -1
  11. package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +113 -91
  12. package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +73 -32
  13. package/src/components/ui/finance/transactions/form-types.ts +2 -0
  14. package/src/components/ui/finance/transactions/transaction-card.tsx +21 -9
  15. package/src/components/ui/money-input.test.tsx +64 -0
  16. package/src/components/ui/money-input.tsx +63 -0
  17. package/src/components/ui/storefront/cart-summary.tsx +114 -29
  18. package/src/components/ui/storefront/checkout-overlay.tsx +27 -0
  19. package/src/components/ui/storefront/hero-panel.tsx +2 -8
  20. package/src/components/ui/storefront/image-panel.tsx +6 -0
  21. package/src/components/ui/storefront/index.ts +11 -0
  22. package/src/components/ui/storefront/listing-card.tsx +84 -22
  23. package/src/components/ui/storefront/product-detail.tsx +289 -0
  24. package/src/components/ui/storefront/product-dialog.tsx +72 -0
  25. package/src/components/ui/storefront/storefront-surface.test.tsx +124 -1
  26. package/src/components/ui/storefront/storefront-surface.tsx +333 -133
  27. package/src/components/ui/storefront/types.ts +23 -1
  28. package/src/components/ui/storefront/utils.ts +111 -27
  29. package/src/components/ui/text-editor/__tests__/content-migration.test.ts +32 -0
  30. package/src/components/ui/text-editor/__tests__/image-extension.test.ts +69 -1
  31. package/src/components/ui/text-editor/__tests__/video-extension.test.ts +47 -0
  32. package/src/components/ui/text-editor/content-migration.ts +41 -18
  33. package/src/components/ui/text-editor/extensions.ts +1 -1
  34. package/src/components/ui/text-editor/image-extension.ts +40 -18
  35. package/src/components/ui/text-editor/video-extension.ts +11 -2
  36. package/src/components/ui/tu-do/boards/__tests__/workspace-projects-client-page.test.tsx +70 -1
  37. package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +127 -0
  38. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +1 -3
  39. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +13 -0
  40. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -0
  41. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +46 -8
  42. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +13 -2
  43. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +3 -1
  44. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +164 -0
  45. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +56 -2
  46. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-display.ts +9 -0
  47. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +8 -16
  48. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +5 -25
  49. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.test.ts +36 -1
  50. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.ts +51 -2
  51. package/src/components/ui/tu-do/boards/workspace-projects-client-page.tsx +13 -3
  52. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +34 -1
  53. package/src/components/ui/tu-do/shared/board-header.tsx +39 -0
  54. package/src/components/ui/tu-do/shared/board-views.tsx +9 -7
  55. package/src/components/ui/tu-do/shared/cursor-overlay-multi-wrapper.tsx +53 -12
  56. package/src/components/ui/tu-do/shared/task-dialog-presentation.test.ts +53 -0
  57. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +19 -0
  58. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +57 -0
  59. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +136 -111
  60. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -1
  61. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +42 -14
  62. package/src/hooks/__tests__/useBoardRealtime.test.tsx +2 -2
  63. package/src/hooks/__tests__/useCursorTracking.test.tsx +212 -0
  64. package/src/hooks/useBoardRealtime.ts +6 -3
  65. package/src/hooks/useBoardRealtime.types.ts +11 -0
  66. package/src/hooks/useCursorTracking.ts +91 -27
  67. package/src/hooks/useTaskUserRealtime.ts +5 -3
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { ShoppingCart } from '@tuturuuu/icons';
3
+ import { ArrowLeft, ShoppingCart } from '@tuturuuu/icons';
4
4
  import type {
5
5
  InventoryStorefront,
6
6
  InventoryStorefrontListing,
@@ -9,11 +9,15 @@ import type {
9
9
  import { cn } from '@tuturuuu/utils/format';
10
10
  import type { ReactNode } from 'react';
11
11
  import { StorefrontCartSummary } from './cart-summary';
12
+ import { StorefrontCheckoutOverlay } from './checkout-overlay';
12
13
  import { StorefrontEmptyListings } from './empty-listings';
13
14
  import { StorefrontHeroPanel } from './hero-panel';
14
15
  import { StorefrontImagePanel } from './image-panel';
15
16
  import { StorefrontListingCard } from './listing-card';
17
+ import { StorefrontProductDetail } from './product-detail';
18
+ import { StorefrontProductDialog } from './product-dialog';
16
19
  import type {
20
+ StorefrontBuyerDefaults,
17
21
  StorefrontCartLine,
18
22
  StorefrontSurfaceLabels,
19
23
  StorefrontSurfaceMode,
@@ -21,7 +25,10 @@ import type {
21
25
  import { mergeStorefrontSurfaceLabels } from './types';
22
26
  import {
23
27
  getAccentStyle,
28
+ getSafeStorefrontHttpUrl,
29
+ getStorefrontLinePrice,
24
30
  getStorefrontListingLimit,
31
+ getStorefrontVariantLimit,
25
32
  sanitizeStorefrontAccentColor,
26
33
  storefrontRadiusClasses,
27
34
  storefrontSurfaceClasses,
@@ -29,64 +36,100 @@ import {
29
36
  } from './utils';
30
37
 
31
38
  export function StorefrontSurface({
39
+ buyerDefaults,
32
40
  cartLines = [],
41
+ cartHref,
33
42
  checkoutHref,
34
43
  className,
35
44
  compactLayout = false,
45
+ detailListingId,
36
46
  emptyAction,
37
47
  headerActions,
38
48
  isDemo: _isDemo = false,
49
+ isRedirecting = false,
39
50
  isSubmitting = false,
40
51
  labels: labelOverrides,
41
52
  listings,
42
53
  mode,
43
54
  notice,
55
+ onBuyNow,
44
56
  onCheckoutSubmit,
45
57
  onDecrement,
58
+ onDetailListingChange,
46
59
  onIncrement,
60
+ onInstantCheckout,
47
61
  selectedListingId,
48
62
  storefront,
63
+ storefrontHref,
49
64
  }: {
65
+ buyerDefaults?: StorefrontBuyerDefaults;
50
66
  cartLines?: StorefrontCartLine[];
67
+ cartHref?: string;
51
68
  checkoutHref?: string;
52
69
  className?: string;
53
70
  compactLayout?: boolean;
71
+ detailListingId?: string | null;
54
72
  emptyAction?: ReactNode;
55
73
  headerActions?: ReactNode;
56
74
  isDemo?: boolean;
75
+ isRedirecting?: boolean;
57
76
  isSubmitting?: boolean;
58
77
  labels?: Partial<StorefrontSurfaceLabels>;
59
78
  listings: InventoryStorefrontListing[];
60
79
  mode: StorefrontSurfaceMode;
61
80
  notice?: ReactNode;
81
+ onBuyNow?: (listingId: string, variantId?: string | null) => void;
62
82
  onCheckoutSubmit?: (formData: FormData) => void;
63
- onDecrement?: (listingId: string) => void;
64
- onIncrement?: (listingId: string, maxQuantity: number) => void;
83
+ onDecrement?: (listingId: string, variantId?: string | null) => void;
84
+ onDetailListingChange?: (listingId: string | null) => void;
85
+ onIncrement?: (
86
+ listingId: string,
87
+ maxQuantity: number,
88
+ variantId?: string | null
89
+ ) => void;
90
+ onInstantCheckout?: () => void;
65
91
  selectedListingId?: string;
66
92
  storefront: InventoryStorefront;
93
+ storefrontHref?: string;
67
94
  }) {
68
95
  const labels = mergeStorefrontSurfaceLabels(labelOverrides);
69
96
  const accentColor = sanitizeStorefrontAccentColor(storefront.accentColor);
70
97
  const radius = storefrontRadiusClasses[storefront.cornerStyle];
98
+ const resolveVariant = (
99
+ listing: InventoryStorefrontListing,
100
+ variantId?: string | null
101
+ ) =>
102
+ variantId
103
+ ? (listing.variants ?? []).find((variant) => variant.id === variantId)
104
+ : undefined;
71
105
  const cartEntries = cartLines.flatMap((line) => {
72
106
  const listing = listings.find((item) => item.id === line.listingId);
73
- return listing ? [{ line, listing }] : [];
107
+ if (!listing) return [];
108
+ return [
109
+ { line, listing, variant: resolveVariant(listing, line.variantId) },
110
+ ];
74
111
  });
75
- const checkoutEntries = cartEntries.filter(({ line, listing }) => {
76
- const quantity = Math.min(
77
- line.quantity,
78
- getStorefrontListingLimit(listing)
79
- );
80
- return quantity > 0;
81
- });
82
- const total = checkoutEntries.reduce((sum, { line, listing }) => {
83
- const quantity = Math.min(
84
- line.quantity,
85
- getStorefrontListingLimit(listing)
112
+ const lineLimit = (entry: (typeof cartEntries)[number]) =>
113
+ entry.variant
114
+ ? getStorefrontVariantLimit(entry.listing, entry.variant)
115
+ : getStorefrontListingLimit(entry.listing);
116
+ const checkoutEntries = cartEntries.filter(
117
+ (entry) => Math.min(entry.line.quantity, lineLimit(entry)) > 0
118
+ );
119
+ const total = checkoutEntries.reduce((sum, entry) => {
120
+ const quantity = Math.min(entry.line.quantity, lineLimit(entry));
121
+ return (
122
+ sum + getStorefrontLinePrice(entry.listing, entry.variant) * quantity
86
123
  );
87
- return sum + listing.price * quantity;
88
124
  }, 0);
89
125
  const cartQuantity = cartLines.reduce((sum, line) => sum + line.quantity, 0);
126
+ const detailListing = detailListingId
127
+ ? listings.find((listing) => listing.id === detailListingId)
128
+ : undefined;
129
+ const selectedListing = selectedListingId
130
+ ? listings.find((listing) => listing.id === selectedListingId)
131
+ : undefined;
132
+ const isProductDetail = mode === 'product' && Boolean(selectedListing);
90
133
  const visibleListings =
91
134
  mode === 'product' && selectedListingId
92
135
  ? listings.filter((listing) => listing.id === selectedListingId)
@@ -99,6 +142,42 @@ export function StorefrontSurface({
99
142
  : visibleListings;
100
143
  const currency = storefront.currency ?? 'USD';
101
144
 
145
+ const cartSummary = (
146
+ <StorefrontCartSummary
147
+ buyerDefaults={buyerDefaults}
148
+ cartEntries={checkoutEntries}
149
+ checkoutHref={checkoutHref}
150
+ currency={currency}
151
+ isCheckout={isCheckout}
152
+ isPreview={isPreview}
153
+ isSubmitting={isSubmitting}
154
+ labels={labels}
155
+ onCheckoutSubmit={onCheckoutSubmit}
156
+ onInstantCheckout={mode === 'cart' ? onInstantCheckout : undefined}
157
+ radius={radius}
158
+ storefront={storefront}
159
+ total={total}
160
+ />
161
+ );
162
+ const cartControlClassName = cn(
163
+ 'inline-flex h-11 min-w-14 shrink-0 items-center justify-center gap-2 border bg-card px-3 font-semibold text-sm tabular-nums transition hover:bg-muted/45 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40',
164
+ radius
165
+ );
166
+ const cartControlStyle =
167
+ cartQuantity > 0
168
+ ? {
169
+ borderColor: 'var(--storefront-accent, var(--primary))',
170
+ color: 'var(--storefront-accent, var(--primary))',
171
+ }
172
+ : undefined;
173
+ const cartControlContent = (
174
+ <>
175
+ <ShoppingCart aria-hidden className="size-5 shrink-0" />
176
+ <span className="sr-only">{labels.cart}: </span>
177
+ <span className="min-w-4 text-center">{cartQuantity}</span>
178
+ </>
179
+ );
180
+
102
181
  return (
103
182
  <main
104
183
  className={cn(
@@ -108,17 +187,34 @@ export function StorefrontSurface({
108
187
  )}
109
188
  style={getAccentStyle(accentColor)}
110
189
  >
190
+ {/* Accent strip — makes the storefront's accent color immediately visible. */}
191
+ <div
192
+ className="h-1 w-full"
193
+ style={{
194
+ backgroundColor: 'var(--storefront-accent, var(--primary))',
195
+ }}
196
+ />
111
197
  {notice ? (
112
198
  <div className="border-border border-b bg-muted/35 px-4 py-2 text-center text-muted-foreground text-sm">
113
199
  {notice}
114
200
  </div>
115
201
  ) : null}
116
202
 
117
- <header className="border-border border-b bg-background/90 backdrop-blur">
203
+ <header className="sticky top-0 z-30 border-border border-b bg-background/80 backdrop-blur-md supports-[backdrop-filter]:bg-background/65">
118
204
  <div className="mx-auto flex max-w-7xl flex-wrap items-center justify-between gap-3 px-4 py-3">
119
205
  <div className="min-w-0">
120
206
  <h1 className="truncate font-semibold text-xl">
121
- {storefront.name}
207
+ {storefrontHref ? (
208
+ <a
209
+ className="block truncate rounded-sm transition hover:text-[var(--storefront-accent,var(--primary))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
210
+ href={storefrontHref}
211
+ title={storefront.name}
212
+ >
213
+ {storefront.name}
214
+ </a>
215
+ ) : (
216
+ storefront.name
217
+ )}
122
218
  </h1>
123
219
  {storefront.description ? (
124
220
  <p className="mt-0.5 line-clamp-1 text-muted-foreground text-sm">
@@ -128,98 +224,198 @@ export function StorefrontSurface({
128
224
  </div>
129
225
  <div className="flex items-center gap-2">
130
226
  {headerActions}
131
- <span
132
- className={cn(
133
- 'inline-flex h-9 min-w-12 items-center justify-center gap-2 border bg-card px-3 font-medium text-sm',
134
- radius
135
- )}
136
- >
137
- <ShoppingCart className="h-4 w-4" />
138
- {cartQuantity}
139
- </span>
227
+ {cartHref ? (
228
+ <a
229
+ aria-label={`${labels.cart}: ${cartQuantity}`}
230
+ className={cartControlClassName}
231
+ href={cartHref}
232
+ style={cartControlStyle}
233
+ >
234
+ {cartControlContent}
235
+ </a>
236
+ ) : (
237
+ <span className={cartControlClassName} style={cartControlStyle}>
238
+ {cartControlContent}
239
+ </span>
240
+ )}
140
241
  </div>
141
242
  </div>
142
243
  </header>
143
244
 
144
- <section
145
- className={cn(
146
- 'mx-auto grid max-w-7xl gap-4 px-4 py-5',
147
- compactLayout ? 'grid-cols-1' : 'lg:grid-cols-[minmax(0,1fr)_340px]'
148
- )}
149
- >
150
- <div className="min-w-0">
151
- <StorefrontHeroPanel
152
- currency={currency}
153
- labels={labels}
154
- listingsCount={listings.length}
155
- radius={radius}
156
- storefront={storefront}
157
- />
245
+ {isCheckout ? (
246
+ <section className="mx-auto w-full max-w-xl px-4 py-8">
247
+ {cartHref ? (
248
+ <a
249
+ className="mb-4 inline-flex items-center gap-1.5 text-muted-foreground text-sm transition hover:text-foreground"
250
+ href={cartHref}
251
+ >
252
+ <ArrowLeft aria-hidden className="size-4" />
253
+ {labels.cart}
254
+ </a>
255
+ ) : null}
256
+ <h2 className="mb-4 font-semibold text-2xl tracking-tight">
257
+ {labels.checkout}
258
+ </h2>
259
+ {cartSummary}
260
+ </section>
261
+ ) : (
262
+ <section
263
+ className={cn(
264
+ 'mx-auto grid max-w-7xl gap-4 px-4 py-5',
265
+ compactLayout ? 'grid-cols-1' : 'lg:grid-cols-[minmax(0,1fr)_340px]'
266
+ )}
267
+ >
268
+ <div className="min-w-0">
269
+ {isProductDetail && selectedListing ? (
270
+ <>
271
+ {storefrontHref ? (
272
+ <a
273
+ className="mb-4 inline-flex items-center gap-1.5 text-muted-foreground text-sm transition hover:text-foreground"
274
+ href={storefrontHref}
275
+ >
276
+ <ArrowLeft aria-hidden className="size-4" />
277
+ {labels.browse}
278
+ </a>
279
+ ) : null}
280
+ <StorefrontProductDetail
281
+ cartHref={cartHref}
282
+ cartLines={cartLines}
283
+ currency={currency}
284
+ isSubmitting={isSubmitting}
285
+ labels={labels}
286
+ listing={selectedListing}
287
+ onBuyNow={onBuyNow}
288
+ onDecrement={onDecrement}
289
+ onIncrement={onIncrement}
290
+ quantity={
291
+ cartLines.find(
292
+ (item) => item.listingId === selectedListing.id
293
+ )?.quantity ?? 0
294
+ }
295
+ radius={radius}
296
+ showInventoryBadges={storefront.showInventoryBadges}
297
+ surfaceClassName={
298
+ storefrontSurfaceClasses[storefront.surfaceStyle]
299
+ }
300
+ />
301
+ </>
302
+ ) : (
303
+ <>
304
+ <StorefrontHeroPanel
305
+ currency={currency}
306
+ labels={labels}
307
+ listingsCount={listings.length}
308
+ radius={radius}
309
+ storefront={storefront}
310
+ />
158
311
 
159
- <StorefrontMerchSections
160
- radius={radius}
161
- sections={storefront.sections ?? []}
162
- />
312
+ <StorefrontMerchSections
313
+ radius={radius}
314
+ sections={storefront.sections ?? []}
315
+ />
163
316
 
164
- <div
165
- className={cn(
166
- 'mt-4',
167
- compactLayout || storefront.layoutStyle === 'list'
168
- ? 'grid gap-3'
169
- : 'grid gap-3 sm:grid-cols-2 xl:grid-cols-3',
170
- !compactLayout &&
171
- storefront.layoutStyle === 'feature' &&
172
- '[&>article:first-child]:sm:col-span-2'
173
- )}
174
- >
175
- {listingRows.length === 0 ? (
176
- <StorefrontEmptyListings
177
- action={emptyAction}
178
- labels={labels}
179
- radius={radius}
180
- />
181
- ) : (
182
- listingRows.map((listing) => {
183
- const line = cartLines.find(
184
- (item) => item.listingId === listing.id
185
- );
317
+ <div
318
+ className={cn(
319
+ 'mt-4 grid gap-4',
320
+ compactLayout
321
+ ? 'sm:grid-cols-2'
322
+ : 'sm:grid-cols-2 xl:grid-cols-3'
323
+ )}
324
+ >
325
+ {listingRows.length === 0 ? (
326
+ showCartListings ? (
327
+ <div
328
+ className={cn(
329
+ 'col-span-full grid min-h-56 place-items-center border border-dashed bg-muted/25 p-6 text-center',
330
+ radius
331
+ )}
332
+ >
333
+ <div className="max-w-sm">
334
+ <ShoppingCart
335
+ aria-hidden
336
+ className="mx-auto size-8 text-muted-foreground"
337
+ />
338
+ <p className="mt-3 font-semibold">
339
+ {labels.emptyCart}
340
+ </p>
341
+ {storefrontHref ? (
342
+ <a
343
+ className="mt-4 inline-flex items-center gap-1.5 font-medium text-[var(--storefront-accent,var(--primary))] text-sm hover:underline"
344
+ href={storefrontHref}
345
+ >
346
+ <ArrowLeft aria-hidden className="size-4" />
347
+ {labels.browse}
348
+ </a>
349
+ ) : null}
350
+ </div>
351
+ </div>
352
+ ) : (
353
+ <StorefrontEmptyListings
354
+ action={emptyAction}
355
+ labels={labels}
356
+ radius={radius}
357
+ />
358
+ )
359
+ ) : (
360
+ listingRows.map((listing) => {
361
+ const line = cartLines.find(
362
+ (item) => item.listingId === listing.id
363
+ );
186
364
 
187
- return (
188
- <StorefrontListingCard
189
- currency={currency}
190
- isList={storefront.layoutStyle === 'list'}
191
- key={listing.id}
192
- labels={labels}
193
- listing={listing}
194
- onDecrement={onDecrement}
195
- onIncrement={onIncrement}
196
- quantity={line?.quantity ?? 0}
197
- radius={radius}
198
- showInventoryBadges={storefront.showInventoryBadges}
199
- surfaceClassName={
200
- storefrontSurfaceClasses[storefront.surfaceStyle]
201
- }
202
- />
203
- );
204
- })
365
+ return (
366
+ <StorefrontListingCard
367
+ currency={currency}
368
+ isList={false}
369
+ key={listing.id}
370
+ labels={labels}
371
+ listing={listing}
372
+ onDecrement={onDecrement}
373
+ onIncrement={onIncrement}
374
+ onOpenDetail={
375
+ onDetailListingChange
376
+ ? (id) => onDetailListingChange(id)
377
+ : undefined
378
+ }
379
+ quantity={line?.quantity ?? 0}
380
+ radius={radius}
381
+ showInventoryBadges={storefront.showInventoryBadges}
382
+ surfaceClassName={
383
+ storefrontSurfaceClasses[storefront.surfaceStyle]
384
+ }
385
+ />
386
+ );
387
+ })
388
+ )}
389
+ </div>
390
+ </>
205
391
  )}
206
392
  </div>
207
- </div>
208
393
 
209
- <StorefrontCartSummary
210
- cartEntries={checkoutEntries}
211
- checkoutHref={checkoutHref}
212
- currency={currency}
213
- isCheckout={isCheckout}
214
- isPreview={isPreview}
215
- isSubmitting={isSubmitting}
216
- labels={labels}
217
- onCheckoutSubmit={onCheckoutSubmit}
218
- radius={radius}
219
- storefront={storefront}
220
- total={total}
221
- />
222
- </section>
394
+ {cartSummary}
395
+ </section>
396
+ )}
397
+
398
+ <StorefrontProductDialog
399
+ cartHref={cartHref}
400
+ cartLines={cartLines}
401
+ currency={currency}
402
+ isSubmitting={isSubmitting}
403
+ labels={labels}
404
+ listing={detailListing ?? null}
405
+ onBuyNow={onBuyNow}
406
+ onDecrement={onDecrement}
407
+ onIncrement={onIncrement}
408
+ onOpenChange={(open) => {
409
+ if (!open) onDetailListingChange?.(null);
410
+ }}
411
+ radius={radius}
412
+ showInventoryBadges={storefront.showInventoryBadges}
413
+ surfaceClassName={storefrontSurfaceClasses[storefront.surfaceStyle]}
414
+ />
415
+
416
+ {isSubmitting || isRedirecting ? (
417
+ <StorefrontCheckoutOverlay label={labels.redirectingToCheckout} />
418
+ ) : null}
223
419
  </main>
224
420
  );
225
421
  }
@@ -240,39 +436,43 @@ function StorefrontMerchSections({
240
436
 
241
437
  return (
242
438
  <div className="mt-4 grid gap-3">
243
- {visibleSections.map((section) => (
244
- <section
245
- className={cn(
246
- 'grid overflow-hidden border border-border bg-card md:grid-cols-[minmax(0,1fr)_280px]',
247
- radius
248
- )}
249
- key={section.id}
250
- >
251
- <div className="flex min-w-0 flex-col justify-center gap-2 p-4">
252
- {section.title ? (
253
- <h2 className="font-semibold text-lg">{section.title}</h2>
254
- ) : null}
255
- {section.description ? (
256
- <p className="text-muted-foreground text-sm leading-6">
257
- {section.description}
258
- </p>
259
- ) : null}
260
- {section.href ? (
261
- <a
262
- className="mt-1 w-fit font-medium text-sm underline-offset-4 hover:underline"
263
- href={section.href}
264
- >
265
- {section.href.replace(/^https?:\/\//u, '')}
266
- </a>
267
- ) : null}
268
- </div>
269
- <StorefrontImagePanel
270
- className="min-h-36 md:min-h-full"
271
- imageUrl={section.imageUrl}
272
- label={section.title ?? 'Storefront section'}
273
- />
274
- </section>
275
- ))}
439
+ {visibleSections.map((section) => {
440
+ const sectionHref = getSafeStorefrontHttpUrl(section.href);
441
+
442
+ return (
443
+ <section
444
+ className={cn(
445
+ 'grid overflow-hidden border border-border bg-card md:grid-cols-[minmax(0,1fr)_280px]',
446
+ radius
447
+ )}
448
+ key={section.id}
449
+ >
450
+ <div className="flex min-w-0 flex-col justify-center gap-2 p-4">
451
+ {section.title ? (
452
+ <h2 className="font-semibold text-lg">{section.title}</h2>
453
+ ) : null}
454
+ {section.description ? (
455
+ <p className="text-muted-foreground text-sm leading-6">
456
+ {section.description}
457
+ </p>
458
+ ) : null}
459
+ {sectionHref ? (
460
+ <a
461
+ className="mt-1 w-fit font-medium text-sm underline-offset-4 hover:underline"
462
+ href={sectionHref}
463
+ >
464
+ {sectionHref.replace(/^https?:\/\//u, '')}
465
+ </a>
466
+ ) : null}
467
+ </div>
468
+ <StorefrontImagePanel
469
+ className="min-h-36 md:min-h-full"
470
+ imageUrl={section.imageUrl}
471
+ label={section.title ?? 'Storefront section'}
472
+ />
473
+ </section>
474
+ );
475
+ })}
276
476
  </div>
277
477
  );
278
478
  }
@@ -1,13 +1,23 @@
1
- import type { InventoryStorefrontListing } from '@tuturuuu/internal-api/inventory';
1
+ import type {
2
+ InventoryListingVariant,
3
+ InventoryStorefrontListing,
4
+ } from '@tuturuuu/internal-api/inventory';
2
5
 
3
6
  export type StorefrontCartLine = {
4
7
  listingId: string;
8
+ variantId?: string | null;
5
9
  quantity: number;
6
10
  };
7
11
 
8
12
  export type StorefrontCartEntry = {
9
13
  line: StorefrontCartLine;
10
14
  listing: InventoryStorefrontListing;
15
+ variant?: InventoryListingVariant;
16
+ };
17
+
18
+ export type StorefrontBuyerDefaults = {
19
+ email?: string | null;
20
+ name?: string | null;
11
21
  };
12
22
 
13
23
  export type StorefrontSurfaceMode =
@@ -22,6 +32,7 @@ export type StorefrontSurfaceLabels = {
22
32
  available: string;
23
33
  browse: string;
24
34
  bundle: string;
35
+ buyNow: string;
25
36
  cart: string;
26
37
  checkout: string;
27
38
  checkoutDisabled: string;
@@ -29,6 +40,11 @@ export type StorefrontSurfaceLabels = {
29
40
  couponNote: string;
30
41
  demoBadge: string;
31
42
  emptyCart: string;
43
+ fromPrice: string;
44
+ instantCheckout: string;
45
+ redirectingToCheckout: string;
46
+ selectOptions: string;
47
+ viewDetails: string;
32
48
  emptyListingsDescription: string;
33
49
  emptyListingsTitle: string;
34
50
  fallbackDescription: string;
@@ -56,6 +72,7 @@ export const defaultStorefrontSurfaceLabels: StorefrontSurfaceLabels = {
56
72
  available: 'available',
57
73
  browse: 'Browse',
58
74
  bundle: 'Bundle',
75
+ buyNow: 'Buy now',
59
76
  cart: 'Cart',
60
77
  checkout: 'Checkout',
61
78
  checkoutDisabled: 'Checkout is disabled in preview',
@@ -63,6 +80,11 @@ export const defaultStorefrontSurfaceLabels: StorefrontSurfaceLabels = {
63
80
  couponNote: 'Have a coupon? You can apply it at checkout.',
64
81
  demoBadge: 'Demo',
65
82
  emptyCart: 'Add a listing to start checkout.',
83
+ fromPrice: 'From',
84
+ instantCheckout: 'Instant checkout',
85
+ redirectingToCheckout: 'Taking you to secure checkout…',
86
+ selectOptions: 'Select options',
87
+ viewDetails: 'View details',
66
88
  emptyListingsDescription:
67
89
  'Publish a listing to make this storefront ready for buyers.',
68
90
  emptyListingsTitle: 'No listings yet',