@voyantjs/sellability-ui 0.13.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/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # @voyantjs/sellability-ui
2
+
3
+ Importable React UI components for Voyant sellability. Bundler-consumed (Vite, Next.js, webpack, etc.).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @voyantjs/sellability-ui @voyantjs/sellability-react @voyantjs/voyant-ui @tanstack/react-query react react-dom
9
+ ```
10
+
11
+ `@voyantjs/voyant-ui` provides the design-system primitives. `@voyantjs/sellability-react` provides the data-layer hooks. Both are required peers.
12
+
13
+ All components accept a `className` prop and merge it with `cn()`. Wrap or compose to extend; use the registry copy-paste path (`npx shadcn add @voyant/...`) for components you want to fork outright.
@@ -0,0 +1,9 @@
1
+ type Props = {
2
+ value: string | null | undefined;
3
+ onChange: (value: string | null) => void;
4
+ placeholder?: string;
5
+ disabled?: boolean;
6
+ };
7
+ export declare function ChannelCombobox({ value, onChange, placeholder, disabled, }: Props): import("react/jsx-runtime").JSX.Element;
8
+ export {};
9
+ //# sourceMappingURL=channel-combobox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-combobox.d.ts","sourceRoot":"","sources":["../../src/components/channel-combobox.tsx"],"names":[],"mappings":"AAYA,KAAK,KAAK,GAAG;IACX,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACxC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAID,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,QAAQ,EACR,WAA+B,EAC/B,QAAQ,GACT,EAAE,KAAK,2CAwEP"}
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useChannel, useChannels } from "@voyantjs/distribution-react";
3
+ import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/voyant-ui/components/combobox";
4
+ import * as React from "react";
5
+ const PAGE_SIZE = 100;
6
+ export function ChannelCombobox({ value, onChange, placeholder = "Select channel…", disabled, }) {
7
+ const [search, setSearch] = React.useState("");
8
+ const listQuery = useChannels({ limit: PAGE_SIZE });
9
+ const selectedQuery = useChannel(value ?? undefined, { enabled: !!value });
10
+ const items = React.useMemo(() => {
11
+ const map = new Map();
12
+ for (const item of listQuery.data?.data ?? []) {
13
+ const matches = !search ||
14
+ item.name.toLowerCase().includes(search.toLowerCase()) ||
15
+ item.kind.toLowerCase().includes(search.toLowerCase());
16
+ if (matches)
17
+ map.set(item.id, item);
18
+ }
19
+ if (selectedQuery.data)
20
+ map.set(selectedQuery.data.id, selectedQuery.data);
21
+ return Array.from(map.values());
22
+ }, [listQuery.data?.data, search, selectedQuery.data]);
23
+ const itemMap = React.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);
24
+ const selected = value ? itemMap.get(value) : undefined;
25
+ const selectedLabel = selected ? selected.name : "";
26
+ const [inputValue, setInputValue] = React.useState(selectedLabel);
27
+ React.useEffect(() => {
28
+ if (selectedLabel)
29
+ setInputValue(selectedLabel);
30
+ }, [selectedLabel]);
31
+ return (_jsxs(Combobox, { items: items.map((item) => item.id), value: value ?? null, inputValue: inputValue, autoHighlight: true, disabled: disabled, itemToStringValue: (id) => itemMap.get(id)?.name ?? "", onInputValueChange: (next) => {
32
+ setInputValue(next);
33
+ setSearch(next);
34
+ if (!next)
35
+ onChange(null);
36
+ }, onValueChange: (next) => {
37
+ const id = next ?? null;
38
+ onChange(id);
39
+ setInputValue(id ? (itemMap.get(id)?.name ?? "") : "");
40
+ }, children: [_jsx(ComboboxInput, { placeholder: placeholder, showClear: !!value }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: listQuery.isPending || selectedQuery.isPending ? "Loading…" : "No channels found." }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
41
+ const item = itemMap.get(id);
42
+ if (!item)
43
+ return null;
44
+ return (_jsx(ComboboxItem, { value: item.id, children: _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate font-medium", children: item.name }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: [item.kind, item.status].filter(Boolean).join(" · ") })] }) }, item.id));
45
+ } }) })] })] }));
46
+ }
@@ -0,0 +1,9 @@
1
+ type Props = {
2
+ value: string | null | undefined;
3
+ onChange: (value: string | null) => void;
4
+ placeholder?: string;
5
+ disabled?: boolean;
6
+ };
7
+ export declare function MarketCombobox({ value, onChange, placeholder, disabled, }: Props): import("react/jsx-runtime").JSX.Element;
8
+ export {};
9
+ //# sourceMappingURL=market-combobox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"market-combobox.d.ts","sourceRoot":"","sources":["../../src/components/market-combobox.tsx"],"names":[],"mappings":"AAYA,KAAK,KAAK,GAAG;IACX,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACxC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAID,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,QAAQ,EACR,WAA+B,EAC/B,QAAQ,GACT,EAAE,KAAK,2CAkEP"}
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMarket, useMarkets } from "@voyantjs/markets-react";
3
+ import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/voyant-ui/components/combobox";
4
+ import * as React from "react";
5
+ const PAGE_SIZE = 25;
6
+ export function MarketCombobox({ value, onChange, placeholder = "Search markets…", disabled, }) {
7
+ const [search, setSearch] = React.useState("");
8
+ const listQuery = useMarkets({ search: search || undefined, limit: PAGE_SIZE });
9
+ const selectedQuery = useMarket(value ?? undefined, { enabled: !!value });
10
+ const items = React.useMemo(() => {
11
+ const map = new Map();
12
+ for (const item of listQuery.data?.data ?? [])
13
+ map.set(item.id, item);
14
+ if (selectedQuery.data)
15
+ map.set(selectedQuery.data.id, selectedQuery.data);
16
+ return Array.from(map.values());
17
+ }, [listQuery.data?.data, selectedQuery.data]);
18
+ const itemMap = React.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);
19
+ const selected = value ? itemMap.get(value) : undefined;
20
+ const selectedLabel = selected ? selected.name : "";
21
+ const [inputValue, setInputValue] = React.useState(selectedLabel);
22
+ React.useEffect(() => {
23
+ if (selectedLabel)
24
+ setInputValue(selectedLabel);
25
+ }, [selectedLabel]);
26
+ return (_jsxs(Combobox, { items: items.map((item) => item.id), value: value ?? null, inputValue: inputValue, autoHighlight: true, disabled: disabled, itemToStringValue: (id) => itemMap.get(id)?.name ?? "", onInputValueChange: (next) => {
27
+ setInputValue(next);
28
+ setSearch(next);
29
+ if (!next)
30
+ onChange(null);
31
+ }, onValueChange: (next) => {
32
+ const id = next ?? null;
33
+ onChange(id);
34
+ setInputValue(id ? (itemMap.get(id)?.name ?? "") : "");
35
+ }, children: [_jsx(ComboboxInput, { placeholder: placeholder, showClear: !!value }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: listQuery.isPending || selectedQuery.isPending ? "Loading…" : "No markets found." }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
36
+ const item = itemMap.get(id);
37
+ if (!item)
38
+ return null;
39
+ return (_jsx(ComboboxItem, { value: item.id, children: _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate font-medium", children: item.name }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: [item.code, item.defaultCurrency].filter(Boolean).join(" · ") })] }) }, item.id));
40
+ } }) })] })] }));
41
+ }
@@ -0,0 +1,10 @@
1
+ import { type SellabilityPolicyRecord } from "@voyantjs/sellability-react";
2
+ type Props = {
3
+ open: boolean;
4
+ onOpenChange: (open: boolean) => void;
5
+ policy?: SellabilityPolicyRecord;
6
+ onSuccess?: (policy: SellabilityPolicyRecord) => void;
7
+ };
8
+ export declare function PolicyDialog({ open, onOpenChange, policy, onSuccess }: Props): import("react/jsx-runtime").JSX.Element;
9
+ export {};
10
+ //# sourceMappingURL=policy-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy-dialog.d.ts","sourceRoot":"","sources":["../../src/components/policy-dialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,uBAAuB,EAE7B,MAAM,6BAA6B,CAAA;AA4EpC,KAAK,KAAK,GAAG;IACX,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,MAAM,CAAC,EAAE,uBAAuB,CAAA;IAChC,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,uBAAuB,KAAK,IAAI,CAAA;CACtD,CAAA;AAED,wBAAgB,YAAY,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,KAAK,2CAiQ5E"}
@@ -0,0 +1,135 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useSellabilityPolicyMutation, } from "@voyantjs/sellability-react";
3
+ import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, Textarea, } from "@voyantjs/voyant-ui/components";
4
+ import { zodResolver } from "@voyantjs/voyant-ui/lib/zod-resolver";
5
+ import { Loader2 } from "lucide-react";
6
+ import { useEffect } from "react";
7
+ import { useForm } from "react-hook-form";
8
+ import { z } from "zod/v4";
9
+ import { ChannelCombobox } from "./channel-combobox";
10
+ import { MarketCombobox } from "./market-combobox";
11
+ import { ProductCombobox } from "./product-combobox";
12
+ import { ProductOptionCombobox } from "./product-option-combobox";
13
+ const POLICY_SCOPES = ["global", "product", "option", "market", "channel"];
14
+ const POLICY_TYPES = [
15
+ "capability",
16
+ "occupancy",
17
+ "pickup",
18
+ "question",
19
+ "allotment",
20
+ "availability_window",
21
+ "currency",
22
+ "custom",
23
+ ];
24
+ const jsonStringSchema = z.string().refine((value) => {
25
+ if (!value || value.trim() === "")
26
+ return true;
27
+ try {
28
+ const parsed = JSON.parse(value);
29
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed);
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }, { message: "Must be a JSON object" });
35
+ const formSchema = z.object({
36
+ name: z.string().min(1, "Name is required").max(255),
37
+ scope: z.enum(POLICY_SCOPES),
38
+ policyType: z.enum(POLICY_TYPES),
39
+ productId: z.string().optional().nullable(),
40
+ optionId: z.string().optional().nullable(),
41
+ marketId: z.string().optional().nullable(),
42
+ channelId: z.string().optional().nullable(),
43
+ priority: z.coerce.number().int(),
44
+ active: z.boolean(),
45
+ conditionsJson: jsonStringSchema,
46
+ effectsJson: jsonStringSchema,
47
+ notes: z.string().optional().nullable(),
48
+ });
49
+ export function PolicyDialog({ open, onOpenChange, policy, onSuccess }) {
50
+ const isEditing = !!policy;
51
+ const { create, update } = useSellabilityPolicyMutation();
52
+ const form = useForm({
53
+ resolver: zodResolver(formSchema),
54
+ defaultValues: {
55
+ name: "",
56
+ scope: "global",
57
+ policyType: "custom",
58
+ productId: "",
59
+ optionId: "",
60
+ marketId: "",
61
+ channelId: "",
62
+ priority: 0,
63
+ active: true,
64
+ conditionsJson: "{}",
65
+ effectsJson: "{}",
66
+ notes: "",
67
+ },
68
+ });
69
+ useEffect(() => {
70
+ if (open && policy) {
71
+ form.reset({
72
+ name: policy.name,
73
+ scope: policy.scope,
74
+ policyType: policy.policyType,
75
+ productId: policy.productId ?? "",
76
+ optionId: policy.optionId ?? "",
77
+ marketId: policy.marketId ?? "",
78
+ channelId: policy.channelId ?? "",
79
+ priority: policy.priority,
80
+ active: policy.active,
81
+ conditionsJson: JSON.stringify(policy.conditions ?? {}, null, 2),
82
+ effectsJson: JSON.stringify(policy.effects ?? {}, null, 2),
83
+ notes: policy.notes ?? "",
84
+ });
85
+ }
86
+ else if (open) {
87
+ form.reset({
88
+ name: "",
89
+ scope: "global",
90
+ policyType: "custom",
91
+ productId: "",
92
+ optionId: "",
93
+ marketId: "",
94
+ channelId: "",
95
+ priority: 0,
96
+ active: true,
97
+ conditionsJson: "{}",
98
+ effectsJson: "{}",
99
+ notes: "",
100
+ });
101
+ }
102
+ }, [form, open, policy]);
103
+ const scope = form.watch("scope");
104
+ const isSubmitting = create.isPending || update.isPending;
105
+ const onSubmit = async (values) => {
106
+ const parseJson = (value) => {
107
+ if (!value || value.trim() === "")
108
+ return {};
109
+ return JSON.parse(value);
110
+ };
111
+ const payload = {
112
+ name: values.name,
113
+ scope: values.scope,
114
+ policyType: values.policyType,
115
+ productId: values.productId || null,
116
+ optionId: values.optionId || null,
117
+ marketId: values.marketId || null,
118
+ channelId: values.channelId || null,
119
+ priority: values.priority,
120
+ active: values.active,
121
+ conditions: parseJson(values.conditionsJson),
122
+ effects: parseJson(values.effectsJson),
123
+ notes: values.notes || null,
124
+ };
125
+ const saved = isEditing
126
+ ? await update.mutateAsync({ id: policy.id, input: payload })
127
+ : await create.mutateAsync(payload);
128
+ onSuccess?.(saved);
129
+ onOpenChange(false);
130
+ };
131
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? "Edit Policy" : "Add Policy" }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Name" }), _jsx(Input, { ...form.register("name"), placeholder: "Block bookings without capability" }), form.formState.errors.name ? (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.name.message })) : null] }), _jsxs("div", { className: "grid grid-cols-3 gap-3", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Scope" }), _jsxs(Select, { items: POLICY_SCOPES.map((x) => ({ label: x.replace(/_/g, " "), value: x })), value: form.watch("scope"), onValueChange: (value) => form.setValue("scope", value), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: POLICY_SCOPES.map((value) => (_jsx(SelectItem, { value: value, className: "capitalize", children: value }, value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Type" }), _jsxs(Select, { items: POLICY_TYPES.map((x) => ({ label: x.replace(/_/g, " "), value: x })), value: form.watch("policyType"), onValueChange: (value) => form.setValue("policyType", value), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: POLICY_TYPES.map((value) => (_jsx(SelectItem, { value: value, className: "capitalize", children: value.replace(/_/g, " ") }, value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Priority" }), _jsx(Input, { ...form.register("priority"), type: "number" })] })] }), scope === "product" ? (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Product" }), _jsx(ProductCombobox, { value: form.watch("productId") ?? null, onChange: (value) => form.setValue("productId", value) })] })) : null, scope === "option" ? (_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Product" }), _jsx(ProductCombobox, { value: form.watch("productId") ?? null, onChange: (value) => {
132
+ form.setValue("productId", value);
133
+ form.setValue("optionId", null);
134
+ } })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Option" }), _jsx(ProductOptionCombobox, { productId: form.watch("productId"), value: form.watch("optionId") ?? null, onChange: (value) => form.setValue("optionId", value) })] })] })) : null, scope === "market" ? (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Market" }), _jsx(MarketCombobox, { value: form.watch("marketId") ?? null, onChange: (value) => form.setValue("marketId", value) })] })) : null, scope === "channel" ? (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Channel" }), _jsx(ChannelCombobox, { value: form.watch("channelId") ?? null, onChange: (value) => form.setValue("channelId", value) })] })) : null, _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Conditions (JSON)" }), _jsx(Textarea, { ...form.register("conditionsJson"), rows: 6, className: "font-mono text-xs" }), form.formState.errors.conditionsJson ? (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.conditionsJson.message })) : null] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Effects (JSON)" }), _jsx(Textarea, { ...form.register("effectsJson"), rows: 6, className: "font-mono text-xs" }), form.formState.errors.effectsJson ? (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.effectsJson.message })) : null] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: form.watch("active"), onCheckedChange: (value) => form.setValue("active", value) }), _jsx(Label, { children: "Active" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Notes" }), _jsx(Textarea, { ...form.register("notes") })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: "Cancel" }), _jsxs(Button, { type: "submit", disabled: isSubmitting, children: [isSubmitting ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, isEditing ? "Save Changes" : "Add Policy"] })] })] })] }) }));
135
+ }
@@ -0,0 +1,9 @@
1
+ type Props = {
2
+ value: string | null | undefined;
3
+ onChange: (value: string | null) => void;
4
+ placeholder?: string;
5
+ disabled?: boolean;
6
+ };
7
+ export declare function ProductCombobox({ value, onChange, placeholder, disabled, }: Props): import("react/jsx-runtime").JSX.Element;
8
+ export {};
9
+ //# sourceMappingURL=product-combobox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-combobox.d.ts","sourceRoot":"","sources":["../../src/components/product-combobox.tsx"],"names":[],"mappings":"AAYA,KAAK,KAAK,GAAG;IACX,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACxC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAID,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,QAAQ,EACR,WAAgC,EAChC,QAAQ,GACT,EAAE,KAAK,2CAkEP"}
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useProduct, useProducts } from "@voyantjs/products-react";
3
+ import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/voyant-ui/components/combobox";
4
+ import * as React from "react";
5
+ const PAGE_SIZE = 25;
6
+ export function ProductCombobox({ value, onChange, placeholder = "Search products…", disabled, }) {
7
+ const [search, setSearch] = React.useState("");
8
+ const listQuery = useProducts({ search: search || undefined, limit: PAGE_SIZE });
9
+ const selectedQuery = useProduct(value ?? undefined, { enabled: !!value });
10
+ const items = React.useMemo(() => {
11
+ const map = new Map();
12
+ for (const item of listQuery.data?.data ?? [])
13
+ map.set(item.id, item);
14
+ if (selectedQuery.data)
15
+ map.set(selectedQuery.data.id, selectedQuery.data);
16
+ return Array.from(map.values());
17
+ }, [listQuery.data?.data, selectedQuery.data]);
18
+ const itemMap = React.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);
19
+ const selected = value ? itemMap.get(value) : undefined;
20
+ const selectedLabel = selected ? selected.name : "";
21
+ const [inputValue, setInputValue] = React.useState(selectedLabel);
22
+ React.useEffect(() => {
23
+ if (selectedLabel)
24
+ setInputValue(selectedLabel);
25
+ }, [selectedLabel]);
26
+ return (_jsxs(Combobox, { items: items.map((item) => item.id), value: value ?? null, inputValue: inputValue, autoHighlight: true, disabled: disabled, itemToStringValue: (id) => itemMap.get(id)?.name ?? "", onInputValueChange: (next) => {
27
+ setInputValue(next);
28
+ setSearch(next);
29
+ if (!next)
30
+ onChange(null);
31
+ }, onValueChange: (next) => {
32
+ const id = next ?? null;
33
+ onChange(id);
34
+ setInputValue(id ? (itemMap.get(id)?.name ?? "") : "");
35
+ }, children: [_jsx(ComboboxInput, { placeholder: placeholder, showClear: !!value }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: listQuery.isPending || selectedQuery.isPending ? "Loading…" : "No products found." }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
36
+ const item = itemMap.get(id);
37
+ if (!item)
38
+ return null;
39
+ return (_jsx(ComboboxItem, { value: item.id, children: _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate font-medium", children: item.name }), _jsxs("span", { className: "truncate text-xs text-muted-foreground", children: [item.status, " \u00B7 ", item.bookingMode] })] }) }, item.id));
40
+ } }) })] })] }));
41
+ }
@@ -0,0 +1,10 @@
1
+ type Props = {
2
+ productId?: string | null;
3
+ value: string | null | undefined;
4
+ onChange: (value: string | null) => void;
5
+ placeholder?: string;
6
+ disabled?: boolean;
7
+ };
8
+ export declare function ProductOptionCombobox({ productId, value, onChange, placeholder, disabled, }: Props): import("react/jsx-runtime").JSX.Element;
9
+ export {};
10
+ //# sourceMappingURL=product-option-combobox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-option-combobox.d.ts","sourceRoot":"","sources":["../../src/components/product-option-combobox.tsx"],"names":[],"mappings":"AAgBA,KAAK,KAAK,GAAG;IACX,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACxC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAID,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,KAAK,EACL,QAAQ,EACR,WAAsC,EACtC,QAAQ,GACT,EAAE,KAAK,2CA4EP"}
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useProductOption, useProductOptions, } from "@voyantjs/products-react";
3
+ import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/voyant-ui/components/combobox";
4
+ import * as React from "react";
5
+ const PAGE_SIZE = 100;
6
+ export function ProductOptionCombobox({ productId, value, onChange, placeholder = "Select product option…", disabled, }) {
7
+ const [search, setSearch] = React.useState("");
8
+ const listQuery = useProductOptions({
9
+ productId: productId || undefined,
10
+ limit: PAGE_SIZE,
11
+ enabled: !!productId,
12
+ });
13
+ const selectedQuery = useProductOption(value, { enabled: !!value });
14
+ const items = React.useMemo(() => {
15
+ const map = new Map();
16
+ for (const item of listQuery.data?.data ?? []) {
17
+ if (!search || item.name.toLowerCase().includes(search.toLowerCase()))
18
+ map.set(item.id, item);
19
+ }
20
+ if (selectedQuery.data)
21
+ map.set(selectedQuery.data.id, selectedQuery.data);
22
+ return Array.from(map.values());
23
+ }, [listQuery.data?.data, search, selectedQuery.data]);
24
+ const itemMap = React.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);
25
+ const selected = value ? itemMap.get(value) : undefined;
26
+ const selectedLabel = selected ? selected.name : "";
27
+ const [inputValue, setInputValue] = React.useState(selectedLabel);
28
+ React.useEffect(() => {
29
+ if (selectedLabel)
30
+ setInputValue(selectedLabel);
31
+ }, [selectedLabel]);
32
+ return (_jsxs(Combobox, { items: items.map((item) => item.id), value: value ?? null, inputValue: inputValue, autoHighlight: true, disabled: disabled || !productId, itemToStringValue: (id) => itemMap.get(id)?.name ?? "", onInputValueChange: (next) => {
33
+ setInputValue(next);
34
+ setSearch(next);
35
+ if (!next)
36
+ onChange(null);
37
+ }, onValueChange: (next) => {
38
+ const id = next ?? null;
39
+ onChange(id);
40
+ setInputValue(id ? (itemMap.get(id)?.name ?? "") : "");
41
+ }, children: [_jsx(ComboboxInput, { placeholder: placeholder, showClear: !!value }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: listQuery.isPending || selectedQuery.isPending
42
+ ? "Loading…"
43
+ : productId
44
+ ? "No product options found."
45
+ : "Select a product first." }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
46
+ const item = itemMap.get(id);
47
+ if (!item)
48
+ return null;
49
+ return (_jsx(ComboboxItem, { value: item.id, children: _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate font-medium", children: item.name }), item.code ? (_jsx("span", { className: "truncate text-xs text-muted-foreground", children: item.code })) : null] }) }, item.id));
50
+ } }) })] })] }));
51
+ }
@@ -0,0 +1,6 @@
1
+ export { ChannelCombobox } from "./components/channel-combobox";
2
+ export { MarketCombobox } from "./components/market-combobox";
3
+ export { PolicyDialog } from "./components/policy-dialog";
4
+ export { ProductCombobox } from "./components/product-combobox";
5
+ export { ProductOptionCombobox } from "./components/product-option-combobox";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAA;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAA;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sCAAsC,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { ChannelCombobox } from "./components/channel-combobox";
2
+ export { MarketCombobox } from "./components/market-combobox";
3
+ export { PolicyDialog } from "./components/policy-dialog";
4
+ export { ProductCombobox } from "./components/product-combobox";
5
+ export { ProductOptionCombobox } from "./components/product-option-combobox";
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@voyantjs/sellability-ui",
3
+ "version": "0.13.0",
4
+ "license": "FSL-1.1-Apache-2.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/voyantjs/voyant.git",
8
+ "directory": "packages/sellability-ui"
9
+ },
10
+ "type": "module",
11
+ "sideEffects": false,
12
+ "exports": {
13
+ ".": "./src/index.ts",
14
+ "./components/*": "./src/components/*.tsx"
15
+ },
16
+ "scripts": {
17
+ "build": "tsc -p tsconfig.build.json",
18
+ "clean": "rm -rf dist",
19
+ "prepack": "pnpm run build",
20
+ "typecheck": "tsc --noEmit",
21
+ "lint": "biome check src/",
22
+ "test": "vitest run --passWithNoTests"
23
+ },
24
+ "peerDependencies": {
25
+ "@tanstack/react-query": "^5.0.0",
26
+ "@tanstack/react-table": "^8.0.0",
27
+ "@voyantjs/distribution-react": "workspace:*",
28
+ "@voyantjs/markets-react": "workspace:*",
29
+ "@voyantjs/products-react": "workspace:*",
30
+ "@voyantjs/sellability-react": "workspace:*",
31
+ "@voyantjs/voyant-ui": "workspace:*",
32
+ "react": "^19.0.0",
33
+ "react-dom": "^19.0.0",
34
+ "react-hook-form": "^7.60.0",
35
+ "zod": "^3.25.76"
36
+ },
37
+ "devDependencies": {
38
+ "@tanstack/react-query": "^5.96.2",
39
+ "@tanstack/react-table": "^8.21.3",
40
+ "@types/react": "^19.2.14",
41
+ "@types/react-dom": "^19.2.3",
42
+ "@voyantjs/distribution-react": "workspace:*",
43
+ "@voyantjs/markets-react": "workspace:*",
44
+ "@voyantjs/products-react": "workspace:*",
45
+ "@voyantjs/sellability-react": "workspace:*",
46
+ "@voyantjs/voyant-typescript-config": "workspace:*",
47
+ "@voyantjs/voyant-ui": "workspace:*",
48
+ "lucide-react": "^0.475.0",
49
+ "react": "^19.2.4",
50
+ "react-dom": "^19.2.4",
51
+ "react-hook-form": "^7.60.0",
52
+ "typescript": "^6.0.2",
53
+ "vitest": "^4.1.2",
54
+ "zod": "^3.25.76"
55
+ },
56
+ "files": [
57
+ "dist"
58
+ ],
59
+ "publishConfig": {
60
+ "access": "public",
61
+ "exports": {
62
+ ".": {
63
+ "types": "./dist/index.d.ts",
64
+ "import": "./dist/index.js"
65
+ },
66
+ "./components/*": {
67
+ "types": "./dist/components/*.d.ts",
68
+ "import": "./dist/components/*.js"
69
+ }
70
+ },
71
+ "main": "./dist/index.js",
72
+ "types": "./dist/index.d.ts"
73
+ }
74
+ }