@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
@@ -0,0 +1,290 @@
1
+ 'use client';
2
+
3
+ import { ArrowRight, Tag, TriangleAlert, Zap } from '@tuturuuu/icons';
4
+ import { ChevronDownIcon } from '@tuturuuu/icons/lucide-static';
5
+ import { cn } from '@tuturuuu/utils/format';
6
+ import type { ReactNode } from 'react';
7
+ import { Button } from '../button';
8
+ import {
9
+ Collapsible,
10
+ CollapsibleContent,
11
+ CollapsibleTrigger,
12
+ } from '../collapsible';
13
+ import { AccentButton } from './accent-button';
14
+ import type {
15
+ StorefrontBuyerDefaults,
16
+ StorefrontCartEntry,
17
+ StorefrontSurfaceLabels,
18
+ } from './types';
19
+ import {
20
+ formatStorefrontPrice,
21
+ getStorefrontLinePrice,
22
+ getStorefrontVariantLabel,
23
+ storefrontCartLineKey,
24
+ } from './utils';
25
+
26
+ export function CartContents({
27
+ cartEntries,
28
+ currency,
29
+ hasCart,
30
+ isCheckoutDisabled,
31
+ labels,
32
+ total,
33
+ }: {
34
+ cartEntries: StorefrontCartEntry[];
35
+ currency: string;
36
+ hasCart: boolean;
37
+ isCheckoutDisabled: boolean;
38
+ labels: StorefrontSurfaceLabels;
39
+ total: number;
40
+ }) {
41
+ return (
42
+ <div className="grid gap-4">
43
+ {hasCart ? (
44
+ <CartLines cartEntries={cartEntries} currency={currency} />
45
+ ) : null}
46
+ <CartTotal currency={currency} labels={labels} total={total} />
47
+ {hasCart && !isCheckoutDisabled ? (
48
+ <p className="flex items-center gap-2 rounded-md border border-border border-dashed bg-muted/30 px-3 py-2 text-muted-foreground text-xs leading-5">
49
+ <Tag className="h-3.5 w-3.5 shrink-0" />
50
+ {labels.couponNote}
51
+ </p>
52
+ ) : null}
53
+ {!hasCart ? (
54
+ <p className="flex items-center gap-2 rounded-md border border-border bg-muted/40 px-3 py-2 text-muted-foreground text-sm">
55
+ <TriangleAlert className="h-4 w-4 shrink-0" />
56
+ {labels.emptyCart}
57
+ </p>
58
+ ) : null}
59
+ </div>
60
+ );
61
+ }
62
+
63
+ export function CartActions({
64
+ canOpenCheckout,
65
+ checkoutHref,
66
+ isCheckoutDisabled,
67
+ isPreview,
68
+ isSubmitting,
69
+ labels,
70
+ onCheckoutOpen,
71
+ onInstantCheckout,
72
+ radius,
73
+ }: {
74
+ canOpenCheckout: boolean;
75
+ checkoutHref?: string;
76
+ isCheckoutDisabled: boolean;
77
+ isPreview: boolean;
78
+ isSubmitting: boolean;
79
+ labels: StorefrontSurfaceLabels;
80
+ onCheckoutOpen?: () => void;
81
+ onInstantCheckout?: () => void;
82
+ radius: string;
83
+ }) {
84
+ if (isPreview || isCheckoutDisabled) {
85
+ return (
86
+ <Button className={cn('w-full', radius)} disabled type="button">
87
+ {labels.checkoutDisabled}
88
+ </Button>
89
+ );
90
+ }
91
+
92
+ if (!canOpenCheckout) {
93
+ return (
94
+ <Button className={cn('w-full', radius)} disabled type="button">
95
+ {labels.checkout}
96
+ <ArrowRight className="size-4 shrink-0" />
97
+ </Button>
98
+ );
99
+ }
100
+
101
+ return (
102
+ <div className="grid gap-2">
103
+ {onInstantCheckout ? (
104
+ <AccentButton
105
+ disabled={isSubmitting}
106
+ onClick={onInstantCheckout}
107
+ radius={radius}
108
+ >
109
+ <Zap className="size-4 shrink-0" />
110
+ {isSubmitting ? labels.reserving : labels.instantCheckout}
111
+ </AccentButton>
112
+ ) : null}
113
+ {onCheckoutOpen ? (
114
+ <AccentButton
115
+ disabled={isSubmitting}
116
+ onClick={onCheckoutOpen}
117
+ radius={radius}
118
+ >
119
+ {labels.checkout}
120
+ <ArrowRight className="size-4 shrink-0" />
121
+ </AccentButton>
122
+ ) : (
123
+ <Button asChild className={cn('w-full', radius)} variant="outline">
124
+ <a href={checkoutHref}>
125
+ {labels.checkout}
126
+ <ArrowRight className="size-4 shrink-0" />
127
+ </a>
128
+ </Button>
129
+ )}
130
+ </div>
131
+ );
132
+ }
133
+
134
+ export function CheckoutSection({
135
+ children,
136
+ defaultOpen,
137
+ meta,
138
+ title,
139
+ }: {
140
+ children: ReactNode;
141
+ defaultOpen?: boolean;
142
+ meta?: string;
143
+ title: string;
144
+ }) {
145
+ return (
146
+ <Collapsible defaultOpen={defaultOpen}>
147
+ <div className="rounded-lg bg-muted/25 px-3 py-2">
148
+ <CollapsibleTrigger
149
+ className="group flex w-full items-center gap-3 text-left"
150
+ type="button"
151
+ >
152
+ <span className="min-w-0 flex-1 font-semibold text-sm">{title}</span>
153
+ {meta ? (
154
+ <span className="max-w-[9rem] overflow-hidden text-ellipsis whitespace-nowrap text-right font-semibold text-sm tabular-nums">
155
+ {meta}
156
+ </span>
157
+ ) : null}
158
+ <ChevronDownIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
159
+ </CollapsibleTrigger>
160
+ <CollapsibleContent className="pt-4">{children}</CollapsibleContent>
161
+ </div>
162
+ </Collapsible>
163
+ );
164
+ }
165
+
166
+ export function CheckoutContactFields({
167
+ buyerDefaults,
168
+ labels,
169
+ }: {
170
+ buyerDefaults?: StorefrontBuyerDefaults;
171
+ labels: StorefrontSurfaceLabels;
172
+ }) {
173
+ const buyerEmail = buyerDefaults?.email?.trim() || undefined;
174
+ const buyerName = buyerDefaults?.name?.trim() || undefined;
175
+ const inputClassName =
176
+ 'h-11 rounded-md border border-input bg-background px-3 text-sm outline-none transition focus-visible:ring-2 focus-visible:ring-ring/40';
177
+ const labelClassName = 'grid gap-1.5 text-sm';
178
+
179
+ return (
180
+ <div className="grid gap-3">
181
+ <label className={labelClassName}>
182
+ <span className="font-medium text-xs">{labels.form.name}</span>
183
+ <input
184
+ autoComplete="name"
185
+ className={inputClassName}
186
+ defaultValue={buyerName}
187
+ name="customerName"
188
+ placeholder={labels.form.name}
189
+ required
190
+ />
191
+ </label>
192
+ <label className={labelClassName}>
193
+ <span className="font-medium text-xs">{labels.form.email}</span>
194
+ <input
195
+ autoComplete="email"
196
+ className={inputClassName}
197
+ defaultValue={buyerEmail}
198
+ name="customerEmail"
199
+ placeholder={labels.form.email}
200
+ required
201
+ type="email"
202
+ />
203
+ </label>
204
+ <label className={labelClassName}>
205
+ <span className="font-medium text-xs">{labels.form.phone}</span>
206
+ <input
207
+ autoComplete="tel"
208
+ className={inputClassName}
209
+ name="customerPhone"
210
+ placeholder={labels.form.phone}
211
+ type="tel"
212
+ />
213
+ </label>
214
+ <textarea
215
+ className="min-h-24 rounded-md border border-input bg-background px-3 py-2.5 text-sm outline-none transition focus-visible:ring-2 focus-visible:ring-ring/40"
216
+ name="note"
217
+ placeholder={labels.form.note}
218
+ />
219
+ </div>
220
+ );
221
+ }
222
+
223
+ function CartLines({
224
+ cartEntries,
225
+ currency,
226
+ }: {
227
+ cartEntries: StorefrontCartEntry[];
228
+ currency: string;
229
+ }) {
230
+ return (
231
+ <div className="-mr-1 grid max-h-72 gap-3 overflow-y-auto pr-1">
232
+ {cartEntries.map(({ line, listing, variant }) => {
233
+ const unitPrice = getStorefrontLinePrice(listing, variant);
234
+ const variantLabel = variant
235
+ ? getStorefrontVariantLabel(variant)
236
+ : null;
237
+ const lineTotal = formatStorefrontPrice(
238
+ unitPrice * line.quantity,
239
+ currency
240
+ );
241
+
242
+ return (
243
+ <div
244
+ className="grid grid-cols-[minmax(0,1fr)_auto] gap-x-3 gap-y-1 text-sm"
245
+ key={storefrontCartLineKey(line.listingId, line.variantId)}
246
+ >
247
+ <div className="min-w-0">
248
+ <p className="line-clamp-2 break-words font-medium leading-5">
249
+ {listing.title}
250
+ </p>
251
+ {variantLabel ? (
252
+ <p className="line-clamp-1 break-words text-muted-foreground text-xs">
253
+ {variantLabel}
254
+ </p>
255
+ ) : null}
256
+ <p className="text-muted-foreground text-xs tabular-nums">
257
+ {line.quantity} × {formatStorefrontPrice(unitPrice, currency)}
258
+ </p>
259
+ </div>
260
+ <span
261
+ className="max-w-[9rem] justify-self-end overflow-hidden text-ellipsis whitespace-nowrap text-right font-medium tabular-nums leading-5 sm:max-w-[11rem]"
262
+ title={lineTotal}
263
+ >
264
+ {lineTotal}
265
+ </span>
266
+ </div>
267
+ );
268
+ })}
269
+ </div>
270
+ );
271
+ }
272
+
273
+ function CartTotal({
274
+ currency,
275
+ labels,
276
+ total,
277
+ }: {
278
+ currency: string;
279
+ labels: StorefrontSurfaceLabels;
280
+ total: number;
281
+ }) {
282
+ return (
283
+ <div className="flex items-center justify-between gap-3 border-border border-t pt-4">
284
+ <span className="text-muted-foreground text-sm">{labels.total}</span>
285
+ <span className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-right font-semibold tabular-nums">
286
+ {formatStorefrontPrice(total, currency)}
287
+ </span>
288
+ </div>
289
+ );
290
+ }
@@ -1,131 +1,155 @@
1
1
  'use client';
