@tuturuuu/ui 0.16.0 → 0.17.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 +18 -0
- package/package.json +6 -5
- package/src/components/ui/storefront/browse-panel.tsx +161 -0
- package/src/components/ui/storefront/bundle-selection-dialog.tsx +1 -1
- package/src/components/ui/storefront/cart-popover.tsx +1 -1
- package/src/components/ui/storefront/hero-panel.tsx +35 -30
- package/src/components/ui/storefront/listing-card.tsx +10 -5
- package/src/components/ui/storefront/merch-sections.tsx +7 -5
- package/src/components/ui/storefront/product-detail.tsx +79 -6
- package/src/components/ui/storefront/product-dialog.tsx +5 -1
- package/src/components/ui/storefront/storefront-surface.test.tsx +72 -0
- package/src/components/ui/storefront/storefront-surface.tsx +67 -63
- package/src/components/ui/storefront/types.ts +28 -0
- package/src/components/ui/storefront/utils.ts +4 -5
- package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +59 -1
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +37 -8
- package/src/components/ui/tu-do/progress/task-progress-page.tsx +42 -54
- package/src/globals.css +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.17.0](https://github.com/tutur3u/platform/compare/ui-v0.16.0...ui-v0.17.0) (2026-07-13)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **storefront:** refine the buyer experience ([45d0e76](https://github.com/tutur3u/platform/commit/45d0e763ff2adc858aa7c76024eb4554879b3977))
|
|
9
|
+
* **tasks:** promote progress tracking workspace ([058503a](https://github.com/tutur3u/platform/commit/058503a96c8df10725ecd3f88738d574760fc944))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* **platform:** address attendance and invoice feedback ([8d2a537](https://github.com/tutur3u/platform/commit/8d2a53782d01427839607c7b0d90eaa3ecc5a1d8))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Performance Improvements
|
|
18
|
+
|
|
19
|
+
* **web:** reduce dashboard request overhead ([93f4b2b](https://github.com/tutur3u/platform/commit/93f4b2b354286b33c5cf8ff9ee0e54a31b18a022))
|
|
20
|
+
|
|
3
21
|
## [0.16.0](https://github.com/tutur3u/platform/compare/ui-v0.15.0...ui-v0.16.0) (2026-07-11)
|
|
4
22
|
|
|
5
23
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -85,12 +85,12 @@
|
|
|
85
85
|
"@tiptap/react": "3.27.3",
|
|
86
86
|
"@tiptap/starter-kit": "3.27.3",
|
|
87
87
|
"@tuturuuu/ai": "0.2.2",
|
|
88
|
-
"@tuturuuu/apis": "0.9.
|
|
88
|
+
"@tuturuuu/apis": "0.9.1",
|
|
89
89
|
"@tuturuuu/hooks": "0.0.2",
|
|
90
90
|
"@tuturuuu/icons": "0.0.6",
|
|
91
|
-
"@tuturuuu/internal-api": "0.
|
|
91
|
+
"@tuturuuu/internal-api": "0.18.0",
|
|
92
92
|
"@tuturuuu/supabase": "0.4.0",
|
|
93
|
-
"@tuturuuu/utils": "0.15.
|
|
93
|
+
"@tuturuuu/utils": "0.15.1",
|
|
94
94
|
"@types/debug": "^4.1.13",
|
|
95
95
|
"browser-image-compression": "^2.0.2",
|
|
96
96
|
"class-variance-authority": "^0.7.1",
|
|
@@ -129,6 +129,7 @@
|
|
|
129
129
|
"react-resizable-panels": "^4.12.1",
|
|
130
130
|
"react-syntax-highlighter": "^16.1.1",
|
|
131
131
|
"recharts": "3.9.2",
|
|
132
|
+
"slot-text": "^0.3.3",
|
|
132
133
|
"sonner": "^2.0.7",
|
|
133
134
|
"streamdown": "^2.5.0",
|
|
134
135
|
"tailwind-scrollbar": "^4.0.2",
|
|
@@ -149,7 +150,7 @@
|
|
|
149
150
|
"@tanstack/react-table": "^8.21.3",
|
|
150
151
|
"@testing-library/jest-dom": "^6.9.1",
|
|
151
152
|
"@testing-library/react": "^16.3.2",
|
|
152
|
-
"@tuturuuu/types": "0.
|
|
153
|
+
"@tuturuuu/types": "0.17.0",
|
|
153
154
|
"@tuturuuu/typescript-config": "0.1.1",
|
|
154
155
|
"@types/html2canvas": "^1.0.0",
|
|
155
156
|
"@types/lodash": "^4.17.24",
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Search } from '@tuturuuu/icons';
|
|
4
|
+
import type { InventoryStorefrontListing } from '@tuturuuu/internal-api/inventory';
|
|
5
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
6
|
+
import type { ReactNode } from 'react';
|
|
7
|
+
import { useMemo, useState } from 'react';
|
|
8
|
+
import { SlotText } from 'slot-text/react';
|
|
9
|
+
import { Button } from '../button';
|
|
10
|
+
import { Input } from '../input';
|
|
11
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../tabs';
|
|
12
|
+
import type { StorefrontSurfaceLabels } from './types';
|
|
13
|
+
|
|
14
|
+
type BrowseFilter = 'all' | 'bundle' | 'product';
|
|
15
|
+
|
|
16
|
+
export function StorefrontBrowsePanel({
|
|
17
|
+
compactLayout,
|
|
18
|
+
emptyListings,
|
|
19
|
+
labels,
|
|
20
|
+
listings,
|
|
21
|
+
renderListing,
|
|
22
|
+
}: {
|
|
23
|
+
compactLayout: boolean;
|
|
24
|
+
emptyListings: ReactNode;
|
|
25
|
+
labels: StorefrontSurfaceLabels;
|
|
26
|
+
listings: InventoryStorefrontListing[];
|
|
27
|
+
renderListing: (listing: InventoryStorefrontListing) => ReactNode;
|
|
28
|
+
}) {
|
|
29
|
+
const [filter, setFilter] = useState<BrowseFilter>('all');
|
|
30
|
+
const [query, setQuery] = useState('');
|
|
31
|
+
const visibleListings = useMemo(() => {
|
|
32
|
+
const normalizedQuery = query.trim().toLocaleLowerCase();
|
|
33
|
+
|
|
34
|
+
return listings.filter((listing) => {
|
|
35
|
+
if (filter !== 'all' && listing.listingType !== filter) return false;
|
|
36
|
+
if (!normalizedQuery) return true;
|
|
37
|
+
|
|
38
|
+
return [listing.title, listing.description]
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.some((value) => value?.toLocaleLowerCase().includes(normalizedQuery));
|
|
41
|
+
});
|
|
42
|
+
}, [filter, listings, query]);
|
|
43
|
+
const resultLabel = labels.visibleItems.replace(
|
|
44
|
+
'{count}',
|
|
45
|
+
String(visibleListings.length)
|
|
46
|
+
);
|
|
47
|
+
const hasActiveFilters = filter !== 'all' || query.trim().length > 0;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<section aria-labelledby="storefront-shop-heading" className="mt-10">
|
|
51
|
+
<div className="flex flex-col gap-5 border-border border-b pb-5 lg:flex-row lg:items-end lg:justify-between">
|
|
52
|
+
<div>
|
|
53
|
+
<p className="font-mono text-muted-foreground text-xs uppercase tracking-[0.16em]">
|
|
54
|
+
{labels.browse}
|
|
55
|
+
</p>
|
|
56
|
+
<h2
|
|
57
|
+
className="mt-2 font-semibold text-2xl tracking-tight"
|
|
58
|
+
id="storefront-shop-heading"
|
|
59
|
+
>
|
|
60
|
+
{labels.shopTitle}
|
|
61
|
+
</h2>
|
|
62
|
+
</div>
|
|
63
|
+
{listings.length ? (
|
|
64
|
+
<div className="grid gap-3 sm:grid-cols-[minmax(14rem,20rem)_auto] sm:items-center">
|
|
65
|
+
<label className="relative block">
|
|
66
|
+
<span className="sr-only">{labels.searchStore}</span>
|
|
67
|
+
<Search className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
|
|
68
|
+
<Input
|
|
69
|
+
className="h-10 rounded-lg border-border bg-background pl-9 shadow-none"
|
|
70
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
71
|
+
placeholder={labels.searchStore}
|
|
72
|
+
type="search"
|
|
73
|
+
value={query}
|
|
74
|
+
/>
|
|
75
|
+
</label>
|
|
76
|
+
<Tabs
|
|
77
|
+
onValueChange={(value) => setFilter(value as BrowseFilter)}
|
|
78
|
+
value={filter}
|
|
79
|
+
>
|
|
80
|
+
<TabsList className="grid h-10 w-full grid-cols-3 rounded-lg bg-muted/60 sm:w-auto">
|
|
81
|
+
<TabsTrigger onClick={() => setFilter('all')} value="all">
|
|
82
|
+
{labels.allItems}
|
|
83
|
+
</TabsTrigger>
|
|
84
|
+
<TabsTrigger
|
|
85
|
+
onClick={() => setFilter('product')}
|
|
86
|
+
value="product"
|
|
87
|
+
>
|
|
88
|
+
{labels.products}
|
|
89
|
+
</TabsTrigger>
|
|
90
|
+
<TabsTrigger onClick={() => setFilter('bundle')} value="bundle">
|
|
91
|
+
{labels.bundles}
|
|
92
|
+
</TabsTrigger>
|
|
93
|
+
</TabsList>
|
|
94
|
+
<TabsContent className="sr-only" forceMount value="all">
|
|
95
|
+
{labels.allItems}
|
|
96
|
+
</TabsContent>
|
|
97
|
+
<TabsContent className="sr-only" forceMount value="product">
|
|
98
|
+
{labels.products}
|
|
99
|
+
</TabsContent>
|
|
100
|
+
<TabsContent className="sr-only" forceMount value="bundle">
|
|
101
|
+
{labels.bundles}
|
|
102
|
+
</TabsContent>
|
|
103
|
+
</Tabs>
|
|
104
|
+
</div>
|
|
105
|
+
) : null}
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div className="sr-only" role="status">
|
|
109
|
+
{resultLabel}
|
|
110
|
+
</div>
|
|
111
|
+
<div
|
|
112
|
+
aria-hidden
|
|
113
|
+
className="mt-4 h-5 font-mono text-muted-foreground text-xs"
|
|
114
|
+
>
|
|
115
|
+
<SlotText
|
|
116
|
+
options={{ bounce: 0.12, duration: 220, stagger: 18 }}
|
|
117
|
+
text={resultLabel}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{listings.length === 0 ? (
|
|
122
|
+
<div className="mt-5">{emptyListings}</div>
|
|
123
|
+
) : visibleListings.length === 0 ? (
|
|
124
|
+
<div className="mt-5 grid min-h-64 place-items-center rounded-xl border border-border border-dashed bg-muted/15 p-8 text-center">
|
|
125
|
+
<div className="max-w-sm">
|
|
126
|
+
<Search className="mx-auto size-5 text-muted-foreground" />
|
|
127
|
+
<h3 className="mt-4 font-semibold text-lg">
|
|
128
|
+
{labels.noResultsTitle}
|
|
129
|
+
</h3>
|
|
130
|
+
<p className="mt-2 text-muted-foreground text-sm leading-6">
|
|
131
|
+
{labels.noResultsDescription}
|
|
132
|
+
</p>
|
|
133
|
+
{hasActiveFilters ? (
|
|
134
|
+
<Button
|
|
135
|
+
className="mt-4"
|
|
136
|
+
onClick={() => {
|
|
137
|
+
setFilter('all');
|
|
138
|
+
setQuery('');
|
|
139
|
+
}}
|
|
140
|
+
size="sm"
|
|
141
|
+
type="button"
|
|
142
|
+
variant="outline"
|
|
143
|
+
>
|
|
144
|
+
{labels.clearFilters}
|
|
145
|
+
</Button>
|
|
146
|
+
) : null}
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
) : (
|
|
150
|
+
<div
|
|
151
|
+
className={cn(
|
|
152
|
+
'mt-5 grid gap-5',
|
|
153
|
+
compactLayout ? 'sm:grid-cols-2' : 'sm:grid-cols-2 xl:grid-cols-3'
|
|
154
|
+
)}
|
|
155
|
+
>
|
|
156
|
+
{visibleListings.map(renderListing)}
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
</section>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
@@ -92,7 +92,7 @@ export function StorefrontBundleSelectionDialog({
|
|
|
92
92
|
}}
|
|
93
93
|
open={open}
|
|
94
94
|
>
|
|
95
|
-
<DialogContent className="grid max-h-[90dvh] max-w-[min(42rem,calc(100vw-1rem))] grid-rows-[auto_auto_minmax(0,1fr)_auto] gap-4 overflow-hidden border-border
|
|
95
|
+
<DialogContent className="grid max-h-[90dvh] max-w-[min(42rem,calc(100vw-1rem))] grid-rows-[auto_auto_minmax(0,1fr)_auto] gap-4 overflow-hidden border-border p-5 shadow-sm sm:rounded-xl">
|
|
96
96
|
<DialogHeader className="text-left">
|
|
97
97
|
<DialogTitle>{labels.bundleSelectionTitle}</DialogTitle>
|
|
98
98
|
<p className="text-muted-foreground text-sm">{listing.title}</p>
|
|
@@ -49,7 +49,7 @@ export function StorefrontCartPopover({
|
|
|
49
49
|
<PopoverContent
|
|
50
50
|
align="end"
|
|
51
51
|
className={cn(
|
|
52
|
-
'w-[min(calc(100vw-2rem),24rem)] border-border
|
|
52
|
+
'w-[min(calc(100vw-2rem),24rem)] border-border p-4 shadow-sm',
|
|
53
53
|
radius
|
|
54
54
|
)}
|
|
55
55
|
sideOffset={10}
|
|
@@ -24,13 +24,40 @@ export function StorefrontHeroPanel({
|
|
|
24
24
|
return (
|
|
25
25
|
<section
|
|
26
26
|
className={cn(
|
|
27
|
-
'
|
|
27
|
+
'grid overflow-hidden md:grid-cols-[minmax(0,1.08fr)_minmax(18rem,0.92fr)]',
|
|
28
28
|
storefrontSurfaceClasses[storefront.surfaceStyle],
|
|
29
29
|
radius
|
|
30
30
|
)}
|
|
31
31
|
>
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
<div className="flex min-h-80 flex-col justify-between gap-10 p-6 sm:p-8 lg:p-10">
|
|
33
|
+
<div className="flex items-center gap-2 font-mono text-muted-foreground text-xs uppercase tracking-[0.16em]">
|
|
34
|
+
<Store className="size-4" />
|
|
35
|
+
<span>{labels.browse}</span>
|
|
36
|
+
</div>
|
|
37
|
+
<div className="max-w-2xl">
|
|
38
|
+
<h2 className="text-balance font-semibold text-4xl tracking-[-0.035em] sm:text-5xl lg:text-6xl">
|
|
39
|
+
{storefront.name}
|
|
40
|
+
</h2>
|
|
41
|
+
<p className="mt-4 max-w-xl text-pretty text-muted-foreground leading-7">
|
|
42
|
+
{storefront.description ?? labels.fallbackDescription}
|
|
43
|
+
</p>
|
|
44
|
+
<div className="mt-6 flex flex-wrap gap-2">
|
|
45
|
+
<Badge className="border-border bg-background" variant="outline">
|
|
46
|
+
{listingsCount} {labels.product}
|
|
47
|
+
</Badge>
|
|
48
|
+
<Badge className="border-border bg-background" variant="outline">
|
|
49
|
+
{currency}
|
|
50
|
+
</Badge>
|
|
51
|
+
<Badge className="border-border bg-background" variant="outline">
|
|
52
|
+
{storefront.visibility === 'public'
|
|
53
|
+
? labels.publicStore
|
|
54
|
+
: labels.privateStore}
|
|
55
|
+
</Badge>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div className="relative min-h-64 border-border border-t md:min-h-full md:border-t-0 md:border-l">
|
|
34
61
|
{heroImage ? (
|
|
35
62
|
<StorefrontImagePanel
|
|
36
63
|
className="absolute inset-0 h-full w-full"
|
|
@@ -39,34 +66,12 @@ export function StorefrontHeroPanel({
|
|
|
39
66
|
priority
|
|
40
67
|
/>
|
|
41
68
|
) : (
|
|
42
|
-
<div className="absolute inset-0
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
{/* Overlaid title block — pulled up onto the lower, faded part of the banner. */}
|
|
49
|
-
<div className="relative -mt-20 flex flex-col gap-4 p-5">
|
|
50
|
-
<div>
|
|
51
|
-
<div className="flex items-center gap-2 text-muted-foreground text-xs">
|
|
52
|
-
<Store className="h-4 w-4" />
|
|
53
|
-
<span>{labels.browse}</span>
|
|
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>
|
|
54
73
|
</div>
|
|
55
|
-
|
|
56
|
-
{storefront.name}
|
|
57
|
-
</h2>
|
|
58
|
-
<p className="mt-2 max-w-2xl text-muted-foreground text-sm leading-6">
|
|
59
|
-
{storefront.description ?? labels.fallbackDescription}
|
|
60
|
-
</p>
|
|
61
|
-
</div>
|
|
62
|
-
<div className="flex flex-wrap gap-2">
|
|
63
|
-
<Badge className="border-border bg-background" variant="outline">
|
|
64
|
-
{listingsCount} {labels.product}
|
|
65
|
-
</Badge>
|
|
66
|
-
<Badge className="border-border bg-background" variant="outline">
|
|
67
|
-
{currency}
|
|
68
|
-
</Badge>
|
|
69
|
-
</div>
|
|
74
|
+
)}
|
|
70
75
|
</div>
|
|
71
76
|
</section>
|
|
72
77
|
);
|
|
@@ -59,10 +59,10 @@ export function StorefrontListingCard({
|
|
|
59
59
|
className={cn(
|
|
60
60
|
surfaceClassName,
|
|
61
61
|
radius,
|
|
62
|
-
'group relative overflow-hidden transition duration-200 hover
|
|
62
|
+
'group relative overflow-hidden transition duration-200 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
|
-
: 'flex min-h-full flex-col
|
|
65
|
+
: 'flex min-h-full flex-col'
|
|
66
66
|
)}
|
|
67
67
|
>
|
|
68
68
|
<div className="relative">
|
|
@@ -80,7 +80,7 @@ export function StorefrontListingCard({
|
|
|
80
80
|
className={cn(
|
|
81
81
|
'overflow-hidden transition duration-300 group-hover:scale-[1.02]',
|
|
82
82
|
radius,
|
|
83
|
-
isList ? 'aspect-square' : 'aspect-[4
|
|
83
|
+
isList ? 'aspect-square' : 'aspect-[5/4] rounded-b-none'
|
|
84
84
|
)}
|
|
85
85
|
imageUrl={listing.imageUrl}
|
|
86
86
|
label={listing.title}
|
|
@@ -92,7 +92,7 @@ export function StorefrontListingCard({
|
|
|
92
92
|
</span>
|
|
93
93
|
) : null}
|
|
94
94
|
</div>
|
|
95
|
-
<div className=
|
|
95
|
+
<div className={cn('min-w-0', isList ? null : 'px-5 pt-5')}>
|
|
96
96
|
<div className="flex flex-wrap items-center gap-2">
|
|
97
97
|
<button
|
|
98
98
|
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"
|
|
@@ -122,7 +122,12 @@ export function StorefrontListingCard({
|
|
|
122
122
|
) : null}
|
|
123
123
|
</div>
|
|
124
124
|
|
|
125
|
-
<div
|
|
125
|
+
<div
|
|
126
|
+
className={cn(
|
|
127
|
+
'mt-auto flex flex-wrap items-center justify-between gap-3',
|
|
128
|
+
isList ? null : 'px-5 pt-4 pb-5'
|
|
129
|
+
)}
|
|
130
|
+
>
|
|
126
131
|
<div className="min-w-0">
|
|
127
132
|
{hasVariants ? (
|
|
128
133
|
<p className="truncate font-semibold tabular-nums">
|
|
@@ -27,21 +27,23 @@ export function StorefrontMerchSections({
|
|
|
27
27
|
if (visibleSections.length === 0) return null;
|
|
28
28
|
|
|
29
29
|
return (
|
|
30
|
-
<div className="mt-
|
|
30
|
+
<div className="mt-6 grid gap-4">
|
|
31
31
|
{visibleSections.map((section) => {
|
|
32
32
|
const sectionHref = getSafeStorefrontHttpUrl(section.href);
|
|
33
33
|
|
|
34
34
|
return (
|
|
35
35
|
<section
|
|
36
36
|
className={cn(
|
|
37
|
-
'grid overflow-hidden border border-border bg-card md:grid-cols-[minmax(0,1fr)
|
|
37
|
+
'grid overflow-hidden border border-border bg-card md:grid-cols-[minmax(0,1fr)_minmax(16rem,0.7fr)]',
|
|
38
38
|
radius
|
|
39
39
|
)}
|
|
40
40
|
key={section.id}
|
|
41
41
|
>
|
|
42
|
-
<div className="flex min-w-0 flex-col justify-center gap-
|
|
42
|
+
<div className="flex min-w-0 flex-col justify-center gap-3 p-6 sm:p-8">
|
|
43
43
|
{section.title ? (
|
|
44
|
-
<h2 className="font-semibold text-
|
|
44
|
+
<h2 className="text-balance font-semibold text-2xl tracking-tight">
|
|
45
|
+
{section.title}
|
|
46
|
+
</h2>
|
|
45
47
|
) : null}
|
|
46
48
|
{section.description ? (
|
|
47
49
|
<p className="text-muted-foreground text-sm leading-6">
|
|
@@ -50,7 +52,7 @@ export function StorefrontMerchSections({
|
|
|
50
52
|
) : null}
|
|
51
53
|
{sectionHref ? (
|
|
52
54
|
<a
|
|
53
|
-
className="mt-
|
|
55
|
+
className="mt-2 w-fit border-border border-b pb-1 font-medium text-sm transition hover:border-foreground"
|
|
54
56
|
href={sectionHref}
|
|
55
57
|
>
|
|
56
58
|
{sectionHref.replace(/^https?:\/\//u, '')}
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
ArrowRight,
|
|
5
|
+
Minus,
|
|
6
|
+
MonitorSmartphone,
|
|
7
|
+
Plus,
|
|
8
|
+
ShieldCheck,
|
|
9
|
+
ShoppingCart,
|
|
10
|
+
Zap,
|
|
11
|
+
} from '@tuturuuu/icons';
|
|
4
12
|
import type {
|
|
5
13
|
InventoryListingVariant,
|
|
14
|
+
InventoryStorefront,
|
|
6
15
|
InventoryStorefrontListing,
|
|
7
16
|
} from '@tuturuuu/internal-api/inventory';
|
|
8
17
|
import { cn } from '@tuturuuu/utils/format';
|
|
@@ -30,6 +39,7 @@ import {
|
|
|
30
39
|
export function StorefrontProductDetail({
|
|
31
40
|
cartHref,
|
|
32
41
|
cartLines,
|
|
42
|
+
checkoutMode,
|
|
33
43
|
currency,
|
|
34
44
|
isSubmitting = false,
|
|
35
45
|
labels,
|
|
@@ -39,11 +49,13 @@ export function StorefrontProductDetail({
|
|
|
39
49
|
onIncrement,
|
|
40
50
|
quantity,
|
|
41
51
|
radius,
|
|
52
|
+
presentation = 'page',
|
|
42
53
|
showInventoryBadges,
|
|
43
54
|
surfaceClassName,
|
|
44
55
|
}: {
|
|
45
56
|
cartHref?: string;
|
|
46
57
|
cartLines?: StorefrontCartLine[];
|
|
58
|
+
checkoutMode?: InventoryStorefront['checkoutMode'];
|
|
47
59
|
currency: string;
|
|
48
60
|
isSubmitting?: boolean;
|
|
49
61
|
labels: StorefrontSurfaceLabels;
|
|
@@ -57,6 +69,7 @@ export function StorefrontProductDetail({
|
|
|
57
69
|
) => void;
|
|
58
70
|
quantity: number;
|
|
59
71
|
radius: string;
|
|
72
|
+
presentation?: 'dialog' | 'page';
|
|
60
73
|
showInventoryBadges: boolean;
|
|
61
74
|
surfaceClassName: string;
|
|
62
75
|
}) {
|
|
@@ -114,19 +127,45 @@ export function StorefrontProductDetail({
|
|
|
114
127
|
compareAtPrice && compareAtPrice > displayPrice
|
|
115
128
|
? Math.round((1 - displayPrice / compareAtPrice) * 100)
|
|
116
129
|
: 0;
|
|
130
|
+
const isDialog = presentation === 'dialog';
|
|
131
|
+
const isSquareTerminal = checkoutMode === 'square_terminal';
|
|
132
|
+
const isPolar = checkoutMode === 'polar';
|
|
117
133
|
|
|
118
134
|
return (
|
|
119
|
-
<div
|
|
120
|
-
|
|
135
|
+
<div
|
|
136
|
+
className={cn(
|
|
137
|
+
'grid min-w-0',
|
|
138
|
+
isDialog
|
|
139
|
+
? 'lg:grid-cols-[minmax(0,1.08fr)_minmax(22rem,0.92fr)]'
|
|
140
|
+
: 'gap-6 lg:grid-cols-2'
|
|
141
|
+
)}
|
|
142
|
+
>
|
|
143
|
+
<div
|
|
144
|
+
className={cn(
|
|
145
|
+
'overflow-hidden',
|
|
146
|
+
surfaceClassName,
|
|
147
|
+
isDialog ? 'border-border border-b lg:border-r lg:border-b-0' : radius
|
|
148
|
+
)}
|
|
149
|
+
>
|
|
121
150
|
<StorefrontImagePanel
|
|
122
|
-
className=
|
|
151
|
+
className={cn(
|
|
152
|
+
isDialog
|
|
153
|
+
? 'aspect-[4/3] min-h-72 lg:aspect-auto lg:h-full lg:min-h-[36rem]'
|
|
154
|
+
: 'aspect-square'
|
|
155
|
+
)}
|
|
123
156
|
imageUrl={imageUrl}
|
|
124
157
|
label={listing.title}
|
|
125
158
|
priority
|
|
126
159
|
/>
|
|
127
160
|
</div>
|
|
128
161
|
|
|
129
|
-
<div
|
|
162
|
+
<div
|
|
163
|
+
className={cn(
|
|
164
|
+
'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'
|
|
167
|
+
)}
|
|
168
|
+
>
|
|
130
169
|
<div className="flex flex-wrap items-center gap-2">
|
|
131
170
|
<Badge className="border-border bg-background" variant="outline">
|
|
132
171
|
{listing.listingType === 'bundle' ? labels.bundle : labels.product}
|
|
@@ -141,7 +180,7 @@ export function StorefrontProductDetail({
|
|
|
141
180
|
) : null}
|
|
142
181
|
</div>
|
|
143
182
|
|
|
144
|
-
<h2 className="text-balance font-semibold text-
|
|
183
|
+
<h2 className="text-balance break-words font-semibold text-2xl tracking-tight sm:text-3xl lg:text-4xl">
|
|
145
184
|
{listing.title}
|
|
146
185
|
</h2>
|
|
147
186
|
|
|
@@ -218,6 +257,40 @@ export function StorefrontProductDetail({
|
|
|
218
257
|
{listing.description ?? labels.fallbackDescription}
|
|
219
258
|
</p>
|
|
220
259
|
|
|
260
|
+
{isSquareTerminal || isPolar ? (
|
|
261
|
+
<div
|
|
262
|
+
className={cn(
|
|
263
|
+
'flex items-start gap-3 border border-border bg-muted/25 p-3.5',
|
|
264
|
+
radius
|
|
265
|
+
)}
|
|
266
|
+
>
|
|
267
|
+
<span
|
|
268
|
+
className={cn(
|
|
269
|
+
'grid size-9 shrink-0 place-items-center border border-border bg-background',
|
|
270
|
+
radius
|
|
271
|
+
)}
|
|
272
|
+
>
|
|
273
|
+
{isSquareTerminal ? (
|
|
274
|
+
<MonitorSmartphone className="size-4" />
|
|
275
|
+
) : (
|
|
276
|
+
<ShieldCheck className="size-4" />
|
|
277
|
+
)}
|
|
278
|
+
</span>
|
|
279
|
+
<div className="min-w-0">
|
|
280
|
+
<p className="font-medium text-sm">
|
|
281
|
+
{isSquareTerminal
|
|
282
|
+
? labels.squareTerminal
|
|
283
|
+
: labels.onlineCheckout}
|
|
284
|
+
</p>
|
|
285
|
+
<p className="mt-0.5 text-muted-foreground text-xs leading-5">
|
|
286
|
+
{isSquareTerminal
|
|
287
|
+
? labels.squareTerminalDescription
|
|
288
|
+
: labels.onlineCheckoutDescription}
|
|
289
|
+
</p>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
) : null}
|
|
293
|
+
|
|
221
294
|
<div className="mt-auto flex flex-wrap items-center gap-3 border-border border-t pt-5">
|
|
222
295
|
{canChange ? (
|
|
223
296
|
needsSelection ? (
|
|
@@ -13,6 +13,7 @@ import type { StorefrontCartLine, StorefrontSurfaceLabels } from './types';
|
|
|
13
13
|
export function StorefrontProductDialog({
|
|
14
14
|
cartHref,
|
|
15
15
|
cartLines,
|
|
16
|
+
checkoutMode,
|
|
16
17
|
currency,
|
|
17
18
|
isSubmitting,
|
|
18
19
|
labels,
|
|
@@ -27,6 +28,7 @@ export function StorefrontProductDialog({
|
|
|
27
28
|
}: {
|
|
28
29
|
cartHref?: string;
|
|
29
30
|
cartLines?: StorefrontCartLine[];
|
|
31
|
+
checkoutMode?: Parameters<typeof StorefrontProductDetail>[0]['checkoutMode'];
|
|
30
32
|
currency: string;
|
|
31
33
|
isSubmitting?: boolean;
|
|
32
34
|
labels: StorefrontSurfaceLabels;
|
|
@@ -45,13 +47,14 @@ export function StorefrontProductDialog({
|
|
|
45
47
|
}) {
|
|
46
48
|
return (
|
|
47
49
|
<Dialog onOpenChange={onOpenChange} open={Boolean(listing)}>
|
|
48
|
-
<DialogContent className="max-h-[
|
|
50
|
+
<DialogContent className="max-h-[92dvh] w-[calc(100%-1rem)] max-w-none overflow-hidden border-border p-0 shadow-2xl sm:w-[calc(100%-2rem)] sm:max-w-6xl">
|
|
49
51
|
{listing ? (
|
|
50
52
|
<>
|
|
51
53
|
<DialogTitle className="sr-only">{listing.title}</DialogTitle>
|
|
52
54
|
<StorefrontProductDetail
|
|
53
55
|
cartHref={cartHref}
|
|
54
56
|
cartLines={cartLines}
|
|
57
|
+
checkoutMode={checkoutMode}
|
|
55
58
|
currency={currency}
|
|
56
59
|
isSubmitting={isSubmitting}
|
|
57
60
|
labels={labels}
|
|
@@ -61,6 +64,7 @@ export function StorefrontProductDialog({
|
|
|
61
64
|
onIncrement={onIncrement}
|
|
62
65
|
quantity={0}
|
|
63
66
|
radius={radius}
|
|
67
|
+
presentation="dialog"
|
|
64
68
|
showInventoryBadges={showInventoryBadges}
|
|
65
69
|
surfaceClassName={surfaceClassName}
|
|
66
70
|
/>
|