@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 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.11.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.14",
28
- "@radix-ui/react-alert-dialog": "^1.1.17",
29
- "@radix-ui/react-aspect-ratio": "^1.1.10",
30
- "@radix-ui/react-avatar": "^1.2.0",
31
- "@radix-ui/react-checkbox": "^1.3.5",
32
- "@radix-ui/react-collapsible": "^1.1.14",
33
- "@radix-ui/react-context-menu": "^2.3.1",
34
- "@radix-ui/react-dialog": "^1.1.17",
35
- "@radix-ui/react-dropdown-menu": "^2.1.18",
36
- "@radix-ui/react-hover-card": "^1.1.17",
37
- "@radix-ui/react-label": "^2.1.10",
38
- "@radix-ui/react-menubar": "^1.1.18",
39
- "@radix-ui/react-navigation-menu": "^1.2.16",
40
- "@radix-ui/react-popover": "^1.1.17",
41
- "@radix-ui/react-progress": "^1.1.10",
42
- "@radix-ui/react-radio-group": "^1.4.1",
43
- "@radix-ui/react-scroll-area": "^1.2.12",
44
- "@radix-ui/react-select": "^2.3.1",
45
- "@radix-ui/react-separator": "^1.1.10",
46
- "@radix-ui/react-slider": "^1.4.1",
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.1",
49
- "@radix-ui/react-tabs": "^1.1.15",
50
- "@radix-ui/react-toast": "^1.2.17",
51
- "@radix-ui/react-toggle": "^1.1.12",
52
- "@radix-ui/react-toggle-group": "^1.1.13",
53
- "@radix-ui/react-tooltip": "^1.2.10",
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.1",
60
+ "@tanstack/react-query": "^5.101.2",
61
61
  "@tanstack/react-table": "^8.21.3",
62
- "@tanstack/react-virtual": "^3.14.3",
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.12.0",
91
+ "@tuturuuu/internal-api": "0.14.0",
92
92
  "@tuturuuu/supabase": "0.4.0",
93
- "@tuturuuu/utils": "0.11.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.41.0",
105
+ "framer-motion": "^12.42.2",
106
106
  "gsap": "^3.15.0",
107
- "html2canvas-pro": "^2.1.1",
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.41.0",
113
- "next": "^16.2.9",
114
- "next-intl": "^4.13.0",
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.8.9",
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.11.0",
129
+ "react-resizable-panels": "^4.12.0",
130
130
  "react-syntax-highlighter": "^16.1.1",
131
- "recharts": "3.9.0",
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.1",
146
+ "@tailwindcss/postcss": "^4.3.2",
147
147
  "@tailwindcss/typography": "^0.5.20",
148
- "@tanstack/react-query": "^5.101.1",
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.13.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.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.0",
163
+ "autoprefixer": "^10.5.2",
164
164
  "jsdom": "^29.1.1",
165
- "postcss": "^8.5.14",
165
+ "postcss": "^8.5.16",
166
166
  "postcss-load-config": "^6.0.1",
167
- "tailwindcss": "^4.3.1",
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.1",
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
- getStorefrontLinePrice,
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 unitPrice = getStorefrontLinePrice(listing, variant);
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 lineTotal = formatStorefrontPrice(
238
- unitPrice * line.quantity,
239
- currency
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(line.listingId, line.variantId)}
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} × {formatStorefrontPrice(unitPrice, currency)}
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" />