@tuturuuu/ui 0.7.0 → 0.9.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 (226) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/biome.json +1 -1
  3. package/package.json +75 -73
  4. package/src/components/ui/accordion.tsx +1 -1
  5. package/src/components/ui/breadcrumb.tsx +1 -1
  6. package/src/components/ui/calendar-app/calendar-page-shell.tsx +4 -0
  7. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +239 -33
  8. package/src/components/ui/calendar-app/components/load-smart-scheduling-tasks.tsx +143 -0
  9. package/src/components/ui/calendar-app/components/priority-view.tsx +10 -3
  10. package/src/components/ui/calendar-app/components/tasks-sidebar.tsx +4 -116
  11. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +67 -2
  12. package/src/components/ui/calendar.tsx +1 -1
  13. package/src/components/ui/carousel.tsx +1 -1
  14. package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +1 -1
  15. package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +1 -1
  16. package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +1 -1
  17. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +1 -1
  18. package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +1 -1
  19. package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +1 -1
  20. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +2 -2
  21. package/src/components/ui/chat/chat-agent-details-utils.test.ts +1 -1
  22. package/src/components/ui/chat/chat-agent-details-utils.tsx +1 -1
  23. package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +2 -2
  24. package/src/components/ui/checkbox.tsx +1 -1
  25. package/src/components/ui/color-picker.tsx +1 -1
  26. package/src/components/ui/command.tsx +1 -1
  27. package/src/components/ui/context-menu.tsx +5 -1
  28. package/src/components/ui/currency-input.test.tsx +43 -0
  29. package/src/components/ui/currency-input.tsx +1 -1
  30. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
  31. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +19 -0
  32. package/src/components/ui/custom/combobox.test.tsx +195 -0
  33. package/src/components/ui/custom/combobox.tsx +273 -156
  34. package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
  35. package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
  36. package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
  37. package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
  38. package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
  39. package/src/components/ui/custom/settings-dialog-shell.tsx +2 -1
  40. package/src/components/ui/custom/theme-toggle.tsx +1 -1
  41. package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +60 -35
  42. package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +176 -167
  43. package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +16 -10
  44. package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +75 -36
  45. package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +39 -42
  46. package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +1 -1
  47. package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +113 -91
  48. package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +73 -32
  49. package/src/components/ui/custom/workspace-select.tsx +8 -3
  50. package/src/components/ui/dialog.test.tsx +52 -0
  51. package/src/components/ui/dialog.tsx +6 -2
  52. package/src/components/ui/dropdown-menu.tsx +5 -1
  53. package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
  54. package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
  55. package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
  56. package/src/components/ui/finance/debts/debts-page.tsx +15 -2
  57. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
  58. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
  59. package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
  60. package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
  61. package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
  62. package/src/components/ui/finance/invoices/utils.ts +3 -1
  63. package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
  64. package/src/components/ui/finance/transactions/form-types.ts +3 -0
  65. package/src/components/ui/finance/transactions/form.tsx +2 -0
  66. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
  67. package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
  68. package/src/components/ui/finance/transactions/transaction-card.tsx +21 -9
  69. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
  70. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
  71. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
  72. package/src/components/ui/finance/wallets/form.test.tsx +51 -3
  73. package/src/components/ui/finance/wallets/form.tsx +15 -4
  74. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  75. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
  76. package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
  77. package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
  78. package/src/components/ui/input-otp.tsx +1 -1
  79. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
  80. package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
  81. package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
  82. package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
  83. package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
  84. package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
  85. package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
  86. package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
  87. package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
  88. package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
  89. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
  90. package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
  91. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
  92. package/src/components/ui/money-input.test.tsx +64 -0
  93. package/src/components/ui/money-input.tsx +63 -0
  94. package/src/components/ui/navigation-menu.tsx +1 -1
  95. package/src/components/ui/pagination.tsx +1 -1
  96. package/src/components/ui/radio-group.tsx +1 -1
  97. package/src/components/ui/select.tsx +5 -1
  98. package/src/components/ui/sheet.tsx +1 -1
  99. package/src/components/ui/sidebar.tsx +1 -1
  100. package/src/components/ui/storefront/cart-popover.tsx +61 -0
  101. package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
  102. package/src/components/ui/storefront/cart-summary.tsx +104 -80
  103. package/src/components/ui/storefront/checkout-overlay.tsx +26 -0
  104. package/src/components/ui/storefront/hero-panel.tsx +2 -8
  105. package/src/components/ui/storefront/image-panel.tsx +6 -0
  106. package/src/components/ui/storefront/index.ts +11 -0
  107. package/src/components/ui/storefront/listing-card.tsx +84 -22
  108. package/src/components/ui/storefront/merch-sections.tsx +70 -0
  109. package/src/components/ui/storefront/product-detail.tsx +289 -0
  110. package/src/components/ui/storefront/product-dialog.tsx +72 -0
  111. package/src/components/ui/storefront/storefront-surface.test.tsx +221 -3
  112. package/src/components/ui/storefront/storefront-surface.tsx +288 -153
  113. package/src/components/ui/storefront/types.ts +27 -1
  114. package/src/components/ui/storefront/utils.ts +117 -27
  115. package/src/components/ui/text-editor/__tests__/content-migration.test.ts +32 -0
  116. package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
  117. package/src/components/ui/text-editor/__tests__/image-extension.test.ts +69 -1
  118. package/src/components/ui/text-editor/__tests__/video-extension.test.ts +47 -0
  119. package/src/components/ui/text-editor/background-color-extension.ts +62 -0
  120. package/src/components/ui/text-editor/color-controls.tsx +284 -0
  121. package/src/components/ui/text-editor/content-migration.ts +41 -18
  122. package/src/components/ui/text-editor/editor.tsx +69 -14
  123. package/src/components/ui/text-editor/extensions.ts +9 -3
  124. package/src/components/ui/text-editor/highlight-extension.ts +22 -0
  125. package/src/components/ui/text-editor/image-extension.ts +40 -18
  126. package/src/components/ui/text-editor/tool-bar.tsx +9 -16
  127. package/src/components/ui/text-editor/video-extension.ts +11 -2
  128. package/src/components/ui/toast.tsx +1 -1
  129. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +270 -0
  130. package/src/components/ui/tu-do/boards/__tests__/workspace-projects-client-page.test.tsx +70 -1
  131. package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
  132. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +222 -109
  133. package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +127 -0
  134. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +113 -46
  135. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
  136. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
  137. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
  138. package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
  139. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
  140. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
  141. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +51 -9
  142. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
  143. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
  144. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -0
  145. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +127 -38
  146. package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
  147. package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
  148. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
  149. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
  150. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
  151. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
  152. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
  153. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
  154. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
  155. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
  156. package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
  157. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +410 -4
  158. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +106 -14
  159. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +443 -19
  160. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +94 -32
  161. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +213 -106
  162. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +186 -0
  163. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +59 -2
  164. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +3 -0
  165. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
  166. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +191 -28
  167. package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
  168. package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
  169. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +7 -0
  170. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-display.ts +9 -0
  171. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +8 -16
  172. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +5 -25
  173. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.test.ts +36 -1
  174. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.ts +51 -2
  175. package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
  176. package/src/components/ui/tu-do/boards/workspace-projects-client-page.tsx +13 -3
  177. package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
  178. package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
  179. package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
  180. package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
  181. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
  182. package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
  183. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +25 -0
  184. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +341 -38
  185. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +253 -0
  186. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +237 -3
  187. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +17 -0
  188. package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
  189. package/src/components/ui/tu-do/shared/board-client.tsx +2 -7
  190. package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
  191. package/src/components/ui/tu-do/shared/board-header.tsx +465 -937
  192. package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
  193. package/src/components/ui/tu-do/shared/board-switcher.tsx +209 -217
  194. package/src/components/ui/tu-do/shared/board-views.tsx +596 -82
  195. package/src/components/ui/tu-do/shared/cursor-overlay-multi-wrapper.tsx +53 -12
  196. package/src/components/ui/tu-do/shared/list-view.tsx +227 -1
  197. package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
  198. package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
  199. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
  200. package/src/components/ui/tu-do/shared/task-dialog-presentation.test.ts +53 -0
  201. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +19 -0
  202. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +57 -0
  203. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +136 -111
  204. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -1
  205. package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
  206. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
  207. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
  208. package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
  209. package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
  210. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +44 -15
  211. package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
  212. package/src/declarations.d.ts +1 -0
  213. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
  214. package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
  215. package/src/hooks/__tests__/useBoardRealtime.test.tsx +2 -2
  216. package/src/hooks/__tests__/useCursorTracking.test.tsx +212 -0
  217. package/src/hooks/use-calendar-sync.tsx +247 -243
  218. package/src/hooks/use-calendar.tsx +323 -138
  219. package/src/hooks/use-task-actions.ts +24 -0
  220. package/src/hooks/use-user-workspace-config.ts +75 -0
  221. package/src/hooks/use-workspace-currency.ts +8 -3
  222. package/src/hooks/useBoardRealtime.ts +6 -3
  223. package/src/hooks/useBoardRealtime.types.ts +11 -0
  224. package/src/hooks/useBoardRealtimeEventHandler.ts +11 -0
  225. package/src/hooks/useCursorTracking.ts +91 -27
  226. package/src/hooks/useTaskUserRealtime.ts +5 -3
