@tuturuuu/ui 0.8.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 (182) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/biome.json +1 -1
  3. package/package.json +73 -71
  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/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
  29. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +19 -0
  30. package/src/components/ui/custom/combobox.test.tsx +195 -0
  31. package/src/components/ui/custom/combobox.tsx +273 -156
  32. package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
  33. package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
  34. package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
  35. package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
  36. package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
  37. package/src/components/ui/custom/settings-dialog-shell.tsx +2 -1
  38. package/src/components/ui/custom/theme-toggle.tsx +1 -1
  39. package/src/components/ui/custom/workspace-select.tsx +8 -3
  40. package/src/components/ui/dialog.test.tsx +52 -0
  41. package/src/components/ui/dialog.tsx +6 -2
  42. package/src/components/ui/dropdown-menu.tsx +5 -1
  43. package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
  44. package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
  45. package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
  46. package/src/components/ui/finance/debts/debts-page.tsx +15 -2
  47. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
  48. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
  49. package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
  50. package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
  51. package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
  52. package/src/components/ui/finance/invoices/utils.ts +3 -1
  53. package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
  54. package/src/components/ui/finance/transactions/form-types.ts +1 -0
  55. package/src/components/ui/finance/transactions/form.tsx +2 -0
  56. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
  57. package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
  58. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
  59. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
  60. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
  61. package/src/components/ui/finance/wallets/form.test.tsx +51 -3
  62. package/src/components/ui/finance/wallets/form.tsx +15 -4
  63. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  64. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
  65. package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
  66. package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
  67. package/src/components/ui/input-otp.tsx +1 -1
  68. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
  69. package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
  70. package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
  71. package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
  72. package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
  73. package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
  74. package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
  75. package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
  76. package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
  77. package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
  78. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
  79. package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
  80. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
  81. package/src/components/ui/navigation-menu.tsx +1 -1
  82. package/src/components/ui/pagination.tsx +1 -1
  83. package/src/components/ui/radio-group.tsx +1 -1
  84. package/src/components/ui/select.tsx +5 -1
  85. package/src/components/ui/sheet.tsx +1 -1
  86. package/src/components/ui/sidebar.tsx +1 -1
  87. package/src/components/ui/storefront/cart-popover.tsx +61 -0
  88. package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
  89. package/src/components/ui/storefront/cart-summary.tsx +93 -154
  90. package/src/components/ui/storefront/checkout-overlay.tsx +4 -5
  91. package/src/components/ui/storefront/listing-card.tsx +1 -1
  92. package/src/components/ui/storefront/merch-sections.tsx +70 -0
  93. package/src/components/ui/storefront/product-detail.tsx +1 -1
  94. package/src/components/ui/storefront/storefront-surface.test.tsx +106 -11
  95. package/src/components/ui/storefront/storefront-surface.tsx +101 -166
  96. package/src/components/ui/storefront/types.ts +4 -0
  97. package/src/components/ui/storefront/utils.ts +6 -0
  98. package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
  99. package/src/components/ui/text-editor/background-color-extension.ts +62 -0
  100. package/src/components/ui/text-editor/color-controls.tsx +284 -0
  101. package/src/components/ui/text-editor/editor.tsx +69 -14
  102. package/src/components/ui/text-editor/extensions.ts +8 -2
  103. package/src/components/ui/text-editor/highlight-extension.ts +22 -0
  104. package/src/components/ui/text-editor/tool-bar.tsx +9 -16
  105. package/src/components/ui/toast.tsx +1 -1
  106. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +270 -0
  107. package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
  108. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +222 -109
  109. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +112 -43
  110. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
  111. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
  112. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
  113. package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
  114. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
  115. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
  116. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +38 -9
  117. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
  118. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
  119. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +81 -30
  120. package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
  121. package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
  122. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
  123. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
  124. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
  125. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
  126. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
  127. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
  128. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
  129. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
  130. package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
  131. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +397 -2
  132. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +103 -13
  133. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +443 -19
  134. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +94 -32
  135. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +213 -106
  136. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +26 -4
  137. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +5 -2
  138. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +3 -0
  139. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
  140. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +191 -28
  141. package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
  142. package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
  143. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +7 -0
  144. package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
  145. package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
  146. package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
  147. package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
  148. package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
  149. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
  150. package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
  151. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +25 -0
  152. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +341 -38
  153. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +253 -0
  154. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +203 -2
  155. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +17 -0
  156. package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
  157. package/src/components/ui/tu-do/shared/board-client.tsx +2 -7
  158. package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
  159. package/src/components/ui/tu-do/shared/board-header.tsx +464 -975
  160. package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
  161. package/src/components/ui/tu-do/shared/board-switcher.tsx +209 -217
  162. package/src/components/ui/tu-do/shared/board-views.tsx +587 -75
  163. package/src/components/ui/tu-do/shared/list-view.tsx +227 -1
  164. package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
  165. package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
  166. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
  167. package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
  168. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
  169. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
  170. package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
  171. package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
  172. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +2 -1
  173. package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
  174. package/src/declarations.d.ts +1 -0
  175. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
  176. package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
  177. package/src/hooks/use-calendar-sync.tsx +247 -243
  178. package/src/hooks/use-calendar.tsx +323 -138
  179. package/src/hooks/use-task-actions.ts +24 -0
  180. package/src/hooks/use-user-workspace-config.ts +75 -0
  181. package/src/hooks/use-workspace-currency.ts +8 -3
  182. package/src/hooks/useBoardRealtimeEventHandler.ts +11 -0
