@tuturuuu/ui 0.6.2 → 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 (108) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/biome.json +1 -1
  3. package/package.json +11 -11
  4. package/src/components/ui/calendar-app/components/calendar-connections.tsx +17 -13
  5. package/src/components/ui/calendar-app/components/connected-accounts-dialog.tsx +2 -5
  6. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +2 -5
  7. package/src/components/ui/calendar.test.tsx +24 -0
  8. package/src/components/ui/calendar.tsx +1 -0
  9. package/src/components/ui/currency-input.test.tsx +43 -0
  10. package/src/components/ui/currency-input.tsx +1 -1
  11. package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +60 -35
  12. package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +176 -167
  13. package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +16 -10
  14. package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +75 -36
  15. package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +39 -42
  16. package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +1 -1
  17. package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +113 -91
  18. package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +73 -32
  19. package/src/components/ui/date-time-picker.tsx +352 -234
  20. package/src/components/ui/finance/categories-tags-tabs.tsx +23 -1
  21. package/src/components/ui/finance/command/finance-command-actions.test.tsx +48 -0
  22. package/src/components/ui/finance/command/finance-command-actions.tsx +200 -0
  23. package/src/components/ui/finance/command/finance-command-provider.test.tsx +151 -0
  24. package/src/components/ui/finance/command/finance-command-provider.tsx +250 -0
  25. package/src/components/ui/finance/command/finance-command-results.tsx +262 -0
  26. package/src/components/ui/finance/invoices/pending-invoices-table.tsx +22 -9
  27. package/src/components/ui/finance/shared/quick-actions.tsx +39 -90
  28. package/src/components/ui/finance/tags/tag-manager.tsx +24 -5
  29. package/src/components/ui/finance/transactions/form-basic-tab.tsx +33 -49
  30. package/src/components/ui/finance/transactions/form-types.ts +5 -0
  31. package/src/components/ui/finance/transactions/form.test.tsx +105 -22
  32. package/src/components/ui/finance/transactions/form.tsx +116 -20
  33. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +13 -6
  34. package/src/components/ui/finance/transactions/transaction-card.tsx +21 -9
  35. package/src/components/ui/finance/transactions/transaction-edit-dialog.test.tsx +25 -1
  36. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +16 -3
  37. package/src/components/ui/finance/transactions/transactionId/transaction-details-client-page.tsx +3 -0
  38. package/src/components/ui/finance/transactions/transactionId/transaction-details-page.tsx +3 -0
  39. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +6 -0
  40. package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +20 -2
  41. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -0
  42. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +7 -2
  43. package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +7 -2
  44. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +38 -1
  45. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +5 -0
  46. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +18 -2
  47. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +3 -0
  48. package/src/components/ui/finance/wallets/wallets-page.tsx +3 -0
  49. package/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx +2 -9
  50. package/src/components/ui/money-input.test.tsx +64 -0
  51. package/src/components/ui/money-input.tsx +63 -0
  52. package/src/components/ui/optional-time-picker.tsx +95 -0
  53. package/src/components/ui/quick-command-center.test.tsx +90 -0
  54. package/src/components/ui/quick-command-center.tsx +190 -0
  55. package/src/components/ui/storefront/cart-summary.tsx +126 -50
  56. package/src/components/ui/storefront/checkout-overlay.tsx +27 -0
  57. package/src/components/ui/storefront/hero-panel.tsx +23 -20
  58. package/src/components/ui/storefront/image-panel.tsx +6 -0
  59. package/src/components/ui/storefront/index.ts +11 -0
  60. package/src/components/ui/storefront/listing-card.tsx +84 -22
  61. package/src/components/ui/storefront/product-detail.tsx +289 -0
  62. package/src/components/ui/storefront/product-dialog.tsx +72 -0
  63. package/src/components/ui/storefront/storefront-surface.test.tsx +132 -5
  64. package/src/components/ui/storefront/storefront-surface.tsx +371 -128
  65. package/src/components/ui/storefront/types.ts +25 -1
  66. package/src/components/ui/storefront/utils.ts +118 -13
  67. package/src/components/ui/text-editor/__tests__/content-migration.test.ts +32 -0
  68. package/src/components/ui/text-editor/__tests__/image-extension.test.ts +69 -1
  69. package/src/components/ui/text-editor/__tests__/video-extension.test.ts +47 -0
  70. package/src/components/ui/text-editor/content-migration.ts +41 -18
  71. package/src/components/ui/text-editor/extensions.ts +1 -1
  72. package/src/components/ui/text-editor/image-extension.ts +40 -18
  73. package/src/components/ui/text-editor/video-extension.ts +11 -2
  74. package/src/components/ui/tu-do/boards/__tests__/workspace-projects-client-page.test.tsx +70 -1
  75. package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +127 -0
  76. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +1 -3
  77. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +13 -0
  78. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -0
  79. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +46 -8
  80. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +13 -2
  81. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +3 -1
  82. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +164 -0
  83. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +56 -2
  84. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-display.ts +9 -0
  85. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +8 -16
  86. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +5 -25
  87. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.test.ts +36 -1
  88. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.ts +51 -2
  89. package/src/components/ui/tu-do/boards/workspace-projects-client-page.tsx +13 -3
  90. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +34 -1
  91. package/src/components/ui/tu-do/shared/board-header.tsx +39 -0
  92. package/src/components/ui/tu-do/shared/board-views.tsx +9 -7
  93. package/src/components/ui/tu-do/shared/cursor-overlay-multi-wrapper.tsx +53 -12
  94. package/src/components/ui/tu-do/shared/task-dialog-presentation.test.ts +53 -0
  95. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +19 -0
  96. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +57 -0
  97. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +136 -111
  98. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -1
  99. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.test.ts +171 -0
  100. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.ts +200 -36
  101. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +21 -2
  102. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +42 -14
  103. package/src/hooks/__tests__/useBoardRealtime.test.tsx +2 -2
  104. package/src/hooks/__tests__/useCursorTracking.test.tsx +212 -0
  105. package/src/hooks/useBoardRealtime.ts +6 -3
  106. package/src/hooks/useBoardRealtime.types.ts +11 -0
  107. package/src/hooks/useCursorTracking.ts +91 -27
  108. package/src/hooks/useTaskUserRealtime.ts +5 -3
