@tuturuuu/ui 0.11.0 → 0.12.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 +16 -0
- package/package.json +47 -47
- package/src/components/ui/storefront/bundle-selection-dialog.tsx +249 -0
- package/src/components/ui/storefront/cart-summary-parts.tsx +28 -8
- package/src/components/ui/storefront/listing-card.tsx +6 -3
- package/src/components/ui/storefront/storefront-surface.test.tsx +134 -1
- package/src/components/ui/storefront/storefront-surface.tsx +66 -7
- package/src/components/ui/storefront/types.ts +15 -0
- package/src/components/ui/storefront/utils.ts +164 -2
- package/src/components/ui/text-editor/__tests__/extensions.test.ts +22 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +18 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.test.tsx +157 -4
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +178 -14
- package/src/components/ui/tu-do/templates/task-template-client.test.tsx +52 -0
- package/src/components/ui/tu-do/templates/task-template-client.tsx +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.12.0](https://github.com/tutur3u/platform/compare/ui-v0.11.1...ui-v0.12.0) (2026-07-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **inventory:** add revenue share and category bundles ([20b2e1e](https://github.com/tutur3u/platform/commit/20b2e1e5302d1db275766b7b4b92d9bdf69de04a))
|
|
9
|
+
|
|
10
|
+
## [0.11.1](https://github.com/tutur3u/platform/compare/ui-v0.11.0...ui-v0.11.1) (2026-07-02)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **cli:** preserve markdown tables in task descriptions ([9faf8ba](https://github.com/tutur3u/platform/commit/9faf8ba44b92565a9dfc2949c496654fc1fea9b3))
|
|
16
|
+
* **tasks:** refresh task template placeholders ([0c3c309](https://github.com/tutur3u/platform/commit/0c3c30935f67e3e64d548c8a13584fd597e887b8))
|
|
17
|
+
* **ui:** stabilize task property popover handoff ([8fb2e62](https://github.com/tutur3u/platform/commit/8fb2e62904d34a3ae7894bfbbb66c285fcf1a1bd))
|
|
18
|
+
|
|
3
19
|
## [0.11.0](https://github.com/tutur3u/platform/compare/ui-v0.10.0...ui-v0.11.0) (2026-06-29)
|
|
4
20
|
|
|
5
21
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,42 +24,42 @@
|
|
|
24
24
|
"@hookform/resolvers": "^5.4.0",
|
|
25
25
|
"@json-render/core": "^0.19.0",
|
|
26
26
|
"@json-render/react": "^0.19.0",
|
|
27
|
-
"@radix-ui/react-accordion": "^1.2.
|
|
28
|
-
"@radix-ui/react-alert-dialog": "^1.1.
|
|
29
|
-
"@radix-ui/react-aspect-ratio": "^1.1.
|
|
30
|
-
"@radix-ui/react-avatar": "^1.2.
|
|
31
|
-
"@radix-ui/react-checkbox": "^1.3.
|
|
32
|
-
"@radix-ui/react-collapsible": "^1.1.
|
|
33
|
-
"@radix-ui/react-context-menu": "^2.3.
|
|
34
|
-
"@radix-ui/react-dialog": "^1.1.
|
|
35
|
-
"@radix-ui/react-dropdown-menu": "^2.1.
|
|
36
|
-
"@radix-ui/react-hover-card": "^1.1.
|
|
37
|
-
"@radix-ui/react-label": "^2.1.
|
|
38
|
-
"@radix-ui/react-menubar": "^1.1.
|
|
39
|
-
"@radix-ui/react-navigation-menu": "^1.2.
|
|
40
|
-
"@radix-ui/react-popover": "^1.1.
|
|
41
|
-
"@radix-ui/react-progress": "^1.1.
|
|
42
|
-
"@radix-ui/react-radio-group": "^1.4.
|
|
43
|
-
"@radix-ui/react-scroll-area": "^1.2.
|
|
44
|
-
"@radix-ui/react-select": "^2.3.
|
|
45
|
-
"@radix-ui/react-separator": "^1.1.
|
|
46
|
-
"@radix-ui/react-slider": "^1.4.
|
|
27
|
+
"@radix-ui/react-accordion": "^1.2.15",
|
|
28
|
+
"@radix-ui/react-alert-dialog": "^1.1.18",
|
|
29
|
+
"@radix-ui/react-aspect-ratio": "^1.1.11",
|
|
30
|
+
"@radix-ui/react-avatar": "^1.2.1",
|
|
31
|
+
"@radix-ui/react-checkbox": "^1.3.6",
|
|
32
|
+
"@radix-ui/react-collapsible": "^1.1.15",
|
|
33
|
+
"@radix-ui/react-context-menu": "^2.3.2",
|
|
34
|
+
"@radix-ui/react-dialog": "^1.1.18",
|
|
35
|
+
"@radix-ui/react-dropdown-menu": "^2.1.19",
|
|
36
|
+
"@radix-ui/react-hover-card": "^1.1.18",
|
|
37
|
+
"@radix-ui/react-label": "^2.1.11",
|
|
38
|
+
"@radix-ui/react-menubar": "^1.1.19",
|
|
39
|
+
"@radix-ui/react-navigation-menu": "^1.2.17",
|
|
40
|
+
"@radix-ui/react-popover": "^1.1.18",
|
|
41
|
+
"@radix-ui/react-progress": "^1.1.11",
|
|
42
|
+
"@radix-ui/react-radio-group": "^1.4.2",
|
|
43
|
+
"@radix-ui/react-scroll-area": "^1.2.13",
|
|
44
|
+
"@radix-ui/react-select": "^2.3.2",
|
|
45
|
+
"@radix-ui/react-separator": "^1.1.11",
|
|
46
|
+
"@radix-ui/react-slider": "^1.4.2",
|
|
47
47
|
"@radix-ui/react-slot": "^1.3.0",
|
|
48
|
-
"@radix-ui/react-switch": "^1.3.
|
|
49
|
-
"@radix-ui/react-tabs": "^1.1.
|
|
50
|
-
"@radix-ui/react-toast": "^1.2.
|
|
51
|
-
"@radix-ui/react-toggle": "^1.1.
|
|
52
|
-
"@radix-ui/react-toggle-group": "^1.1.
|
|
53
|
-
"@radix-ui/react-tooltip": "^1.2.
|
|
48
|
+
"@radix-ui/react-switch": "^1.3.2",
|
|
49
|
+
"@radix-ui/react-tabs": "^1.1.16",
|
|
50
|
+
"@radix-ui/react-toast": "^1.2.18",
|
|
51
|
+
"@radix-ui/react-toggle": "^1.1.13",
|
|
52
|
+
"@radix-ui/react-toggle-group": "^1.1.14",
|
|
53
|
+
"@radix-ui/react-tooltip": "^1.2.11",
|
|
54
54
|
"@streamdown/cjk": "^1.0.3",
|
|
55
55
|
"@streamdown/code": "^1.1.1",
|
|
56
56
|
"@streamdown/math": "^1.0.2",
|
|
57
57
|
"@streamdown/mermaid": "^1.0.2",
|
|
58
58
|
"@tanstack/react-hotkeys": "^0.10.0",
|
|
59
59
|
"@tanstack/react-pacer": "^0.22.1",
|
|
60
|
-
"@tanstack/react-query": "^5.101.
|
|
60
|
+
"@tanstack/react-query": "^5.101.2",
|
|
61
61
|
"@tanstack/react-table": "^8.21.3",
|
|
62
|
-
"@tanstack/react-virtual": "^3.14.
|
|
62
|
+
"@tanstack/react-virtual": "^3.14.5",
|
|
63
63
|
"@tiptap/core": "3.27.1",
|
|
64
64
|
"@tiptap/extension-collaboration": "3.27.1",
|
|
65
65
|
"@tiptap/extension-collaboration-caret": "3.27.1",
|
|
@@ -88,9 +88,9 @@
|
|
|
88
88
|
"@tuturuuu/apis": "0.7.0",
|
|
89
89
|
"@tuturuuu/hooks": "0.0.2",
|
|
90
90
|
"@tuturuuu/icons": "0.0.6",
|
|
91
|
-
"@tuturuuu/internal-api": "0.
|
|
91
|
+
"@tuturuuu/internal-api": "0.14.0",
|
|
92
92
|
"@tuturuuu/supabase": "0.4.0",
|
|
93
|
-
"@tuturuuu/utils": "0.
|
|
93
|
+
"@tuturuuu/utils": "0.12.1",
|
|
94
94
|
"@types/debug": "^4.1.13",
|
|
95
95
|
"browser-image-compression": "^2.0.2",
|
|
96
96
|
"class-variance-authority": "^0.7.1",
|
|
@@ -102,18 +102,18 @@
|
|
|
102
102
|
"debug": "^4.4.3",
|
|
103
103
|
"embla-carousel-react": "^8.6.0",
|
|
104
104
|
"eventemitter3": "^5.0.4",
|
|
105
|
-
"framer-motion": "^12.
|
|
105
|
+
"framer-motion": "^12.42.2",
|
|
106
106
|
"gsap": "^3.15.0",
|
|
107
|
-
"html2canvas-pro": "^2.
|
|
107
|
+
"html2canvas-pro": "^2.2.1",
|
|
108
108
|
"input-otp": "^1.4.2",
|
|
109
109
|
"katex": "^0.17.0",
|
|
110
110
|
"lodash": "4.18.1",
|
|
111
111
|
"moment": "^2.30.1",
|
|
112
|
-
"motion": "^12.
|
|
113
|
-
"next": "^16.
|
|
114
|
-
"next-intl": "^4.13.
|
|
112
|
+
"motion": "^12.42.2",
|
|
113
|
+
"next": "^16.3.0-preview.5",
|
|
114
|
+
"next-intl": "^4.13.1",
|
|
115
115
|
"next-themes": "^0.4.6",
|
|
116
|
-
"nuqs": "^2.
|
|
116
|
+
"nuqs": "^2.9.0",
|
|
117
117
|
"prosemirror-state": "^1.4.4",
|
|
118
118
|
"qrcode.react": "^4.2.0",
|
|
119
119
|
"react": "^19.2.7",
|
|
@@ -126,9 +126,9 @@
|
|
|
126
126
|
"react-papaparse": "^4.4.0",
|
|
127
127
|
"react-pdf": "^10.4.1",
|
|
128
128
|
"react-resizable": "^4.0.2",
|
|
129
|
-
"react-resizable-panels": "^4.
|
|
129
|
+
"react-resizable-panels": "^4.12.0",
|
|
130
130
|
"react-syntax-highlighter": "^16.1.1",
|
|
131
|
-
"recharts": "3.9.
|
|
131
|
+
"recharts": "3.9.1",
|
|
132
132
|
"sonner": "^2.0.7",
|
|
133
133
|
"streamdown": "^2.5.0",
|
|
134
134
|
"tailwind-scrollbar": "^4.0.2",
|
|
@@ -143,34 +143,34 @@
|
|
|
143
143
|
"zod": "^4.4.3"
|
|
144
144
|
},
|
|
145
145
|
"devDependencies": {
|
|
146
|
-
"@tailwindcss/postcss": "^4.3.
|
|
146
|
+
"@tailwindcss/postcss": "^4.3.2",
|
|
147
147
|
"@tailwindcss/typography": "^0.5.20",
|
|
148
|
-
"@tanstack/react-query": "^5.101.
|
|
148
|
+
"@tanstack/react-query": "^5.101.2",
|
|
149
149
|
"@tanstack/react-table": "^8.21.3",
|
|
150
150
|
"@testing-library/jest-dom": "^6.9.1",
|
|
151
151
|
"@testing-library/react": "^16.3.2",
|
|
152
|
-
"@tuturuuu/types": "0.
|
|
152
|
+
"@tuturuuu/types": "0.14.0",
|
|
153
153
|
"@tuturuuu/typescript-config": "0.1.1",
|
|
154
154
|
"@types/html2canvas": "^1.0.0",
|
|
155
155
|
"@types/lodash": "^4.17.24",
|
|
156
|
-
"@types/node": "^26.0.
|
|
156
|
+
"@types/node": "^26.0.1",
|
|
157
157
|
"@types/react": "^19.2.17",
|
|
158
158
|
"@types/react-dom": "^19.2.3",
|
|
159
159
|
"@types/react-resizable": "^4.0.0",
|
|
160
160
|
"@types/react-syntax-highlighter": "^15.5.13",
|
|
161
161
|
"@types/uuid": "^11.0.0",
|
|
162
162
|
"@vitejs/plugin-react": "^6.0.3",
|
|
163
|
-
"autoprefixer": "^10.5.
|
|
163
|
+
"autoprefixer": "^10.5.2",
|
|
164
164
|
"jsdom": "^29.1.1",
|
|
165
|
-
"postcss": "^8.5.
|
|
165
|
+
"postcss": "^8.5.16",
|
|
166
166
|
"postcss-load-config": "^6.0.1",
|
|
167
|
-
"tailwindcss": "^4.3.
|
|
167
|
+
"tailwindcss": "^4.3.2",
|
|
168
168
|
"typescript": "7.0.1-rc",
|
|
169
169
|
"uuid": "^14.0.1",
|
|
170
170
|
"vitest": "^4.1.9"
|
|
171
171
|
},
|
|
172
172
|
"peerDependencies": {
|
|
173
|
-
"@tanstack/react-query": "^5.101.
|
|
173
|
+
"@tanstack/react-query": "^5.101.2",
|
|
174
174
|
"@tanstack/react-table": "^8.21.3"
|
|
175
175
|
},
|
|
176
176
|
"exports": {
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Check, PackagePlus, Search } from '@tuturuuu/icons';
|
|
4
|
+
import type {
|
|
5
|
+
InventoryBundle,
|
|
6
|
+
InventoryBundleCategoryCandidate,
|
|
7
|
+
InventoryStorefrontListing,
|
|
8
|
+
} from '@tuturuuu/internal-api/inventory';
|
|
9
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
10
|
+
import { useMemo, useState } from 'react';
|
|
11
|
+
import { Badge } from '../badge';
|
|
12
|
+
import { Button } from '../button';
|
|
13
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../dialog';
|
|
14
|
+
import { Input } from '../input';
|
|
15
|
+
import type { StorefrontCartLine, StorefrontSurfaceLabels } from './types';
|
|
16
|
+
import {
|
|
17
|
+
createStorefrontBundleSelectionKey,
|
|
18
|
+
formatStorefrontPrice,
|
|
19
|
+
getStorefrontBundleSelectionSubtotal,
|
|
20
|
+
} from './utils';
|
|
21
|
+
|
|
22
|
+
type BundleSelectionDialogProps = {
|
|
23
|
+
bundle: InventoryBundle | null;
|
|
24
|
+
currency: string;
|
|
25
|
+
labels: StorefrontSurfaceLabels;
|
|
26
|
+
listing: InventoryStorefrontListing | null;
|
|
27
|
+
onAdd?: (line: StorefrontCartLine) => void;
|
|
28
|
+
onOpenChange: (open: boolean) => void;
|
|
29
|
+
open: boolean;
|
|
30
|
+
radius: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function StorefrontBundleSelectionDialog({
|
|
34
|
+
bundle,
|
|
35
|
+
currency,
|
|
36
|
+
labels,
|
|
37
|
+
listing,
|
|
38
|
+
onAdd,
|
|
39
|
+
onOpenChange,
|
|
40
|
+
open,
|
|
41
|
+
radius,
|
|
42
|
+
}: BundleSelectionDialogProps) {
|
|
43
|
+
const [query, setQuery] = useState('');
|
|
44
|
+
const [selected, setSelected] = useState<Record<string, string[]>>({});
|
|
45
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
46
|
+
const selections = useMemo(() => {
|
|
47
|
+
if (!bundle) return {};
|
|
48
|
+
|
|
49
|
+
return Object.fromEntries(
|
|
50
|
+
bundle.categoryComponents.map((component) => {
|
|
51
|
+
const candidates = component.candidates ?? [];
|
|
52
|
+
const items = (selected[component.id] ?? []).flatMap((key) => {
|
|
53
|
+
const candidate = candidates.find(
|
|
54
|
+
(item) => getCandidateKey(item) === key
|
|
55
|
+
);
|
|
56
|
+
return candidate ? [candidateToSelection(candidate)] : [];
|
|
57
|
+
});
|
|
58
|
+
return [component.id, items];
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
}, [bundle, selected]);
|
|
62
|
+
const selectionLine: StorefrontCartLine | null =
|
|
63
|
+
listing && bundle
|
|
64
|
+
? {
|
|
65
|
+
bundleSelections: selections,
|
|
66
|
+
listingId: listing.id,
|
|
67
|
+
quantity: 1,
|
|
68
|
+
selectionKey: createStorefrontBundleSelectionKey(bundle, selections),
|
|
69
|
+
}
|
|
70
|
+
: null;
|
|
71
|
+
const subtotal =
|
|
72
|
+
bundle && selectionLine
|
|
73
|
+
? getStorefrontBundleSelectionSubtotal(bundle, selectionLine)
|
|
74
|
+
: null;
|
|
75
|
+
const isComplete =
|
|
76
|
+
Boolean(bundle?.categoryComponents.length) &&
|
|
77
|
+
bundle?.categoryComponents.every(
|
|
78
|
+
(component) =>
|
|
79
|
+
(selected[component.id]?.length ?? 0) === component.quantityRequired
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (!bundle || !listing) return null;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Dialog
|
|
86
|
+
onOpenChange={(nextOpen) => {
|
|
87
|
+
if (!nextOpen) {
|
|
88
|
+
setQuery('');
|
|
89
|
+
setSelected({});
|
|
90
|
+
}
|
|
91
|
+
onOpenChange(nextOpen);
|
|
92
|
+
}}
|
|
93
|
+
open={open}
|
|
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/60 p-5 sm:rounded-2xl">
|
|
96
|
+
<DialogHeader className="text-left">
|
|
97
|
+
<DialogTitle>{labels.bundleSelectionTitle}</DialogTitle>
|
|
98
|
+
<p className="text-muted-foreground text-sm">{listing.title}</p>
|
|
99
|
+
</DialogHeader>
|
|
100
|
+
<div className="grid gap-2">
|
|
101
|
+
<label className="relative block">
|
|
102
|
+
<Search className="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
103
|
+
<Input
|
|
104
|
+
className="h-10 pl-9"
|
|
105
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
106
|
+
placeholder={labels.searchBundleItems}
|
|
107
|
+
value={query}
|
|
108
|
+
/>
|
|
109
|
+
</label>
|
|
110
|
+
<p className="rounded-md border border-border bg-muted/25 px-3 py-2 text-muted-foreground text-xs">
|
|
111
|
+
{labels.cheapestFreePreview}
|
|
112
|
+
</p>
|
|
113
|
+
</div>
|
|
114
|
+
<div className="-mr-1 grid gap-4 overflow-y-auto pr-1">
|
|
115
|
+
{bundle.categoryComponents.map((component) => {
|
|
116
|
+
const selectedKeys = selected[component.id] ?? [];
|
|
117
|
+
const filteredCandidates = (component.candidates ?? []).filter(
|
|
118
|
+
(candidate) =>
|
|
119
|
+
!normalizedQuery ||
|
|
120
|
+
candidate.title.toLowerCase().includes(normalizedQuery)
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<section className="grid gap-2" key={component.id}>
|
|
125
|
+
<div className="flex items-center justify-between gap-3">
|
|
126
|
+
<div className="min-w-0">
|
|
127
|
+
<p className="truncate font-medium text-sm">
|
|
128
|
+
{component.categoryName}
|
|
129
|
+
</p>
|
|
130
|
+
<p className="text-muted-foreground text-xs">
|
|
131
|
+
{labels.selectedItems
|
|
132
|
+
.replace('{selected}', String(selectedKeys.length))
|
|
133
|
+
.replace(
|
|
134
|
+
'{required}',
|
|
135
|
+
String(component.quantityRequired)
|
|
136
|
+
)}
|
|
137
|
+
</p>
|
|
138
|
+
</div>
|
|
139
|
+
<Badge
|
|
140
|
+
className="border-border bg-background"
|
|
141
|
+
variant="outline"
|
|
142
|
+
>
|
|
143
|
+
{labels.requiredItems.replace(
|
|
144
|
+
'{count}',
|
|
145
|
+
String(component.quantityRequired)
|
|
146
|
+
)}
|
|
147
|
+
</Badge>
|
|
148
|
+
</div>
|
|
149
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
150
|
+
{filteredCandidates.map((candidate) => {
|
|
151
|
+
const key = getCandidateKey(candidate);
|
|
152
|
+
const active = selectedKeys.includes(key);
|
|
153
|
+
const full =
|
|
154
|
+
!active &&
|
|
155
|
+
selectedKeys.length >= component.quantityRequired;
|
|
156
|
+
const disabled =
|
|
157
|
+
full || candidate.availableQuantity === 0 || !onAdd;
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<button
|
|
161
|
+
className={cn(
|
|
162
|
+
'grid min-h-20 gap-1 rounded-lg border bg-background p-3 text-left text-sm transition',
|
|
163
|
+
active
|
|
164
|
+
? 'border-[var(--storefront-accent,var(--primary))] bg-[var(--storefront-accent-soft,var(--muted))]'
|
|
165
|
+
: 'border-border hover:border-foreground/35',
|
|
166
|
+
disabled ? 'cursor-not-allowed opacity-60' : null
|
|
167
|
+
)}
|
|
168
|
+
disabled={disabled}
|
|
169
|
+
key={key}
|
|
170
|
+
onClick={() =>
|
|
171
|
+
setSelected((current) => ({
|
|
172
|
+
...current,
|
|
173
|
+
[component.id]: active
|
|
174
|
+
? selectedKeys.filter((item) => item !== key)
|
|
175
|
+
: [...selectedKeys, key],
|
|
176
|
+
}))
|
|
177
|
+
}
|
|
178
|
+
type="button"
|
|
179
|
+
>
|
|
180
|
+
<span className="flex min-w-0 items-center justify-between gap-2">
|
|
181
|
+
<span className="line-clamp-2 font-medium">
|
|
182
|
+
{candidate.title}
|
|
183
|
+
</span>
|
|
184
|
+
{active ? <Check className="h-4 w-4" /> : null}
|
|
185
|
+
</span>
|
|
186
|
+
<span className="text-muted-foreground text-xs">
|
|
187
|
+
{candidate.unitName || candidate.unitId} /{' '}
|
|
188
|
+
{candidate.warehouseName || candidate.warehouseId}
|
|
189
|
+
</span>
|
|
190
|
+
<span className="font-semibold tabular-nums">
|
|
191
|
+
{formatStorefrontPrice(candidate.price, currency)}
|
|
192
|
+
</span>
|
|
193
|
+
</button>
|
|
194
|
+
);
|
|
195
|
+
})}
|
|
196
|
+
</div>
|
|
197
|
+
</section>
|
|
198
|
+
);
|
|
199
|
+
})}
|
|
200
|
+
</div>
|
|
201
|
+
<div className="flex flex-wrap items-center justify-between gap-3 border-border border-t pt-4">
|
|
202
|
+
<div className="min-w-0">
|
|
203
|
+
<p className="text-muted-foreground text-xs">{labels.total}</p>
|
|
204
|
+
<p className="font-semibold tabular-nums">
|
|
205
|
+
{formatStorefrontPrice(subtotal ?? 0, currency)}
|
|
206
|
+
</p>
|
|
207
|
+
</div>
|
|
208
|
+
<Button
|
|
209
|
+
className={cn('min-w-36', radius)}
|
|
210
|
+
disabled={!isComplete || !selectionLine || !onAdd}
|
|
211
|
+
onClick={() => {
|
|
212
|
+
if (!selectionLine || !onAdd) return;
|
|
213
|
+
onAdd(selectionLine);
|
|
214
|
+
onOpenChange(false);
|
|
215
|
+
setQuery('');
|
|
216
|
+
setSelected({});
|
|
217
|
+
}}
|
|
218
|
+
type="button"
|
|
219
|
+
>
|
|
220
|
+
<PackagePlus className="h-4 w-4" />
|
|
221
|
+
{labels.add}
|
|
222
|
+
</Button>
|
|
223
|
+
</div>
|
|
224
|
+
</DialogContent>
|
|
225
|
+
</Dialog>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function candidateToSelection(candidate: InventoryBundleCategoryCandidate) {
|
|
230
|
+
return {
|
|
231
|
+
listingId: candidate.listingId,
|
|
232
|
+
productId: candidate.productId,
|
|
233
|
+
quantity: 1,
|
|
234
|
+
unitId: candidate.unitId,
|
|
235
|
+
variantId: candidate.variantId,
|
|
236
|
+
warehouseId: candidate.warehouseId,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function getCandidateKey(candidate: InventoryBundleCategoryCandidate) {
|
|
241
|
+
return [
|
|
242
|
+
candidate.selectionKind,
|
|
243
|
+
candidate.listingId ?? '',
|
|
244
|
+
candidate.variantId ?? '',
|
|
245
|
+
candidate.productId,
|
|
246
|
+
candidate.unitId,
|
|
247
|
+
candidate.warehouseId,
|
|
248
|
+
].join(':');
|
|
249
|
+
}
|
|
@@ -18,7 +18,8 @@ import type {
|
|
|
18
18
|
} from './types';
|
|
19
19
|
import {
|
|
20
20
|
formatStorefrontPrice,
|
|
21
|
-
|
|
21
|
+
getStorefrontBundleSelectionLabels,
|
|
22
|
+
getStorefrontCartLineSubtotal,
|
|
22
23
|
getStorefrontVariantLabel,
|
|
23
24
|
storefrontCartLineKey,
|
|
24
25
|
} from './utils';
|
|
@@ -229,20 +230,30 @@ function CartLines({
|
|
|
229
230
|
}) {
|
|
230
231
|
return (
|
|
231
232
|
<div className="-mr-1 grid max-h-72 gap-3 overflow-y-auto pr-1">
|
|
232
|
-
{cartEntries.map(({ line, listing, variant }) => {
|
|
233
|
-
const
|
|
233
|
+
{cartEntries.map(({ bundle, line, listing, variant }) => {
|
|
234
|
+
const selectedSubtotal = getStorefrontCartLineSubtotal({
|
|
235
|
+
bundle,
|
|
236
|
+
line,
|
|
237
|
+
listing,
|
|
238
|
+
variant,
|
|
239
|
+
});
|
|
234
240
|
const variantLabel = variant
|
|
235
241
|
? getStorefrontVariantLabel(variant)
|
|
236
242
|
: null;
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
243
|
+
const selectionLabels = getStorefrontBundleSelectionLabels(
|
|
244
|
+
bundle,
|
|
245
|
+
line
|
|
240
246
|
);
|
|
247
|
+
const lineTotal = formatStorefrontPrice(selectedSubtotal, currency);
|
|
241
248
|
|
|
242
249
|
return (
|
|
243
250
|
<div
|
|
244
251
|
className="grid grid-cols-[minmax(0,1fr)_auto] gap-x-3 gap-y-1 text-sm"
|
|
245
|
-
key={storefrontCartLineKey(
|
|
252
|
+
key={storefrontCartLineKey(
|
|
253
|
+
line.listingId,
|
|
254
|
+
line.variantId,
|
|
255
|
+
line.selectionKey
|
|
256
|
+
)}
|
|
246
257
|
>
|
|
247
258
|
<div className="min-w-0">
|
|
248
259
|
<p className="line-clamp-2 break-words font-medium leading-5">
|
|
@@ -253,8 +264,17 @@ function CartLines({
|
|
|
253
264
|
{variantLabel}
|
|
254
265
|
</p>
|
|
255
266
|
) : null}
|
|
267
|
+
{selectionLabels.length ? (
|
|
268
|
+
<p className="line-clamp-2 break-words text-muted-foreground text-xs">
|
|
269
|
+
{selectionLabels.join(', ')}
|
|
270
|
+
</p>
|
|
271
|
+
) : null}
|
|
256
272
|
<p className="text-muted-foreground text-xs tabular-nums">
|
|
257
|
-
{line.quantity} ×
|
|
273
|
+
{line.quantity} ×{' '}
|
|
274
|
+
{formatStorefrontPrice(
|
|
275
|
+
Math.round(selectedSubtotal / Math.max(1, line.quantity)),
|
|
276
|
+
currency
|
|
277
|
+
)}
|
|
258
278
|
</p>
|
|
259
279
|
</div>
|
|
260
280
|
<span
|
|
@@ -21,6 +21,7 @@ export function StorefrontListingCard({
|
|
|
21
21
|
labels,
|
|
22
22
|
listing,
|
|
23
23
|
onDecrement,
|
|
24
|
+
onConfigureBundle,
|
|
24
25
|
onIncrement,
|
|
25
26
|
onOpenDetail,
|
|
26
27
|
quantity,
|
|
@@ -33,6 +34,7 @@ export function StorefrontListingCard({
|
|
|
33
34
|
labels: StorefrontSurfaceLabels;
|
|
34
35
|
listing: InventoryStorefrontListing;
|
|
35
36
|
onDecrement?: (listingId: string, variantId?: string | null) => void;
|
|
37
|
+
onConfigureBundle?: () => void;
|
|
36
38
|
onIncrement?: (
|
|
37
39
|
listingId: string,
|
|
38
40
|
maxQuantity: number,
|
|
@@ -45,6 +47,7 @@ export function StorefrontListingCard({
|
|
|
45
47
|
surfaceClassName: string;
|
|
46
48
|
}) {
|
|
47
49
|
const hasVariants = listingHasVariants(listing);
|
|
50
|
+
const needsBundleConfiguration = Boolean(onConfigureBundle);
|
|
48
51
|
const limit = getStorefrontListingLimit(listing);
|
|
49
52
|
const disabled = limit === 0 || quantity >= limit;
|
|
50
53
|
const canChange = Boolean(onIncrement || onDecrement);
|
|
@@ -139,10 +142,10 @@ export function StorefrontListingCard({
|
|
|
139
142
|
</p>
|
|
140
143
|
) : null}
|
|
141
144
|
</div>
|
|
142
|
-
{hasVariants ? (
|
|
145
|
+
{hasVariants || needsBundleConfiguration ? (
|
|
143
146
|
<AccentButton
|
|
144
|
-
disabled={limit === 0 || !openDetail}
|
|
145
|
-
onClick={openDetail}
|
|
147
|
+
disabled={limit === 0 || (!openDetail && !onConfigureBundle)}
|
|
148
|
+
onClick={needsBundleConfiguration ? onConfigureBundle : openDetail}
|
|
146
149
|
radius={radius}
|
|
147
150
|
>
|
|
148
151
|
<SlidersHorizontal className="h-4 w-4" />
|