@@ -1,19 +1,30 @@
1
1
  'use client';
2
2
 
3
- import { ShoppingCart } from '@tuturuuu/icons';
3
+ import { ArrowLeft } from '@tuturuuu/icons';
4
4
  import type {
5
5
  InventoryStorefront,
6
6
  InventoryStorefrontListing,
7
- InventoryStorefrontSection,
8
7
  } from '@tuturuuu/internal-api/inventory';
9
8
  import { cn } from '@tuturuuu/utils/format';
10
- import type { ReactNode } from 'react';
9
+ import { type ReactNode, useState } from 'react';
10
+ import {
11
+ Dialog,
12
+ DialogContent,
13
+ DialogDescription,
14
+ DialogHeader,
15
+ DialogTitle,
16
+ } from '../dialog';
17
+ import { StorefrontCartPopover } from './cart-popover';
11
18
  import { StorefrontCartSummary } from './cart-summary';
19
+ import { StorefrontCheckoutOverlay } from './checkout-overlay';
12
20
  import { StorefrontEmptyListings } from './empty-listings';
13
21
  import { StorefrontHeroPanel } from './hero-panel';
14
- import { StorefrontImagePanel } from './image-panel';
15
22
  import { StorefrontListingCard } from './listing-card';
23
+ import { StorefrontMerchSections } from './merch-sections';
24
+ import { StorefrontProductDetail } from './product-detail';
25
+ import { StorefrontProductDialog } from './product-dialog';
16
26
  import type {
27
+ StorefrontBuyerDefaults,
17
28
  StorefrontCartLine,
18
29
  StorefrontSurfaceLabels,
19
30
  StorefrontSurfaceMode,
@@ -21,7 +32,9 @@ import type {
21
32
  import { mergeStorefrontSurfaceLabels } from './types';
22
33
  import {
23
34
  getAccentStyle,
35
+ getStorefrontLinePrice,
24
36
  getStorefrontListingLimit,
37
+ getStorefrontVariantLimit,
25
38
  sanitizeStorefrontAccentColor,
26
39
  storefrontRadiusClasses,
27
40
  storefrontSurfaceClasses,
@@ -29,76 +42,175 @@ import {
29
42
  } from './utils';
30
43
 
31
44
  export function StorefrontSurface({
45
+ buyerDefaults,
32
46
  cartLines = [],
47
+ cartHref,
48
+ checkoutOpen,
33
49
  checkoutHref,
34
50
  className,
35
51
  compactLayout = false,
52
+ detailListingId,
36
53
  emptyAction,
37
54
  headerActions,
38
55
  isDemo: _isDemo = false,
56
+ isRedirecting = false,
39
57
  isSubmitting = false,
40
58
  labels: labelOverrides,
41
59
  listings,
42
60
  mode,
43
61
  notice,
62
+ onBuyNow,
63
+ onCheckoutOpen,
64
+ onCheckoutOpenChange,
44
65
  onCheckoutSubmit,
45
66
  onDecrement,
67
+ onDetailListingChange,
46
68
  onIncrement,
69
+ onInstantCheckout,
47
70
  selectedListingId,
48
71
  storefront,
72
+ storefrontHref,
49
73
  }: {
74
+ buyerDefaults?: StorefrontBuyerDefaults;
50
75
  cartLines?: StorefrontCartLine[];
76
+ cartHref?: string;
77
+ checkoutOpen?: boolean;
51
78
  checkoutHref?: string;
52
79
  className?: string;
53
80
  compactLayout?: boolean;
81
+ detailListingId?: string | null;
54
82
  emptyAction?: ReactNode;
55
83
  headerActions?: ReactNode;
56
84
  isDemo?: boolean;
85
+ isRedirecting?: boolean;
57
86
  isSubmitting?: boolean;
58
87
  labels?: Partial<StorefrontSurfaceLabels>;
59
88
  listings: InventoryStorefrontListing[];
60
89
  mode: StorefrontSurfaceMode;
61
90
  notice?: ReactNode;
91
+ onBuyNow?: (listingId: string, variantId?: string | null) => void;
92
+ onCheckoutOpen?: () => void;
93
+ onCheckoutOpenChange?: (open: boolean) => void;
62
94
  onCheckoutSubmit?: (formData: FormData) => void;
63
- onDecrement?: (listingId: string) => void;
64
- onIncrement?: (listingId: string, maxQuantity: number) => void;
95
+ onDecrement?: (listingId: string, variantId?: string | null) => void;
96
+ onDetailListingChange?: (listingId: string | null) => void;
97
+ onIncrement?: (
98
+ listingId: string,
99
+ maxQuantity: number,
100
+ variantId?: string | null
101
+ ) => void;
102
+ onInstantCheckout?: () => void;
65
103
  selectedListingId?: string;
66
104
  storefront: InventoryStorefront;
105
+ storefrontHref?: string;
67
106
  }) {
68
107
  const labels = mergeStorefrontSurfaceLabels(labelOverrides);
108
+ const [isCartPopoverOpen, setIsCartPopoverOpen] = useState(false);
69
109
  const accentColor = sanitizeStorefrontAccentColor(storefront.accentColor);
70
110
  const radius = storefrontRadiusClasses[storefront.cornerStyle];
111
+ const resolveVariant = (
112
+ listing: InventoryStorefrontListing,
113
+ variantId?: string | null
114
+ ) =>
115
+ variantId
116
+ ? (listing.variants ?? []).find((variant) => variant.id === variantId)
117
+ : undefined;
71
118
  const cartEntries = cartLines.flatMap((line) => {
72
119
  const listing = listings.find((item) => item.id === line.listingId);
73
- return listing ? [{ line, listing }] : [];
74
- });
75
- const checkoutEntries = cartEntries.filter(({ line, listing }) => {
76
- const quantity = Math.min(
77
- line.quantity,
78
- getStorefrontListingLimit(listing)
79
- );
80
- return quantity > 0;
120
+ if (!listing) return [];
121
+ return [
122
+ { line, listing, variant: resolveVariant(listing, line.variantId) },
123
+ ];
81
124
  });
82
- const total = checkoutEntries.reduce((sum, { line, listing }) => {
83
- const quantity = Math.min(
84
- line.quantity,
85
- getStorefrontListingLimit(listing)
125
+ const lineLimit = (entry: (typeof cartEntries)[number]) =>
126
+ entry.variant
127
+ ? getStorefrontVariantLimit(entry.listing, entry.variant)
128
+ : getStorefrontListingLimit(entry.listing);
129
+ const checkoutEntries = cartEntries.filter(
130
+ (entry) => Math.min(entry.line.quantity, lineLimit(entry)) > 0
131
+ );
132
+ const total = checkoutEntries.reduce((sum, entry) => {
133
+ const quantity = Math.min(entry.line.quantity, lineLimit(entry));
134
+ return (
135
+ sum + getStorefrontLinePrice(entry.listing, entry.variant) * quantity
86
136
  );
87
- return sum + listing.price * quantity;
88
137
  }, 0);
89
138
  const cartQuantity = cartLines.reduce((sum, line) => sum + line.quantity, 0);
139
+ const detailListing = detailListingId
140
+ ? listings.find((listing) => listing.id === detailListingId)
141
+ : undefined;
142
+ const selectedListing = selectedListingId
143
+ ? listings.find((listing) => listing.id === selectedListingId)
144
+ : undefined;
145
+ const isProductDetail = mode === 'product' && Boolean(selectedListing);
90
146
  const visibleListings =
91
147
  mode === 'product' && selectedListingId
92
148
  ? listings.filter((listing) => listing.id === selectedListingId)
93
149
  : listings;
94
150
  const isCheckout = mode === 'checkout';
151
+ const isCheckoutDialogOpen = checkoutOpen ?? isCheckout;
95
152
  const isPreview = mode === 'preview';
96
- const showCartListings = mode === 'cart' || isCheckout;
97
- const listingRows = showCartListings
98
- ? cartEntries.map(({ listing }) => listing)
99
- : visibleListings;
153
+ const isCartPage = mode === 'cart';
154
+ const listingRows = visibleListings;
100
155
  const currency = storefront.currency ?? 'USD';
156
+ const handleCheckoutOpen = () => {
157
+ setIsCartPopoverOpen(false);
158
+ onCheckoutOpen?.();
159
+ };
101
160
 
161
+ const cartSummary = (
162
+ <StorefrontCartSummary
163
+ buyerDefaults={buyerDefaults}
164
+ cartEntries={checkoutEntries}
165
+ checkoutHref={checkoutHref}
166
+ currency={currency}
167
+ isCheckout={false}
168
+ isPreview={isPreview}
169
+ isSubmitting={isSubmitting}
170
+ labels={labels}
171
+ onCheckoutOpen={handleCheckoutOpen}
172
+ onCheckoutSubmit={onCheckoutSubmit}
173
+ onInstantCheckout={mode === 'cart' ? onInstantCheckout : undefined}
174
+ radius={radius}
175
+ storefront={storefront}
176
+ total={total}
177
+ variant="panel"
178
+ />
179
+ );
180
+ const cartPopoverSummary = (
181
+ <StorefrontCartSummary
182
+ buyerDefaults={buyerDefaults}
183
+ cartEntries={checkoutEntries}
184
+ checkoutHref={checkoutHref}
185
+ currency={currency}
186
+ isPreview={isPreview}
187
+ isSubmitting={isSubmitting}
188
+ labels={labels}
189
+ onCheckoutOpen={handleCheckoutOpen}
190
+ onCheckoutSubmit={onCheckoutSubmit}
191
+ radius={radius}
192
+ storefront={storefront}
193
+ total={total}
194
+ variant="popover"
195
+ />
196
+ );
197
+ const checkoutDialogSummary = (
198
+ <StorefrontCartSummary
199
+ buyerDefaults={buyerDefaults}
200
+ cartEntries={checkoutEntries}
201
+ checkoutHref={checkoutHref}
202
+ currency={currency}
203
+ isCheckout
204
+ isPreview={isPreview}
205
+ isSubmitting={isSubmitting}
206
+ labels={labels}
207
+ onCheckoutSubmit={onCheckoutSubmit}
208
+ radius={radius}
209
+ storefront={storefront}
210
+ total={total}
211
+ variant="checkout"
212
+ />
213
+ );
102
214
  return (
103
215
  <main
104
216
  className={cn(
@@ -114,11 +226,21 @@ export function StorefrontSurface({
114
226
  </div>
115
227
  ) : null}
116
228
 
117
- <header className="border-border border-b bg-background/90 backdrop-blur">
229
+ <header className="sticky top-0 z-30 border-border border-b bg-background/80 backdrop-blur-md supports-[backdrop-filter]:bg-background/65">
118
230
  <div className="mx-auto flex max-w-7xl flex-wrap items-center justify-between gap-3 px-4 py-3">
119
231
  <div className="min-w-0">
120
232
  <h1 className="truncate font-semibold text-xl">
121
- {storefront.name}
233
+ {storefrontHref ? (
234
+ <a
235
+ className="block truncate rounded-sm transition hover:text-[var(--storefront-accent-text,var(--primary))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
236
+ href={storefrontHref}
237
+ title={storefront.name}
238
+ >
239
+ {storefront.name}
240
+ </a>
241
+ ) : (
242
+ storefront.name
243
+ )}
122
244
  </h1>
123
245
  {storefront.description ? (
124
246
  <p className="mt-0.5 line-clamp-1 text-muted-foreground text-sm">
@@ -128,151 +250,164 @@ export function StorefrontSurface({
128
250
  </div>
129
251
  <div className="flex items-center gap-2">
130
252
  {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
- )}
253
+ <StorefrontCartPopover
254
+ cartQuantity={cartQuantity}
255
+ labels={labels}
256
+ onOpenChange={setIsCartPopoverOpen}
257
+ open={isCartPopoverOpen}
258
+ radius={radius}
136
259
  >
137
- <ShoppingCart className="h-4 w-4" />
138
- {cartQuantity}
139
- </span>
260
+ {cartPopoverSummary}
261
+ </StorefrontCartPopover>
140
262
  </div>
141
263
  </div>
142
264
  </header>
143
265
 
144
266
  <section
145
267
  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]'
268
+ 'mx-auto max-w-7xl px-4 py-5',
269
+ compactLayout ? 'max-w-5xl' : null
148
270
  )}
149
271
  >
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
- />
272
+ {isCartPage ? (
273
+ <div className="mx-auto max-w-2xl">{cartSummary}</div>
274
+ ) : (
275
+ <div className="min-w-0">
276
+ {isProductDetail && selectedListing ? (
277
+ <>
278
+ {storefrontHref ? (
279
+ <a
280
+ className="mb-4 inline-flex items-center gap-1.5 text-muted-foreground text-sm transition hover:text-foreground"
281
+ href={storefrontHref}
282
+ >
283
+ <ArrowLeft aria-hidden className="size-4" />
284
+ {labels.browse}
285
+ </a>
286
+ ) : null}
287
+ <StorefrontProductDetail
288
+ cartHref={cartHref}
289
+ cartLines={cartLines}
290
+ currency={currency}
291
+ isSubmitting={isSubmitting}
292
+ labels={labels}
293
+ listing={selectedListing}
294
+ onBuyNow={onBuyNow}
295
+ onDecrement={onDecrement}
296
+ onIncrement={onIncrement}
297
+ quantity={
298
+ cartLines.find(
299
+ (item) => item.listingId === selectedListing.id
300
+ )?.quantity ?? 0
301
+ }
302
+ radius={radius}
303
+ showInventoryBadges={storefront.showInventoryBadges}
304
+ surfaceClassName={
305
+ storefrontSurfaceClasses[storefront.surfaceStyle]
306
+ }
307
+ />
308
+ </>
309
+ ) : (
310
+ <>
311
+ <StorefrontHeroPanel
312
+ currency={currency}
313
+ labels={labels}
314
+ listingsCount={listings.length}
315
+ radius={radius}
316
+ storefront={storefront}
317
+ />
158
318
 
159
- <StorefrontMerchSections
160
- radius={radius}
161
- sections={storefront.sections ?? []}
162
- />
319
+ <StorefrontMerchSections
320
+ radius={radius}
321
+ sections={storefront.sections ?? []}
322
+ />
163
323
 
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
- );
324
+ <div
325
+ className={cn(
326
+ 'mt-4 grid gap-4',
327
+ compactLayout
328
+ ? 'sm:grid-cols-2'
329
+ : 'sm:grid-cols-2 xl:grid-cols-3'
330
+ )}
331
+ >
332
+ {listingRows.length === 0 ? (
333
+ <StorefrontEmptyListings
334
+ action={emptyAction}
335
+ labels={labels}
336
+ radius={radius}
337
+ />
338
+ ) : (
339
+ listingRows.map((listing) => {
340
+ const line = cartLines.find(
341
+ (item) => item.listingId === listing.id
342
+ );
186
343
 
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
- })
344
+ return (
345
+ <StorefrontListingCard
346
+ currency={currency}
347
+ isList={false}
348
+ key={listing.id}
349
+ labels={labels}
350
+ listing={listing}
351
+ onDecrement={onDecrement}
352
+ onIncrement={onIncrement}
353
+ onOpenDetail={
354
+ onDetailListingChange
355
+ ? (id) => onDetailListingChange(id)
356
+ : undefined
357
+ }
358
+ quantity={line?.quantity ?? 0}
359
+ radius={radius}
360
+ showInventoryBadges={storefront.showInventoryBadges}
361
+ surfaceClassName={
362
+ storefrontSurfaceClasses[storefront.surfaceStyle]
363
+ }
364
+ />
365
+ );
366
+ })
367
+ )}
368
+ </div>
369
+ </>
205
370
  )}
206
371
  </div>
207
- </div>
208
-
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
- />
372
+ )}
222
373
  </section>
223
- </main>
224
- );
225
- }
226
-
227
- function StorefrontMerchSections({
228
- radius,
229
- sections,
230
- }: {
231
- radius: string;
232
- sections: InventoryStorefrontSection[];
233
- }) {
234
- const visibleSections = sections
235
- .filter((section) => section.status === 'published')
236
- .filter((section) => section.sectionType !== 'cover')
237
- .sort((a, b) => a.sortOrder - b.sortOrder);
238
374
 
239
- if (visibleSections.length === 0) return null;
375
+ <StorefrontProductDialog
376
+ cartHref={cartHref}
377
+ cartLines={cartLines}
378
+ currency={currency}
379
+ isSubmitting={isSubmitting}
380
+ labels={labels}
381
+ listing={detailListing ?? null}
382
+ onBuyNow={onBuyNow}
383
+ onDecrement={onDecrement}
384
+ onIncrement={onIncrement}
385
+ onOpenChange={(open) => {
386
+ if (!open) onDetailListingChange?.(null);
387
+ }}
388
+ radius={radius}
389
+ showInventoryBadges={storefront.showInventoryBadges}
390
+ surfaceClassName={storefrontSurfaceClasses[storefront.surfaceStyle]}
391
+ />
240
392
 
241
- return (
242
- <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}
393
+ <Dialog
394
+ onOpenChange={(open) => onCheckoutOpenChange?.(open)}
395
+ open={isCheckoutDialogOpen}
396
+ >
397
+ <DialogContent className="grid max-h-[92dvh] max-w-[min(42rem,calc(100vw-1rem))] grid-rows-[auto_minmax(0,1fr)] gap-0 overflow-hidden border-border/60 p-0 shadow-2xl sm:rounded-2xl">
398
+ <DialogHeader className="px-5 pt-5 pb-2 text-left sm:px-6">
399
+ <DialogTitle>{labels.checkout}</DialogTitle>
400
+ <DialogDescription>{labels.reservedCopy}</DialogDescription>
401
+ </DialogHeader>
402
+ <div className="min-h-0 overflow-y-auto px-5 pt-3 pb-5 sm:px-6">
403
+ {checkoutDialogSummary}
268
404
  </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
- ))}
276
- </div>
405
+ </DialogContent>
406
+ </Dialog>
407
+
408
+ {isSubmitting || isRedirecting ? (
409
+ <StorefrontCheckoutOverlay label={labels.redirectingToCheckout} />
410
+ ) : null}
411
+ </main>
277
412
  );
278
413
  }
@@ -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,13 +32,21 @@ 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
+ contactDetails: string;
29
41
  couponNote: string;
30
42
  demoBadge: string;
31
43
  emptyCart: string;
44
+ fromPrice: string;
45
+ instantCheckout: string;
46
+ orderSummary: string;
47
+ redirectingToCheckout: string;
48
+ selectOptions: string;
49
+ viewDetails: string;
32
50
  emptyListingsDescription: string;
33
51
  emptyListingsTitle: string;
34
52
  fallbackDescription: string;
@@ -56,13 +74,21 @@ export const defaultStorefrontSurfaceLabels: StorefrontSurfaceLabels = {
56
74
  available: 'available',
57
75
  browse: 'Browse',
58
76
  bundle: 'Bundle',
77
+ buyNow: 'Buy now',
59
78
  cart: 'Cart',
60
79
  checkout: 'Checkout',
61
80
  checkoutDisabled: 'Checkout is disabled in preview',
62
81
  checkoutDisabledBadge: 'Checkout disabled',
82
+ contactDetails: 'Contact details',
63
83
  couponNote: 'Have a coupon? You can apply it at checkout.',
64
84
  demoBadge: 'Demo',
65
85
  emptyCart: 'Add a listing to start checkout.',
86
+ fromPrice: 'From',
87
+ instantCheckout: 'Instant checkout',
88
+ orderSummary: 'Order summary',
89
+ redirectingToCheckout: 'Taking you to secure checkout…',
90
+ selectOptions: 'Select options',
91
+ viewDetails: 'View details',
66
92
  emptyListingsDescription:
67
93
  'Publish a listing to make this storefront ready for buyers.',
68
94
  emptyListingsTitle: 'No listings yet',