@@ -1,18 +1,23 @@
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,
7
+ InventoryStorefrontSection,
7
8
  } from '@tuturuuu/internal-api/inventory';
8
9
  import { cn } from '@tuturuuu/utils/format';
9
10
  import type { ReactNode } from 'react';
10
- import { Badge } from '../badge';
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';
15
+ import { StorefrontImagePanel } from './image-panel';
14
16
  import { StorefrontListingCard } from './listing-card';
17
+ import { StorefrontProductDetail } from './product-detail';
18
+ import { StorefrontProductDialog } from './product-dialog';
15
19
  import type {
20
+ StorefrontBuyerDefaults,
16
21
  StorefrontCartLine,
17
22
  StorefrontSurfaceLabels,
18
23
  StorefrontSurfaceMode,
@@ -20,69 +25,111 @@ import type {
20
25
  import { mergeStorefrontSurfaceLabels } from './types';
21
26
  import {
22
27
  getAccentStyle,
28
+ getSafeStorefrontHttpUrl,
29
+ getStorefrontLinePrice,
23
30
  getStorefrontListingLimit,
31
+ getStorefrontVariantLimit,
24
32
  sanitizeStorefrontAccentColor,
25
33
  storefrontRadiusClasses,
26
34
  storefrontSurfaceClasses,
35
+ storefrontThemeClasses,
27
36
  } from './utils';
28
37
 
29
38
  export function StorefrontSurface({
39
+ buyerDefaults,
30
40
  cartLines = [],
41
+ cartHref,
31
42
  checkoutHref,
32
43
  className,
44
+ compactLayout = false,
45
+ detailListingId,
33
46
  emptyAction,
34
47
  headerActions,
35
- isDemo = false,
48
+ isDemo: _isDemo = false,
49
+ isRedirecting = false,
36
50
  isSubmitting = false,
37
51
  labels: labelOverrides,
38
52
  listings,
39
53
  mode,
40
54
  notice,
55
+ onBuyNow,
41
56
  onCheckoutSubmit,
42
57
  onDecrement,
58
+ onDetailListingChange,
43
59
  onIncrement,
60
+ onInstantCheckout,
44
61
  selectedListingId,
45
62
  storefront,
63
+ storefrontHref,
46
64
  }: {
65
+ buyerDefaults?: StorefrontBuyerDefaults;
47
66
  cartLines?: StorefrontCartLine[];
67
+ cartHref?: string;
48
68
  checkoutHref?: string;
49
69
  className?: string;
70
+ compactLayout?: boolean;
71
+ detailListingId?: string | null;
50
72
  emptyAction?: ReactNode;
51
73
  headerActions?: ReactNode;
52
74
  isDemo?: boolean;
75
+ isRedirecting?: boolean;
53
76
  isSubmitting?: boolean;
54
77
  labels?: Partial<StorefrontSurfaceLabels>;
55
78
  listings: InventoryStorefrontListing[];
56
79
  mode: StorefrontSurfaceMode;
57
80
  notice?: ReactNode;
81
+ onBuyNow?: (listingId: string, variantId?: string | null) => void;
58
82
  onCheckoutSubmit?: (formData: FormData) => void;
59
- onDecrement?: (listingId: string) => void;
60
- 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;
61
91
  selectedListingId?: string;
62
92
  storefront: InventoryStorefront;
93
+ storefrontHref?: string;
63
94
  }) {
64
95
  const labels = mergeStorefrontSurfaceLabels(labelOverrides);
65
96
  const accentColor = sanitizeStorefrontAccentColor(storefront.accentColor);
66
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;
67
105
  const cartEntries = cartLines.flatMap((line) => {
68
106
  const listing = listings.find((item) => item.id === line.listingId);
69
- return listing ? [{ line, listing }] : [];
107
+ if (!listing) return [];
108
+ return [
109
+ { line, listing, variant: resolveVariant(listing, line.variantId) },
110
+ ];
70
111
  });
71
- const checkoutEntries = cartEntries.filter(({ line, listing }) => {
72
- const quantity = Math.min(
73
- line.quantity,
74
- getStorefrontListingLimit(listing)
75
- );
76
- return quantity > 0;
77
- });
78
- const total = checkoutEntries.reduce((sum, { line, listing }) => {
79
- const quantity = Math.min(
80
- line.quantity,
81
- 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
82
123
  );
83
- return sum + listing.price * quantity;
84
124
  }, 0);
85
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);
86
133
  const visibleListings =
87
134
  mode === 'product' && selectedListingId
88
135
  ? listings.filter((listing) => listing.id === selectedListingId)
@@ -95,141 +142,337 @@ export function StorefrontSurface({
95
142
  : visibleListings;
96
143
  const currency = storefront.currency ?? 'USD';
97
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
+
98
181
  return (
99
182
  <main
100
- className={cn('min-h-dvh bg-background text-foreground', className)}
183
+ className={cn(
184
+ 'min-h-dvh bg-background text-foreground',
185
+ storefrontThemeClasses[storefront.themePreset],
186
+ className
187
+ )}
101
188
  style={getAccentStyle(accentColor)}
102
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
+ />
103
197
  {notice ? (
104
198
  <div className="border-border border-b bg-muted/35 px-4 py-2 text-center text-muted-foreground text-sm">
105
199
  {notice}
106
200
  </div>
107
201
  ) : null}
108
202
 
109
- <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">
110
204
  <div className="mx-auto flex max-w-7xl flex-wrap items-center justify-between gap-3 px-4 py-3">
111
205
  <div className="min-w-0">
112
- <div className="flex flex-wrap items-center gap-2 text-muted-foreground text-xs">
113
- <span>
114
- {storefront.visibility === 'private'
115
- ? labels.privateStore
116
- : labels.publicStore}
117
- </span>
118
- {isDemo ? (
119
- <Badge variant="secondary">{labels.demoBadge}</Badge>
120
- ) : null}
121
- {isPreview ? (
122
- <Badge
123
- className="border-border bg-background"
124
- variant="outline"
125
- >
126
- {labels.previewBadge}
127
- </Badge>
128
- ) : null}
129
- {storefront.checkoutMode === 'simulated' ? (
130
- <Badge
131
- className="border-border bg-background"
132
- variant="outline"
133
- >
134
- {labels.simulatedBadge}
135
- </Badge>
136
- ) : null}
137
- {storefront.checkoutMode === 'disabled' ? (
138
- <Badge
139
- className="border-border bg-background"
140
- variant="outline"
206
+ <h1 className="truncate font-semibold text-xl">
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}
141
212
  >
142
- {labels.checkoutDisabledBadge}
143
- </Badge>
144
- ) : null}
145
- </div>
146
- <h1 className="mt-0.5 truncate font-semibold text-xl">
147
- {storefront.name}
213
+ {storefront.name}
214
+ </a>
215
+ ) : (
216
+ storefront.name
217
+ )}
148
218
  </h1>
219
+ {storefront.description ? (
220
+ <p className="mt-0.5 line-clamp-1 text-muted-foreground text-sm">
221
+ {storefront.description}
222
+ </p>
223
+ ) : null}
149
224
  </div>
150
225
  <div className="flex items-center gap-2">
151
226
  {headerActions}
152
- <span
153
- className={cn(
154
- 'inline-flex h-9 min-w-12 items-center justify-center gap-2 border bg-card px-3 font-medium text-sm',
155
- radius
156
- )}
157
- >
158
- <ShoppingCart className="h-4 w-4" />
159
- {cartQuantity}
160
- </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
+ )}
161
241
  </div>
