@tuturuuu/ui 0.18.0 → 0.19.1
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.
- package/CHANGELOG.md +39 -0
- package/package.json +76 -71
- package/src/components/ui/calendar-app/calendar-client-page.tsx +19 -1
- package/src/components/ui/calendar-app/calendar-page-shell.tsx +37 -16
- package/src/components/ui/calendar-app/components/calendar-connections-manager-helpers.ts +70 -0
- package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +516 -578
- package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +1 -0
- package/src/components/ui/calendar-app/components/calendar-sync-settings-panel.tsx +155 -0
- package/src/components/ui/calendar-app/components/merge-provider-calendars.test.ts +46 -0
- package/src/components/ui/calendar-app/components/merge-provider-calendars.ts +81 -0
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +22 -129
- package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +17 -3
- package/src/components/ui/chat/chat-agent-details-utils.tsx +2 -0
- package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +17 -6
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +35 -0
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +17 -0
- package/src/components/ui/custom/nav-link.test.tsx +1 -0
- package/src/components/ui/custom/nav-link.tsx +4 -1
- package/src/components/ui/custom/settings-dialog-shell.tsx +23 -14
- package/src/components/ui/custom/statistic-card.tsx +3 -5
- package/src/components/ui/custom/structure.test.tsx +4 -0
- package/src/components/ui/custom/user-filters.tsx +4 -7
- package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +1 -1
- package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +1 -1
- package/src/components/ui/custom/workspace-select.tsx +38 -53
- package/src/components/ui/finance/analytics/category-spending-chart.test.tsx +3 -3
- package/src/components/ui/finance/budgets/budget-card.test.tsx +3 -3
- package/src/components/ui/finance/debts/debt-loan-card.test.tsx +3 -3
- package/src/components/ui/finance/debts/debt-loan-detail-cards.test.tsx +3 -3
- package/src/components/ui/finance/invoices/charts/invoice-totals-chart.test.tsx +3 -3
- package/src/components/ui/finance/invoices/columns.test.tsx +3 -3
- package/src/components/ui/finance/invoices/components/invoice-customer-select-card.tsx +1 -1
- package/src/components/ui/finance/shared/numbers-visibility-toggle.test.tsx +1 -1
- package/src/components/ui/finance/tags/tag-manager.test.tsx +2 -2
- package/src/components/ui/finance/transactions/categories/columns.test.tsx +5 -5
- package/src/components/ui/finance/transactions/columns.test.tsx +2 -2
- package/src/components/ui/finance/transactions/confidential-field.test.tsx +2 -2
- package/src/components/ui/finance/transactions/transaction-edit-dialog.test.tsx +2 -2
- package/src/components/ui/finance/transactions/transaction-statistics.test.tsx +3 -3
- package/src/components/ui/finance/wallets/form.test.tsx +3 -3
- package/src/components/ui/finance/wallets/wallets-data-table.test.tsx +10 -0
- package/src/components/ui/legacy/calendar/calendar-cell.tsx +1 -1
- package/src/components/ui/storefront/accent-button.tsx +4 -1
- package/src/components/ui/storefront/browse-panel.tsx +6 -1
- package/src/components/ui/storefront/cart-summary-parts.tsx +18 -4
- package/src/components/ui/storefront/cart-summary.tsx +22 -6
- package/src/components/ui/storefront/hero-panel.tsx +13 -17
- package/src/components/ui/storefront/index.ts +2 -0
- package/src/components/ui/storefront/listing-card.tsx +6 -4
- package/src/components/ui/storefront/product-detail.tsx +51 -21
- package/src/components/ui/storefront/product-dialog.tsx +8 -1
- package/src/components/ui/storefront/storefront-surface.test.tsx +79 -3
- package/src/components/ui/storefront/storefront-surface.tsx +151 -100
- package/src/components/ui/storefront/types.ts +3 -0
- package/src/components/ui/storefront/utils.ts +5 -1
- package/src/components/ui/text-editor/__tests__/task-mention-checkbox.test.ts +23 -6
- package/src/components/ui/text-editor/__tests__/upload-placeholder.test.ts +1 -10
- package/src/components/ui/text-editor/color-controls.tsx +1 -1
- package/src/globals.css +20 -0
- package/src/globals.test.ts +75 -0
- package/src/hooks/__tests__/use-calendar-sync.test.tsx +103 -1
- package/src/hooks/use-calendar-sync.tsx +51 -14
- package/src/lib/task-personal-external.ts +1 -1
- package/vendor/xlsx/LICENSE +201 -0
- package/vendor/xlsx/types/index.d.ts +1065 -0
- package/vendor/xlsx/xlsx.js +28103 -0
- package/vendor/xlsx/xlsx.mjs +28228 -0
- package/vendor/xlsx-0.20.3.tgz +0 -0
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
InventoryStorefrontListing,
|
|
8
8
|
} from '@tuturuuu/internal-api/inventory';
|
|
9
9
|
import { cn } from '@tuturuuu/utils/format';
|
|
10
|
-
import { type ReactNode, useState } from 'react';
|
|
10
|
+
import { type ReactNode, useMemo, useState } from 'react';
|
|
11
11
|
import {
|
|
12
12
|
Dialog,
|
|
13
13
|
DialogContent,
|
|
@@ -29,6 +29,7 @@ import { StorefrontProductDialog } from './product-dialog';
|
|
|
29
29
|
import type {
|
|
30
30
|
StorefrontBuyerDefaults,
|
|
31
31
|
StorefrontCartLine,
|
|
32
|
+
StorefrontLinkComponent,
|
|
32
33
|
StorefrontSurfaceLabels,
|
|
33
34
|
StorefrontSurfaceMode,
|
|
34
35
|
} from './types';
|
|
@@ -59,6 +60,7 @@ export function StorefrontSurface({
|
|
|
59
60
|
isDemo: _isDemo = false,
|
|
60
61
|
isRedirecting = false,
|
|
61
62
|
isSubmitting = false,
|
|
63
|
+
linkComponent,
|
|
62
64
|
labels: labelOverrides,
|
|
63
65
|
listings,
|
|
64
66
|
mode,
|
|
@@ -75,6 +77,7 @@ export function StorefrontSurface({
|
|
|
75
77
|
selectedListingId,
|
|
76
78
|
storefront,
|
|
77
79
|
storefrontHref,
|
|
80
|
+
withinSharedShell = false,
|
|
78
81
|
}: {
|
|
79
82
|
bundles?: InventoryBundle[];
|
|
80
83
|
buyerDefaults?: StorefrontBuyerDefaults;
|
|
@@ -90,6 +93,7 @@ export function StorefrontSurface({
|
|
|
90
93
|
isDemo?: boolean;
|
|
91
94
|
isRedirecting?: boolean;
|
|
92
95
|
isSubmitting?: boolean;
|
|
96
|
+
linkComponent?: StorefrontLinkComponent;
|
|
93
97
|
labels?: Partial<StorefrontSurfaceLabels>;
|
|
94
98
|
listings: InventoryStorefrontListing[];
|
|
95
99
|
mode: StorefrontSurfaceMode;
|
|
@@ -110,77 +114,112 @@ export function StorefrontSurface({
|
|
|
110
114
|
selectedListingId?: string;
|
|
111
115
|
storefront: InventoryStorefront;
|
|
112
116
|
storefrontHref?: string;
|
|
117
|
+
withinSharedShell?: boolean;
|
|
113
118
|
}) {
|
|
114
119
|
const labels = mergeStorefrontSurfaceLabels(labelOverrides);
|
|
120
|
+
const NavigationLink = linkComponent ?? 'a';
|
|
115
121
|
const [isCartPopoverOpen, setIsCartPopoverOpen] = useState(false);
|
|
116
122
|
const [bundleSelectionListingId, setBundleSelectionListingId] = useState<
|
|
117
123
|
string | null
|
|
118
124
|
>(null);
|
|
119
125
|
const accentColor = sanitizeStorefrontAccentColor(storefront.accentColor);
|
|
120
126
|
const radius = storefrontRadiusClasses[storefront.cornerStyle];
|
|
121
|
-
const
|
|
122
|
-
listing
|
|
123
|
-
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
127
|
+
const listingsById = useMemo(
|
|
128
|
+
() => new Map(listings.map((listing) => [listing.id, listing])),
|
|
129
|
+
[listings]
|
|
130
|
+
);
|
|
131
|
+
const bundlesById = useMemo(
|
|
132
|
+
() => new Map(bundles.map((bundle) => [bundle.id, bundle])),
|
|
133
|
+
[bundles]
|
|
134
|
+
);
|
|
135
|
+
const cartEntries = useMemo(
|
|
136
|
+
() =>
|
|
137
|
+
cartLines.flatMap((line) => {
|
|
138
|
+
const listing = listingsById.get(line.listingId);
|
|
139
|
+
if (!listing) return [];
|
|
140
|
+
return [
|
|
141
|
+
{
|
|
142
|
+
bundle: listing.bundleId
|
|
143
|
+
? bundlesById.get(listing.bundleId)
|
|
144
|
+
: undefined,
|
|
145
|
+
line,
|
|
146
|
+
listing,
|
|
147
|
+
variant: line.variantId
|
|
148
|
+
? (listing.variants ?? []).find(
|
|
149
|
+
(variant) => variant.id === line.variantId
|
|
150
|
+
)
|
|
151
|
+
: undefined,
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
}),
|
|
155
|
+
[bundlesById, cartLines, listingsById]
|
|
156
|
+
);
|
|
157
|
+
const checkoutEntries = useMemo(
|
|
158
|
+
() =>
|
|
159
|
+
cartEntries.filter((entry) => {
|
|
160
|
+
const limit = entry.variant
|
|
161
|
+
? getStorefrontVariantLimit(entry.listing, entry.variant)
|
|
162
|
+
: getStorefrontListingLimit(entry.listing);
|
|
163
|
+
return Math.min(entry.line.quantity, limit) > 0;
|
|
164
|
+
}),
|
|
165
|
+
[cartEntries]
|
|
166
|
+
);
|
|
167
|
+
const total = useMemo(
|
|
168
|
+
() =>
|
|
169
|
+
checkoutEntries.reduce((sum, entry) => {
|
|
170
|
+
const limit = entry.variant
|
|
171
|
+
? getStorefrontVariantLimit(entry.listing, entry.variant)
|
|
172
|
+
: getStorefrontListingLimit(entry.listing);
|
|
173
|
+
const quantity = Math.min(entry.line.quantity, limit);
|
|
174
|
+
return (
|
|
175
|
+
sum +
|
|
176
|
+
getStorefrontCartLineSubtotal({
|
|
177
|
+
bundle: entry.bundle,
|
|
178
|
+
line: { ...entry.line, quantity },
|
|
179
|
+
listing: entry.listing,
|
|
180
|
+
variant: entry.variant,
|
|
181
|
+
})
|
|
182
|
+
);
|
|
183
|
+
}, 0),
|
|
184
|
+
[checkoutEntries]
|
|
185
|
+
);
|
|
186
|
+
const cartQuantityByListing = useMemo(() => {
|
|
187
|
+
const quantities = new Map<string, number>();
|
|
188
|
+
|
|
189
|
+
for (const line of cartLines) {
|
|
190
|
+
quantities.set(
|
|
191
|
+
line.listingId,
|
|
192
|
+
(quantities.get(line.listingId) ?? 0) + line.quantity
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return quantities;
|
|
197
|
+
}, [cartLines]);
|
|
198
|
+
const cartQuantity = useMemo(
|
|
199
|
+
() => cartLines.reduce((sum, line) => sum + line.quantity, 0),
|
|
200
|
+
[cartLines]
|
|
148
201
|
);
|
|
149
|
-
const total = checkoutEntries.reduce((sum, entry) => {
|
|
150
|
-
const quantity = Math.min(entry.line.quantity, lineLimit(entry));
|
|
151
|
-
return (
|
|
152
|
-
sum +
|
|
153
|
-
getStorefrontCartLineSubtotal({
|
|
154
|
-
bundle: entry.bundle,
|
|
155
|
-
line: { ...entry.line, quantity },
|
|
156
|
-
listing: entry.listing,
|
|
157
|
-
variant: entry.variant,
|
|
158
|
-
})
|
|
159
|
-
);
|
|
160
|
-
}, 0);
|
|
161
|
-
const cartQuantity = cartLines.reduce((sum, line) => sum + line.quantity, 0);
|
|
162
202
|
const detailListing = detailListingId
|
|
163
|
-
?
|
|
203
|
+
? listingsById.get(detailListingId)
|
|
164
204
|
: undefined;
|
|
165
205
|
const selectedListing = selectedListingId
|
|
166
|
-
?
|
|
206
|
+
? listingsById.get(selectedListingId)
|
|
167
207
|
: undefined;
|
|
168
208
|
const isProductDetail = mode === 'product' && Boolean(selectedListing);
|
|
169
|
-
const visibleListings =
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
209
|
+
const visibleListings = useMemo(
|
|
210
|
+
() =>
|
|
211
|
+
mode === 'product' && selectedListing ? [selectedListing] : listings,
|
|
212
|
+
[listings, mode, selectedListing]
|
|
213
|
+
);
|
|
173
214
|
const isCheckout = mode === 'checkout';
|
|
174
215
|
const isCheckoutDialogOpen = checkoutOpen ?? isCheckout;
|
|
175
216
|
const isPreview = mode === 'preview';
|
|
176
217
|
const isCartPage = mode === 'cart';
|
|
177
218
|
const bundleSelectionListing = bundleSelectionListingId
|
|
178
|
-
?
|
|
219
|
+
? listingsById.get(bundleSelectionListingId)
|
|
179
220
|
: null;
|
|
180
221
|
const bundleSelectionBundle = bundleSelectionListing?.bundleId
|
|
181
|
-
? (
|
|
182
|
-
(bundle) => bundle.id === bundleSelectionListing.bundleId
|
|
183
|
-
) ?? null)
|
|
222
|
+
? (bundlesById.get(bundleSelectionListing.bundleId) ?? null)
|
|
184
223
|
: null;
|
|
185
224
|
const currency = storefront.currency ?? 'USD';
|
|
186
225
|
const handleCheckoutOpen = () => {
|
|
@@ -198,6 +237,7 @@ export function StorefrontSurface({
|
|
|
198
237
|
isPreview={isPreview}
|
|
199
238
|
isSubmitting={isSubmitting}
|
|
200
239
|
labels={labels}
|
|
240
|
+
linkComponent={linkComponent}
|
|
201
241
|
onCheckoutOpen={handleCheckoutOpen}
|
|
202
242
|
onCheckoutSubmit={onCheckoutSubmit}
|
|
203
243
|
onInstantCheckout={mode === 'cart' ? onInstantCheckout : undefined}
|
|
@@ -216,6 +256,7 @@ export function StorefrontSurface({
|
|
|
216
256
|
isPreview={isPreview}
|
|
217
257
|
isSubmitting={isSubmitting}
|
|
218
258
|
labels={labels}
|
|
259
|
+
linkComponent={linkComponent}
|
|
219
260
|
onCheckoutOpen={handleCheckoutOpen}
|
|
220
261
|
onCheckoutSubmit={onCheckoutSubmit}
|
|
221
262
|
radius={radius}
|
|
@@ -234,6 +275,7 @@ export function StorefrontSurface({
|
|
|
234
275
|
isPreview={isPreview}
|
|
235
276
|
isSubmitting={isSubmitting}
|
|
236
277
|
labels={labels}
|
|
278
|
+
linkComponent={linkComponent}
|
|
237
279
|
onCheckoutSubmit={onCheckoutSubmit}
|
|
238
280
|
radius={radius}
|
|
239
281
|
storefront={storefront}
|
|
@@ -244,7 +286,9 @@ export function StorefrontSurface({
|
|
|
244
286
|
return (
|
|
245
287
|
<main
|
|
246
288
|
className={cn(
|
|
247
|
-
|
|
289
|
+
withinSharedShell
|
|
290
|
+
? 'min-h-[calc(100dvh-4.3125rem)] bg-background text-foreground'
|
|
291
|
+
: 'min-h-dvh bg-background text-foreground',
|
|
248
292
|
storefrontThemeClasses[storefront.themePreset],
|
|
249
293
|
className
|
|
250
294
|
)}
|
|
@@ -256,51 +300,53 @@ export function StorefrontSurface({
|
|
|
256
300
|
</div>
|
|
257
301
|
) : null}
|
|
258
302
|
|
|
259
|
-
|
|
260
|
-
<
|
|
261
|
-
<div className="
|
|
262
|
-
<
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
<span
|
|
270
|
-
aria-hidden
|
|
271
|
-
className="grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-muted/35 font-mono text-xs uppercase"
|
|
272
|
-
>
|
|
273
|
-
{storefront.name.slice(0, 1)}
|
|
274
|
-
</span>
|
|
275
|
-
<span className="truncate">{storefront.name}</span>
|
|
276
|
-
</a>
|
|
277
|
-
) : (
|
|
278
|
-
<span className="flex min-w-0 items-center gap-3">
|
|
279
|
-
<span
|
|
280
|
-
aria-hidden
|
|
281
|
-
className="grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-muted/35 font-mono text-xs uppercase"
|
|
303
|
+
{withinSharedShell ? null : (
|
|
304
|
+
<header className="sticky top-0 z-30 border-border border-b bg-background/95 backdrop-blur-md supports-[backdrop-filter]:bg-background/85">
|
|
305
|
+
<div className="mx-auto flex max-w-7xl items-center justify-between gap-4 px-5 py-3 sm:px-6">
|
|
306
|
+
<div className="min-w-0">
|
|
307
|
+
<h1 className="truncate font-semibold text-base tracking-tight">
|
|
308
|
+
{storefrontHref ? (
|
|
309
|
+
<NavigationLink
|
|
310
|
+
className="flex min-w-0 items-center gap-3 rounded-sm transition hover:text-[var(--storefront-accent-text,var(--primary))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
|
|
311
|
+
href={storefrontHref}
|
|
312
|
+
title={storefront.name}
|
|
282
313
|
>
|
|
283
|
-
|
|
314
|
+
<span
|
|
315
|
+
aria-hidden
|
|
316
|
+
className="grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-muted/35 font-mono text-xs uppercase"
|
|
317
|
+
>
|
|
318
|
+
{storefront.name.slice(0, 1)}
|
|
319
|
+
</span>
|
|
320
|
+
<span className="truncate">{storefront.name}</span>
|
|
321
|
+
</NavigationLink>
|
|
322
|
+
) : (
|
|
323
|
+
<span className="flex min-w-0 items-center gap-3">
|
|
324
|
+
<span
|
|
325
|
+
aria-hidden
|
|
326
|
+
className="grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-muted/35 font-mono text-xs uppercase"
|
|
327
|
+
>
|
|
328
|
+
{storefront.name.slice(0, 1)}
|
|
329
|
+
</span>
|
|
330
|
+
<span className="truncate">{storefront.name}</span>
|
|
284
331
|
</span>
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
332
|
+
)}
|
|
333
|
+
</h1>
|
|
334
|
+
</div>
|
|
335
|
+
<div className="flex items-center gap-2">
|
|
336
|
+
{headerActions}
|
|
337
|
+
<StorefrontCartPopover
|
|
338
|
+
cartQuantity={cartQuantity}
|
|
339
|
+
labels={labels}
|
|
340
|
+
onOpenChange={setIsCartPopoverOpen}
|
|
341
|
+
open={isCartPopoverOpen}
|
|
342
|
+
radius={radius}
|
|
343
|
+
>
|
|
344
|
+
{cartPopoverSummary}
|
|
345
|
+
</StorefrontCartPopover>
|
|
346
|
+
</div>
|
|
289
347
|
</div>
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
<StorefrontCartPopover
|
|
293
|
-
cartQuantity={cartQuantity}
|
|
294
|
-
labels={labels}
|
|
295
|
-
onOpenChange={setIsCartPopoverOpen}
|
|
296
|
-
open={isCartPopoverOpen}
|
|
297
|
-
radius={radius}
|
|
298
|
-
>
|
|
299
|
-
{cartPopoverSummary}
|
|
300
|
-
</StorefrontCartPopover>
|
|
301
|
-
</div>
|
|
302
|
-
</div>
|
|
303
|
-
</header>
|
|
348
|
+
</header>
|
|
349
|
+
)}
|
|
304
350
|
|
|
305
351
|
<section
|
|
306
352
|
className={cn(
|
|
@@ -315,13 +361,13 @@ export function StorefrontSurface({
|
|
|
315
361
|
{isProductDetail && selectedListing ? (
|
|
316
362
|
<>
|
|
317
363
|
{storefrontHref ? (
|
|
318
|
-
<
|
|
364
|
+
<NavigationLink
|
|
319
365
|
className="mb-4 inline-flex items-center gap-1.5 text-muted-foreground text-sm transition hover:text-foreground"
|
|
320
366
|
href={storefrontHref}
|
|
321
367
|
>
|
|
322
368
|
<ArrowLeft aria-hidden className="size-4" />
|
|
323
369
|
{labels.browse}
|
|
324
|
-
</
|
|
370
|
+
</NavigationLink>
|
|
325
371
|
) : null}
|
|
326
372
|
<StorefrontProductDetail
|
|
327
373
|
cartHref={cartHref}
|
|
@@ -330,6 +376,7 @@ export function StorefrontSurface({
|
|
|
330
376
|
currency={currency}
|
|
331
377
|
isSubmitting={isSubmitting}
|
|
332
378
|
labels={labels}
|
|
379
|
+
linkComponent={linkComponent}
|
|
333
380
|
listing={selectedListing}
|
|
334
381
|
onBuyNow={onBuyNow}
|
|
335
382
|
onDecrement={onDecrement}
|
|
@@ -370,15 +417,18 @@ export function StorefrontSurface({
|
|
|
370
417
|
radius={radius}
|
|
371
418
|
/>
|
|
372
419
|
}
|
|
420
|
+
flushTop={
|
|
421
|
+
!storefront.coverImageUrl &&
|
|
422
|
+
!storefront.heroImageUrl &&
|
|
423
|
+
(storefront.sections?.length ?? 0) === 0
|
|
424
|
+
}
|
|
373
425
|
labels={labels}
|
|
374
426
|
listings={visibleListings}
|
|
375
427
|
renderListing={(listing) => {
|
|
376
428
|
const listingBundle = listing.bundleId
|
|
377
|
-
?
|
|
429
|
+
? bundlesById.get(listing.bundleId)
|
|
378
430
|
: undefined;
|
|
379
|
-
const quantity =
|
|
380
|
-
.filter((item) => item.listingId === listing.id)
|
|
381
|
-
.reduce((sum, item) => sum + item.quantity, 0);
|
|
431
|
+
const quantity = cartQuantityByListing.get(listing.id) ?? 0;
|
|
382
432
|
|
|
383
433
|
return (
|
|
384
434
|
<StorefrontListingCard
|
|
@@ -422,6 +472,7 @@ export function StorefrontSurface({
|
|
|
422
472
|
currency={currency}
|
|
423
473
|
isSubmitting={isSubmitting}
|
|
424
474
|
labels={labels}
|
|
475
|
+
linkComponent={linkComponent}
|
|
425
476
|
listing={detailListing ?? null}
|
|
426
477
|
onBuyNow={onBuyNow}
|
|
427
478
|
onDecrement={onDecrement}
|
|
@@ -4,6 +4,9 @@ import type {
|
|
|
4
4
|
InventoryListingVariant,
|
|
5
5
|
InventoryStorefrontListing,
|
|
6
6
|
} from '@tuturuuu/internal-api/inventory';
|
|
7
|
+
import type { ElementType } from 'react';
|
|
8
|
+
|
|
9
|
+
export type StorefrontLinkComponent = ElementType<{ href: string }>;
|
|
7
10
|
|
|
8
11
|
export type StorefrontCartLine = {
|
|
9
12
|
listingId: string;
|
|
@@ -187,7 +187,11 @@ export function getStorefrontBundleSelectionSubtotal(
|
|
|
187
187
|
bundle: InventoryBundle | undefined,
|
|
188
188
|
line: StorefrontCartLine
|
|
189
189
|
) {
|
|
190
|
-
if (
|
|
190
|
+
if (
|
|
191
|
+
!bundle?.categoryComponents?.length ||
|
|
192
|
+
!line.bundleSelections ||
|
|
193
|
+
bundle.pricingMode === 'fixed_price'
|
|
194
|
+
) {
|
|
191
195
|
return null;
|
|
192
196
|
}
|
|
193
197
|
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
|
+
function resolveMentionChecked(
|
|
4
|
+
manualOverride: boolean | null,
|
|
5
|
+
nodeChecked: boolean | undefined,
|
|
6
|
+
allMentionedTasksCompleted: boolean
|
|
7
|
+
) {
|
|
8
|
+
return manualOverride ?? nodeChecked ?? allMentionedTasksCompleted;
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
// Mock the Supabase client
|
|
4
12
|
vi.mock('@tuturuuu/supabase/next/client', () => ({
|
|
5
13
|
createClient: () => ({
|
|
@@ -220,8 +228,11 @@ describe('Task Mention Detection', () => {
|
|
|
220
228
|
const nodeChecked: boolean | undefined = false;
|
|
221
229
|
const allMentionedTasksCompleted = false;
|
|
222
230
|
|
|
223
|
-
const result =
|
|
224
|
-
manualOverride
|
|
231
|
+
const result = resolveMentionChecked(
|
|
232
|
+
manualOverride,
|
|
233
|
+
nodeChecked,
|
|
234
|
+
allMentionedTasksCompleted
|
|
235
|
+
);
|
|
225
236
|
|
|
226
237
|
expect(result).toBe(true);
|
|
227
238
|
});
|
|
@@ -231,8 +242,11 @@ describe('Task Mention Detection', () => {
|
|
|
231
242
|
const nodeChecked: boolean | undefined = true;
|
|
232
243
|
const allMentionedTasksCompleted = false;
|
|
233
244
|
|
|
234
|
-
const result =
|
|
235
|
-
manualOverride
|
|
245
|
+
const result = resolveMentionChecked(
|
|
246
|
+
manualOverride,
|
|
247
|
+
nodeChecked,
|
|
248
|
+
allMentionedTasksCompleted
|
|
249
|
+
);
|
|
236
250
|
|
|
237
251
|
expect(result).toBe(true);
|
|
238
252
|
});
|
|
@@ -242,8 +256,11 @@ describe('Task Mention Detection', () => {
|
|
|
242
256
|
const nodeChecked: boolean | undefined = undefined;
|
|
243
257
|
const allMentionedTasksCompleted = true;
|
|
244
258
|
|
|
245
|
-
const result =
|
|
246
|
-
manualOverride
|
|
259
|
+
const result = resolveMentionChecked(
|
|
260
|
+
manualOverride,
|
|
261
|
+
nodeChecked,
|
|
262
|
+
allMentionedTasksCompleted
|
|
263
|
+
);
|
|
247
264
|
|
|
248
265
|
expect(result).toBe(true);
|
|
249
266
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PluginKey } from '@tiptap/pm/state';
|
|
2
|
-
import {
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
3
|
import {
|
|
4
4
|
createLoadingPlaceholder,
|
|
5
5
|
findUploadPlaceholder,
|
|
@@ -68,19 +68,10 @@ describe('upload-placeholder', () => {
|
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
describe('createLoadingPlaceholder', () => {
|
|
71
|
-
let styleElement: HTMLStyleElement | null;
|
|
72
|
-
|
|
73
71
|
beforeEach(() => {
|
|
74
72
|
// Clean up any existing style elements
|
|
75
73
|
const existing = document.querySelector('#upload-spinner-styles');
|
|
76
74
|
if (existing) existing.remove();
|
|
77
|
-
styleElement = null;
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
afterEach(() => {
|
|
81
|
-
if (styleElement) {
|
|
82
|
-
styleElement.remove();
|
|
83
|
-
}
|
|
84
75
|
});
|
|
85
76
|
|
|
86
77
|
it('should create placeholder for image', () => {
|
package/src/globals.css
CHANGED
|
@@ -29,6 +29,26 @@
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/*
|
|
33
|
+
* iOS Safari zooms the viewport when a focused editable control computes below
|
|
34
|
+
* 16px. Protect touch forms across every app that consumes the shared UI styles
|
|
35
|
+
* without disabling the user's normal pinch-to-zoom controls.
|
|
36
|
+
*/
|
|
37
|
+
@supports (-webkit-touch-callout: none) {
|
|
38
|
+
@media (pointer: coarse) {
|
|
39
|
+
input:not([type="button"]):not([type="checkbox"]):not([type="color"]):not(
|
|
40
|
+
[type="file"]
|
|
41
|
+
):not([type="hidden"]):not([type="image"]):not([type="radio"]):not(
|
|
42
|
+
[type="range"]
|
|
43
|
+
):not([type="reset"]):not([type="submit"]),
|
|
44
|
+
select[class],
|
|
45
|
+
textarea[class],
|
|
46
|
+
[contenteditable="true"][class] {
|
|
47
|
+
font-size: 16px;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
32
52
|
@layer utilities {
|
|
33
53
|
body {
|
|
34
54
|
font-family: Arial, Helvetica, sans-serif;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const repoRoot = existsSync(resolve(process.cwd(), 'packages/ui/package.json'))
|
|
6
|
+
? process.cwd()
|
|
7
|
+
: resolve(process.cwd(), '../..');
|
|
8
|
+
|
|
9
|
+
function readRepoFile(path: string) {
|
|
10
|
+
return readFileSync(resolve(repoRoot, path), 'utf8');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function sourceFiles(directory: string): string[] {
|
|
14
|
+
if (!existsSync(directory)) return [];
|
|
15
|
+
|
|
16
|
+
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
17
|
+
const path = resolve(directory, entry.name);
|
|
18
|
+
if (entry.isDirectory()) return sourceFiles(path);
|
|
19
|
+
return /\.(css|ts|tsx)$/u.test(entry.name) ? [path] : [];
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('shared mobile form ergonomics', () => {
|
|
24
|
+
it('exports an iOS focus-zoom guard without disabling pinch zoom', () => {
|
|
25
|
+
const packageJson = JSON.parse(
|
|
26
|
+
readRepoFile('packages/ui/package.json')
|
|
27
|
+
) as {
|
|
28
|
+
exports: Record<string, string>;
|
|
29
|
+
};
|
|
30
|
+
const styles = readRepoFile('packages/ui/src/globals.css');
|
|
31
|
+
|
|
32
|
+
expect(packageJson.exports['./globals.css']).toBe('./src/globals.css');
|
|
33
|
+
expect(styles).toContain('@supports (-webkit-touch-callout: none)');
|
|
34
|
+
expect(styles).toContain('@media (pointer: coarse)');
|
|
35
|
+
expect(styles).toContain('input:not([type="button"])');
|
|
36
|
+
expect(styles).toContain('select[class],');
|
|
37
|
+
expect(styles).toContain('textarea[class],');
|
|
38
|
+
expect(styles).toContain('[contenteditable="true"][class]');
|
|
39
|
+
expect(styles).toMatch(/font-size:\s*16px/u);
|
|
40
|
+
expect(styles).not.toMatch(/user-scalable|maximum-scale/iu);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('reaches every web app that consumes the shared UI package', () => {
|
|
44
|
+
const appsDirectory = resolve(repoRoot, 'apps');
|
|
45
|
+
const appNames = readdirSync(appsDirectory, { withFileTypes: true })
|
|
46
|
+
.filter((entry) => entry.isDirectory())
|
|
47
|
+
.map((entry) => entry.name)
|
|
48
|
+
.filter((appName) => {
|
|
49
|
+
const manifestPath = resolve(appsDirectory, appName, 'package.json');
|
|
50
|
+
if (!existsSync(manifestPath)) return false;
|
|
51
|
+
|
|
52
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as {
|
|
53
|
+
dependencies?: Record<string, string>;
|
|
54
|
+
};
|
|
55
|
+
return Boolean(manifest.dependencies?.['@tuturuuu/ui']);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(appNames.length).toBeGreaterThan(0);
|
|
59
|
+
|
|
60
|
+
for (const appName of appNames) {
|
|
61
|
+
const appSources = sourceFiles(resolve(appsDirectory, appName, 'src'));
|
|
62
|
+
const importsSharedStyles = appSources.some((path) => {
|
|
63
|
+
const source = readFileSync(path, 'utf8');
|
|
64
|
+
return (
|
|
65
|
+
source.includes('@tuturuuu/ui/globals.css') ||
|
|
66
|
+
source.includes('@tuturuuu/tasks-ui/globals.css')
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(importsSharedStyles, `${appName} must load shared UI styles`).toBe(
|
|
71
|
+
true
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|