2
2
 
3
- import { ArrowRight, Tag, TriangleAlert } from '@tuturuuu/icons';
3
+ import { ArrowRight } from '@tuturuuu/icons';
4
4
  import type { InventoryStorefront } from '@tuturuuu/internal-api/inventory';
5
5
  import { cn } from '@tuturuuu/utils/format';
6
6
  import type { FormEvent } from 'react';
7
7
  import { Badge } from '../badge';
8
- import { Button } from '../button';
9
8
  import { AccentButton } from './accent-button';
10
- import type { StorefrontCartEntry, StorefrontSurfaceLabels } from './types';
9
+ import {
10
+ CartActions,
11
+ CartContents,
12
+ CheckoutContactFields,
13
+ CheckoutSection,
14
+ } from './cart-summary-parts';
15
+ import type {
16
+ StorefrontBuyerDefaults,
17
+ StorefrontCartEntry,
18
+ StorefrontSurfaceLabels,
19
+ } from './types';
11
20
  import { formatStorefrontPrice, storefrontSurfaceClasses } from './utils';
12
21
 
22
+ type StorefrontCartSummaryVariant = 'checkout' | 'panel' | 'popover';
23
+
13
24
  export function StorefrontCartSummary({
25
+ buyerDefaults,
14
26
  cartEntries,
15
27
  checkoutHref,
28
+ className,
16
29
  currency,
17
30
  isCheckout,
18
31
  isPreview,
19
32
  isSubmitting,
20
33
  labels,
34
+ onCheckoutOpen,
21
35
  onCheckoutSubmit,
36
+ onInstantCheckout,
22
37
  radius,
23
38
  storefront,
24
39
  total,
40
+ variant,
25
41
  }: {
42
+ buyerDefaults?: StorefrontBuyerDefaults;
26
43
  cartEntries: StorefrontCartEntry[];
27
44
  checkoutHref?: string;
45
+ className?: string;
28
46
  currency: string;
29
- isCheckout: boolean;
47
+ isCheckout?: boolean;
30
48
  isPreview: boolean;
31
49
  isSubmitting: boolean;
32
50
  labels: StorefrontSurfaceLabels;
51
+ onCheckoutOpen?: () => void;
33
52
  onCheckoutSubmit?: (formData: FormData) => void;
53
+ onInstantCheckout?: () => void;
34
54
  radius: string;
35
55
  storefront: InventoryStorefront;
36
56
  total: number;
57
+ variant?: StorefrontCartSummaryVariant;
37
58
  }) {
59
+ const presentation = variant ?? (isCheckout ? 'checkout' : 'panel');
38
60
  const hasCart = cartEntries.length > 0;
39
61
  const isCheckoutDisabled = storefront.checkoutMode === 'disabled';
40
62
  const submitDisabled =
41
63
  !hasCart || isSubmitting || isCheckoutDisabled || !onCheckoutSubmit;
42
- const canOpenCheckout = hasCart && Boolean(checkoutHref);
64
+ const canOpenCheckout = hasCart && Boolean(checkoutHref || onCheckoutOpen);
43
65
 
44
- return (
45
- <aside
46
- className={cn(
47
- 'h-fit p-4 lg:sticky lg:top-4',
48
- storefrontSurfaceClasses[storefront.surfaceStyle],
49
- radius
50
- )}
51
- >
52
- <div className="flex items-center justify-between gap-3">
53
- <p className="font-semibold">{labels.cart}</p>
54
- <Badge className="border-border bg-background" variant="outline">
55
- {cartEntries.length}
56
- </Badge>
57
- </div>
58
- <p className="mt-2 text-muted-foreground text-sm leading-6">
59
- {labels.reservedCopy}
60
- </p>
61
- <div className="mt-4 grid gap-2">
62
- {cartEntries.map(({ line, listing }) => (
63
- <div
64
- className="flex items-center justify-between gap-3 text-sm"
65
- key={line.listingId}
66
- >
67
- <span className="min-w-0 truncate">
68
- {line.quantity} x {listing.title}
69
- </span>
70
- <span className="font-medium">
71
- {formatStorefrontPrice(listing.price * line.quantity, currency)}
72
- </span>
73
- </div>
74
- ))}
75
- </div>
76
- <div className="mt-4 flex items-center justify-between border-border border-t pt-4">
77
- <span className="text-muted-foreground text-sm">{labels.total}</span>
78
- <span className="font-semibold">
79
- {formatStorefrontPrice(total, currency)}
80
- </span>
81
- </div>
82
- {hasCart && !isCheckoutDisabled ? (
83
- <p className="mt-3 flex items-center gap-2 rounded-md border border-border border-dashed bg-muted/30 px-3 py-2 text-muted-foreground text-xs leading-5">
84
- <Tag className="h-3.5 w-3.5 shrink-0" />
85
- {labels.couponNote}
86
- </p>
87
- ) : null}
88
- {!hasCart ? (
89
- <p className="mt-4 flex items-center gap-2 rounded-md border border-border bg-muted/40 px-3 py-2 text-muted-foreground text-sm">
90
- <TriangleAlert className="h-4 w-4" />
91
- {labels.emptyCart}
92
- </p>
93
- ) : null}
94
- {isCheckout ? (
66
+ if (presentation === 'checkout') {
67
+ return (
68
+ <section
69
+ aria-label={labels.checkout}
70
+ className={cn('grid gap-5', className)}
71
+ >
95
72
  <form
96
- className="mt-4 grid gap-2"
73
+ className="grid gap-5"
97
74
  onSubmit={(event: FormEvent<HTMLFormElement>) => {
98
75
  event.preventDefault();
99
76
  onCheckoutSubmit?.(new FormData(event.currentTarget));
100
77
  }}
101
78
  >
102
- <textarea
103
- className="min-h-20 rounded-md border border-input bg-background px-3 py-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
104
- name="note"
105
- placeholder={labels.form.note}
106
- />
79
+ <CheckoutSection
80
+ defaultOpen={cartEntries.length > 1}
81
+ meta={formatStorefrontPrice(total, currency)}
82
+ title={labels.orderSummary}
83
+ >
84
+ <CartContents
85
+ cartEntries={cartEntries}
86
+ currency={currency}
87
+ hasCart={hasCart}
88
+ isCheckoutDisabled={isCheckoutDisabled}
89
+ labels={labels}
90
+ total={total}
91
+ />
92
+ </CheckoutSection>
93
+
94
+ <CheckoutSection defaultOpen title={labels.contactDetails}>
95
+ <CheckoutContactFields
96
+ buyerDefaults={buyerDefaults}
97
+ labels={labels}
98
+ />
99
+ </CheckoutSection>
100
+
107
101
  <AccentButton disabled={submitDisabled} radius={radius}>
108
102
  {isSubmitting ? labels.reserving : labels.reserve}
109
- <ArrowRight className="h-4 w-4" />
103
+ <ArrowRight className="size-4 shrink-0" />
110
104
  </AccentButton>
111
105
  </form>
112
- ) : isPreview || isCheckoutDisabled ? (
113
- <Button className={cn('mt-4 w-full', radius)} disabled type="button">
114
- {labels.checkoutDisabled}
115
- </Button>
116
- ) : canOpenCheckout ? (
117
- <Button asChild className={cn('mt-4 w-full', radius)}>
118
- <a href={checkoutHref}>
119
- {labels.checkout}
120
- <ArrowRight className="h-4 w-4" />
121
- </a>
122
- </Button>
123
- ) : (
124
- <Button className={cn('mt-4 w-full', radius)} disabled type="button">
125
- {labels.checkout}
126
- <ArrowRight className="h-4 w-4" />
127
- </Button>
106
+ </section>
107
+ );
108
+ }
109
+
110
+ return (
111
+ <section
112
+ aria-label={labels.cart}
113
+ className={cn(
114
+ 'h-fit',
115
+ presentation === 'panel'
116
+ ? cn('p-4', storefrontSurfaceClasses[storefront.surfaceStyle], radius)
117
+ : 'grid gap-4',
118
+ className
128
119
  )}
129
- </aside>
120
+ >
121
+ <div>
122
+ <div className="flex items-center justify-between gap-3">
123
+ <p className="font-semibold">{labels.cart}</p>
124
+ <Badge className="border-border bg-background" variant="outline">
125
+ {cartEntries.length}
126
+ </Badge>
127
+ </div>
128
+ <p className="mt-2 text-muted-foreground text-sm leading-6">
129
+ {labels.reservedCopy}
130
+ </p>
131
+ </div>
132
+
133
+ <CartContents
134
+ cartEntries={cartEntries}
135
+ currency={currency}
136
+ hasCart={hasCart}
137
+ isCheckoutDisabled={isCheckoutDisabled}
138
+ labels={labels}
139
+ total={total}
140
+ />
141
+
142
+ <CartActions
143
+ canOpenCheckout={canOpenCheckout}
144
+ checkoutHref={checkoutHref}
145
+ isCheckoutDisabled={isCheckoutDisabled}
146
+ isPreview={isPreview}
147
+ isSubmitting={isSubmitting}
148
+ labels={labels}
149
+ onCheckoutOpen={onCheckoutOpen}
150
+ onInstantCheckout={onInstantCheckout}
151
+ radius={radius}
152
+ />
153
+ </section>
130
154
  );
131
155
  }
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+
3
+ import { Loader2 } from '@tuturuuu/icons';
4
+
5
+ /**
6
+ * Full-screen blocking overlay shown while the checkout session or embedded
7
+ * payment intent is opening, so the buyer sees progress instead of a frozen
8
+ * button.
9
+ */
10
+ export function StorefrontCheckoutOverlay({ label }: { label: string }) {
11
+ return (
12
+ <div
13
+ aria-busy="true"
14
+ aria-live="assertive"
15
+ className="fixed inset-0 z-[100] grid place-items-center bg-background/80 backdrop-blur-sm"
16
+ role="status"
17
+ >
18
+ <div className="flex flex-col items-center gap-4 px-6 text-center">
19
+ <span className="grid size-14 place-items-center rounded-full bg-[var(--storefront-accent-soft,var(--muted))] text-[var(--storefront-accent-text,var(--primary))]">
20
+ <Loader2 className="size-7 animate-spin" />
21
+ </span>
22
+ <p className="font-medium text-sm">{label}</p>
23
+ </div>
24
+ </div>
25
+ );
26
+ }
@@ -36,6 +36,7 @@ export function StorefrontHeroPanel({
36
36
  className="absolute inset-0 h-full w-full"
37
37
  imageUrl={heroImage}
38
38
  label={storefront.name}
39
+ priority
39
40
  />
40
41
  ) : (
41
42
  <div className="absolute inset-0 bg-gradient-to-br from-muted via-muted/60 to-background" />
@@ -51,14 +52,7 @@ export function StorefrontHeroPanel({
51
52
  <Store className="h-4 w-4" />
52
53
  <span>{labels.browse}</span>
53
54
  </div>
54
- <h2
55
- className={cn(
56
- 'mt-2 text-balance font-semibold tracking-normal',
57
- storefront.themePreset === 'editorial'
58
- ? 'text-3xl md:text-4xl'
59
- : 'text-2xl'
60
- )}
61
- >
55
+ <h2 className="mt-2 text-balance font-semibold text-3xl tracking-tight md:text-4xl">
62
56
  {storefront.name}
63
57
  </h2>
64
58
  <p className="mt-2 max-w-2xl text-muted-foreground text-sm leading-6">
@@ -6,10 +6,13 @@ export function StorefrontImagePanel({
6
6
  className,
7
7
  imageUrl,
8
8
  label,
9
+ priority = false,
9
10
  }: {
10
11
  className?: string;
11
12
  imageUrl: string | null;
12
13
  label: string;
14
+ /** Eager-load above-the-fold images (e.g. the hero) to protect LCP. */
15
+ priority?: boolean;
13
16
  }) {
14
17
  if (imageUrl) {
15
18
  return (
@@ -17,6 +20,9 @@ export function StorefrontImagePanel({
17
20
  <img
18
21
  alt=""
19
22
  className={cn('w-full object-cover', className)}
23
+ decoding="async"
24
+ fetchPriority={priority ? 'high' : 'auto'}
25
+ loading={priority ? 'eager' : 'lazy'}
20
26
  src={imageUrl}
21
27
  />
22
28
  );
@@ -1,5 +1,9 @@
1
+ export { StorefrontCheckoutOverlay } from './checkout-overlay';
2
+ export { StorefrontProductDetail } from './product-detail';
3
+ export { StorefrontProductDialog } from './product-dialog';
1
4
  export { StorefrontSurface } from './storefront-surface';
2
5
  export type {
6
+ StorefrontBuyerDefaults,
3
7
  StorefrontCartEntry,
4
8
  StorefrontCartLine,
5
9
  StorefrontSurfaceLabels,
@@ -7,6 +11,13 @@ export type {
7
11
  } from './types';
8
12
  export {
9
13
  formatStorefrontPrice,
14
+ getStorefrontLinePrice,
15
+ getStorefrontListingFromPrice,
10
16
  getStorefrontListingLimit,
17
+ getStorefrontListingVariants,
18
+ getStorefrontVariantLabel,
19
+ getStorefrontVariantLimit,
20
+ listingHasVariants,
11
21
  sanitizeStorefrontAccentColor,
22
+ storefrontCartLineKey,
12
23
  } from './utils';