@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,19 +1,26 @@
1
1
  'use client';
2
2
 
3
- import { ArrowLeft, 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';
12
19
  import { StorefrontCheckoutOverlay } from './checkout-overlay';
13
20
  import { StorefrontEmptyListings } from './empty-listings';
14
21
  import { StorefrontHeroPanel } from './hero-panel';
15
- import { StorefrontImagePanel } from './image-panel';
16
22
  import { StorefrontListingCard } from './listing-card';
23
+ import { StorefrontMerchSections } from './merch-sections';
17
24
  import { StorefrontProductDetail } from './product-detail';
18
25
  import { StorefrontProductDialog } from './product-dialog';
19
26
  import type {
@@ -25,7 +32,6 @@ import type {
25
32
  import { mergeStorefrontSurfaceLabels } from './types';
26
33
  import {
27
34
  getAccentStyle,
28
- getSafeStorefrontHttpUrl,
29
35
  getStorefrontLinePrice,
30
36
  getStorefrontListingLimit,
31
37
  getStorefrontVariantLimit,
@@ -39,6 +45,7 @@ export function StorefrontSurface({
39
45
  buyerDefaults,
40
46
  cartLines = [],
41
47
  cartHref,
48
+ checkoutOpen,
42
49
  checkoutHref,
43
50
  className,
44
51
  compactLayout = false,
@@ -53,6 +60,8 @@ export function StorefrontSurface({
53
60
  mode,
54
61
  notice,
55
62
  onBuyNow,
63
+ onCheckoutOpen,
64
+ onCheckoutOpenChange,
56
65
  onCheckoutSubmit,
57
66
  onDecrement,
58
67
  onDetailListingChange,
@@ -65,6 +74,7 @@ export function StorefrontSurface({
65
74
  buyerDefaults?: StorefrontBuyerDefaults;
66
75
  cartLines?: StorefrontCartLine[];
67
76
  cartHref?: string;
77
+ checkoutOpen?: boolean;
68
78
  checkoutHref?: string;
69
79
  className?: string;
70
80
  compactLayout?: boolean;
@@ -79,6 +89,8 @@ export function StorefrontSurface({
79
89
  mode: StorefrontSurfaceMode;
80
90
  notice?: ReactNode;
81
91
  onBuyNow?: (listingId: string, variantId?: string | null) => void;
92
+ onCheckoutOpen?: () => void;
93
+ onCheckoutOpenChange?: (open: boolean) => void;
82
94
  onCheckoutSubmit?: (formData: FormData) => void;
83
95
  onDecrement?: (listingId: string, variantId?: string | null) => void;
84
96
  onDetailListingChange?: (listingId: string | null) => void;
@@ -93,6 +105,7 @@ export function StorefrontSurface({
93
105
  storefrontHref?: string;
94
106
  }) {
95
107
  const labels = mergeStorefrontSurfaceLabels(labelOverrides);
108
+ const [isCartPopoverOpen, setIsCartPopoverOpen] = useState(false);
96
109
  const accentColor = sanitizeStorefrontAccentColor(storefront.accentColor);
97
110
  const radius = storefrontRadiusClasses[storefront.cornerStyle];
98
111
  const resolveVariant = (
@@ -135,12 +148,15 @@ export function StorefrontSurface({
135
148
  ? listings.filter((listing) => listing.id === selectedListingId)
136
149
  : listings;
137
150
  const isCheckout = mode === 'checkout';
151
+ const isCheckoutDialogOpen = checkoutOpen ?? isCheckout;
138
152
  const isPreview = mode === 'preview';
139
- const showCartListings = mode === 'cart' || isCheckout;
140
- const listingRows = showCartListings
141
- ? cartEntries.map(({ listing }) => listing)
142
- : visibleListings;
153
+ const isCartPage = mode === 'cart';
154
+ const listingRows = visibleListings;
143
155
  const currency = storefront.currency ?? 'USD';
156
+ const handleCheckoutOpen = () => {
157
+ setIsCartPopoverOpen(false);
158
+ onCheckoutOpen?.();
159
+ };
144
160
 
145
161
  const cartSummary = (
146
162
  <StorefrontCartSummary
@@ -148,36 +164,53 @@ export function StorefrontSurface({
148
164
  cartEntries={checkoutEntries}
149
165
  checkoutHref={checkoutHref}
150
166
  currency={currency}
151
- isCheckout={isCheckout}
167
+ isCheckout={false}
152
168
  isPreview={isPreview}
153
169
  isSubmitting={isSubmitting}
154
170
  labels={labels}
171
+ onCheckoutOpen={handleCheckoutOpen}
155
172
  onCheckoutSubmit={onCheckoutSubmit}
156
173
  onInstantCheckout={mode === 'cart' ? onInstantCheckout : undefined}
157
174
  radius={radius}
158
175
  storefront={storefront}
159
176
  total={total}
177
+ variant="panel"
160
178
  />
161
179
  );
162
- const cartControlClassName = cn(
163
- 'inline-flex h-11 min-w-14 shrink-0 items-center justify-center gap-2 border bg-card px-3 font-semibold text-sm tabular-nums transition hover:bg-muted/45 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40',
164
- radius
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
+ />
165
196
  );
166
- const cartControlStyle =
167
- cartQuantity > 0
168
- ? {
169
- borderColor: 'var(--storefront-accent, var(--primary))',
170
- color: 'var(--storefront-accent, var(--primary))',
171
- }
172
- : undefined;
173
- const cartControlContent = (
174
- <>
175
- <ShoppingCart aria-hidden className="size-5 shrink-0" />
176
- <span className="sr-only">{labels.cart}: </span>
177
- <span className="min-w-4 text-center">{cartQuantity}</span>
178
- </>
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
+ />
179
213
  );
180
-
181
214
  return (
182
215
  <main
183
216
  className={cn(
@@ -187,13 +220,6 @@ export function StorefrontSurface({
187
220
  )}
188
221
  style={getAccentStyle(accentColor)}
189
222
  >
190
- {/* Accent strip — makes the storefront's accent color immediately visible. */}
191
- <div
192
- className="h-1 w-full"
193
- style={{
194
- backgroundColor: 'var(--storefront-accent, var(--primary))',
195
- }}
196
- />
197
223
  {notice ? (
198
224
  <div className="border-border border-b bg-muted/35 px-4 py-2 text-center text-muted-foreground text-sm">
199
225
  {notice}
@@ -206,7 +232,7 @@ export function StorefrontSurface({
206
232
  <h1 className="truncate font-semibold text-xl">
207
233
  {storefrontHref ? (
208
234
  <a
209
- className="block truncate rounded-sm transition hover:text-[var(--storefront-accent,var(--primary))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
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"
210
236
  href={storefrontHref}
211
237
  title={storefront.name}
212
238
  >
@@ -224,47 +250,28 @@ export function StorefrontSurface({
224
250
  </div>
225
251
  <div className="flex items-center gap-2">
226
252
  {headerActions}
227
- {cartHref ? (
228
- <a
229
- aria-label={`${labels.cart}: ${cartQuantity}`}
230
- className={cartControlClassName}
231
- href={cartHref}
232
- style={cartControlStyle}
233
- >
234
- {cartControlContent}
235
- </a>
236
- ) : (
237
- <span className={cartControlClassName} style={cartControlStyle}>
238
- {cartControlContent}
239
- </span>
240
- )}
253
+ <StorefrontCartPopover
254
+ cartQuantity={cartQuantity}
255
+ labels={labels}
256
+ onOpenChange={setIsCartPopoverOpen}
257
+ open={isCartPopoverOpen}
258
+ radius={radius}
259
+ >
260
+ {cartPopoverSummary}
261
+ </StorefrontCartPopover>
241
262
  </div>
242
263
  </div>
243
264
  </header>
244
265
 
245
- {isCheckout ? (
246
- <section className="mx-auto w-full max-w-xl px-4 py-8">
247
- {cartHref ? (
248
- <a
249
- className="mb-4 inline-flex items-center gap-1.5 text-muted-foreground text-sm transition hover:text-foreground"
250
- href={cartHref}
251
- >
252
- <ArrowLeft aria-hidden className="size-4" />
253
- {labels.cart}
254
- </a>
255
- ) : null}
256
- <h2 className="mb-4 font-semibold text-2xl tracking-tight">
257
- {labels.checkout}
258
- </h2>
259
- {cartSummary}
260
- </section>
261
- ) : (
262
- <section
263
- className={cn(
264
- 'mx-auto grid max-w-7xl gap-4 px-4 py-5',
265
- compactLayout ? 'grid-cols-1' : 'lg:grid-cols-[minmax(0,1fr)_340px]'
266
- )}
267
- >
266
+ <section
267
+ className={cn(
268
+ 'mx-auto max-w-7xl px-4 py-5',
269
+ compactLayout ? 'max-w-5xl' : null
270
+ )}
271
+ >
272
+ {isCartPage ? (
273
+ <div className="mx-auto max-w-2xl">{cartSummary}</div>
274
+ ) : (
268
275
  <div className="min-w-0">
269
276
  {isProductDetail && selectedListing ? (
270
277
  <>
@@ -323,39 +330,11 @@ export function StorefrontSurface({
323
330
  )}
324
331
  >
325
332
  {listingRows.length === 0 ? (
326
- showCartListings ? (
327
- <div
328
- className={cn(
329
- 'col-span-full grid min-h-56 place-items-center border border-dashed bg-muted/25 p-6 text-center',
330
- radius
331
- )}
332
- >
333
- <div className="max-w-sm">
334
- <ShoppingCart
335
- aria-hidden
336
- className="mx-auto size-8 text-muted-foreground"
337
- />
338
- <p className="mt-3 font-semibold">
339
- {labels.emptyCart}
340
- </p>
341
- {storefrontHref ? (
342
- <a
343
- className="mt-4 inline-flex items-center gap-1.5 font-medium text-[var(--storefront-accent,var(--primary))] text-sm hover:underline"
344
- href={storefrontHref}
345
- >
346
- <ArrowLeft aria-hidden className="size-4" />
347
- {labels.browse}
348
- </a>
349
- ) : null}
350
- </div>
351
- </div>
352
- ) : (
353
- <StorefrontEmptyListings
354
- action={emptyAction}
355
- labels={labels}
356
- radius={radius}
357
- />
358
- )
333
+ <StorefrontEmptyListings
334
+ action={emptyAction}
335
+ labels={labels}
336
+ radius={radius}
337
+ />
359
338
  ) : (
360
339
  listingRows.map((listing) => {
361
340
  const line = cartLines.find(
@@ -390,10 +369,8 @@ export function StorefrontSurface({
390
369
  </>
391
370
  )}
392
371
  </div>
393
-
394
- {cartSummary}
395
- </section>
396
- )}
372
+ )}
373
+ </section>
397
374
 
398
375
  <StorefrontProductDialog
399
376
  cartHref={cartHref}
@@ -413,66 +390,24 @@ export function StorefrontSurface({
413
390
  surfaceClassName={storefrontSurfaceClasses[storefront.surfaceStyle]}
414
391
  />
415
392
 
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}
404
+ </div>
405
+ </DialogContent>
406
+ </Dialog>
407
+
416
408
  {isSubmitting || isRedirecting ? (
417
409
  <StorefrontCheckoutOverlay label={labels.redirectingToCheckout} />
418
410
  ) : null}
419
411
  </main>
420
412
  );
421
413
  }
422
-
423
- function StorefrontMerchSections({
424
- radius,
425
- sections,
426
- }: {
427
- radius: string;
428
- sections: InventoryStorefrontSection[];
429
- }) {
430
- const visibleSections = sections
431
- .filter((section) => section.status === 'published')
432
- .filter((section) => section.sectionType !== 'cover')
433
- .sort((a, b) => a.sortOrder - b.sortOrder);
434
-
435
- if (visibleSections.length === 0) return null;
436
-
437
- return (
438
- <div className="mt-4 grid gap-3">
439
- {visibleSections.map((section) => {
440
- const sectionHref = getSafeStorefrontHttpUrl(section.href);
441
-
442
- return (
443
- <section
444
- className={cn(
445
- 'grid overflow-hidden border border-border bg-card md:grid-cols-[minmax(0,1fr)_280px]',
446
- radius
447
- )}
448
- key={section.id}
449
- >
450
- <div className="flex min-w-0 flex-col justify-center gap-2 p-4">
451
- {section.title ? (
452
- <h2 className="font-semibold text-lg">{section.title}</h2>
453
- ) : null}
454
- {section.description ? (
455
- <p className="text-muted-foreground text-sm leading-6">
456
- {section.description}
457
- </p>
458
- ) : null}
459
- {sectionHref ? (
460
- <a
461
- className="mt-1 w-fit font-medium text-sm underline-offset-4 hover:underline"
462
- href={sectionHref}
463
- >
464
- {sectionHref.replace(/^https?:\/\//u, '')}
465
- </a>
466
- ) : null}
467
- </div>
468
- <StorefrontImagePanel
469
- className="min-h-36 md:min-h-full"
470
- imageUrl={section.imageUrl}
471
- label={section.title ?? 'Storefront section'}
472
- />
473
- </section>
474
- );
475
- })}
476
- </div>
477
- );
478
- }
@@ -37,11 +37,13 @@ export type StorefrontSurfaceLabels = {
37
37
  checkout: string;
38
38
  checkoutDisabled: string;
39
39
  checkoutDisabledBadge: string;
40
+ contactDetails: string;
40
41
  couponNote: string;
41
42
  demoBadge: string;
42
43
  emptyCart: string;
43
44
  fromPrice: string;
44
45
  instantCheckout: string;
46
+ orderSummary: string;
45
47
  redirectingToCheckout: string;
46
48
  selectOptions: string;
47
49
  viewDetails: string;
@@ -77,11 +79,13 @@ export const defaultStorefrontSurfaceLabels: StorefrontSurfaceLabels = {
77
79
  checkout: 'Checkout',
78
80
  checkoutDisabled: 'Checkout is disabled in preview',
79
81
  checkoutDisabledBadge: 'Checkout disabled',
82
+ contactDetails: 'Contact details',
80
83
  couponNote: 'Have a coupon? You can apply it at checkout.',
81
84
  demoBadge: 'Demo',
82
85
  emptyCart: 'Add a listing to start checkout.',
83
86
  fromPrice: 'From',
84
87
  instantCheckout: 'Instant checkout',
88
+ orderSummary: 'Order summary',
85
89
  redirectingToCheckout: 'Taking you to secure checkout…',
86
90
  selectOptions: 'Select options',
87
91
  viewDetails: 'View details',
@@ -49,7 +49,10 @@ export const storefrontThemeClasses: Record<
49
49
 
50
50
  export type StorefrontAccentStyle = CSSProperties & {
51
51
  '--storefront-accent'?: string;
52
+ '--storefront-accent-border'?: string;
52
53
  '--storefront-accent-foreground'?: string;
54
+ '--storefront-accent-soft'?: string;
55
+ '--storefront-accent-text'?: string;
53
56
  };
54
57
 
55
58
  export function sanitizeStorefrontAccentColor(value?: string | null) {
@@ -182,7 +185,10 @@ export function getAccentStyle(
182
185
 
183
186
  return {
184
187
  '--storefront-accent': accentColor,
188
+ '--storefront-accent-border': `color-mix(in oklab, ${accentColor} 42%, var(--border))`,
185
189
  '--storefront-accent-foreground': getAccentForeground(accentColor),
190
+ '--storefront-accent-soft': `color-mix(in oklab, ${accentColor} 14%, var(--background))`,
191
+ '--storefront-accent-text': `color-mix(in oklab, ${accentColor} 68%, var(--foreground))`,
186
192
  };
187
193
  }
188
194
 
@@ -1,3 +1,4 @@
1
+ import { generateHTML, generateJSON } from '@tiptap/core';
1
2
  import type SupabaseProvider from '@tuturuuu/ui/hooks/supabase-provider';
2
3
  import { describe, expect, it } from 'vitest';
3
4
  import * as Y from 'yjs';
@@ -34,6 +35,128 @@ describe('text editor extensions', () => {
34
35
  expect(names).toContain('collaborationCaret');
35
36
  });
36
37
 
38
+ it('registers theme-aware highlight, text color, and background color extensions', () => {
39
+ const names = extensionNames({});
40
+
41
+ expect(names).toContain('highlight');
42
+ expect(names).toContain('textStyle');
43
+ expect(names).toContain('color');
44
+ expect(names).toContain('backgroundColor');
45
+ });
46
+
47
+ it('round-trips theme-aware highlight, text color, and background color marks', () => {
48
+ const extensions = getEditorExtensions();
49
+ const content = {
50
+ type: 'doc',
51
+ content: [
52
+ {
53
+ type: 'paragraph',
54
+ attrs: { textAlign: null },
55
+ content: [
56
+ {
57
+ type: 'text',
58
+ marks: [
59
+ {
60
+ type: 'highlight',
61
+ attrs: {
62
+ color: 'var(--calendar-bg-yellow)',
63
+ textColor: 'var(--yellow)',
64
+ },
65
+ },
66
+ ],
67
+ text: 'Highlighted',
68
+ },
69
+ { type: 'text', text: ' ' },
70
+ {
71
+ type: 'text',
72
+ marks: [
73
+ {
74
+ type: 'textStyle',
75
+ attrs: {
76
+ color: 'var(--blue)',
77
+ backgroundColor: 'var(--calendar-bg-blue)',
78
+ },
79
+ },
80
+ ],
81
+ text: 'Colored',
82
+ },
83
+ ],
84
+ },
85
+ ],
86
+ };
87
+
88
+ const html = generateHTML(content, extensions);
89
+
90
+ expect(html).toContain('var(--calendar-bg-yellow)');
91
+ expect(html).toContain('var(--yellow)');
92
+ expect(html).toContain('var(--calendar-bg-blue)');
93
+ expect(html).toContain('var(--blue)');
94
+
95
+ const parsed = generateJSON(html, extensions);
96
+ const paragraph = parsed.content?.[0];
97
+ const highlightedText = paragraph?.content?.[0];
98
+ const coloredText = paragraph?.content?.[2];
99
+
100
+ expect(highlightedText?.marks).toEqual(
101
+ expect.arrayContaining([
102
+ expect.objectContaining({
103
+ type: 'highlight',
104
+ attrs: expect.objectContaining({
105
+ color: 'var(--calendar-bg-yellow)',
106
+ textColor: 'var(--yellow)',
107
+ }),
108
+ }),
109
+ ])
110
+ );
111
+ expect(coloredText?.marks).toEqual(
112
+ expect.arrayContaining([
113
+ expect.objectContaining({
114
+ type: 'textStyle',
115
+ attrs: expect.objectContaining({
116
+ color: 'var(--blue)',
117
+ backgroundColor: 'var(--calendar-bg-blue)',
118
+ }),
119
+ }),
120
+ ])
121
+ );
122
+ });
123
+
124
+ it('keeps legacy and raw hex highlight content valid', () => {
125
+ const html = generateHTML(
126
+ {
127
+ type: 'doc',
128
+ content: [
129
+ {
130
+ type: 'paragraph',
131
+ attrs: { textAlign: null },
132
+ content: [
133
+ {
134
+ type: 'text',
135
+ marks: [{ type: 'highlight' }],
136
+ text: 'Legacy',
137
+ },
138
+ { type: 'text', text: ' ' },
139
+ {
140
+ type: 'text',
141
+ marks: [
142
+ {
143
+ type: 'highlight',
144
+ attrs: { color: '#FFF59D' },
145
+ },
146
+ ],
147
+ text: 'Hex',
148
+ },
149
+ ],
150
+ },
151
+ ],
152
+ },
153
+ getEditorExtensions()
154
+ );
155
+
156
+ expect(html).toContain('<mark');
157
+ expect(html).toContain('#FFF59D');
158
+ });
159
+
37
160
  it('round-trips task mention workspace metadata through HTML attrs', () => {
38
161
  const renderOutput = (Mention.config as any).renderHTML({
39
162
  HTMLAttributes: {
@@ -0,0 +1,62 @@
1
+ import { Extension } from '@tiptap/core';
2
+
3
+ export interface BackgroundColorOptions {
4
+ types: string[];
5
+ }
6
+
7
+ declare module '@tiptap/core' {
8
+ interface Commands<ReturnType> {
9
+ backgroundColor: {
10
+ setBackgroundColor: (color: string) => ReturnType;
11
+ unsetBackgroundColor: () => ReturnType;
12
+ };
13
+ }
14
+ }
15
+
16
+ export const BackgroundColor = Extension.create<BackgroundColorOptions>({
17
+ name: 'backgroundColor',
18
+
19
+ addOptions() {
20
+ return {
21
+ types: ['textStyle'],
22
+ };
23
+ },
24
+
25
+ addGlobalAttributes() {
26
+ return [
27
+ {
28
+ types: this.options.types,
29
+ attributes: {
30
+ backgroundColor: {
31
+ default: null,
32
+ parseHTML: (element) =>
33
+ element.style.getPropertyValue('background-color') || null,
34
+ renderHTML: (attributes) => {
35
+ if (!attributes.backgroundColor) return {};
36
+
37
+ return {
38
+ style: `background-color: ${attributes.backgroundColor}`,
39
+ };
40
+ },
41
+ },
42
+ },
43
+ },
44
+ ];
45
+ },
46
+
47
+ addCommands() {
48
+ return {
49
+ setBackgroundColor:
50
+ (color) =>
51
+ ({ chain }) =>
52
+ chain().setMark('textStyle', { backgroundColor: color }).run(),
53
+ unsetBackgroundColor:
54
+ () =>
55
+ ({ chain }) =>
56
+ chain()
57
+ .setMark('textStyle', { backgroundColor: null })
58
+ .removeEmptyTextStyle()
59
+ .run(),
60
+ };
61
+ },
62
+ });