162
242
  </div>
163
243
  </header>
164
244
 
165
- <section className="mx-auto grid max-w-7xl gap-4 px-4 py-5 lg:grid-cols-[minmax(0,1fr)_340px]">
166
- <div className="min-w-0">
167
- <StorefrontHeroPanel
168
- currency={currency}
169
- labels={labels}
170
- listingsCount={listings.length}
171
- radius={radius}
172
- storefront={storefront}
173
- />
174
-
175
- <div
176
- className={cn(
177
- 'mt-4',
178
- storefront.layoutStyle === 'list'
179
- ? 'grid gap-3'
180
- : 'grid gap-3 sm:grid-cols-2 xl:grid-cols-3',
181
- storefront.layoutStyle === 'feature' &&
182
- '[&>article:first-child]:sm:col-span-2'
183
- )}
184
- >
185
- {listingRows.length === 0 ? (
186
- <StorefrontEmptyListings
187
- action={emptyAction}
188
- labels={labels}
189
- radius={radius}
190
- />
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
+ </>
191
302
  ) : (
192
- listingRows.map((listing) => {
193
- const line = cartLines.find(
194
- (item) => item.listingId === listing.id
195
- );
196
-
197
- return (
198
- <StorefrontListingCard
199
- currency={currency}
200
- isList={storefront.layoutStyle === 'list'}
201
- key={listing.id}
202
- labels={labels}
203
- listing={listing}
204
- onDecrement={onDecrement}
205
- onIncrement={onIncrement}
206
- quantity={line?.quantity ?? 0}
207
- radius={radius}
208
- showInventoryBadges={storefront.showInventoryBadges}
209
- surfaceClassName={
210
- storefrontSurfaceClasses[storefront.surfaceStyle]
211
- }
212
- />
213
- );
214
- })
303
+ <>
304
+ <StorefrontHeroPanel
305
+ currency={currency}
306
+ labels={labels}
307
+ listingsCount={listings.length}
308
+ radius={radius}
309
+ storefront={storefront}
310
+ />
311
+
312
+ <StorefrontMerchSections
313
+ radius={radius}
314
+ sections={storefront.sections ?? []}
315
+ />
316
+
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
+ );
364
+
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
+ </>
215
391
  )}
