@tuturuuu/ui 0.18.0 → 0.19.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.
- package/CHANGELOG.md +32 -0
- package/package.json +70 -69
- 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
|
@@ -105,6 +105,16 @@ describe('wallets data table infinite loading', () => {
|
|
|
105
105
|
mocks.searchParams = new URLSearchParams();
|
|
106
106
|
|
|
107
107
|
globalThis.IntersectionObserver = class IntersectionObserver {
|
|
108
|
+
readonly callback: IntersectionObserverCallback;
|
|
109
|
+
readonly options?: IntersectionObserverInit;
|
|
110
|
+
|
|
111
|
+
constructor(
|
|
112
|
+
callback: IntersectionObserverCallback,
|
|
113
|
+
options?: IntersectionObserverInit
|
|
114
|
+
) {
|
|
115
|
+
this.callback = callback;
|
|
116
|
+
this.options = options;
|
|
117
|
+
}
|
|
108
118
|
disconnect() {}
|
|
109
119
|
observe() {}
|
|
110
120
|
takeRecords() {
|
|
@@ -5,11 +5,13 @@ import type { ReactNode } from 'react';
|
|
|
5
5
|
|
|
6
6
|
export function AccentButton({
|
|
7
7
|
children,
|
|
8
|
+
className,
|
|
8
9
|
disabled,
|
|
9
10
|
onClick,
|
|
10
11
|
radius,
|
|
11
12
|
}: {
|
|
12
13
|
children: ReactNode;
|
|
14
|
+
className?: string;
|
|
13
15
|
disabled?: boolean;
|
|
14
16
|
onClick?: () => void;
|
|
15
17
|
radius: string;
|
|
@@ -21,7 +23,8 @@ export function AccentButton({
|
|
|
21
23
|
'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
22
24
|
'[--accent-bg:var(--storefront-accent,var(--primary))] [--accent-fg:var(--storefront-accent-foreground,var(--primary-foreground))]',
|
|
23
25
|
'bg-[var(--accent-bg)] text-[var(--accent-fg)] hover:opacity-90',
|
|
24
|
-
radius
|
|
26
|
+
radius,
|
|
27
|
+
className
|
|
25
28
|
)}
|
|
26
29
|
disabled={disabled}
|
|
27
30
|
onClick={onClick}
|
|
@@ -16,12 +16,14 @@ type BrowseFilter = 'all' | 'bundle' | 'product';
|
|
|
16
16
|
export function StorefrontBrowsePanel({
|
|
17
17
|
compactLayout,
|
|
18
18
|
emptyListings,
|
|
19
|
+
flushTop = false,
|
|
19
20
|
labels,
|
|
20
21
|
listings,
|
|
21
22
|
renderListing,
|
|
22
23
|
}: {
|
|
23
24
|
compactLayout: boolean;
|
|
24
25
|
emptyListings: ReactNode;
|
|
26
|
+
flushTop?: boolean;
|
|
25
27
|
labels: StorefrontSurfaceLabels;
|
|
26
28
|
listings: InventoryStorefrontListing[];
|
|
27
29
|
renderListing: (listing: InventoryStorefrontListing) => ReactNode;
|
|
@@ -47,7 +49,10 @@ export function StorefrontBrowsePanel({
|
|
|
47
49
|
const hasActiveFilters = filter !== 'all' || query.trim().length > 0;
|
|
48
50
|
|
|
49
51
|
return (
|
|
50
|
-
<section
|
|
52
|
+
<section
|
|
53
|
+
aria-labelledby="storefront-shop-heading"
|
|
54
|
+
className={flushTop ? undefined : 'mt-10'}
|
|
55
|
+
>
|
|
51
56
|
<div className="flex flex-col gap-5 border-border border-b pb-5 lg:flex-row lg:items-end lg:justify-between">
|
|
52
57
|
<div>
|
|
53
58
|
<p className="font-mono text-muted-foreground text-xs uppercase tracking-[0.16em]">
|
|
@@ -14,6 +14,7 @@ import { AccentButton } from './accent-button';
|
|
|
14
14
|
import type {
|
|
15
15
|
StorefrontBuyerDefaults,
|
|
16
16
|
StorefrontCartEntry,
|
|
17
|
+
StorefrontLinkComponent,
|
|
17
18
|
StorefrontSurfaceLabels,
|
|
18
19
|
} from './types';
|
|
19
20
|
import {
|
|
@@ -68,6 +69,7 @@ export function CartActions({
|
|
|
68
69
|
isPreview,
|
|
69
70
|
isSubmitting,
|
|
70
71
|
labels,
|
|
72
|
+
linkComponent,
|
|
71
73
|
onCheckoutOpen,
|
|
72
74
|
onInstantCheckout,
|
|
73
75
|
radius,
|
|
@@ -78,13 +80,20 @@ export function CartActions({
|
|
|
78
80
|
isPreview: boolean;
|
|
79
81
|
isSubmitting: boolean;
|
|
80
82
|
labels: StorefrontSurfaceLabels;
|
|
83
|
+
linkComponent?: StorefrontLinkComponent;
|
|
81
84
|
onCheckoutOpen?: () => void;
|
|
82
85
|
onInstantCheckout?: () => void;
|
|
83
86
|
radius: string;
|
|
84
87
|
}) {
|
|
88
|
+
const NavigationLink = linkComponent ?? 'a';
|
|
85
89
|
if (isPreview || isCheckoutDisabled) {
|
|
86
90
|
return (
|
|
87
|
-
<Button
|
|
91
|
+
<Button
|
|
92
|
+
className={cn('w-full', radius)}
|
|
93
|
+
disabled
|
|
94
|
+
type="button"
|
|
95
|
+
variant="secondary"
|
|
96
|
+
>
|
|
88
97
|
{labels.checkoutDisabled}
|
|
89
98
|
</Button>
|
|
90
99
|
);
|
|
@@ -92,7 +101,12 @@ export function CartActions({
|
|
|
92
101
|
|
|
93
102
|
if (!canOpenCheckout) {
|
|
94
103
|
return (
|
|
95
|
-
<Button
|
|
104
|
+
<Button
|
|
105
|
+
className={cn('w-full', radius)}
|
|
106
|
+
disabled
|
|
107
|
+
type="button"
|
|
108
|
+
variant="secondary"
|
|
109
|
+
>
|
|
96
110
|
{labels.checkout}
|
|
97
111
|
<ArrowRight className="size-4 shrink-0" />
|
|
98
112
|
</Button>
|
|
@@ -122,10 +136,10 @@ export function CartActions({
|
|
|
122
136
|
</AccentButton>
|
|
123
137
|
) : (
|
|
124
138
|
<Button asChild className={cn('w-full', radius)} variant="outline">
|
|
125
|
-
<
|
|
139
|
+
<NavigationLink href={checkoutHref ?? '#'}>
|
|
126
140
|
{labels.checkout}
|
|
127
141
|
<ArrowRight className="size-4 shrink-0" />
|
|
128
|
-
</
|
|
142
|
+
</NavigationLink>
|
|
129
143
|
</Button>
|
|
130
144
|
)}
|
|
131
145
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { ArrowRight } from '@tuturuuu/icons';
|
|
3
|
+
import { ArrowRight, ShoppingCart } 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';
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import type {
|
|
16
16
|
StorefrontBuyerDefaults,
|
|
17
17
|
StorefrontCartEntry,
|
|
18
|
+
StorefrontLinkComponent,
|
|
18
19
|
StorefrontSurfaceLabels,
|
|
19
20
|
} from './types';
|
|
20
21
|
import { formatStorefrontPrice, storefrontSurfaceClasses } from './utils';
|
|
@@ -31,6 +32,7 @@ export function StorefrontCartSummary({
|
|
|
31
32
|
isPreview,
|
|
32
33
|
isSubmitting,
|
|
33
34
|
labels,
|
|
35
|
+
linkComponent,
|
|
34
36
|
onCheckoutOpen,
|
|
35
37
|
onCheckoutSubmit,
|
|
36
38
|
onInstantCheckout,
|
|
@@ -48,6 +50,7 @@ export function StorefrontCartSummary({
|
|
|
48
50
|
isPreview: boolean;
|
|
49
51
|
isSubmitting: boolean;
|
|
50
52
|
labels: StorefrontSurfaceLabels;
|
|
53
|
+
linkComponent?: StorefrontLinkComponent;
|
|
51
54
|
onCheckoutOpen?: () => void;
|
|
52
55
|
onCheckoutSubmit?: (formData: FormData) => void;
|
|
53
56
|
onInstantCheckout?: () => void;
|
|
@@ -113,19 +116,31 @@ export function StorefrontCartSummary({
|
|
|
113
116
|
className={cn(
|
|
114
117
|
'h-fit',
|
|
115
118
|
presentation === 'panel'
|
|
116
|
-
? cn(
|
|
119
|
+
? cn(
|
|
120
|
+
'grid gap-5 p-5 sm:p-6',
|
|
121
|
+
storefrontSurfaceClasses[storefront.surfaceStyle],
|
|
122
|
+
radius
|
|
123
|
+
)
|
|
117
124
|
: 'grid gap-4',
|
|
118
125
|
className
|
|
119
126
|
)}
|
|
120
127
|
>
|
|
121
|
-
<div>
|
|
122
|
-
<div className="flex items-center
|
|
123
|
-
<
|
|
128
|
+
<div className="grid gap-2">
|
|
129
|
+
<div className="flex items-center gap-3">
|
|
130
|
+
<span
|
|
131
|
+
className={cn(
|
|
132
|
+
'grid size-9 shrink-0 place-items-center border border-border bg-background',
|
|
133
|
+
radius
|
|
134
|
+
)}
|
|
135
|
+
>
|
|
136
|
+
<ShoppingCart className="size-4" />
|
|
137
|
+
</span>
|
|
138
|
+
<p className="min-w-0 flex-1 font-semibold">{labels.cart}</p>
|
|
124
139
|
<Badge className="border-border bg-background" variant="outline">
|
|
125
140
|
{cartEntries.length}
|
|
126
141
|
</Badge>
|
|
127
142
|
</div>
|
|
128
|
-
<p className="
|
|
143
|
+
<p className="text-pretty text-muted-foreground text-sm leading-6">
|
|
129
144
|
{labels.reservedCopy}
|
|
130
145
|
</p>
|
|
131
146
|
</div>
|
|
@@ -146,6 +161,7 @@ export function StorefrontCartSummary({
|
|
|
146
161
|
isPreview={isPreview}
|
|
147
162
|
isSubmitting={isSubmitting}
|
|
148
163
|
labels={labels}
|
|
164
|
+
linkComponent={linkComponent}
|
|
149
165
|
onCheckoutOpen={onCheckoutOpen}
|
|
150
166
|
onInstantCheckout={onInstantCheckout}
|
|
151
167
|
radius={radius}
|
|
@@ -21,6 +21,8 @@ export function StorefrontHeroPanel({
|
|
|
21
21
|
}) {
|
|
22
22
|
const heroImage = storefront.coverImageUrl ?? storefront.heroImageUrl;
|
|
23
23
|
|
|
24
|
+
if (!heroImage) return null;
|
|
25
|
+
|
|
24
26
|
return (
|
|
25
27
|
<section
|
|
26
28
|
className={cn(
|
|
@@ -38,9 +40,11 @@ export function StorefrontHeroPanel({
|
|
|
38
40
|
<h2 className="text-balance font-semibold text-4xl tracking-[-0.035em] sm:text-5xl lg:text-6xl">
|
|
39
41
|
{storefront.name}
|
|
40
42
|
</h2>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
{storefront.description?.trim() ? (
|
|
44
|
+
<p className="mt-4 max-w-xl text-pretty text-muted-foreground leading-7">
|
|
45
|
+
{storefront.description.trim()}
|
|
46
|
+
</p>
|
|
47
|
+
) : null}
|
|
44
48
|
<div className="mt-6 flex flex-wrap gap-2">
|
|
45
49
|
<Badge className="border-border bg-background" variant="outline">
|
|
46
50
|
{listingsCount} {labels.product}
|
|
@@ -58,20 +62,12 @@ export function StorefrontHeroPanel({
|
|
|
58
62
|
</div>
|
|
59
63
|
|
|
60
64
|
<div className="relative min-h-64 border-border border-t md:min-h-full md:border-t-0 md:border-l">
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
/>
|
|
68
|
-
) : (
|
|
69
|
-
<div className="absolute inset-0 grid place-items-center bg-muted/35">
|
|
70
|
-
<div className="grid size-24 place-items-center rounded-xl border border-border bg-background/80">
|
|
71
|
-
<Store className="size-8 text-muted-foreground" />
|
|
72
|
-
</div>
|
|
73
|
-
</div>
|
|
74
|
-
)}
|
|
65
|
+
<StorefrontImagePanel
|
|
66
|
+
className="absolute inset-0 h-full w-full"
|
|
67
|
+
imageUrl={heroImage}
|
|
68
|
+
label={storefront.name}
|
|
69
|
+
priority
|
|
70
|
+
/>
|
|
75
71
|
</div>
|
|
76
72
|
</section>
|
|
77
73
|
);
|
|
@@ -6,11 +6,13 @@ export type {
|
|
|
6
6
|
StorefrontBuyerDefaults,
|
|
7
7
|
StorefrontCartEntry,
|
|
8
8
|
StorefrontCartLine,
|
|
9
|
+
StorefrontLinkComponent,
|
|
9
10
|
StorefrontSurfaceLabels,
|
|
10
11
|
StorefrontSurfaceMode,
|
|
11
12
|
} from './types';
|
|
12
13
|
export {
|
|
13
14
|
formatStorefrontPrice,
|
|
15
|
+
getAccentStyle,
|
|
14
16
|
getStorefrontLinePrice,
|
|
15
17
|
getStorefrontListingFromPrice,
|
|
16
18
|
getStorefrontListingLimit,
|
|
@@ -59,7 +59,7 @@ export function StorefrontListingCard({
|
|
|
59
59
|
className={cn(
|
|
60
60
|
surfaceClassName,
|
|
61
61
|
radius,
|
|
62
|
-
'group relative overflow-hidden transition duration-200 hover:border-foreground/20 hover:shadow-foreground/5 hover:shadow-sm',
|
|
62
|
+
'group relative overflow-hidden transition duration-200 [contain-intrinsic-size:auto_420px] [content-visibility:auto] hover:border-foreground/20 hover:shadow-foreground/5 hover:shadow-sm',
|
|
63
63
|
isList
|
|
64
64
|
? 'grid gap-4 p-3 sm:grid-cols-[112px_minmax(0,1fr)_auto] sm:items-center'
|
|
65
65
|
: 'flex min-h-full flex-col'
|
|
@@ -106,9 +106,11 @@ export function StorefrontListingCard({
|
|
|
106
106
|
{listing.listingType === 'bundle' ? labels.bundle : labels.product}
|
|
107
107
|
</Badge>
|
|
108
108
|
</div>
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
{listing.description?.trim() ? (
|
|
110
|
+
<p className="mt-1 line-clamp-2 text-muted-foreground text-sm leading-6">
|
|
111
|
+
{listing.description.trim()}
|
|
112
|
+
</p>
|
|
113
|
+
) : null}
|
|
112
114
|
{showInventoryBadges ? (
|
|
113
115
|
<p className="mt-2 text-muted-foreground text-xs">
|
|
114
116
|
{limit === 0
|
|
@@ -20,7 +20,11 @@ import { Badge } from '../badge';
|
|
|
20
20
|
import { Button } from '../button';
|
|
21
21
|
import { AccentButton } from './accent-button';
|
|
22
22
|
import { StorefrontImagePanel } from './image-panel';
|
|
23
|
-
import type {
|
|
23
|
+
import type {
|
|
24
|
+
StorefrontCartLine,
|
|
25
|
+
StorefrontLinkComponent,
|
|
26
|
+
StorefrontSurfaceLabels,
|
|
27
|
+
} from './types';
|
|
24
28
|
import {
|
|
25
29
|
formatStorefrontPrice,
|
|
26
30
|
getStorefrontLinePrice,
|
|
@@ -43,6 +47,7 @@ export function StorefrontProductDetail({
|
|
|
43
47
|
currency,
|
|
44
48
|
isSubmitting = false,
|
|
45
49
|
labels,
|
|
50
|
+
linkComponent,
|
|
46
51
|
listing,
|
|
47
52
|
onBuyNow,
|
|
48
53
|
onDecrement,
|
|
@@ -59,6 +64,7 @@ export function StorefrontProductDetail({
|
|
|
59
64
|
currency: string;
|
|
60
65
|
isSubmitting?: boolean;
|
|
61
66
|
labels: StorefrontSurfaceLabels;
|
|
67
|
+
linkComponent?: StorefrontLinkComponent;
|
|
62
68
|
listing: InventoryStorefrontListing;
|
|
63
69
|
onBuyNow?: (listingId: string, variantId?: string | null) => void;
|
|
64
70
|
onDecrement?: (listingId: string, variantId?: string | null) => void;
|
|
@@ -73,6 +79,7 @@ export function StorefrontProductDetail({
|
|
|
73
79
|
showInventoryBadges: boolean;
|
|
74
80
|
surfaceClassName: string;
|
|
75
81
|
}) {
|
|
82
|
+
const NavigationLink = linkComponent ?? 'a';
|
|
76
83
|
const options = listing.options ?? [];
|
|
77
84
|
const variants = useMemo(
|
|
78
85
|
() => getStorefrontListingVariants(listing),
|
|
@@ -130,13 +137,15 @@ export function StorefrontProductDetail({
|
|
|
130
137
|
const isDialog = presentation === 'dialog';
|
|
131
138
|
const isSquareTerminal = checkoutMode === 'square_terminal';
|
|
132
139
|
const isPolar = checkoutMode === 'polar';
|
|
140
|
+
const description = listing.description?.trim();
|
|
141
|
+
const hasMatchedActions = canChange && Boolean(onBuyNow);
|
|
133
142
|
|
|
134
143
|
return (
|
|
135
144
|
<div
|
|
136
145
|
className={cn(
|
|
137
146
|
'grid min-w-0',
|
|
138
147
|
isDialog
|
|
139
|
-
? 'lg:grid-cols-[minmax(0,1.08fr)_minmax(22rem,0.92fr)]'
|
|
148
|
+
? 'lg:h-[min(44rem,88dvh)] lg:grid-cols-[minmax(0,1.08fr)_minmax(22rem,0.92fr)]'
|
|
140
149
|
: 'gap-6 lg:grid-cols-2'
|
|
141
150
|
)}
|
|
142
151
|
>
|
|
@@ -150,7 +159,7 @@ export function StorefrontProductDetail({
|
|
|
150
159
|
<StorefrontImagePanel
|
|
151
160
|
className={cn(
|
|
152
161
|
isDialog
|
|
153
|
-
? 'aspect-[4/3] min-h-72 lg:aspect-auto lg:h-full lg:min-h-
|
|
162
|
+
? 'aspect-[4/3] min-h-72 lg:aspect-auto lg:h-full lg:min-h-0'
|
|
154
163
|
: 'aspect-square'
|
|
155
164
|
)}
|
|
156
165
|
imageUrl={imageUrl}
|
|
@@ -162,8 +171,7 @@ export function StorefrontProductDetail({
|
|
|
162
171
|
<div
|
|
163
172
|
className={cn(
|
|
164
173
|
'flex min-w-0 flex-col gap-5',
|
|
165
|
-
isDialog &&
|
|
166
|
-
'p-5 sm:p-7 lg:max-h-[min(44rem,90dvh)] lg:overflow-y-auto lg:p-9'
|
|
174
|
+
isDialog && 'p-5 sm:p-7 lg:h-full lg:overflow-y-auto lg:p-9'
|
|
167
175
|
)}
|
|
168
176
|
>
|
|
169
177
|
<div className="flex flex-wrap items-center gap-2">
|
|
@@ -253,9 +261,11 @@ export function StorefrontProductDetail({
|
|
|
253
261
|
</p>
|
|
254
262
|
) : null}
|
|
255
263
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
264
|
+
{description ? (
|
|
265
|
+
<p className="text-pretty text-muted-foreground leading-7">
|
|
266
|
+
{description}
|
|
267
|
+
</p>
|
|
268
|
+
) : null}
|
|
259
269
|
|
|
260
270
|
{isSquareTerminal || isPolar ? (
|
|
261
271
|
<div
|
|
@@ -291,14 +301,24 @@ export function StorefrontProductDetail({
|
|
|
291
301
|
</div>
|
|
292
302
|
) : null}
|
|
293
303
|
|
|
294
|
-
<div
|
|
304
|
+
<div
|
|
305
|
+
className={cn(
|
|
306
|
+
'mt-auto grid w-full grid-cols-1 gap-2 border-border border-t pt-5',
|
|
307
|
+
hasMatchedActions && 'grid-cols-2'
|
|
308
|
+
)}
|
|
309
|
+
>
|
|
295
310
|
{canChange ? (
|
|
296
311
|
needsSelection ? (
|
|
297
|
-
<Button
|
|
312
|
+
<Button
|
|
313
|
+
className={cn('h-11 w-full', radius)}
|
|
314
|
+
disabled
|
|
315
|
+
type="button"
|
|
316
|
+
variant="secondary"
|
|
317
|
+
>
|
|
298
318
|
{labels.selectOptions}
|
|
299
319
|
</Button>
|
|
300
320
|
) : cartQuantity > 0 ? (
|
|
301
|
-
<div className="
|
|
321
|
+
<div className="grid h-11 w-full grid-cols-[2.75rem_minmax(2.5rem,1fr)_2.75rem] items-center gap-1">
|
|
302
322
|
<Button
|
|
303
323
|
aria-label={`${labels.quantity} -`}
|
|
304
324
|
className={cn('h-11 w-11 p-0', radius)}
|
|
@@ -323,36 +343,46 @@ export function StorefrontProductDetail({
|
|
|
323
343
|
</Button>
|
|
324
344
|
</div>
|
|
325
345
|
) : (
|
|
326
|
-
<
|
|
346
|
+
<Button
|
|
347
|
+
className={cn('h-11 w-full', radius)}
|
|
327
348
|
disabled={addDisabled}
|
|
328
349
|
onClick={() => onIncrement?.(listing.id, limit, variantId)}
|
|
329
|
-
|
|
350
|
+
type="button"
|
|
351
|
+
variant="secondary"
|
|
330
352
|
>
|
|
331
353
|
<Plus className="h-4 w-4" />
|
|
332
354
|
{soldOut ? labels.soldOut : labels.add}
|
|
333
|
-
</
|
|
355
|
+
</Button>
|
|
334
356
|
)
|
|
335
357
|
) : null}
|
|
336
358
|
|
|
337
359
|
{onBuyNow ? (
|
|
338
|
-
<
|
|
339
|
-
className=
|
|
360
|
+
<AccentButton
|
|
361
|
+
className="h-11 w-full"
|
|
340
362
|
disabled={buyNowDisabled}
|
|
341
363
|
onClick={() => onBuyNow(listing.id, variantId)}
|
|
342
|
-
|
|
364
|
+
radius={radius}
|
|
343
365
|
>
|
|
344
366
|
<Zap className="size-4 shrink-0" />
|
|
345
367
|
{labels.buyNow}
|
|
346
|
-
</
|
|
368
|
+
</AccentButton>
|
|
347
369
|
) : null}
|
|
348
370
|
|
|
349
371
|
{cartHref && cartQuantity > 0 ? (
|
|
350
|
-
<Button
|
|
351
|
-
|
|
372
|
+
<Button
|
|
373
|
+
asChild
|
|
374
|
+
className={cn(
|
|
375
|
+
'h-11 w-full',
|
|
376
|
+
hasMatchedActions && 'col-span-2',
|
|
377
|
+
radius
|
|
378
|
+
)}
|
|
379
|
+
variant="outline"
|
|
380
|
+
>
|
|
381
|
+
<NavigationLink href={cartHref}>
|
|
352
382
|
<ShoppingCart className="size-4 shrink-0" />
|
|
353
383
|
{labels.cart}
|
|
354
384
|
<ArrowRight className="size-4 shrink-0" />
|
|
355
|
-
</
|
|
385
|
+
</NavigationLink>
|
|
356
386
|
</Button>
|
|
357
387
|
) : null}
|
|
358
388
|
</div>
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
import type { InventoryStorefrontListing } from '@tuturuuu/internal-api/inventory';
|
|
4
4
|
import { Dialog, DialogContent, DialogTitle } from '../dialog';
|
|
5
5
|
import { StorefrontProductDetail } from './product-detail';
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
StorefrontCartLine,
|
|
8
|
+
StorefrontLinkComponent,
|
|
9
|
+
StorefrontSurfaceLabels,
|
|
10
|
+
} from './types';
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* Large product detail dialog opened from a listing card. Reuses the same
|
|
@@ -17,6 +21,7 @@ export function StorefrontProductDialog({
|
|
|
17
21
|
currency,
|
|
18
22
|
isSubmitting,
|
|
19
23
|
labels,
|
|
24
|
+
linkComponent,
|
|
20
25
|
listing,
|
|
21
26
|
onBuyNow,
|
|
22
27
|
onDecrement,
|
|
@@ -32,6 +37,7 @@ export function StorefrontProductDialog({
|
|
|
32
37
|
currency: string;
|
|
33
38
|
isSubmitting?: boolean;
|
|
34
39
|
labels: StorefrontSurfaceLabels;
|
|
40
|
+
linkComponent?: StorefrontLinkComponent;
|
|
35
41
|
listing: InventoryStorefrontListing | null;
|
|
36
42
|
onBuyNow?: (listingId: string, variantId?: string | null) => void;
|
|
37
43
|
onDecrement?: (listingId: string, variantId?: string | null) => void;
|
|
@@ -58,6 +64,7 @@ export function StorefrontProductDialog({
|
|
|
58
64
|
currency={currency}
|
|
59
65
|
isSubmitting={isSubmitting}
|
|
60
66
|
labels={labels}
|
|
67
|
+
linkComponent={linkComponent}
|
|
61
68
|
listing={listing}
|
|
62
69
|
onBuyNow={onBuyNow}
|
|
63
70
|
onDecrement={onDecrement}
|
|
@@ -162,6 +162,22 @@ describe('StorefrontSurface', () => {
|
|
|
162
162
|
expect(sanitizeStorefrontAccentColor('red')).toBeNull();
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
+
it('omits the duplicate hero card when no cover media is configured', () => {
|
|
166
|
+
render(
|
|
167
|
+
<StorefrontSurface
|
|
168
|
+
listings={[listing]}
|
|
169
|
+
mode="store"
|
|
170
|
+
storefront={storefront}
|
|
171
|
+
/>
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
expect(screen.getAllByText('Preview Store')).toHaveLength(1);
|
|
175
|
+
expect(screen.queryByText('Buyer-facing copy')).not.toBeInTheDocument();
|
|
176
|
+
expect(
|
|
177
|
+
screen.getByRole('heading', { name: 'Shop' }).closest('section')
|
|
178
|
+
).not.toHaveClass('mt-10');
|
|
179
|
+
});
|
|
180
|
+
|
|
165
181
|
it('renders a preview empty state without enabling checkout', () => {
|
|
166
182
|
render(
|
|
167
183
|
<StorefrontSurface
|
|
@@ -176,7 +192,7 @@ describe('StorefrontSurface', () => {
|
|
|
176
192
|
/>
|
|
177
193
|
);
|
|
178
194
|
|
|
179
|
-
expect(screen.getAllByText('Preview Store')).toHaveLength(
|
|
195
|
+
expect(screen.getAllByText('Preview Store')).toHaveLength(1);
|
|
180
196
|
expect(screen.getByText('No buyer listings')).toBeInTheDocument();
|
|
181
197
|
expect(screen.getByText('Create a listing next.')).toBeInTheDocument();
|
|
182
198
|
fireEvent.click(screen.getByRole('button', { name: 'Cart: 0' }));
|
|
@@ -244,6 +260,7 @@ describe('StorefrontSurface', () => {
|
|
|
244
260
|
it('opens a wide, readable Square Terminal product dialog', () => {
|
|
245
261
|
const longListing = {
|
|
246
262
|
...listing,
|
|
263
|
+
description: null,
|
|
247
264
|
title: 'Shikishi Board (8.8 x 10 inches, 253 mm x 225 mm)',
|
|
248
265
|
};
|
|
249
266
|
|
|
@@ -252,6 +269,7 @@ describe('StorefrontSurface', () => {
|
|
|
252
269
|
detailListingId={longListing.id}
|
|
253
270
|
labels={{
|
|
254
271
|
buyNow: 'Pay at terminal',
|
|
272
|
+
fallbackDescription: 'Available from this workspace storefront.',
|
|
255
273
|
squareTerminal: 'Square Terminal',
|
|
256
274
|
squareTerminalDescription: 'Finish payment on the counter device.',
|
|
257
275
|
}}
|
|
@@ -259,6 +277,7 @@ describe('StorefrontSurface', () => {
|
|
|
259
277
|
mode="store"
|
|
260
278
|
onBuyNow={() => undefined}
|
|
261
279
|
onDetailListingChange={() => undefined}
|
|
280
|
+
onIncrement={() => undefined}
|
|
262
281
|
storefront={{ ...storefront, checkoutMode: 'square_terminal' }}
|
|
263
282
|
/>
|
|
264
283
|
);
|
|
@@ -283,6 +302,16 @@ describe('StorefrontSurface', () => {
|
|
|
283
302
|
expect(
|
|
284
303
|
screen.getByRole('button', { name: 'Pay at terminal' })
|
|
285
304
|
).toBeEnabled();
|
|
305
|
+
expect(
|
|
306
|
+
screen.queryByText('Available from this workspace storefront.')
|
|
307
|
+
).not.toBeInTheDocument();
|
|
308
|
+
|
|
309
|
+
const addButton = screen.getByRole('button', { name: 'Add' });
|
|
310
|
+
const payButton = screen.getByRole('button', { name: 'Pay at terminal' });
|
|
311
|
+
expect(addButton).toHaveClass('h-11', 'w-full', 'bg-secondary');
|
|
312
|
+
expect(payButton).toHaveClass('h-11', 'w-full');
|
|
313
|
+
expect(addButton.parentElement).toBe(payButton.parentElement);
|
|
314
|
+
expect(addButton.parentElement).toHaveClass('grid-cols-2');
|
|
286
315
|
});
|
|
287
316
|
|
|
288
317
|
it('keeps the compatibility cart page as a full cart review', () => {
|
|
@@ -299,7 +328,12 @@ describe('StorefrontSurface', () => {
|
|
|
299
328
|
/>
|
|
300
329
|
);
|
|
301
330
|
|
|
302
|
-
expect(screen.getByRole('region', { name: 'Cart' })).
|
|
331
|
+
expect(screen.getByRole('region', { name: 'Cart' })).toHaveClass(
|
|
332
|
+
'grid',
|
|
333
|
+
'gap-5',
|
|
334
|
+
'p-5',
|
|
335
|
+
'sm:p-6'
|
|
336
|
+
);
|
|
303
337
|
expect(screen.getByText('1:1 Mentoring')).toBeInTheDocument();
|
|
304
338
|
expect(screen.getAllByText('$2.00')).toHaveLength(2);
|
|
305
339
|
expect(screen.getByRole('button', { name: /Checkout/ })).toBeEnabled();
|
|
@@ -379,6 +413,48 @@ describe('StorefrontSurface', () => {
|
|
|
379
413
|
);
|
|
380
414
|
});
|
|
381
415
|
|
|
416
|
+
it('keeps a configured bundle fixed price independent of component count', () => {
|
|
417
|
+
const fixedBundle: InventoryBundle = {
|
|
418
|
+
...categoryBundle,
|
|
419
|
+
id: 'fixed-bundle',
|
|
420
|
+
name: 'Three for two dollars',
|
|
421
|
+
price: 200,
|
|
422
|
+
pricingMode: 'fixed_price',
|
|
423
|
+
};
|
|
424
|
+
const fixedListing: InventoryStorefrontListing = {
|
|
425
|
+
...bundleListing,
|
|
426
|
+
bundleId: fixedBundle.id,
|
|
427
|
+
id: 'fixed-bundle-listing',
|
|
428
|
+
price: 200,
|
|
429
|
+
title: fixedBundle.name,
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
render(
|
|
433
|
+
<StorefrontSurface
|
|
434
|
+
bundles={[fixedBundle]}
|
|
435
|
+
cartLines={[
|
|
436
|
+
{
|
|
437
|
+
bundleSelections: {
|
|
438
|
+
'component-keychains': [
|
|
439
|
+
{ listingId: 'listing-keychain-a', quantity: 1 },
|
|
440
|
+
{ listingId: 'listing-keychain-b', quantity: 1 },
|
|
441
|
+
{ listingId: 'listing-keychain-c', quantity: 1 },
|
|
442
|
+
],
|
|
443
|
+
},
|
|
444
|
+
listingId: fixedListing.id,
|
|
445
|
+
quantity: 1,
|
|
446
|
+
},
|
|
447
|
+
]}
|
|
448
|
+
listings={[fixedListing]}
|
|
449
|
+
mode="cart"
|
|
450
|
+
storefront={storefront}
|
|
451
|
+
/>
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
expect(screen.getAllByText('$2.00').length).toBeGreaterThan(0);
|
|
455
|
+
expect(screen.queryByText('$27.00')).not.toBeInTheDocument();
|
|
456
|
+
});
|
|
457
|
+
|
|
382
458
|
it('prefills checkout buyer details while keeping editable form fields', () => {
|
|
383
459
|
render(
|
|
384
460
|
<StorefrontSurface
|
|
@@ -420,7 +496,7 @@ describe('StorefrontSurface', () => {
|
|
|
420
496
|
);
|
|
421
497
|
|
|
422
498
|
expect(screen.queryByText('Simulated checkout')).not.toBeInTheDocument();
|
|
423
|
-
expect(screen.getAllByText('Preview Store')).toHaveLength(
|
|
499
|
+
expect(screen.getAllByText('Preview Store')).toHaveLength(1);
|
|
424
500
|
});
|
|
425
501
|
|
|
426
502
|
it('blocks disabled checkout without showing checkout mode badges', () => {
|