@tuturuuu/ui 0.6.1 → 0.7.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 +25 -0
- package/README.md +3 -3
- package/biome.json +1 -1
- package/package.json +8 -8
- package/src/components/ui/calendar-app/components/calendar-connections.tsx +17 -13
- package/src/components/ui/calendar-app/components/connected-accounts-dialog.tsx +2 -5
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +2 -5
- package/src/components/ui/calendar.test.tsx +24 -0
- package/src/components/ui/calendar.tsx +1 -0
- package/src/components/ui/date-time-picker.tsx +352 -234
- package/src/components/ui/finance/categories-tags-tabs.tsx +23 -1
- package/src/components/ui/finance/command/finance-command-actions.test.tsx +48 -0
- package/src/components/ui/finance/command/finance-command-actions.tsx +200 -0
- package/src/components/ui/finance/command/finance-command-provider.test.tsx +151 -0
- package/src/components/ui/finance/command/finance-command-provider.tsx +250 -0
- package/src/components/ui/finance/command/finance-command-results.tsx +262 -0
- package/src/components/ui/finance/invoices/pending-invoices-table.tsx +22 -9
- package/src/components/ui/finance/shared/quick-actions.tsx +39 -90
- package/src/components/ui/finance/tags/tag-manager.tsx +24 -5
- package/src/components/ui/finance/transactions/form-basic-tab.tsx +33 -49
- package/src/components/ui/finance/transactions/form-types.ts +3 -0
- package/src/components/ui/finance/transactions/form.test.tsx +105 -22
- package/src/components/ui/finance/transactions/form.tsx +116 -20
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +13 -6
- package/src/components/ui/finance/transactions/transaction-edit-dialog.test.tsx +25 -1
- package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +16 -3
- package/src/components/ui/finance/transactions/transactionId/transaction-details-client-page.tsx +3 -0
- package/src/components/ui/finance/transactions/transactionId/transaction-details-page.tsx +3 -0
- package/src/components/ui/finance/transactions/transactions-create-summary.tsx +6 -0
- package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +20 -2
- package/src/components/ui/finance/transactions/transactions-page.tsx +4 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +7 -2
- package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +7 -2
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +38 -1
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +5 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +18 -2
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +3 -0
- package/src/components/ui/finance/wallets/wallets-page.tsx +3 -0
- package/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx +2 -9
- package/src/components/ui/optional-time-picker.tsx +95 -0
- package/src/components/ui/quick-command-center.test.tsx +90 -0
- package/src/components/ui/quick-command-center.tsx +190 -0
- package/src/components/ui/storefront/cart-summary.tsx +18 -27
- package/src/components/ui/storefront/hero-panel.tsx +22 -13
- package/src/components/ui/storefront/storefront-surface.test.tsx +8 -4
- package/src/components/ui/storefront/storefront-surface.tsx +84 -41
- package/src/components/ui/storefront/types.ts +2 -0
- package/src/components/ui/storefront/utils.ts +21 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.test.ts +171 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.ts +200 -36
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +21 -2
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
5
|
+
import {
|
|
6
|
+
CommandDialog,
|
|
7
|
+
CommandEmpty,
|
|
8
|
+
CommandGroup,
|
|
9
|
+
CommandInput,
|
|
10
|
+
CommandItem,
|
|
11
|
+
CommandList,
|
|
12
|
+
CommandShortcut,
|
|
13
|
+
} from './command';
|
|
14
|
+
|
|
15
|
+
export interface QuickCommandCenterItem {
|
|
16
|
+
id: string;
|
|
17
|
+
title: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
icon?: ReactNode;
|
|
21
|
+
keywords?: string[];
|
|
22
|
+
onSelect: () => void;
|
|
23
|
+
shortcut?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface QuickCommandCenterGroup {
|
|
27
|
+
heading: string;
|
|
28
|
+
id: string;
|
|
29
|
+
items: QuickCommandCenterItem[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface QuickCommandCenterProps {
|
|
33
|
+
digitShortcuts?: boolean;
|
|
34
|
+
emptyLabel: string;
|
|
35
|
+
groups: QuickCommandCenterGroup[];
|
|
36
|
+
onOpenChange: (open: boolean) => void;
|
|
37
|
+
onSearchValueChange?: (value: string) => void;
|
|
38
|
+
open: boolean;
|
|
39
|
+
placeholder: string;
|
|
40
|
+
searchValue?: string;
|
|
41
|
+
title: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function matchesSearch(item: QuickCommandCenterItem, query: string) {
|
|
46
|
+
if (!query) return true;
|
|
47
|
+
|
|
48
|
+
const haystack = [
|
|
49
|
+
item.title,
|
|
50
|
+
item.description,
|
|
51
|
+
item.shortcut,
|
|
52
|
+
...(item.keywords ?? []),
|
|
53
|
+
]
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.join(' ')
|
|
56
|
+
.toLowerCase();
|
|
57
|
+
|
|
58
|
+
return haystack.includes(query.toLowerCase());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function QuickCommandCenter({
|
|
62
|
+
digitShortcuts = false,
|
|
63
|
+
emptyLabel,
|
|
64
|
+
groups,
|
|
65
|
+
onOpenChange,
|
|
66
|
+
onSearchValueChange,
|
|
67
|
+
open,
|
|
68
|
+
placeholder,
|
|
69
|
+
searchValue,
|
|
70
|
+
title,
|
|
71
|
+
description,
|
|
72
|
+
}: QuickCommandCenterProps) {
|
|
73
|
+
const [internalSearch, setInternalSearch] = useState('');
|
|
74
|
+
const query = searchValue ?? internalSearch;
|
|
75
|
+
const setQuery = onSearchValueChange ?? setInternalSearch;
|
|
76
|
+
|
|
77
|
+
const visibleGroups = useMemo(
|
|
78
|
+
() =>
|
|
79
|
+
groups
|
|
80
|
+
.map((group) => ({
|
|
81
|
+
...group,
|
|
82
|
+
items: group.items.filter((item) => matchesSearch(item, query)),
|
|
83
|
+
}))
|
|
84
|
+
.filter((group) => group.items.length > 0),
|
|
85
|
+
[groups, query]
|
|
86
|
+
);
|
|
87
|
+
const visibleItems = visibleGroups.flatMap((group) => group.items);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!(open && digitShortcuts)) return;
|
|
91
|
+
|
|
92
|
+
const handleDigitShortcut = (event: KeyboardEvent) => {
|
|
93
|
+
if (
|
|
94
|
+
event.metaKey ||
|
|
95
|
+
event.ctrlKey ||
|
|
96
|
+
event.altKey ||
|
|
97
|
+
event.isComposing ||
|
|
98
|
+
!/^[0-9]$/u.test(event.key)
|
|
99
|
+
) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const index = event.key === '0' ? 9 : Number(event.key) - 1;
|
|
104
|
+
const item = visibleItems[index];
|
|
105
|
+
|
|
106
|
+
if (!item || item.disabled) return;
|
|
107
|
+
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
event.stopPropagation();
|
|
110
|
+
event.stopImmediatePropagation();
|
|
111
|
+
item.onSelect();
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
window.addEventListener('keydown', handleDigitShortcut, {
|
|
115
|
+
capture: true,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return () =>
|
|
119
|
+
window.removeEventListener('keydown', handleDigitShortcut, {
|
|
120
|
+
capture: true,
|
|
121
|
+
});
|
|
122
|
+
}, [digitShortcuts, open, visibleItems]);
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<CommandDialog
|
|
126
|
+
open={open}
|
|
127
|
+
onOpenChange={onOpenChange}
|
|
128
|
+
title={title}
|
|
129
|
+
description={description ?? title}
|
|
130
|
+
contentClassName="max-w-xl"
|
|
131
|
+
>
|
|
132
|
+
<CommandInput
|
|
133
|
+
placeholder={placeholder}
|
|
134
|
+
value={query}
|
|
135
|
+
onValueChange={setQuery}
|
|
136
|
+
/>
|
|
137
|
+
<CommandList className="max-h-[min(70vh,28rem)]">
|
|
138
|
+
<CommandEmpty>{emptyLabel}</CommandEmpty>
|
|
139
|
+
{visibleGroups.map((group) => (
|
|
140
|
+
<CommandGroup heading={group.heading} key={group.id}>
|
|
141
|
+
{group.items.map((item) => {
|
|
142
|
+
const itemIndex = visibleItems.findIndex(
|
|
143
|
+
(visibleItem) => visibleItem.id === item.id
|
|
144
|
+
);
|
|
145
|
+
const digitShortcut =
|
|
146
|
+
digitShortcuts && itemIndex >= 0 && itemIndex < 10
|
|
147
|
+
? itemIndex === 9
|
|
148
|
+
? '0'
|
|
149
|
+
: String(itemIndex + 1)
|
|
150
|
+
: item.shortcut;
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<CommandItem
|
|
154
|
+
disabled={item.disabled}
|
|
155
|
+
key={item.id}
|
|
156
|
+
onSelect={() => {
|
|
157
|
+
if (!item.disabled) item.onSelect();
|
|
158
|
+
}}
|
|
159
|
+
value={[
|
|
160
|
+
item.title,
|
|
161
|
+
item.description,
|
|
162
|
+
item.shortcut,
|
|
163
|
+
...(item.keywords ?? []),
|
|
164
|
+
]
|
|
165
|
+
.filter(Boolean)
|
|
166
|
+
.join(' ')}
|
|
167
|
+
>
|
|
168
|
+
{item.icon}
|
|
169
|
+
<span className="min-w-0 flex-1">
|
|
170
|
+
<span className="block truncate font-medium">
|
|
171
|
+
{item.title}
|
|
172
|
+
</span>
|
|
173
|
+
{item.description && (
|
|
174
|
+
<span className="block truncate text-muted-foreground text-xs">
|
|
175
|
+
{item.description}
|
|
176
|
+
</span>
|
|
177
|
+
)}
|
|
178
|
+
</span>
|
|
179
|
+
{digitShortcut && (
|
|
180
|
+
<CommandShortcut>{digitShortcut}</CommandShortcut>
|
|
181
|
+
)}
|
|
182
|
+
</CommandItem>
|
|
183
|
+
);
|
|
184
|
+
})}
|
|
185
|
+
</CommandGroup>
|
|
186
|
+
))}
|
|
187
|
+
</CommandList>
|
|
188
|
+
</CommandDialog>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { ArrowRight, TriangleAlert } from '@tuturuuu/icons';
|
|
3
|
+
import { ArrowRight, Tag, TriangleAlert } 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';
|
|
@@ -37,7 +37,9 @@ export function StorefrontCartSummary({
|
|
|
37
37
|
}) {
|
|
38
38
|
const hasCart = cartEntries.length > 0;
|
|
39
39
|
const isCheckoutDisabled = storefront.checkoutMode === 'disabled';
|
|
40
|
-
const submitDisabled =
|
|
40
|
+
const submitDisabled =
|
|
41
|
+
!hasCart || isSubmitting || isCheckoutDisabled || !onCheckoutSubmit;
|
|
42
|
+
const canOpenCheckout = hasCart && Boolean(checkoutHref);
|
|
41
43
|
|
|
42
44
|
return (
|
|
43
45
|
<aside
|
|
@@ -77,6 +79,12 @@ export function StorefrontCartSummary({
|
|
|
77
79
|
{formatStorefrontPrice(total, currency)}
|
|
78
80
|
</span>
|
|
79
81
|
</div>
|
|
82
|
+
{hasCart && !isCheckoutDisabled ? (
|
|
83
|
+
<p className="mt-3 flex items-center gap-2 rounded-md border border-border border-dashed bg-muted/30 px-3 py-2 text-muted-foreground text-xs leading-5">
|
|
84
|
+
<Tag className="h-3.5 w-3.5 shrink-0" />
|
|
85
|
+
{labels.couponNote}
|
|
86
|
+
</p>
|
|
87
|
+
) : null}
|
|
80
88
|
{!hasCart ? (
|
|
81
89
|
<p className="mt-4 flex items-center gap-2 rounded-md border border-border bg-muted/40 px-3 py-2 text-muted-foreground text-sm">
|
|
82
90
|
<TriangleAlert className="h-4 w-4" />
|
|
@@ -91,24 +99,6 @@ export function StorefrontCartSummary({
|
|
|
91
99
|
onCheckoutSubmit?.(new FormData(event.currentTarget));
|
|
92
100
|
}}
|
|
93
101
|
>
|
|
94
|
-
<input
|
|
95
|
-
className="h-10 rounded-md border border-input bg-background px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
|
|
96
|
-
name="name"
|
|
97
|
-
placeholder={labels.form.name}
|
|
98
|
-
required
|
|
99
|
-
/>
|
|
100
|
-
<input
|
|
101
|
-
className="h-10 rounded-md border border-input bg-background px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
|
|
102
|
-
name="email"
|
|
103
|
-
placeholder={labels.form.email}
|
|
104
|
-
required
|
|
105
|
-
type="email"
|
|
106
|
-
/>
|
|
107
|
-
<input
|
|
108
|
-
className="h-10 rounded-md border border-input bg-background px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
|
|
109
|
-
name="phone"
|
|
110
|
-
placeholder={labels.form.phone}
|
|
111
|
-
/>
|
|
112
102
|
<textarea
|
|
113
103
|
className="min-h-20 rounded-md border border-input bg-background px-3 py-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
|
|
114
104
|
name="note"
|
|
@@ -123,17 +113,18 @@ export function StorefrontCartSummary({
|
|
|
123
113
|
<Button className={cn('mt-4 w-full', radius)} disabled type="button">
|
|
124
114
|
{labels.checkoutDisabled}
|
|
125
115
|
</Button>
|
|
126
|
-
) : (
|
|
127
|
-
<Button
|
|
128
|
-
|
|
129
|
-
className={cn('mt-4 w-full', radius)}
|
|
130
|
-
disabled={!hasCart}
|
|
131
|
-
>
|
|
132
|
-
<a aria-disabled={!hasCart} href={hasCart ? checkoutHref : undefined}>
|
|
116
|
+
) : canOpenCheckout ? (
|
|
117
|
+
<Button asChild className={cn('mt-4 w-full', radius)}>
|
|
118
|
+
<a href={checkoutHref}>
|
|
133
119
|
{labels.checkout}
|
|
134
120
|
<ArrowRight className="h-4 w-4" />
|
|
135
121
|
</a>
|
|
136
122
|
</Button>
|
|
123
|
+
) : (
|
|
124
|
+
<Button className={cn('mt-4 w-full', radius)} disabled type="button">
|
|
125
|
+
{labels.checkout}
|
|
126
|
+
<ArrowRight className="h-4 w-4" />
|
|
127
|
+
</Button>
|
|
137
128
|
)}
|
|
138
129
|
</aside>
|
|
139
130
|
);
|
|
@@ -19,18 +19,33 @@ export function StorefrontHeroPanel({
|
|
|
19
19
|
radius: string;
|
|
20
20
|
storefront: InventoryStorefront;
|
|
21
21
|
}) {
|
|
22
|
+
const heroImage = storefront.coverImageUrl ?? storefront.heroImageUrl;
|
|
23
|
+
|
|
22
24
|
return (
|
|
23
25
|
<section
|
|
24
26
|
className={cn(
|
|
25
|
-
'
|
|
27
|
+
'relative isolate overflow-hidden',
|
|
26
28
|
storefrontSurfaceClasses[storefront.surfaceStyle],
|
|
27
|
-
radius
|
|
28
|
-
storefront.themePreset === 'editorial'
|
|
29
|
-
? 'md:grid-cols-[minmax(0,1.15fr)_360px]'
|
|
30
|
-
: 'md:grid-cols-[minmax(0,1fr)_280px]'
|
|
29
|
+
radius
|
|
31
30
|
)}
|
|
32
31
|
>
|
|
33
|
-
|
|
32
|
+
{/* Full-width featured banner backdrop. */}
|
|
33
|
+
<div className="relative h-40 w-full sm:h-52 md:h-60">
|
|
34
|
+
{heroImage ? (
|
|
35
|
+
<StorefrontImagePanel
|
|
36
|
+
className="absolute inset-0 h-full w-full"
|
|
37
|
+
imageUrl={heroImage}
|
|
38
|
+
label={storefront.name}
|
|
39
|
+
/>
|
|
40
|
+
) : (
|
|
41
|
+
<div className="absolute inset-0 bg-gradient-to-br from-muted via-muted/60 to-background" />
|
|
42
|
+
)}
|
|
43
|
+
{/* Scrim that fades the banner into the page so overlaid text stays legible. */}
|
|
44
|
+
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/55 to-transparent" />
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
{/* Overlaid title block — pulled up onto the lower, faded part of the banner. */}
|
|
48
|
+
<div className="relative -mt-20 flex flex-col gap-4 p-5">
|
|
34
49
|
<div>
|
|
35
50
|
<div className="flex items-center gap-2 text-muted-foreground text-xs">
|
|
36
51
|
<Store className="h-4 w-4" />
|
|
@@ -38,7 +53,7 @@ export function StorefrontHeroPanel({
|
|
|
38
53
|
</div>
|
|
39
54
|
<h2
|
|
40
55
|
className={cn(
|
|
41
|
-
'mt-
|
|
56
|
+
'mt-2 text-balance font-semibold tracking-normal',
|
|
42
57
|
storefront.themePreset === 'editorial'
|
|
43
58
|
? 'text-3xl md:text-4xl'
|
|
44
59
|
: 'text-2xl'
|
|
@@ -59,12 +74,6 @@ export function StorefrontHeroPanel({
|
|
|
59
74
|
</Badge>
|
|
60
75
|
</div>
|
|
61
76
|
</div>
|
|
62
|
-
|
|
63
|
-
<StorefrontImagePanel
|
|
64
|
-
className="min-h-44 md:min-h-full"
|
|
65
|
-
imageUrl={storefront.heroImageUrl}
|
|
66
|
-
label={storefront.name}
|
|
67
|
-
/>
|
|
68
77
|
</section>
|
|
69
78
|
);
|
|
70
79
|
}
|
|
@@ -6,7 +6,9 @@ import { sanitizeStorefrontAccentColor } from './utils';
|
|
|
6
6
|
|
|
7
7
|
const storefront: InventoryStorefront = {
|
|
8
8
|
accentColor: '#abc',
|
|
9
|
+
analyticsEnabled: true,
|
|
9
10
|
cornerStyle: 'rounded',
|
|
11
|
+
coverImageUrl: null,
|
|
10
12
|
createdAt: '2026-06-12T00:00:00.000Z',
|
|
11
13
|
currency: 'USD',
|
|
12
14
|
checkoutMode: 'polar',
|
|
@@ -16,6 +18,7 @@ const storefront: InventoryStorefront = {
|
|
|
16
18
|
layoutStyle: 'grid',
|
|
17
19
|
listingsCount: 0,
|
|
18
20
|
name: 'Preview Store',
|
|
21
|
+
sections: [],
|
|
19
22
|
showInventoryBadges: true,
|
|
20
23
|
slug: 'preview-store',
|
|
21
24
|
status: 'published',
|
|
@@ -53,7 +56,7 @@ describe('StorefrontSurface', () => {
|
|
|
53
56
|
expect(screen.getByText('Preview checkout disabled')).toBeDisabled();
|
|
54
57
|
});
|
|
55
58
|
|
|
56
|
-
it('
|
|
59
|
+
it('keeps simulated storefront chrome customer-facing', () => {
|
|
57
60
|
render(
|
|
58
61
|
<StorefrontSurface
|
|
59
62
|
labels={{ simulatedBadge: 'Simulated checkout' }}
|
|
@@ -63,10 +66,11 @@ describe('StorefrontSurface', () => {
|
|
|
63
66
|
/>
|
|
64
67
|
);
|
|
65
68
|
|
|
66
|
-
expect(screen.
|
|
69
|
+
expect(screen.queryByText('Simulated checkout')).not.toBeInTheDocument();
|
|
70
|
+
expect(screen.getAllByText('Preview Store')).toHaveLength(2);
|
|
67
71
|
});
|
|
68
72
|
|
|
69
|
-
it('
|
|
73
|
+
it('blocks disabled checkout without showing checkout mode badges', () => {
|
|
70
74
|
render(
|
|
71
75
|
<StorefrontSurface
|
|
72
76
|
labels={{
|
|
@@ -79,7 +83,7 @@ describe('StorefrontSurface', () => {
|
|
|
79
83
|
/>
|
|
80
84
|
);
|
|
81
85
|
|
|
82
|
-
expect(screen.
|
|
86
|
+
expect(screen.queryByText('Checkout disabled')).not.toBeInTheDocument();
|
|
83
87
|
expect(screen.getByText('Checkout unavailable')).toBeDisabled();
|
|
84
88
|
});
|
|
85
89
|
});
|
|
@@ -4,13 +4,14 @@ import { ShoppingCart } from '@tuturuuu/icons';
|
|
|
4
4
|
import type {
|
|
5
5
|
InventoryStorefront,
|
|
6
6
|
InventoryStorefrontListing,
|
|
7
|
+
InventoryStorefrontSection,
|
|
7
8
|
} from '@tuturuuu/internal-api/inventory';
|
|
8
9
|
import { cn } from '@tuturuuu/utils/format';
|
|
9
10
|
import type { ReactNode } from 'react';
|
|
10
|
-
import { Badge } from '../badge';
|
|
11
11
|
import { StorefrontCartSummary } from './cart-summary';
|
|
12
12
|
import { StorefrontEmptyListings } from './empty-listings';
|
|
13
13
|
import { StorefrontHeroPanel } from './hero-panel';
|
|
14
|
+
import { StorefrontImagePanel } from './image-panel';
|
|
14
15
|
import { StorefrontListingCard } from './listing-card';
|
|
15
16
|
import type {
|
|
16
17
|
StorefrontCartLine,
|
|
@@ -24,15 +25,17 @@ import {
|
|
|
24
25
|
sanitizeStorefrontAccentColor,
|
|
25
26
|
storefrontRadiusClasses,
|
|
26
27
|
storefrontSurfaceClasses,
|
|
28
|
+
storefrontThemeClasses,
|
|
27
29
|
} from './utils';
|
|
28
30
|
|
|
29
31
|
export function StorefrontSurface({
|
|
30
32
|
cartLines = [],
|
|
31
33
|
checkoutHref,
|
|
32
34
|
className,
|
|
35
|
+
compactLayout = false,
|
|
33
36
|
emptyAction,
|
|
34
37
|
headerActions,
|
|
35
|
-
isDemo = false,
|
|
38
|
+
isDemo: _isDemo = false,
|
|
36
39
|
isSubmitting = false,
|
|
37
40
|
labels: labelOverrides,
|
|
38
41
|
listings,
|
|
@@ -47,6 +50,7 @@ export function StorefrontSurface({
|
|
|
47
50
|
cartLines?: StorefrontCartLine[];
|
|
48
51
|
checkoutHref?: string;
|
|
49
52
|
className?: string;
|
|
53
|
+
compactLayout?: boolean;
|
|
50
54
|
emptyAction?: ReactNode;
|
|
51
55
|
headerActions?: ReactNode;
|
|
52
56
|
isDemo?: boolean;
|
|
@@ -97,7 +101,11 @@ export function StorefrontSurface({
|
|
|
97
101
|
|
|
98
102
|
return (
|
|
99
103
|
<main
|
|
100
|
-
className={cn(
|
|
104
|
+
className={cn(
|
|
105
|
+
'min-h-dvh bg-background text-foreground',
|
|
106
|
+
storefrontThemeClasses[storefront.themePreset],
|
|
107
|
+
className
|
|
108
|
+
)}
|
|
101
109
|
style={getAccentStyle(accentColor)}
|
|
102
110
|
>
|
|
103
111
|
{notice ? (
|
|
@@ -109,43 +117,14 @@ export function StorefrontSurface({
|
|
|
109
117
|
<header className="border-border border-b bg-background/90 backdrop-blur">
|
|
110
118
|
<div className="mx-auto flex max-w-7xl flex-wrap items-center justify-between gap-3 px-4 py-3">
|
|
111
119
|
<div className="min-w-0">
|
|
112
|
-
<
|
|
113
|
-
<span>
|
|
114
|
-
{storefront.visibility === 'private'
|
|
115
|
-
? labels.privateStore
|
|
116
|
-
: labels.publicStore}
|
|
117
|
-
</span>
|
|
118
|
-
{isDemo ? (
|
|
119
|
-
<Badge variant="secondary">{labels.demoBadge}</Badge>
|
|
120
|
-
) : null}
|
|
121
|
-
{isPreview ? (
|
|
122
|
-
<Badge
|
|
123
|
-
className="border-border bg-background"
|
|
124
|
-
variant="outline"
|
|
125
|
-
>
|
|
126
|
-
{labels.previewBadge}
|
|
127
|
-
</Badge>
|
|
128
|
-
) : null}
|
|
129
|
-
{storefront.checkoutMode === 'simulated' ? (
|
|
130
|
-
<Badge
|
|
131
|
-
className="border-border bg-background"
|
|
132
|
-
variant="outline"
|
|
133
|
-
>
|
|
134
|
-
{labels.simulatedBadge}
|
|
135
|
-
</Badge>
|
|
136
|
-
) : null}
|
|
137
|
-
{storefront.checkoutMode === 'disabled' ? (
|
|
138
|
-
<Badge
|
|
139
|
-
className="border-border bg-background"
|
|
140
|
-
variant="outline"
|
|
141
|
-
>
|
|
142
|
-
{labels.checkoutDisabledBadge}
|
|
143
|
-
</Badge>
|
|
144
|
-
) : null}
|
|
145
|
-
</div>
|
|
146
|
-
<h1 className="mt-0.5 truncate font-semibold text-xl">
|
|
120
|
+
<h1 className="truncate font-semibold text-xl">
|
|
147
121
|
{storefront.name}
|
|
148
122
|
</h1>
|
|
123
|
+
{storefront.description ? (
|
|
124
|
+
<p className="mt-0.5 line-clamp-1 text-muted-foreground text-sm">
|
|
125
|
+
{storefront.description}
|
|
126
|
+
</p>
|
|
127
|
+
) : null}
|
|
149
128
|
</div>
|
|
150
129
|
<div className="flex items-center gap-2">
|
|
151
130
|
{headerActions}
|
|
@@ -162,7 +141,12 @@ export function StorefrontSurface({
|
|
|
162
141
|
</div>
|
|
163
142
|
</header>
|
|
164
143
|
|
|
165
|
-
<section
|
|
144
|
+
<section
|
|
145
|
+
className={cn(
|
|
146
|
+
'mx-auto grid max-w-7xl gap-4 px-4 py-5',
|
|
147
|
+
compactLayout ? 'grid-cols-1' : 'lg:grid-cols-[minmax(0,1fr)_340px]'
|
|
148
|
+
)}
|
|
149
|
+
>
|
|
166
150
|
<div className="min-w-0">
|
|
167
151
|
<StorefrontHeroPanel
|
|
168
152
|
currency={currency}
|
|
@@ -172,13 +156,19 @@ export function StorefrontSurface({
|
|
|
172
156
|
storefront={storefront}
|
|
173
157
|
/>
|
|
174
158
|
|
|
159
|
+
<StorefrontMerchSections
|
|
160
|
+
radius={radius}
|
|
161
|
+
sections={storefront.sections ?? []}
|
|
162
|
+
/>
|
|
163
|
+
|
|
175
164
|
<div
|
|
176
165
|
className={cn(
|
|
177
166
|
'mt-4',
|
|
178
|
-
storefront.layoutStyle === 'list'
|
|
167
|
+
compactLayout || storefront.layoutStyle === 'list'
|
|
179
168
|
? 'grid gap-3'
|
|
180
169
|
: 'grid gap-3 sm:grid-cols-2 xl:grid-cols-3',
|
|
181
|
-
|
|
170
|
+
!compactLayout &&
|
|
171
|
+
storefront.layoutStyle === 'feature' &&
|
|
182
172
|
'[&>article:first-child]:sm:col-span-2'
|
|
183
173
|
)}
|
|
184
174
|
>
|
|
@@ -233,3 +223,56 @@ export function StorefrontSurface({
|
|
|
233
223
|
</main>
|
|
234
224
|
);
|
|
235
225
|
}
|
|
226
|
+
|
|
227
|
+
function StorefrontMerchSections({
|
|
228
|
+
radius,
|
|
229
|
+
sections,
|
|
230
|
+
}: {
|
|
231
|
+
radius: string;
|
|
232
|
+
sections: InventoryStorefrontSection[];
|
|
233
|
+
}) {
|
|
234
|
+
const visibleSections = sections
|
|
235
|
+
.filter((section) => section.status === 'published')
|
|
236
|
+
.filter((section) => section.sectionType !== 'cover')
|
|
237
|
+
.sort((a, b) => a.sortOrder - b.sortOrder);
|
|
238
|
+
|
|
239
|
+
if (visibleSections.length === 0) return null;
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<div className="mt-4 grid gap-3">
|
|
243
|
+
{visibleSections.map((section) => (
|
|
244
|
+
<section
|
|
245
|
+
className={cn(
|
|
246
|
+
'grid overflow-hidden border border-border bg-card md:grid-cols-[minmax(0,1fr)_280px]',
|
|
247
|
+
radius
|
|
248
|
+
)}
|
|
249
|
+
key={section.id}
|
|
250
|
+
>
|
|
251
|
+
<div className="flex min-w-0 flex-col justify-center gap-2 p-4">
|
|
252
|
+
{section.title ? (
|
|
253
|
+
<h2 className="font-semibold text-lg">{section.title}</h2>
|
|
254
|
+
) : null}
|
|
255
|
+
{section.description ? (
|
|
256
|
+
<p className="text-muted-foreground text-sm leading-6">
|
|
257
|
+
{section.description}
|
|
258
|
+
</p>
|
|
259
|
+
) : null}
|
|
260
|
+
{section.href ? (
|
|
261
|
+
<a
|
|
262
|
+
className="mt-1 w-fit font-medium text-sm underline-offset-4 hover:underline"
|
|
263
|
+
href={section.href}
|
|
264
|
+
>
|
|
265
|
+
{section.href.replace(/^https?:\/\//u, '')}
|
|
266
|
+
</a>
|
|
267
|
+
) : null}
|
|
268
|
+
</div>
|
|
269
|
+
<StorefrontImagePanel
|
|
270
|
+
className="min-h-36 md:min-h-full"
|
|
271
|
+
imageUrl={section.imageUrl}
|
|
272
|
+
label={section.title ?? 'Storefront section'}
|
|
273
|
+
/>
|
|
274
|
+
</section>
|
|
275
|
+
))}
|
|
276
|
+
</div>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
@@ -26,6 +26,7 @@ export type StorefrontSurfaceLabels = {
|
|
|
26
26
|
checkout: string;
|
|
27
27
|
checkoutDisabled: string;
|
|
28
28
|
checkoutDisabledBadge: string;
|
|
29
|
+
couponNote: string;
|
|
29
30
|
demoBadge: string;
|
|
30
31
|
emptyCart: string;
|
|
31
32
|
emptyListingsDescription: string;
|
|
@@ -59,6 +60,7 @@ export const defaultStorefrontSurfaceLabels: StorefrontSurfaceLabels = {
|
|
|
59
60
|
checkout: 'Checkout',
|
|
60
61
|
checkoutDisabled: 'Checkout is disabled in preview',
|
|
61
62
|
checkoutDisabledBadge: 'Checkout disabled',
|
|
63
|
+
couponNote: 'Have a coupon? You can apply it at checkout.',
|
|
62
64
|
demoBadge: 'Demo',
|
|
63
65
|
emptyCart: 'Add a listing to start checkout.',
|
|
64
66
|
emptyListingsDescription:
|
|
@@ -23,6 +23,27 @@ export const storefrontSurfaceClasses: Record<
|
|
|
23
23
|
solid: 'border border-border bg-card shadow-sm shadow-foreground/5',
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Theme presets change the storefront's typographic personality so the choice
|
|
28
|
+
* is actually visible to shoppers. Applied at the surface root and inherited by
|
|
29
|
+
* headings/body inside.
|
|
30
|
+
*/
|
|
31
|
+
export const storefrontThemeClasses: Record<
|
|
32
|
+
InventoryStorefront['themePreset'],
|
|
33
|
+
string
|
|
34
|
+
> = {
|
|
35
|
+
// Spacious, headline-led magazine feel with serif headings.
|
|
36
|
+
editorial:
|
|
37
|
+
'font-sans [&_h1]:font-serif [&_h1]:tracking-tight [&_h2]:font-serif [&_h2]:tracking-tight',
|
|
38
|
+
// Refined boutique look: airy, wide-tracked uppercase headings.
|
|
39
|
+
boutique:
|
|
40
|
+
'font-sans [&_h1]:uppercase [&_h1]:tracking-[0.12em] [&_h2]:tracking-wide',
|
|
41
|
+
// Dense, scannable product-catalog density.
|
|
42
|
+
catalog: 'font-sans text-[0.95rem] [&_h1]:tracking-tight',
|
|
43
|
+
// Clean default.
|
|
44
|
+
minimal: 'font-sans',
|
|
45
|
+
};
|
|
46
|
+
|
|
26
47
|
export type StorefrontAccentStyle = CSSProperties & {
|
|
27
48
|
'--storefront-accent'?: string;
|
|
28
49
|
'--storefront-accent-foreground'?: string;
|