@@ -1,216 +1,155 @@
1
1
  'use client';
2
2
 
3
- import { ArrowRight, Tag, TriangleAlert, Zap } 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 { StorefrontImagePanel } from './image-panel';
9
+ import {
10
+ CartActions,
11
+ CartContents,
12
+ CheckoutContactFields,
13
+ CheckoutSection,
14
+ } from './cart-summary-parts';
11
15
  import type {
12
16
  StorefrontBuyerDefaults,
13
17
  StorefrontCartEntry,
14
18
  StorefrontSurfaceLabels,
15
19
  } from './types';
16
- import {
17
- formatStorefrontPrice,
18
- getStorefrontLinePrice,
19
- getStorefrontVariantLabel,
20
- storefrontCartLineKey,
21
- storefrontSurfaceClasses,
22
- } from './utils';
20
+ import { formatStorefrontPrice, storefrontSurfaceClasses } from './utils';
21
+
22
+ type StorefrontCartSummaryVariant = 'checkout' | 'panel' | 'popover';
23
23
 
24
24
  export function StorefrontCartSummary({
25
25
  buyerDefaults,
26
26
  cartEntries,
27
27
  checkoutHref,
28
+ className,
28
29
  currency,
29
30
  isCheckout,
30
31
  isPreview,
31
32
  isSubmitting,
32
33
  labels,
34
+ onCheckoutOpen,
33
35
  onCheckoutSubmit,
34
36
  onInstantCheckout,
35
37
  radius,
36
38
  storefront,
37
39
  total,
40
+ variant,
38
41
  }: {
39
42
  buyerDefaults?: StorefrontBuyerDefaults;
40
43
  cartEntries: StorefrontCartEntry[];
41
44
  checkoutHref?: string;
45
+ className?: string;
42
46
  currency: string;
43
- isCheckout: boolean;
47
+ isCheckout?: boolean;
44
48
  isPreview: boolean;
45
49
  isSubmitting: boolean;
46
50
  labels: StorefrontSurfaceLabels;
51
+ onCheckoutOpen?: () => void;
47
52
  onCheckoutSubmit?: (formData: FormData) => void;
48
53
  onInstantCheckout?: () => void;
49
54
  radius: string;
50
55
  storefront: InventoryStorefront;
51
56
  total: number;
57
+ variant?: StorefrontCartSummaryVariant;
52
58
  }) {
59
+ const presentation = variant ?? (isCheckout ? 'checkout' : 'panel');
53
60
  const hasCart = cartEntries.length > 0;
54
61
  const isCheckoutDisabled = storefront.checkoutMode === 'disabled';
55
62
  const submitDisabled =
56
63
  !hasCart || isSubmitting || isCheckoutDisabled || !onCheckoutSubmit;
57
- const canOpenCheckout = hasCart && Boolean(checkoutHref);
58
- const buyerEmail = buyerDefaults?.email?.trim() || undefined;
59
- const buyerName = buyerDefaults?.name?.trim() || undefined;
60
- const inputClassName =
61
- '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';
62
- const labelClassName = 'grid gap-1.5 text-sm';
64
+ const canOpenCheckout = hasCart && Boolean(checkoutHref || onCheckoutOpen);
63
65
 
64
- return (
65
- <aside
66
- className={cn(
67
- 'h-fit p-4 lg:sticky lg:top-4',
68
- isCheckout ? 'p-5 sm:p-6' : null,
69
- storefrontSurfaceClasses[storefront.surfaceStyle],
70
- radius
71
- )}
72
- >
73
- <div className="flex items-center justify-between gap-3">
74
- <p className="font-semibold">{labels.cart}</p>
75
- <Badge className="border-border bg-background" variant="outline">
76
- {cartEntries.length}
77
- </Badge>
78
- </div>
79
- <p className="mt-2 text-muted-foreground text-sm leading-6">
80
- {labels.reservedCopy}
81
- </p>
82
- <div className="mt-4 -mr-1 grid max-h-72 gap-2.5 overflow-y-auto pr-1">
83
- {cartEntries.map(({ line, listing, variant }) => {
84
- const unitPrice = getStorefrontLinePrice(listing, variant);
85
- const variantLabel = variant
86
- ? getStorefrontVariantLabel(variant)
87
- : null;
88
- return (
89
- <div
90
- className="flex items-center gap-3 text-sm"
91
- key={storefrontCartLineKey(line.listingId, line.variantId)}
92
- >
93
- <StorefrontImagePanel
94
- className={cn('size-10 shrink-0 rounded-md', radius)}
95
- imageUrl={variant?.imageUrl ?? listing.imageUrl}
96
- label={listing.title}
97
- />
98
- <div className="min-w-0 flex-1">
99
- <p className="truncate font-medium">{listing.title}</p>
100
- {variantLabel ? (
101
- <p className="truncate text-muted-foreground text-xs">
102
- {variantLabel}
103
- </p>
104
- ) : null}
105
- <p className="truncate text-muted-foreground text-xs tabular-nums">
106
- {line.quantity} × {formatStorefrontPrice(unitPrice, currency)}
107
- </p>
108
- </div>
109
- <span className="shrink-0 whitespace-nowrap font-medium tabular-nums">
110
- {formatStorefrontPrice(unitPrice * line.quantity, currency)}
111
- </span>
112
- </div>
113
- );
114
- })}
115
- </div>
116
- <div className="mt-4 flex items-center justify-between gap-2 border-border border-t pt-4">
117
- <span className="text-muted-foreground text-sm">{labels.total}</span>
118
- <span className="shrink-0 whitespace-nowrap font-semibold tabular-nums">
119
- {formatStorefrontPrice(total, currency)}
120
- </span>
121
- </div>
122
- {hasCart && !isCheckoutDisabled ? (
123
- <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">
124
- <Tag className="h-3.5 w-3.5 shrink-0" />
125
- {labels.couponNote}
126
- </p>
127
- ) : null}
128
- {!hasCart ? (
129
- <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">
130
- <TriangleAlert className="h-4 w-4" />
131
- {labels.emptyCart}
132
- </p>
133
- ) : null}
134
- {isCheckout ? (
66
+ if (presentation === 'checkout') {
67
+ return (
68
+ <section
69
+ aria-label={labels.checkout}
70
+ className={cn('grid gap-5', className)}
71
+ >
135
72
  <form
136
- className="mt-5 grid gap-3"
73
+ className="grid gap-5"
137
74
  onSubmit={(event: FormEvent<HTMLFormElement>) => {
138
75
  event.preventDefault();
139
76
  onCheckoutSubmit?.(new FormData(event.currentTarget));
140
77
  }}
141
78
  >
142
- <label className={labelClassName}>
143
- <span className="font-medium text-xs">{labels.form.name}</span>
144
- <input
145
- autoComplete="name"
146
- className={inputClassName}
147
- defaultValue={buyerName}
148
- name="customerName"
149
- placeholder={labels.form.name}
150
- required
151
- />
152
- </label>
153
- <label className={labelClassName}>
154
- <span className="font-medium text-xs">{labels.form.email}</span>
155
- <input
156
- autoComplete="email"
157
- className={inputClassName}
158
- defaultValue={buyerEmail}
159
- name="customerEmail"
160
- placeholder={labels.form.email}
161
- required
162
- type="email"
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}
163
91
  />
164
- </label>
165
- <label className={labelClassName}>
166
- <span className="font-medium text-xs">{labels.form.phone}</span>
167
- <input
168
- autoComplete="tel"
169
- className={inputClassName}
170
- name="customerPhone"
171
- placeholder={labels.form.phone}
172
- type="tel"
92
+ </CheckoutSection>
93
+
94
+ <CheckoutSection defaultOpen title={labels.contactDetails}>
95
+ <CheckoutContactFields
96
+ buyerDefaults={buyerDefaults}
97
+ labels={labels}
173
98
  />
174
- </label>
175
- <textarea
176
- 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"
177
- name="note"
178
- placeholder={labels.form.note}
179
- />
99
+ </CheckoutSection>
100
+
180
101
  <AccentButton disabled={submitDisabled} radius={radius}>
181
102
  {isSubmitting ? labels.reserving : labels.reserve}
182
103
  <ArrowRight className="size-4 shrink-0" />
183
104
  </AccentButton>
184
105
  </form>
185
- ) : isPreview || isCheckoutDisabled ? (
186
- <Button className={cn('mt-4 w-full', radius)} disabled type="button">
187
- {labels.checkoutDisabled}
188
- </Button>
189
- ) : canOpenCheckout ? (
190
- <div className="mt-4 grid gap-2">
191
- {onInstantCheckout ? (
192
- <AccentButton
193
- disabled={isSubmitting}
194
- onClick={onInstantCheckout}
195
- radius={radius}
196
- >
197
- <Zap className="size-4 shrink-0" />
198
- {isSubmitting ? labels.reserving : labels.instantCheckout}
199
- </AccentButton>
200
- ) : null}
201
- <Button asChild className={cn('w-full', radius)} variant="outline">
202
- <a href={checkoutHref}>
203
- {labels.checkout}
204
- <ArrowRight className="size-4 shrink-0" />
205
- </a>
206
- </Button>
207
- </div>
208
- ) : (
209
- <Button className={cn('mt-4 w-full', radius)} disabled type="button">
210
- {labels.checkout}
211
- <ArrowRight className="size-4 shrink-0" />
212
- </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
213
119
  )}
214
- </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>
215
154
  );
216
155
  }
@@ -3,10 +3,9 @@
3
3
  import { Loader2 } from '@tuturuuu/icons';
4
4
 
5
5
  /**
6
- * Full-screen blocking overlay shown while a checkout session is being created
7
- * and while the browser is redirecting to the Polar-hosted checkout. Kept
8
- * visible across the redirect (the page is navigating away) so the buyer always
9
- * sees progress instead of a frozen button.
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.
10
9
  */
11
10
  export function StorefrontCheckoutOverlay({ label }: { label: string }) {
12
11
  return (
@@ -17,7 +16,7 @@ export function StorefrontCheckoutOverlay({ label }: { label: string }) {
17
16
  role="status"
18
17
  >
19
18
  <div className="flex flex-col items-center gap-4 px-6 text-center">
20
- <span className="grid size-14 place-items-center rounded-full bg-[var(--storefront-accent,var(--primary))]/10 text-[var(--storefront-accent,var(--primary))]">
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))]">
21
20
  <Loader2 className="size-7 animate-spin" />
22
21
  </span>
23
22
  <p className="font-medium text-sm">{label}</p>
@@ -92,7 +92,7 @@ export function StorefrontListingCard({
92
92
  <div className="min-w-0">
93
93
  <div className="flex flex-wrap items-center gap-2">
94
94
  <button
95
- className="min-w-0 truncate text-left font-semibold transition hover:text-[var(--storefront-accent,var(--primary))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 disabled:cursor-default disabled:hover:text-foreground"
95
+ className="min-w-0 truncate text-left font-semibold transition hover:text-[var(--storefront-accent-text,var(--primary))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 disabled:cursor-default disabled:hover:text-foreground"
96
96
  disabled={!openDetail}
97
97
  onClick={openDetail}
98
98
  type="button"
@@ -0,0 +1,70 @@
1
+ import type { InventoryStorefrontSection } from '@tuturuuu/internal-api/inventory';
2
+ import { cn } from '@tuturuuu/utils/format';
3
+ import { StorefrontImagePanel } from './image-panel';
4
+ import { getSafeStorefrontHttpUrl } from './utils';
5
+
6
+ export function StorefrontMerchSections({
7
+ radius,
8
+ sections,
9
+ }: {
10
+ radius: string;
11
+ sections: InventoryStorefrontSection[];
12
+ }) {
13
+ const visibleSections = sections
14
+ .filter((section) => section.status === 'published')
15
+ .filter((section) => section.sectionType !== 'cover')
16
+ .filter((section) => {
17
+ const sectionHref = getSafeStorefrontHttpUrl(section.href);
18
+ return Boolean(
19
+ section.title?.trim() ||
20
+ section.description?.trim() ||
21
+ section.imageUrl?.trim() ||
22
+ sectionHref
23
+ );
24
+ })
25
+ .sort((a, b) => a.sortOrder - b.sortOrder);
26
+
27
+ if (visibleSections.length === 0) return null;
28
+
29
+ return (
30
+ <div className="mt-4 grid gap-3">
31
+ {visibleSections.map((section) => {
32
+ const sectionHref = getSafeStorefrontHttpUrl(section.href);
33
+
34
+ return (
35
+ <section
36
+ className={cn(
37
+ 'grid overflow-hidden border border-border bg-card md:grid-cols-[minmax(0,1fr)_280px]',
38
+ radius
39
+ )}
40
+ key={section.id}
41
+ >
42
+ <div className="flex min-w-0 flex-col justify-center gap-2 p-4">
43
+ {section.title ? (
44
+ <h2 className="font-semibold text-lg">{section.title}</h2>
45
+ ) : null}
46
+ {section.description ? (
47
+ <p className="text-muted-foreground text-sm leading-6">
48
+ {section.description}
49
+ </p>
50
+ ) : null}
51
+ {sectionHref ? (
52
+ <a
53
+ className="mt-1 w-fit font-medium text-sm underline-offset-4 hover:underline"
54
+ href={sectionHref}
55
+ >
56
+ {sectionHref.replace(/^https?:\/\//u, '')}
57
+ </a>
58
+ ) : null}
59
+ </div>
60
+ <StorefrontImagePanel
61
+ className="min-h-36 md:min-h-full"
62
+ imageUrl={section.imageUrl}
63
+ label={section.title ?? 'Storefront section'}
64
+ />
65
+ </section>
66
+ );
67
+ })}
68
+ </div>
69
+ );
70
+ }
@@ -182,7 +182,7 @@ export function StorefrontProductDetail({
182
182
  'inline-flex h-10 items-center justify-center border px-3 font-medium text-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40',
183
183
  radius,
184
184
  isActive
185
- ? 'border-[var(--storefront-accent,var(--primary))] bg-[var(--storefront-accent,var(--primary))]/10 text-[var(--storefront-accent,var(--primary))]'
185
+ ? 'border-[var(--storefront-accent-border,var(--border))] bg-[var(--storefront-accent-soft,var(--muted))] text-[var(--storefront-accent-text,var(--primary))]'
186
186
  : 'border-border bg-card hover:bg-muted/45'
187
187
  )}
188
188
  key={value.id}
@@ -1,11 +1,11 @@
1
- import { render, screen } from '@testing-library/react';
1
+ import { fireEvent, render, screen } from '@testing-library/react';
2
2
  import type {
3
3
  InventoryStorefront,
4
4
  InventoryStorefrontListing,
5
5
  } from '@tuturuuu/internal-api/inventory';
6
6
  import { describe, expect, it } from 'vitest';
7
7
  import { StorefrontSurface } from './storefront-surface';
8
- import { sanitizeStorefrontAccentColor } from './utils';
8
+ import { formatStorefrontPrice, sanitizeStorefrontAccentColor } from './utils';
9
9
 
10
10
  const storefront: InventoryStorefront = {
11
11
  accentColor: '#abc',
@@ -56,6 +56,15 @@ const listing: InventoryStorefrontListing = {
56
56
  wsId: storefront.wsId,
57
57
  };
58
58
 
59
+ const secondListing: InventoryStorefrontListing = {
60
+ ...listing,
61
+ id: 'listing-2',
62
+ price: 2500,
63
+ productId: 'product-2',
64
+ title: 'Team Workshop With Long Name',
65
+ unitId: 'unit-2',
66
+ };
67
+
59
68
  describe('StorefrontSurface', () => {
60
69
  it('sanitizes hex accent colors only', () => {
61
70
  expect(sanitizeStorefrontAccentColor('#abc')).toBe('#aabbcc');
@@ -80,10 +89,11 @@ describe('StorefrontSurface', () => {
80
89
  expect(screen.getAllByText('Preview Store')).toHaveLength(2);
81
90
  expect(screen.getByText('No buyer listings')).toBeInTheDocument();
82
91
  expect(screen.getByText('Create a listing next.')).toBeInTheDocument();
92
+ fireEvent.click(screen.getByRole('button', { name: 'Cart: 0' }));
83
93
  expect(screen.getByText('Preview checkout disabled')).toBeDisabled();
84
94
  });
85
95
 
86
- it('links storefront chrome back to the store and keeps the cart icon stable', () => {
96
+ it('links storefront chrome back to the store and opens cart from the header popover', () => {
87
97
  render(
88
98
  <StorefrontSurface
89
99
  cartHref="/preview-store/cart"
@@ -100,10 +110,68 @@ describe('StorefrontSurface', () => {
100
110
  '/preview-store'
101
111
  );
102
112
 
103
- const cartLink = screen.getByRole('link', { name: 'Cart: 2' });
104
- expect(cartLink).toHaveAttribute('href', '/preview-store/cart');
105
- expect(cartLink).toHaveClass('h-11', 'min-w-14', 'shrink-0');
106
- expect(cartLink.querySelector('svg')).toHaveClass('size-5', 'shrink-0');
113
+ expect(screen.queryByText('$2.00')).not.toBeInTheDocument();
114
+
115
+ const cartButton = screen.getByRole('button', { name: 'Cart: 2' });
116
+ expect(cartButton).toHaveClass('h-11', 'min-w-14', 'shrink-0');
117
+ expect(cartButton.querySelector('svg')).toHaveClass('size-5', 'shrink-0');
118
+
119
+ fireEvent.click(cartButton);
120
+
121
+ expect(screen.getByRole('region', { name: 'Cart' })).toBeInTheDocument();
122
+ expect(screen.getAllByText('$2.00')).toHaveLength(2);
123
+ expect(screen.getAllByText('1M')).toHaveLength(1);
124
+ });
125
+
126
+ it('keeps the compatibility cart page as a full cart review', () => {
127
+ render(
128
+ <StorefrontSurface
129
+ cartHref="/preview-store/cart"
130
+ cartLines={[{ listingId: listing.id, quantity: 2 }]}
131
+ checkoutHref="/preview-store/checkout"
132
+ listings={[listing]}
133
+ mode="cart"
134
+ onCheckoutOpen={() => undefined}
135
+ storefront={storefront}
136
+ storefrontHref="/preview-store"
137
+ />
138
+ );
139
+
140
+ expect(screen.getByRole('region', { name: 'Cart' })).toBeInTheDocument();
141
+ expect(screen.getByText('1:1 Mentoring')).toBeInTheDocument();
142
+ expect(screen.getAllByText('$2.00')).toHaveLength(2);
143
+ expect(screen.getByRole('button', { name: /Checkout/ })).toBeEnabled();
144
+ });
145
+
146
+ it('keeps long item names and large totals inside the cart row bounds', () => {
147
+ const largeListing: InventoryStorefrontListing = {
148
+ ...listing,
149
+ price: 123_456_789,
150
+ title:
151
+ 'A very long product title with specifications, measurements, and buyer-facing detail',
152
+ };
153
+ const expectedTotal = formatStorefrontPrice(largeListing.price * 5, 'USD');
154
+
155
+ render(
156
+ <StorefrontSurface
157
+ cartLines={[{ listingId: largeListing.id, quantity: 5 }]}
158
+ listings={[largeListing]}
159
+ mode="cart"
160
+ storefront={storefront}
161
+ />
162
+ );
163
+
164
+ const amount = screen.getByTitle(expectedTotal);
165
+ expect(amount).toHaveClass(
166
+ 'max-w-[9rem]',
167
+ 'overflow-hidden',
168
+ 'text-ellipsis',
169
+ 'text-right'
170
+ );
171
+ expect(screen.getByText(largeListing.title)).toHaveClass(
172
+ 'line-clamp-2',
173
+ 'break-words'
174
+ );
107
175
  });
108
176
 
109
177
  it('prefills checkout buyer details while keeping editable form fields', () => {
@@ -113,14 +181,23 @@ describe('StorefrontSurface', () => {
113
181
  email: 'buyer@example.com',
114
182
  name: 'Sokora Buyer',
115
183
  }}
116
- cartLines={[{ listingId: listing.id, quantity: 1 }]}
117
- listings={[listing]}
184
+ cartLines={[
185
+ { listingId: listing.id, quantity: 1 },
186
+ { listingId: secondListing.id, quantity: 1 },
187
+ ]}
188
+ listings={[listing, secondListing]}
118
189
  mode="checkout"
119
190
  onCheckoutSubmit={() => undefined}
120
191
  storefront={storefront}
121
192
  />
122
193
  );
123
194
 
195
+ expect(
196
+ screen.getByRole('button', { name: /Order summary/ })
197
+ ).toBeInTheDocument();
198
+ expect(
199
+ screen.getByRole('button', { name: 'Contact details' })
200
+ ).toBeInTheDocument();
124
201
  expect(screen.getByLabelText('Name')).toHaveValue('Sokora Buyer');
125
202
  expect(screen.getByLabelText('Email')).toHaveValue('buyer@example.com');
126
203
  expect(screen.getByLabelText('Name')).toBeEnabled();
@@ -155,6 +232,7 @@ describe('StorefrontSurface', () => {
155
232
  );
156
233
 
157
234
  expect(screen.queryByText('Checkout disabled')).not.toBeInTheDocument();
235
+ fireEvent.click(screen.getByRole('button', { name: 'Cart: 0' }));
158
236
  expect(screen.getByText('Checkout unavailable')).toBeDisabled();
159
237
  });
160
238
 
@@ -166,6 +244,22 @@ describe('StorefrontSurface', () => {
166
244
  storefront={{
167
245
  ...storefront,
168
246
  sections: [
247
+ {
248
+ createdAt: null,
249
+ description: null,
250
+ href: null,
251
+ id: 'section-empty',
252
+ imageUrl: null,
253
+ items: [],
254
+ metadata: {},
255
+ sectionType: 'promo',
256
+ sortOrder: 0,
257
+ status: 'published',
258
+ storefrontId: storefront.id,
259
+ title: null,
260
+ updatedAt: null,
261
+ wsId: storefront.wsId,
262
+ },
169
263
  {
170
264
  createdAt: null,
171
265
  description: null,
@@ -175,7 +269,7 @@ describe('StorefrontSurface', () => {
175
269
  items: [],
176
270
  metadata: {},
177
271
  sectionType: 'promo',
178
- sortOrder: 0,
272
+ sortOrder: 1,
179
273
  status: 'published',
180
274
  storefrontId: storefront.id,
181
275
  title: 'Unsafe section',
@@ -191,7 +285,7 @@ describe('StorefrontSurface', () => {
191
285
  items: [],
192
286
  metadata: {},
193
287
  sectionType: 'promo',
194
- sortOrder: 1,
288
+ sortOrder: 2,
195
289
  status: 'published',
196
290
  storefrontId: storefront.id,
197
291
  title: 'Safe section',
@@ -204,6 +298,7 @@ describe('StorefrontSurface', () => {
204
298
  );
205
299
 
206
300
  expect(screen.getByText('Safe section')).toBeInTheDocument();
301
+ expect(screen.queryByText('Storefront section')).not.toBeInTheDocument();
207
302
  expect(
208
303
  screen.getByRole('link', { name: 'example.com/promo' })
209
304
  ).toHaveAttribute('href', 'https://example.com/promo');