@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +3 -3
  3. package/biome.json +1 -1
  4. package/package.json +8 -8
  5. package/src/components/ui/calendar-app/components/calendar-connections.tsx +17 -13
  6. package/src/components/ui/calendar-app/components/connected-accounts-dialog.tsx +2 -5
  7. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +2 -5
  8. package/src/components/ui/calendar.test.tsx +24 -0
  9. package/src/components/ui/calendar.tsx +1 -0
  10. package/src/components/ui/date-time-picker.tsx +352 -234
  11. package/src/components/ui/finance/categories-tags-tabs.tsx +23 -1
  12. package/src/components/ui/finance/command/finance-command-actions.test.tsx +48 -0
  13. package/src/components/ui/finance/command/finance-command-actions.tsx +200 -0
  14. package/src/components/ui/finance/command/finance-command-provider.test.tsx +151 -0
  15. package/src/components/ui/finance/command/finance-command-provider.tsx +250 -0
  16. package/src/components/ui/finance/command/finance-command-results.tsx +262 -0
  17. package/src/components/ui/finance/invoices/pending-invoices-table.tsx +22 -9
  18. package/src/components/ui/finance/shared/quick-actions.tsx +39 -90
  19. package/src/components/ui/finance/tags/tag-manager.tsx +24 -5
  20. package/src/components/ui/finance/transactions/form-basic-tab.tsx +33 -49
  21. package/src/components/ui/finance/transactions/form-types.ts +3 -0
  22. package/src/components/ui/finance/transactions/form.test.tsx +105 -22
  23. package/src/components/ui/finance/transactions/form.tsx +116 -20
  24. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +13 -6
  25. package/src/components/ui/finance/transactions/transaction-edit-dialog.test.tsx +25 -1
  26. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +16 -3
  27. package/src/components/ui/finance/transactions/transactionId/transaction-details-client-page.tsx +3 -0
  28. package/src/components/ui/finance/transactions/transactionId/transaction-details-page.tsx +3 -0
  29. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +6 -0
  30. package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +20 -2
  31. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -0
  32. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +7 -2
  33. package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +7 -2
  34. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +38 -1
  35. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +5 -0
  36. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +18 -2
  37. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +3 -0
  38. package/src/components/ui/finance/wallets/wallets-page.tsx +3 -0
  39. package/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx +2 -9
  40. package/src/components/ui/optional-time-picker.tsx +95 -0
  41. package/src/components/ui/quick-command-center.test.tsx +90 -0
  42. package/src/components/ui/quick-command-center.tsx +190 -0
  43. package/src/components/ui/storefront/cart-summary.tsx +18 -27
  44. package/src/components/ui/storefront/hero-panel.tsx +22 -13
  45. package/src/components/ui/storefront/storefront-surface.test.tsx +8 -4
  46. package/src/components/ui/storefront/storefront-surface.tsx +84 -41
  47. package/src/components/ui/storefront/types.ts +2 -0
  48. package/src/components/ui/storefront/utils.ts +21 -0
  49. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.test.ts +171 -0
  50. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.ts +200 -36
  51. 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 = !hasCart || isSubmitting || isCheckoutDisabled;
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
- asChild
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
- 'grid min-h-44 overflow-hidden',
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
- <div className="flex min-w-0 flex-col justify-between gap-6 p-5">
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-3 text-balance font-semibold tracking-normal',
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('shows simulated checkout mode badges', () => {
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.getByText('Simulated checkout')).toBeInTheDocument();
69
+ expect(screen.queryByText('Simulated checkout')).not.toBeInTheDocument();
70
+ expect(screen.getAllByText('Preview Store')).toHaveLength(2);
67
71
  });
68
72
 
69
- it('shows disabled checkout mode badges and blocks checkout', () => {
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.getByText('Checkout disabled')).toBeInTheDocument();
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('min-h-dvh bg-background text-foreground', className)}
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
- <div className="flex flex-wrap items-center gap-2 text-muted-foreground text-xs">
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 className="mx-auto grid max-w-7xl gap-4 px-4 py-5 lg:grid-cols-[minmax(0,1fr)_340px]">
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
- storefront.layoutStyle === 'feature' &&
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;