216
392
  </div>
217
- </div>
218
393
 
219
- <StorefrontCartSummary
220
- cartEntries={checkoutEntries}
221
- checkoutHref={checkoutHref}
222
- currency={currency}
223
- isCheckout={isCheckout}
224
- isPreview={isPreview}
225
- isSubmitting={isSubmitting}
226
- labels={labels}
227
- onCheckoutSubmit={onCheckoutSubmit}
228
- radius={radius}
229
- storefront={storefront}
230
- total={total}
231
- />
232
- </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}
233
419
  </main>
234
420
  );
235
421
  }
422
+
423
+ function StorefrontMerchSections({
424
+ radius,
425
+ sections,
426
+ }: {
427
+ radius: string;
428
+ sections: InventoryStorefrontSection[];
429
+ }) {
430
+ const visibleSections = sections
431
+ .filter((section) => section.status === 'published')
432
+ .filter((section) => section.sectionType !== 'cover')
433
+ .sort((a, b) => a.sortOrder - b.sortOrder);
434
+
435
+ if (visibleSections.length === 0) return null;
436
+
437
+ return (
438
+ <div className="mt-4 grid gap-3">
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
+ })}
476
+ </div>
477
+ );
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,12 +32,19 @@ 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;
28
39
  checkoutDisabledBadge: string;
40
+ couponNote: string;
29
41
  demoBadge: string;
30
42
  emptyCart: string;
43
+ fromPrice: string;
44
+ instantCheckout: string;
45
+ redirectingToCheckout: string;
46
+ selectOptions: string;
47
+ viewDetails: string;
31
48
  emptyListingsDescription: string;
32
49
  emptyListingsTitle: string;
33
50
  fallbackDescription: string;
@@ -55,12 +72,19 @@ export const defaultStorefrontSurfaceLabels: StorefrontSurfaceLabels = {
55
72
  available: 'available',
56
73
  browse: 'Browse',
57
74
  bundle: 'Bundle',
75
+ buyNow: 'Buy now',
58
76
  cart: 'Cart',
59
77
  checkout: 'Checkout',
60
78
  checkoutDisabled: 'Checkout is disabled in preview',
61
79
  checkoutDisabledBadge: 'Checkout disabled',
80
+ couponNote: 'Have a coupon? You can apply it at checkout.',
62
81
  demoBadge: 'Demo',
63
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',
64
88
  emptyListingsDescription:
65
89
  'Publish a listing to make this storefront ready for buyers.',
66
90
  emptyListingsTitle: 'No listings yet',