@nucleux/model-selection 0.0.0-stage → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nucleux
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,45 @@
1
- # Temporary Holding Version
1
+ # @nucleux/model-selection
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ModelSelection — choose which model handles the generation — Nucleux agentic UI for React.
4
+
5
+ Part of **[Nucleux](https://github.com/intelligaia-open-labs/nucleux)** — agentic UI components for React. Install a single component, or the full kit via [`@nucleux/react`](https://www.npmjs.com/package/@nucleux/react).
6
+
7
+ ## Exports
8
+
9
+ - **`ModelSelection`** — lets the user pick the model for the task. Props: `models`, `value` / `defaultValue` / `onValueChange`, `label`, and
10
+ - `variant`: `"dropdown"` (default — a composer-anchored listbox: arrow keys, Home/End, Escape returns focus to the trigger), `"tradeoff"` (radio cards with speed, quality and cost in human terms), `"auto-route"` (on by default — "Aria picks the model per task", shows `routedModel`; switching it off reveals the dropdown)
11
+ - `autoRoute` / `defaultAutoRoute` (true) / `onAutoRouteChange`, `routedModel`, `agentName`
12
+ - **`ModelOption`**: `value`, `label`, `description`, `icon`, plus `recommended` (a "Recommended" badge), `speed`, `quality`, `cost` (human terms, never parameter names), `disabled` with `unavailableReason` (the row stays and says what answers instead).
13
+ - Types: `ModelSelectionProps`, `ModelSelectionVariant`, `ModelOption`.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pnpm add @nucleux/model-selection @nucleux/tokens
19
+ ```
20
+
21
+ `react` / `react-dom` are peer dependencies.
22
+
23
+ ## Usage
24
+
25
+ ```tsx
26
+ import { ModelSelection } from "@nucleux/model-selection";
27
+
28
+ <ModelSelection
29
+ variant="tradeoff"
30
+ defaultValue="sonnet"
31
+ models={[
32
+ { value: "opus", label: "Opus", quality: "Best quality", speed: "Slower", cost: "$$$" },
33
+ { value: "sonnet", label: "Sonnet", quality: "Balanced", speed: "Medium", cost: "$$", recommended: true },
34
+ { value: "haiku", label: "Haiku", quality: "Good enough", speed: "Fastest", cost: "$" },
35
+ ]}
36
+ onValueChange={setModel}
37
+ />
38
+ ```
39
+
40
+ Requires the theme once in your app: `import "@nucleux/tokens/styles.css";`
41
+ and the Tailwind preset from `@nucleux/tokens/preset`.
42
+
43
+ ## License
44
+
45
+ MIT © Nucleux
package/dist/index.cjs ADDED
@@ -0,0 +1,257 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var React = require('react');
5
+ var utils = require('@nucleux/utils');
6
+ var badge = require('@nucleux/badge');
7
+ var radioGroup = require('@nucleux/radio-group');
8
+ var _switch = require('@nucleux/switch');
9
+ var lucideReact = require('lucide-react');
10
+ var jsxRuntime = require('react/jsx-runtime');
11
+
12
+ function _interopNamespace(e) {
13
+ if (e && e.__esModule) return e;
14
+ var n = Object.create(null);
15
+ if (e) {
16
+ Object.keys(e).forEach(function (k) {
17
+ if (k !== 'default') {
18
+ var d = Object.getOwnPropertyDescriptor(e, k);
19
+ Object.defineProperty(n, k, d.get ? d : {
20
+ enumerable: true,
21
+ get: function () { return e[k]; }
22
+ });
23
+ }
24
+ });
25
+ }
26
+ n.default = e;
27
+ return Object.freeze(n);
28
+ }
29
+
30
+ var React__namespace = /*#__PURE__*/_interopNamespace(React);
31
+
32
+ // src/index.tsx
33
+ function tradeoffLine(m) {
34
+ const parts = [m.speed, m.quality].filter((p) => p != null && p !== "");
35
+ if (!parts.length) return null;
36
+ return parts.map((p, i) => /* @__PURE__ */ jsxRuntime.jsxs(React__namespace.Fragment, { children: [
37
+ i > 0 && " \xB7 ",
38
+ p
39
+ ] }, i));
40
+ }
41
+ var RecommendedBadge = () => /* @__PURE__ */ jsxRuntime.jsx(badge.Badge, { variant: "outline", className: "border-brand/40 px-1.5 py-0 text-[10px] uppercase tracking-wide text-brand", children: "Recommended" });
42
+ function ModelDropdown({ models, current, select, label, className }) {
43
+ const [open, setOpen] = React__namespace.useState(false);
44
+ const rootRef = React__namespace.useRef(null);
45
+ const triggerRef = React__namespace.useRef(null);
46
+ const listRef = React__namespace.useRef(null);
47
+ const listId = React__namespace.useId();
48
+ const close = React__namespace.useCallback((refocus) => {
49
+ setOpen(false);
50
+ if (refocus) triggerRef.current?.focus();
51
+ }, []);
52
+ React__namespace.useEffect(() => {
53
+ if (!open) return;
54
+ const onDown = (e) => {
55
+ if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
56
+ };
57
+ const onKey = (e) => e.key === "Escape" && close(true);
58
+ document.addEventListener("mousedown", onDown);
59
+ document.addEventListener("keydown", onKey);
60
+ return () => {
61
+ document.removeEventListener("mousedown", onDown);
62
+ document.removeEventListener("keydown", onKey);
63
+ };
64
+ }, [open, close]);
65
+ const options = () => Array.from(listRef.current?.querySelectorAll('[role="option"]:not([aria-disabled="true"])') ?? []);
66
+ React__namespace.useEffect(() => {
67
+ if (!open) return;
68
+ const all = options();
69
+ (all.find((o) => o.getAttribute("aria-selected") === "true") ?? all[0])?.focus();
70
+ }, [open]);
71
+ const onListKeyDown = (e) => {
72
+ const all = options();
73
+ const i = all.indexOf(document.activeElement);
74
+ const go = (n) => all[(n + all.length) % all.length]?.focus();
75
+ if (e.key === "ArrowDown") e.preventDefault(), go(i + 1);
76
+ else if (e.key === "ArrowUp") e.preventDefault(), go(i - 1);
77
+ else if (e.key === "Home") e.preventDefault(), go(0);
78
+ else if (e.key === "End") e.preventDefault(), go(all.length - 1);
79
+ else if (e.key === "Tab") close(false);
80
+ };
81
+ const selected = models.find((m) => m.value === current);
82
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: rootRef, className: utils.cn("relative inline-block", className), children: [
83
+ /* @__PURE__ */ jsxRuntime.jsxs(
84
+ "button",
85
+ {
86
+ ref: triggerRef,
87
+ type: "button",
88
+ "aria-label": label,
89
+ "aria-haspopup": "listbox",
90
+ "aria-expanded": open,
91
+ "aria-controls": open ? listId : void 0,
92
+ onClick: () => setOpen((o) => !o),
93
+ onKeyDown: (e) => {
94
+ if ((e.key === "ArrowDown" || e.key === "ArrowUp") && !open) {
95
+ e.preventDefault();
96
+ setOpen(true);
97
+ }
98
+ },
99
+ className: "inline-flex items-center gap-2 rounded-lg border border-border bg-background px-3 py-2 text-sm font-medium text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&>svg]:size-4",
100
+ children: [
101
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { className: "text-brand", "aria-hidden": "true" }),
102
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: selected?.label ?? "Select model" }),
103
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { className: utils.cn("text-muted-foreground transition-transform", open && "rotate-180"), "aria-hidden": "true" })
104
+ ]
105
+ }
106
+ ),
107
+ open && /* @__PURE__ */ jsxRuntime.jsx(
108
+ "div",
109
+ {
110
+ ref: listRef,
111
+ id: listId,
112
+ role: "listbox",
113
+ "aria-label": label,
114
+ onKeyDown: onListKeyDown,
115
+ className: "absolute z-50 mt-1 flex min-w-64 flex-col gap-0.5 rounded-xl border border-border bg-background p-1.5 shadow-md animate-nx-fade-in motion-reduce:animate-none",
116
+ children: models.map((model) => {
117
+ const active = model.value === current;
118
+ const line = tradeoffLine(model);
119
+ return /* @__PURE__ */ jsxRuntime.jsxs(
120
+ "button",
121
+ {
122
+ type: "button",
123
+ role: "option",
124
+ "aria-selected": active,
125
+ "aria-disabled": model.disabled || void 0,
126
+ tabIndex: -1,
127
+ onClick: () => {
128
+ if (model.disabled) return;
129
+ select(model.value);
130
+ close(true);
131
+ },
132
+ className: utils.cn(
133
+ "flex items-start gap-2.5 rounded-lg px-2 py-2 text-left transition-colors focus-visible:outline-none",
134
+ model.disabled ? "cursor-not-allowed opacity-60" : "hover:bg-accent focus-visible:bg-accent",
135
+ active && "bg-brand-muted/40"
136
+ ),
137
+ children: [
138
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-0.5 inline-flex size-4 shrink-0 items-center justify-center text-brand [&_svg]:size-4", children: active ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { "aria-hidden": "true" }) : model.icon }),
139
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [
140
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
141
+ model.label,
142
+ model.recommended && /* @__PURE__ */ jsxRuntime.jsx(RecommendedBadge, {})
143
+ ] }),
144
+ model.description && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: model.description }),
145
+ line && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: line }),
146
+ model.disabled && model.unavailableReason && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-foreground", children: model.unavailableReason })
147
+ ] }),
148
+ model.cost && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 text-xs font-semibold text-muted-foreground", children: model.cost })
149
+ ]
150
+ },
151
+ model.value
152
+ );
153
+ })
154
+ }
155
+ )
156
+ ] });
157
+ }
158
+ var ModelSelection = React__namespace.forwardRef(
159
+ ({
160
+ models,
161
+ value,
162
+ defaultValue,
163
+ onValueChange,
164
+ label = "Model",
165
+ variant = "dropdown",
166
+ autoRoute,
167
+ defaultAutoRoute = true,
168
+ onAutoRouteChange,
169
+ routedModel,
170
+ agentName = "Aria",
171
+ className,
172
+ ...props
173
+ }, ref) => {
174
+ const [internal, setInternal] = React__namespace.useState(defaultValue ?? models[0]?.value);
175
+ const current = value !== void 0 ? value : internal;
176
+ const select = (next) => {
177
+ if (value === void 0) setInternal(next);
178
+ onValueChange?.(next);
179
+ };
180
+ const [autoInternal, setAutoInternal] = React__namespace.useState(defaultAutoRoute);
181
+ const auto = autoRoute !== void 0 ? autoRoute : autoInternal;
182
+ const setAuto = (on) => {
183
+ if (autoRoute === void 0) setAutoInternal(on);
184
+ onAutoRouteChange?.(on);
185
+ };
186
+ const autoLabelId = React__namespace.useId();
187
+ const headingId = React__namespace.useId();
188
+ if (variant === "tradeoff") {
189
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref, className: utils.cn("flex flex-col gap-2", className), ...props, children: [
190
+ /* @__PURE__ */ jsxRuntime.jsx("p", { id: headingId, className: "sr-only", children: label }),
191
+ /* @__PURE__ */ jsxRuntime.jsx(
192
+ radioGroup.RadioGroup,
193
+ {
194
+ "aria-labelledby": headingId,
195
+ value: current,
196
+ onValueChange: select,
197
+ className: "gap-1.5",
198
+ children: models.map((model) => {
199
+ const id = `${headingId}-${model.value}`;
200
+ const line = tradeoffLine(model);
201
+ return /* @__PURE__ */ jsxRuntime.jsxs(
202
+ "label",
203
+ {
204
+ className: utils.cn(
205
+ "flex cursor-pointer items-center gap-3 rounded-lg border border-border bg-background px-3 py-2 transition-colors hover:bg-accent",
206
+ "has-[[aria-checked=true]]:border-brand has-[[aria-checked=true]]:bg-brand-muted/40",
207
+ model.disabled && "cursor-not-allowed opacity-60 hover:bg-background"
208
+ ),
209
+ children: [
210
+ /* @__PURE__ */ jsxRuntime.jsx(radioGroup.Radio, { value: model.value, disabled: model.disabled, "aria-labelledby": id }),
211
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { id, className: "flex min-w-0 flex-1 flex-col", children: [
212
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-2 text-sm font-semibold text-foreground", children: [
213
+ model.label,
214
+ model.recommended && /* @__PURE__ */ jsxRuntime.jsx(RecommendedBadge, {})
215
+ ] }),
216
+ (line ?? model.description) && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: line ?? model.description }),
217
+ model.disabled && model.unavailableReason && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-foreground", children: model.unavailableReason })
218
+ ] }),
219
+ model.cost && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 text-xs font-semibold text-muted-foreground", children: model.cost })
220
+ ]
221
+ },
222
+ model.value
223
+ );
224
+ })
225
+ }
226
+ )
227
+ ] });
228
+ }
229
+ if (variant === "auto-route") {
230
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref, className: utils.cn("flex flex-col gap-2", className), ...props, children: [
231
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 rounded-lg border border-border bg-background px-3.5 py-2.5", children: [
232
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-col", children: [
233
+ /* @__PURE__ */ jsxRuntime.jsx("span", { id: autoLabelId, className: "text-sm font-semibold text-foreground", children: "Auto-route" }),
234
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground", children: [
235
+ agentName,
236
+ " picks the model per task"
237
+ ] })
238
+ ] }),
239
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
240
+ auto && routedModel && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground", "aria-live": "polite", children: [
241
+ "currently: ",
242
+ routedModel
243
+ ] }),
244
+ /* @__PURE__ */ jsxRuntime.jsx(_switch.Switch, { checked: auto, onCheckedChange: setAuto, "aria-labelledby": autoLabelId })
245
+ ] })
246
+ ] }),
247
+ !auto && /* @__PURE__ */ jsxRuntime.jsx(ModelDropdown, { models, current, select, label })
248
+ ] });
249
+ }
250
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, className: utils.cn("relative inline-block", className), ...props, children: /* @__PURE__ */ jsxRuntime.jsx(ModelDropdown, { models, current, select, label }) });
251
+ }
252
+ );
253
+ ModelSelection.displayName = "ModelSelection";
254
+
255
+ exports.ModelSelection = ModelSelection;
256
+ //# sourceMappingURL=index.cjs.map
257
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.tsx"],"names":["jsxs","React","jsx","Badge","cn","Sparkles","ChevronDown","Check","RadioGroup","Radio","Switch"],"mappings":"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","file":"index.cjs","sourcesContent":["import * as React from \"react\";\nimport { cn } from \"@nucleux/utils\";\nimport { Badge } from \"@nucleux/badge\";\nimport { Radio, RadioGroup } from \"@nucleux/radio-group\";\nimport { Switch } from \"@nucleux/switch\";\nimport { Check, ChevronDown, Sparkles } from \"lucide-react\";\n\n/** Layout from the spec: a compact dropdown, tradeoff cards, or an auto-route toggle that reveals the picker. */\nexport type ModelSelectionVariant = \"dropdown\" | \"tradeoff\" | \"auto-route\";\n\nexport interface ModelOption {\n /** Stable value passed to `onValueChange`. */\n value: string;\n /** Display label, e.g. `\"Nebula 5.5 Instant\"`. */\n label: React.ReactNode;\n /** Supporting line under the label — what the model is *for*, e.g. `\"Everyday tasks\"`. */\n description?: React.ReactNode;\n /** Optional leading icon. */\n icon?: React.ReactNode;\n /** Marks the suggested default with a \"Recommended\" badge. */\n recommended?: boolean;\n /** Speed in human terms, e.g. `\"Fastest\"`. */\n speed?: React.ReactNode;\n /** Quality in human terms, e.g. `\"Best quality\"`. */\n quality?: React.ReactNode;\n /** Cost in human terms, e.g. `\"$$\"` or `\"Uses more of your allowance\"`. */\n cost?: React.ReactNode;\n /** The option can't be chosen right now (allowance used up, not in your plan). The row stays. */\n disabled?: boolean;\n /** Why it is disabled and what answers instead, e.g. `\"Limit reached until 3 pm — Sonnet answers instead\"`. */\n unavailableReason?: React.ReactNode;\n}\n\nexport interface ModelSelectionProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n /** The available models. */\n models: ModelOption[];\n /** Selected value (controlled). */\n value?: string;\n /** Initial value (uncontrolled). */\n defaultValue?: string;\n /** Called when a model is chosen. */\n onValueChange?: (value: string) => void;\n /** Accessible label for the trigger. Defaults to `\"Model\"`. */\n label?: string;\n /** Layout. Defaults to `\"dropdown\"`. */\n variant?: ModelSelectionVariant;\n /** Auto-route on (controlled), for `variant=\"auto-route\"`. */\n autoRoute?: boolean;\n /** Initial auto-route state (uncontrolled). Defaults to `true`: auto-route is the default. */\n defaultAutoRoute?: boolean;\n /** Called when auto-route is switched. */\n onAutoRouteChange?: (on: boolean) => void;\n /** The model auto-route is using now, shown as \"currently: …\". */\n routedModel?: React.ReactNode;\n /** Who routes, for the description. Defaults to `\"Aria\"`. */\n agentName?: React.ReactNode;\n}\n\n/** \"Fastest · Best quality · $$\" from whichever tradeoff facts are given. */\nfunction tradeoffLine(m: ModelOption): React.ReactNode {\n const parts = [m.speed, m.quality].filter((p) => p != null && p !== \"\");\n if (!parts.length) return null;\n return parts.map((p, i) => (\n <React.Fragment key={i}>\n {i > 0 && \" · \"}\n {p}\n </React.Fragment>\n ));\n}\n\nconst RecommendedBadge = () => (\n <Badge variant=\"outline\" className=\"border-brand/40 px-1.5 py-0 text-[10px] uppercase tracking-wide text-brand\">\n Recommended\n </Badge>\n);\n\ninterface PickerProps {\n models: ModelOption[];\n current: string | undefined;\n select: (value: string) => void;\n label: string;\n className?: string;\n}\n\n/** The composer-anchored dropdown (listbox) used by the \"dropdown\" and \"auto-route\" variants. */\nfunction ModelDropdown({ models, current, select, label, className }: PickerProps) {\n const [open, setOpen] = React.useState(false);\n const rootRef = React.useRef<HTMLDivElement>(null);\n const triggerRef = React.useRef<HTMLButtonElement>(null);\n const listRef = React.useRef<HTMLDivElement>(null);\n const listId = React.useId();\n\n const close = React.useCallback((refocus: boolean) => {\n setOpen(false);\n if (refocus) triggerRef.current?.focus();\n }, []);\n\n React.useEffect(() => {\n if (!open) return;\n const onDown = (e: MouseEvent) => {\n if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false);\n };\n const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && close(true);\n document.addEventListener(\"mousedown\", onDown);\n document.addEventListener(\"keydown\", onKey);\n return () => {\n document.removeEventListener(\"mousedown\", onDown);\n document.removeEventListener(\"keydown\", onKey);\n };\n }, [open, close]);\n\n const options = () =>\n Array.from(listRef.current?.querySelectorAll<HTMLElement>('[role=\"option\"]:not([aria-disabled=\"true\"])') ?? []);\n\n // Focus the selected option (or the first available) when the list opens.\n React.useEffect(() => {\n if (!open) return;\n const all = options();\n (all.find((o) => o.getAttribute(\"aria-selected\") === \"true\") ?? all[0])?.focus();\n }, [open]);\n\n const onListKeyDown = (e: React.KeyboardEvent) => {\n const all = options();\n const i = all.indexOf(document.activeElement as HTMLElement);\n const go = (n: number) => all[(n + all.length) % all.length]?.focus();\n if (e.key === \"ArrowDown\") (e.preventDefault(), go(i + 1));\n else if (e.key === \"ArrowUp\") (e.preventDefault(), go(i - 1));\n else if (e.key === \"Home\") (e.preventDefault(), go(0));\n else if (e.key === \"End\") (e.preventDefault(), go(all.length - 1));\n else if (e.key === \"Tab\") close(false);\n };\n\n const selected = models.find((m) => m.value === current);\n\n return (\n <div ref={rootRef} className={cn(\"relative inline-block\", className)}>\n <button\n ref={triggerRef}\n type=\"button\"\n aria-label={label}\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n aria-controls={open ? listId : undefined}\n onClick={() => setOpen((o) => !o)}\n onKeyDown={(e) => {\n if ((e.key === \"ArrowDown\" || e.key === \"ArrowUp\") && !open) {\n e.preventDefault();\n setOpen(true);\n }\n }}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border bg-background px-3 py-2 text-sm font-medium text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&>svg]:size-4\"\n >\n <Sparkles className=\"text-brand\" aria-hidden=\"true\" />\n <span>{selected?.label ?? \"Select model\"}</span>\n <ChevronDown className={cn(\"text-muted-foreground transition-transform\", open && \"rotate-180\")} aria-hidden=\"true\" />\n </button>\n {open && (\n <div\n ref={listRef}\n id={listId}\n role=\"listbox\"\n aria-label={label}\n onKeyDown={onListKeyDown}\n className=\"absolute z-50 mt-1 flex min-w-64 flex-col gap-0.5 rounded-xl border border-border bg-background p-1.5 shadow-md animate-nx-fade-in motion-reduce:animate-none\"\n >\n {models.map((model) => {\n const active = model.value === current;\n const line = tradeoffLine(model);\n return (\n <button\n key={model.value}\n type=\"button\"\n role=\"option\"\n aria-selected={active}\n aria-disabled={model.disabled || undefined}\n tabIndex={-1}\n onClick={() => {\n if (model.disabled) return;\n select(model.value);\n close(true);\n }}\n className={cn(\n \"flex items-start gap-2.5 rounded-lg px-2 py-2 text-left transition-colors focus-visible:outline-none\",\n model.disabled ? \"cursor-not-allowed opacity-60\" : \"hover:bg-accent focus-visible:bg-accent\",\n active && \"bg-brand-muted/40\",\n )}\n >\n <span className=\"mt-0.5 inline-flex size-4 shrink-0 items-center justify-center text-brand [&_svg]:size-4\">\n {active ? <Check aria-hidden=\"true\" /> : model.icon}\n </span>\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"flex items-center gap-2 text-sm font-medium text-foreground\">\n {model.label}\n {model.recommended && <RecommendedBadge />}\n </span>\n {model.description && <span className=\"text-xs text-muted-foreground\">{model.description}</span>}\n {line && <span className=\"text-xs text-muted-foreground\">{line}</span>}\n {model.disabled && model.unavailableReason && (\n <span className=\"text-xs text-foreground\">{model.unavailableReason}</span>\n )}\n </span>\n {model.cost && <span className=\"shrink-0 text-xs font-semibold text-muted-foreground\">{model.cost}</span>}\n </button>\n );\n })}\n </div>\n )}\n </div>\n );\n}\n\n/**\n * Lets the user pick the model for the task. `variant=\"dropdown\"` (default) is\n * a composer-anchored listbox (arrow keys, Home/End, Escape returns focus);\n * `\"tradeoff\"` lays models out as radio cards with speed, quality and cost in\n * human terms; `\"auto-route\"` defaults to letting the agent route per task,\n * shows which model it is using, and reveals the picker when switched off.\n * Recommended and unavailable models are marked in words; an unavailable model\n * keeps its row and says what answers instead.\n * Source: Figma \"Context Expansion / Model Selection\".\n */\nexport const ModelSelection = React.forwardRef<HTMLDivElement, ModelSelectionProps>(\n (\n {\n models,\n value,\n defaultValue,\n onValueChange,\n label = \"Model\",\n variant = \"dropdown\",\n autoRoute,\n defaultAutoRoute = true,\n onAutoRouteChange,\n routedModel,\n agentName = \"Aria\",\n className,\n ...props\n },\n ref,\n ) => {\n const [internal, setInternal] = React.useState<string | undefined>(defaultValue ?? models[0]?.value);\n const current = value !== undefined ? value : internal;\n const select = (next: string) => {\n if (value === undefined) setInternal(next);\n onValueChange?.(next);\n };\n\n const [autoInternal, setAutoInternal] = React.useState(defaultAutoRoute);\n const auto = autoRoute !== undefined ? autoRoute : autoInternal;\n const setAuto = (on: boolean) => {\n if (autoRoute === undefined) setAutoInternal(on);\n onAutoRouteChange?.(on);\n };\n const autoLabelId = React.useId();\n const headingId = React.useId();\n\n if (variant === \"tradeoff\") {\n return (\n <div ref={ref} className={cn(\"flex flex-col gap-2\", className)} {...props}>\n <p id={headingId} className=\"sr-only\">\n {label}\n </p>\n <RadioGroup\n aria-labelledby={headingId}\n value={current}\n onValueChange={select}\n className=\"gap-1.5\"\n >\n {models.map((model) => {\n const id = `${headingId}-${model.value}`;\n const line = tradeoffLine(model);\n return (\n <label\n key={model.value}\n className={cn(\n \"flex cursor-pointer items-center gap-3 rounded-lg border border-border bg-background px-3 py-2 transition-colors hover:bg-accent\",\n \"has-[[aria-checked=true]]:border-brand has-[[aria-checked=true]]:bg-brand-muted/40\",\n model.disabled && \"cursor-not-allowed opacity-60 hover:bg-background\",\n )}\n >\n <Radio value={model.value} disabled={model.disabled} aria-labelledby={id} />\n <span id={id} className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"flex items-center gap-2 text-sm font-semibold text-foreground\">\n {model.label}\n {model.recommended && <RecommendedBadge />}\n </span>\n {(line ?? model.description) && (\n <span className=\"text-xs text-muted-foreground\">{line ?? model.description}</span>\n )}\n {model.disabled && model.unavailableReason && (\n <span className=\"text-xs text-foreground\">{model.unavailableReason}</span>\n )}\n </span>\n {model.cost && (\n <span className=\"shrink-0 text-xs font-semibold text-muted-foreground\">{model.cost}</span>\n )}\n </label>\n );\n })}\n </RadioGroup>\n </div>\n );\n }\n\n if (variant === \"auto-route\") {\n return (\n <div ref={ref} className={cn(\"flex flex-col gap-2\", className)} {...props}>\n <div className=\"flex items-center justify-between gap-3 rounded-lg border border-border bg-background px-3.5 py-2.5\">\n <div className=\"flex min-w-0 flex-col\">\n <span id={autoLabelId} className=\"text-sm font-semibold text-foreground\">\n Auto-route\n </span>\n <span className=\"text-xs text-muted-foreground\">{agentName} picks the model per task</span>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n {auto && routedModel && (\n <span className=\"text-xs text-muted-foreground\" aria-live=\"polite\">\n currently: {routedModel}\n </span>\n )}\n <Switch checked={auto} onCheckedChange={setAuto} aria-labelledby={autoLabelId} />\n </div>\n </div>\n {!auto && <ModelDropdown models={models} current={current} select={select} label={label} />}\n </div>\n );\n }\n\n return (\n <div ref={ref} className={cn(\"relative inline-block\", className)} {...props}>\n <ModelDropdown models={models} current={current} select={select} label={label} />\n </div>\n );\n },\n);\nModelSelection.displayName = \"ModelSelection\";\n"]}
@@ -0,0 +1,63 @@
1
+ import * as React from 'react';
2
+
3
+ /** Layout from the spec: a compact dropdown, tradeoff cards, or an auto-route toggle that reveals the picker. */
4
+ type ModelSelectionVariant = "dropdown" | "tradeoff" | "auto-route";
5
+ interface ModelOption {
6
+ /** Stable value passed to `onValueChange`. */
7
+ value: string;
8
+ /** Display label, e.g. `"Nebula 5.5 Instant"`. */
9
+ label: React.ReactNode;
10
+ /** Supporting line under the label — what the model is *for*, e.g. `"Everyday tasks"`. */
11
+ description?: React.ReactNode;
12
+ /** Optional leading icon. */
13
+ icon?: React.ReactNode;
14
+ /** Marks the suggested default with a "Recommended" badge. */
15
+ recommended?: boolean;
16
+ /** Speed in human terms, e.g. `"Fastest"`. */
17
+ speed?: React.ReactNode;
18
+ /** Quality in human terms, e.g. `"Best quality"`. */
19
+ quality?: React.ReactNode;
20
+ /** Cost in human terms, e.g. `"$$"` or `"Uses more of your allowance"`. */
21
+ cost?: React.ReactNode;
22
+ /** The option can't be chosen right now (allowance used up, not in your plan). The row stays. */
23
+ disabled?: boolean;
24
+ /** Why it is disabled and what answers instead, e.g. `"Limit reached until 3 pm — Sonnet answers instead"`. */
25
+ unavailableReason?: React.ReactNode;
26
+ }
27
+ interface ModelSelectionProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
28
+ /** The available models. */
29
+ models: ModelOption[];
30
+ /** Selected value (controlled). */
31
+ value?: string;
32
+ /** Initial value (uncontrolled). */
33
+ defaultValue?: string;
34
+ /** Called when a model is chosen. */
35
+ onValueChange?: (value: string) => void;
36
+ /** Accessible label for the trigger. Defaults to `"Model"`. */
37
+ label?: string;
38
+ /** Layout. Defaults to `"dropdown"`. */
39
+ variant?: ModelSelectionVariant;
40
+ /** Auto-route on (controlled), for `variant="auto-route"`. */
41
+ autoRoute?: boolean;
42
+ /** Initial auto-route state (uncontrolled). Defaults to `true`: auto-route is the default. */
43
+ defaultAutoRoute?: boolean;
44
+ /** Called when auto-route is switched. */
45
+ onAutoRouteChange?: (on: boolean) => void;
46
+ /** The model auto-route is using now, shown as "currently: …". */
47
+ routedModel?: React.ReactNode;
48
+ /** Who routes, for the description. Defaults to `"Aria"`. */
49
+ agentName?: React.ReactNode;
50
+ }
51
+ /**
52
+ * Lets the user pick the model for the task. `variant="dropdown"` (default) is
53
+ * a composer-anchored listbox (arrow keys, Home/End, Escape returns focus);
54
+ * `"tradeoff"` lays models out as radio cards with speed, quality and cost in
55
+ * human terms; `"auto-route"` defaults to letting the agent route per task,
56
+ * shows which model it is using, and reveals the picker when switched off.
57
+ * Recommended and unavailable models are marked in words; an unavailable model
58
+ * keeps its row and says what answers instead.
59
+ * Source: Figma "Context Expansion / Model Selection".
60
+ */
61
+ declare const ModelSelection: React.ForwardRefExoticComponent<ModelSelectionProps & React.RefAttributes<HTMLDivElement>>;
62
+
63
+ export { type ModelOption, ModelSelection, type ModelSelectionProps, type ModelSelectionVariant };
@@ -0,0 +1,63 @@
1
+ import * as React from 'react';
2
+
3
+ /** Layout from the spec: a compact dropdown, tradeoff cards, or an auto-route toggle that reveals the picker. */
4
+ type ModelSelectionVariant = "dropdown" | "tradeoff" | "auto-route";
5
+ interface ModelOption {
6
+ /** Stable value passed to `onValueChange`. */
7
+ value: string;
8
+ /** Display label, e.g. `"Nebula 5.5 Instant"`. */
9
+ label: React.ReactNode;
10
+ /** Supporting line under the label — what the model is *for*, e.g. `"Everyday tasks"`. */
11
+ description?: React.ReactNode;
12
+ /** Optional leading icon. */
13
+ icon?: React.ReactNode;
14
+ /** Marks the suggested default with a "Recommended" badge. */
15
+ recommended?: boolean;
16
+ /** Speed in human terms, e.g. `"Fastest"`. */
17
+ speed?: React.ReactNode;
18
+ /** Quality in human terms, e.g. `"Best quality"`. */
19
+ quality?: React.ReactNode;
20
+ /** Cost in human terms, e.g. `"$$"` or `"Uses more of your allowance"`. */
21
+ cost?: React.ReactNode;
22
+ /** The option can't be chosen right now (allowance used up, not in your plan). The row stays. */
23
+ disabled?: boolean;
24
+ /** Why it is disabled and what answers instead, e.g. `"Limit reached until 3 pm — Sonnet answers instead"`. */
25
+ unavailableReason?: React.ReactNode;
26
+ }
27
+ interface ModelSelectionProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
28
+ /** The available models. */
29
+ models: ModelOption[];
30
+ /** Selected value (controlled). */
31
+ value?: string;
32
+ /** Initial value (uncontrolled). */
33
+ defaultValue?: string;
34
+ /** Called when a model is chosen. */
35
+ onValueChange?: (value: string) => void;
36
+ /** Accessible label for the trigger. Defaults to `"Model"`. */
37
+ label?: string;
38
+ /** Layout. Defaults to `"dropdown"`. */
39
+ variant?: ModelSelectionVariant;
40
+ /** Auto-route on (controlled), for `variant="auto-route"`. */
41
+ autoRoute?: boolean;
42
+ /** Initial auto-route state (uncontrolled). Defaults to `true`: auto-route is the default. */
43
+ defaultAutoRoute?: boolean;
44
+ /** Called when auto-route is switched. */
45
+ onAutoRouteChange?: (on: boolean) => void;
46
+ /** The model auto-route is using now, shown as "currently: …". */
47
+ routedModel?: React.ReactNode;
48
+ /** Who routes, for the description. Defaults to `"Aria"`. */
49
+ agentName?: React.ReactNode;
50
+ }
51
+ /**
52
+ * Lets the user pick the model for the task. `variant="dropdown"` (default) is
53
+ * a composer-anchored listbox (arrow keys, Home/End, Escape returns focus);
54
+ * `"tradeoff"` lays models out as radio cards with speed, quality and cost in
55
+ * human terms; `"auto-route"` defaults to letting the agent route per task,
56
+ * shows which model it is using, and reveals the picker when switched off.
57
+ * Recommended and unavailable models are marked in words; an unavailable model
58
+ * keeps its row and says what answers instead.
59
+ * Source: Figma "Context Expansion / Model Selection".
60
+ */
61
+ declare const ModelSelection: React.ForwardRefExoticComponent<ModelSelectionProps & React.RefAttributes<HTMLDivElement>>;
62
+
63
+ export { type ModelOption, ModelSelection, type ModelSelectionProps, type ModelSelectionVariant };
package/dist/index.js ADDED
@@ -0,0 +1,235 @@
1
+ "use client";
2
+ import * as React from 'react';
3
+ import { cn } from '@nucleux/utils';
4
+ import { Badge } from '@nucleux/badge';
5
+ import { RadioGroup, Radio } from '@nucleux/radio-group';
6
+ import { Switch } from '@nucleux/switch';
7
+ import { Sparkles, ChevronDown, Check } from 'lucide-react';
8
+ import { jsxs, jsx } from 'react/jsx-runtime';
9
+
10
+ // src/index.tsx
11
+ function tradeoffLine(m) {
12
+ const parts = [m.speed, m.quality].filter((p) => p != null && p !== "");
13
+ if (!parts.length) return null;
14
+ return parts.map((p, i) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
15
+ i > 0 && " \xB7 ",
16
+ p
17
+ ] }, i));
18
+ }
19
+ var RecommendedBadge = () => /* @__PURE__ */ jsx(Badge, { variant: "outline", className: "border-brand/40 px-1.5 py-0 text-[10px] uppercase tracking-wide text-brand", children: "Recommended" });
20
+ function ModelDropdown({ models, current, select, label, className }) {
21
+ const [open, setOpen] = React.useState(false);
22
+ const rootRef = React.useRef(null);
23
+ const triggerRef = React.useRef(null);
24
+ const listRef = React.useRef(null);
25
+ const listId = React.useId();
26
+ const close = React.useCallback((refocus) => {
27
+ setOpen(false);
28
+ if (refocus) triggerRef.current?.focus();
29
+ }, []);
30
+ React.useEffect(() => {
31
+ if (!open) return;
32
+ const onDown = (e) => {
33
+ if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
34
+ };
35
+ const onKey = (e) => e.key === "Escape" && close(true);
36
+ document.addEventListener("mousedown", onDown);
37
+ document.addEventListener("keydown", onKey);
38
+ return () => {
39
+ document.removeEventListener("mousedown", onDown);
40
+ document.removeEventListener("keydown", onKey);
41
+ };
42
+ }, [open, close]);
43
+ const options = () => Array.from(listRef.current?.querySelectorAll('[role="option"]:not([aria-disabled="true"])') ?? []);
44
+ React.useEffect(() => {
45
+ if (!open) return;
46
+ const all = options();
47
+ (all.find((o) => o.getAttribute("aria-selected") === "true") ?? all[0])?.focus();
48
+ }, [open]);
49
+ const onListKeyDown = (e) => {
50
+ const all = options();
51
+ const i = all.indexOf(document.activeElement);
52
+ const go = (n) => all[(n + all.length) % all.length]?.focus();
53
+ if (e.key === "ArrowDown") e.preventDefault(), go(i + 1);
54
+ else if (e.key === "ArrowUp") e.preventDefault(), go(i - 1);
55
+ else if (e.key === "Home") e.preventDefault(), go(0);
56
+ else if (e.key === "End") e.preventDefault(), go(all.length - 1);
57
+ else if (e.key === "Tab") close(false);
58
+ };
59
+ const selected = models.find((m) => m.value === current);
60
+ return /* @__PURE__ */ jsxs("div", { ref: rootRef, className: cn("relative inline-block", className), children: [
61
+ /* @__PURE__ */ jsxs(
62
+ "button",
63
+ {
64
+ ref: triggerRef,
65
+ type: "button",
66
+ "aria-label": label,
67
+ "aria-haspopup": "listbox",
68
+ "aria-expanded": open,
69
+ "aria-controls": open ? listId : void 0,
70
+ onClick: () => setOpen((o) => !o),
71
+ onKeyDown: (e) => {
72
+ if ((e.key === "ArrowDown" || e.key === "ArrowUp") && !open) {
73
+ e.preventDefault();
74
+ setOpen(true);
75
+ }
76
+ },
77
+ className: "inline-flex items-center gap-2 rounded-lg border border-border bg-background px-3 py-2 text-sm font-medium text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&>svg]:size-4",
78
+ children: [
79
+ /* @__PURE__ */ jsx(Sparkles, { className: "text-brand", "aria-hidden": "true" }),
80
+ /* @__PURE__ */ jsx("span", { children: selected?.label ?? "Select model" }),
81
+ /* @__PURE__ */ jsx(ChevronDown, { className: cn("text-muted-foreground transition-transform", open && "rotate-180"), "aria-hidden": "true" })
82
+ ]
83
+ }
84
+ ),
85
+ open && /* @__PURE__ */ jsx(
86
+ "div",
87
+ {
88
+ ref: listRef,
89
+ id: listId,
90
+ role: "listbox",
91
+ "aria-label": label,
92
+ onKeyDown: onListKeyDown,
93
+ className: "absolute z-50 mt-1 flex min-w-64 flex-col gap-0.5 rounded-xl border border-border bg-background p-1.5 shadow-md animate-nx-fade-in motion-reduce:animate-none",
94
+ children: models.map((model) => {
95
+ const active = model.value === current;
96
+ const line = tradeoffLine(model);
97
+ return /* @__PURE__ */ jsxs(
98
+ "button",
99
+ {
100
+ type: "button",
101
+ role: "option",
102
+ "aria-selected": active,
103
+ "aria-disabled": model.disabled || void 0,
104
+ tabIndex: -1,
105
+ onClick: () => {
106
+ if (model.disabled) return;
107
+ select(model.value);
108
+ close(true);
109
+ },
110
+ className: cn(
111
+ "flex items-start gap-2.5 rounded-lg px-2 py-2 text-left transition-colors focus-visible:outline-none",
112
+ model.disabled ? "cursor-not-allowed opacity-60" : "hover:bg-accent focus-visible:bg-accent",
113
+ active && "bg-brand-muted/40"
114
+ ),
115
+ children: [
116
+ /* @__PURE__ */ jsx("span", { className: "mt-0.5 inline-flex size-4 shrink-0 items-center justify-center text-brand [&_svg]:size-4", children: active ? /* @__PURE__ */ jsx(Check, { "aria-hidden": "true" }) : model.icon }),
117
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [
118
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
119
+ model.label,
120
+ model.recommended && /* @__PURE__ */ jsx(RecommendedBadge, {})
121
+ ] }),
122
+ model.description && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: model.description }),
123
+ line && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: line }),
124
+ model.disabled && model.unavailableReason && /* @__PURE__ */ jsx("span", { className: "text-xs text-foreground", children: model.unavailableReason })
125
+ ] }),
126
+ model.cost && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs font-semibold text-muted-foreground", children: model.cost })
127
+ ]
128
+ },
129
+ model.value
130
+ );
131
+ })
132
+ }
133
+ )
134
+ ] });
135
+ }
136
+ var ModelSelection = React.forwardRef(
137
+ ({
138
+ models,
139
+ value,
140
+ defaultValue,
141
+ onValueChange,
142
+ label = "Model",
143
+ variant = "dropdown",
144
+ autoRoute,
145
+ defaultAutoRoute = true,
146
+ onAutoRouteChange,
147
+ routedModel,
148
+ agentName = "Aria",
149
+ className,
150
+ ...props
151
+ }, ref) => {
152
+ const [internal, setInternal] = React.useState(defaultValue ?? models[0]?.value);
153
+ const current = value !== void 0 ? value : internal;
154
+ const select = (next) => {
155
+ if (value === void 0) setInternal(next);
156
+ onValueChange?.(next);
157
+ };
158
+ const [autoInternal, setAutoInternal] = React.useState(defaultAutoRoute);
159
+ const auto = autoRoute !== void 0 ? autoRoute : autoInternal;
160
+ const setAuto = (on) => {
161
+ if (autoRoute === void 0) setAutoInternal(on);
162
+ onAutoRouteChange?.(on);
163
+ };
164
+ const autoLabelId = React.useId();
165
+ const headingId = React.useId();
166
+ if (variant === "tradeoff") {
167
+ return /* @__PURE__ */ jsxs("div", { ref, className: cn("flex flex-col gap-2", className), ...props, children: [
168
+ /* @__PURE__ */ jsx("p", { id: headingId, className: "sr-only", children: label }),
169
+ /* @__PURE__ */ jsx(
170
+ RadioGroup,
171
+ {
172
+ "aria-labelledby": headingId,
173
+ value: current,
174
+ onValueChange: select,
175
+ className: "gap-1.5",
176
+ children: models.map((model) => {
177
+ const id = `${headingId}-${model.value}`;
178
+ const line = tradeoffLine(model);
179
+ return /* @__PURE__ */ jsxs(
180
+ "label",
181
+ {
182
+ className: cn(
183
+ "flex cursor-pointer items-center gap-3 rounded-lg border border-border bg-background px-3 py-2 transition-colors hover:bg-accent",
184
+ "has-[[aria-checked=true]]:border-brand has-[[aria-checked=true]]:bg-brand-muted/40",
185
+ model.disabled && "cursor-not-allowed opacity-60 hover:bg-background"
186
+ ),
187
+ children: [
188
+ /* @__PURE__ */ jsx(Radio, { value: model.value, disabled: model.disabled, "aria-labelledby": id }),
189
+ /* @__PURE__ */ jsxs("span", { id, className: "flex min-w-0 flex-1 flex-col", children: [
190
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 text-sm font-semibold text-foreground", children: [
191
+ model.label,
192
+ model.recommended && /* @__PURE__ */ jsx(RecommendedBadge, {})
193
+ ] }),
194
+ (line ?? model.description) && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: line ?? model.description }),
195
+ model.disabled && model.unavailableReason && /* @__PURE__ */ jsx("span", { className: "text-xs text-foreground", children: model.unavailableReason })
196
+ ] }),
197
+ model.cost && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs font-semibold text-muted-foreground", children: model.cost })
198
+ ]
199
+ },
200
+ model.value
201
+ );
202
+ })
203
+ }
204
+ )
205
+ ] });
206
+ }
207
+ if (variant === "auto-route") {
208
+ return /* @__PURE__ */ jsxs("div", { ref, className: cn("flex flex-col gap-2", className), ...props, children: [
209
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 rounded-lg border border-border bg-background px-3.5 py-2.5", children: [
210
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col", children: [
211
+ /* @__PURE__ */ jsx("span", { id: autoLabelId, className: "text-sm font-semibold text-foreground", children: "Auto-route" }),
212
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
213
+ agentName,
214
+ " picks the model per task"
215
+ ] })
216
+ ] }),
217
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
218
+ auto && routedModel && /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", "aria-live": "polite", children: [
219
+ "currently: ",
220
+ routedModel
221
+ ] }),
222
+ /* @__PURE__ */ jsx(Switch, { checked: auto, onCheckedChange: setAuto, "aria-labelledby": autoLabelId })
223
+ ] })
224
+ ] }),
225
+ !auto && /* @__PURE__ */ jsx(ModelDropdown, { models, current, select, label })
226
+ ] });
227
+ }
228
+ return /* @__PURE__ */ jsx("div", { ref, className: cn("relative inline-block", className), ...props, children: /* @__PURE__ */ jsx(ModelDropdown, { models, current, select, label }) });
229
+ }
230
+ );
231
+ ModelSelection.displayName = "ModelSelection";
232
+
233
+ export { ModelSelection };
234
+ //# sourceMappingURL=index.js.map
235
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import * as React from \"react\";\nimport { cn } from \"@nucleux/utils\";\nimport { Badge } from \"@nucleux/badge\";\nimport { Radio, RadioGroup } from \"@nucleux/radio-group\";\nimport { Switch } from \"@nucleux/switch\";\nimport { Check, ChevronDown, Sparkles } from \"lucide-react\";\n\n/** Layout from the spec: a compact dropdown, tradeoff cards, or an auto-route toggle that reveals the picker. */\nexport type ModelSelectionVariant = \"dropdown\" | \"tradeoff\" | \"auto-route\";\n\nexport interface ModelOption {\n /** Stable value passed to `onValueChange`. */\n value: string;\n /** Display label, e.g. `\"Nebula 5.5 Instant\"`. */\n label: React.ReactNode;\n /** Supporting line under the label — what the model is *for*, e.g. `\"Everyday tasks\"`. */\n description?: React.ReactNode;\n /** Optional leading icon. */\n icon?: React.ReactNode;\n /** Marks the suggested default with a \"Recommended\" badge. */\n recommended?: boolean;\n /** Speed in human terms, e.g. `\"Fastest\"`. */\n speed?: React.ReactNode;\n /** Quality in human terms, e.g. `\"Best quality\"`. */\n quality?: React.ReactNode;\n /** Cost in human terms, e.g. `\"$$\"` or `\"Uses more of your allowance\"`. */\n cost?: React.ReactNode;\n /** The option can't be chosen right now (allowance used up, not in your plan). The row stays. */\n disabled?: boolean;\n /** Why it is disabled and what answers instead, e.g. `\"Limit reached until 3 pm — Sonnet answers instead\"`. */\n unavailableReason?: React.ReactNode;\n}\n\nexport interface ModelSelectionProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n /** The available models. */\n models: ModelOption[];\n /** Selected value (controlled). */\n value?: string;\n /** Initial value (uncontrolled). */\n defaultValue?: string;\n /** Called when a model is chosen. */\n onValueChange?: (value: string) => void;\n /** Accessible label for the trigger. Defaults to `\"Model\"`. */\n label?: string;\n /** Layout. Defaults to `\"dropdown\"`. */\n variant?: ModelSelectionVariant;\n /** Auto-route on (controlled), for `variant=\"auto-route\"`. */\n autoRoute?: boolean;\n /** Initial auto-route state (uncontrolled). Defaults to `true`: auto-route is the default. */\n defaultAutoRoute?: boolean;\n /** Called when auto-route is switched. */\n onAutoRouteChange?: (on: boolean) => void;\n /** The model auto-route is using now, shown as \"currently: …\". */\n routedModel?: React.ReactNode;\n /** Who routes, for the description. Defaults to `\"Aria\"`. */\n agentName?: React.ReactNode;\n}\n\n/** \"Fastest · Best quality · $$\" from whichever tradeoff facts are given. */\nfunction tradeoffLine(m: ModelOption): React.ReactNode {\n const parts = [m.speed, m.quality].filter((p) => p != null && p !== \"\");\n if (!parts.length) return null;\n return parts.map((p, i) => (\n <React.Fragment key={i}>\n {i > 0 && \" · \"}\n {p}\n </React.Fragment>\n ));\n}\n\nconst RecommendedBadge = () => (\n <Badge variant=\"outline\" className=\"border-brand/40 px-1.5 py-0 text-[10px] uppercase tracking-wide text-brand\">\n Recommended\n </Badge>\n);\n\ninterface PickerProps {\n models: ModelOption[];\n current: string | undefined;\n select: (value: string) => void;\n label: string;\n className?: string;\n}\n\n/** The composer-anchored dropdown (listbox) used by the \"dropdown\" and \"auto-route\" variants. */\nfunction ModelDropdown({ models, current, select, label, className }: PickerProps) {\n const [open, setOpen] = React.useState(false);\n const rootRef = React.useRef<HTMLDivElement>(null);\n const triggerRef = React.useRef<HTMLButtonElement>(null);\n const listRef = React.useRef<HTMLDivElement>(null);\n const listId = React.useId();\n\n const close = React.useCallback((refocus: boolean) => {\n setOpen(false);\n if (refocus) triggerRef.current?.focus();\n }, []);\n\n React.useEffect(() => {\n if (!open) return;\n const onDown = (e: MouseEvent) => {\n if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false);\n };\n const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && close(true);\n document.addEventListener(\"mousedown\", onDown);\n document.addEventListener(\"keydown\", onKey);\n return () => {\n document.removeEventListener(\"mousedown\", onDown);\n document.removeEventListener(\"keydown\", onKey);\n };\n }, [open, close]);\n\n const options = () =>\n Array.from(listRef.current?.querySelectorAll<HTMLElement>('[role=\"option\"]:not([aria-disabled=\"true\"])') ?? []);\n\n // Focus the selected option (or the first available) when the list opens.\n React.useEffect(() => {\n if (!open) return;\n const all = options();\n (all.find((o) => o.getAttribute(\"aria-selected\") === \"true\") ?? all[0])?.focus();\n }, [open]);\n\n const onListKeyDown = (e: React.KeyboardEvent) => {\n const all = options();\n const i = all.indexOf(document.activeElement as HTMLElement);\n const go = (n: number) => all[(n + all.length) % all.length]?.focus();\n if (e.key === \"ArrowDown\") (e.preventDefault(), go(i + 1));\n else if (e.key === \"ArrowUp\") (e.preventDefault(), go(i - 1));\n else if (e.key === \"Home\") (e.preventDefault(), go(0));\n else if (e.key === \"End\") (e.preventDefault(), go(all.length - 1));\n else if (e.key === \"Tab\") close(false);\n };\n\n const selected = models.find((m) => m.value === current);\n\n return (\n <div ref={rootRef} className={cn(\"relative inline-block\", className)}>\n <button\n ref={triggerRef}\n type=\"button\"\n aria-label={label}\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n aria-controls={open ? listId : undefined}\n onClick={() => setOpen((o) => !o)}\n onKeyDown={(e) => {\n if ((e.key === \"ArrowDown\" || e.key === \"ArrowUp\") && !open) {\n e.preventDefault();\n setOpen(true);\n }\n }}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border bg-background px-3 py-2 text-sm font-medium text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&>svg]:size-4\"\n >\n <Sparkles className=\"text-brand\" aria-hidden=\"true\" />\n <span>{selected?.label ?? \"Select model\"}</span>\n <ChevronDown className={cn(\"text-muted-foreground transition-transform\", open && \"rotate-180\")} aria-hidden=\"true\" />\n </button>\n {open && (\n <div\n ref={listRef}\n id={listId}\n role=\"listbox\"\n aria-label={label}\n onKeyDown={onListKeyDown}\n className=\"absolute z-50 mt-1 flex min-w-64 flex-col gap-0.5 rounded-xl border border-border bg-background p-1.5 shadow-md animate-nx-fade-in motion-reduce:animate-none\"\n >\n {models.map((model) => {\n const active = model.value === current;\n const line = tradeoffLine(model);\n return (\n <button\n key={model.value}\n type=\"button\"\n role=\"option\"\n aria-selected={active}\n aria-disabled={model.disabled || undefined}\n tabIndex={-1}\n onClick={() => {\n if (model.disabled) return;\n select(model.value);\n close(true);\n }}\n className={cn(\n \"flex items-start gap-2.5 rounded-lg px-2 py-2 text-left transition-colors focus-visible:outline-none\",\n model.disabled ? \"cursor-not-allowed opacity-60\" : \"hover:bg-accent focus-visible:bg-accent\",\n active && \"bg-brand-muted/40\",\n )}\n >\n <span className=\"mt-0.5 inline-flex size-4 shrink-0 items-center justify-center text-brand [&_svg]:size-4\">\n {active ? <Check aria-hidden=\"true\" /> : model.icon}\n </span>\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"flex items-center gap-2 text-sm font-medium text-foreground\">\n {model.label}\n {model.recommended && <RecommendedBadge />}\n </span>\n {model.description && <span className=\"text-xs text-muted-foreground\">{model.description}</span>}\n {line && <span className=\"text-xs text-muted-foreground\">{line}</span>}\n {model.disabled && model.unavailableReason && (\n <span className=\"text-xs text-foreground\">{model.unavailableReason}</span>\n )}\n </span>\n {model.cost && <span className=\"shrink-0 text-xs font-semibold text-muted-foreground\">{model.cost}</span>}\n </button>\n );\n })}\n </div>\n )}\n </div>\n );\n}\n\n/**\n * Lets the user pick the model for the task. `variant=\"dropdown\"` (default) is\n * a composer-anchored listbox (arrow keys, Home/End, Escape returns focus);\n * `\"tradeoff\"` lays models out as radio cards with speed, quality and cost in\n * human terms; `\"auto-route\"` defaults to letting the agent route per task,\n * shows which model it is using, and reveals the picker when switched off.\n * Recommended and unavailable models are marked in words; an unavailable model\n * keeps its row and says what answers instead.\n * Source: Figma \"Context Expansion / Model Selection\".\n */\nexport const ModelSelection = React.forwardRef<HTMLDivElement, ModelSelectionProps>(\n (\n {\n models,\n value,\n defaultValue,\n onValueChange,\n label = \"Model\",\n variant = \"dropdown\",\n autoRoute,\n defaultAutoRoute = true,\n onAutoRouteChange,\n routedModel,\n agentName = \"Aria\",\n className,\n ...props\n },\n ref,\n ) => {\n const [internal, setInternal] = React.useState<string | undefined>(defaultValue ?? models[0]?.value);\n const current = value !== undefined ? value : internal;\n const select = (next: string) => {\n if (value === undefined) setInternal(next);\n onValueChange?.(next);\n };\n\n const [autoInternal, setAutoInternal] = React.useState(defaultAutoRoute);\n const auto = autoRoute !== undefined ? autoRoute : autoInternal;\n const setAuto = (on: boolean) => {\n if (autoRoute === undefined) setAutoInternal(on);\n onAutoRouteChange?.(on);\n };\n const autoLabelId = React.useId();\n const headingId = React.useId();\n\n if (variant === \"tradeoff\") {\n return (\n <div ref={ref} className={cn(\"flex flex-col gap-2\", className)} {...props}>\n <p id={headingId} className=\"sr-only\">\n {label}\n </p>\n <RadioGroup\n aria-labelledby={headingId}\n value={current}\n onValueChange={select}\n className=\"gap-1.5\"\n >\n {models.map((model) => {\n const id = `${headingId}-${model.value}`;\n const line = tradeoffLine(model);\n return (\n <label\n key={model.value}\n className={cn(\n \"flex cursor-pointer items-center gap-3 rounded-lg border border-border bg-background px-3 py-2 transition-colors hover:bg-accent\",\n \"has-[[aria-checked=true]]:border-brand has-[[aria-checked=true]]:bg-brand-muted/40\",\n model.disabled && \"cursor-not-allowed opacity-60 hover:bg-background\",\n )}\n >\n <Radio value={model.value} disabled={model.disabled} aria-labelledby={id} />\n <span id={id} className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"flex items-center gap-2 text-sm font-semibold text-foreground\">\n {model.label}\n {model.recommended && <RecommendedBadge />}\n </span>\n {(line ?? model.description) && (\n <span className=\"text-xs text-muted-foreground\">{line ?? model.description}</span>\n )}\n {model.disabled && model.unavailableReason && (\n <span className=\"text-xs text-foreground\">{model.unavailableReason}</span>\n )}\n </span>\n {model.cost && (\n <span className=\"shrink-0 text-xs font-semibold text-muted-foreground\">{model.cost}</span>\n )}\n </label>\n );\n })}\n </RadioGroup>\n </div>\n );\n }\n\n if (variant === \"auto-route\") {\n return (\n <div ref={ref} className={cn(\"flex flex-col gap-2\", className)} {...props}>\n <div className=\"flex items-center justify-between gap-3 rounded-lg border border-border bg-background px-3.5 py-2.5\">\n <div className=\"flex min-w-0 flex-col\">\n <span id={autoLabelId} className=\"text-sm font-semibold text-foreground\">\n Auto-route\n </span>\n <span className=\"text-xs text-muted-foreground\">{agentName} picks the model per task</span>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n {auto && routedModel && (\n <span className=\"text-xs text-muted-foreground\" aria-live=\"polite\">\n currently: {routedModel}\n </span>\n )}\n <Switch checked={auto} onCheckedChange={setAuto} aria-labelledby={autoLabelId} />\n </div>\n </div>\n {!auto && <ModelDropdown models={models} current={current} select={select} label={label} />}\n </div>\n );\n }\n\n return (\n <div ref={ref} className={cn(\"relative inline-block\", className)} {...props}>\n <ModelDropdown models={models} current={current} select={select} label={label} />\n </div>\n );\n },\n);\nModelSelection.displayName = \"ModelSelection\";\n"]}
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
1
  {
2
2
  "name": "@nucleux/model-selection",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.7.0",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "description": "Model Selection — choose which model handles the request — Nucleux agentic UI for React.",
7
+ "keywords": [
8
+ "nucleux",
9
+ "react",
10
+ "ui",
11
+ "components",
12
+ "agentic",
13
+ "ai",
14
+ "tailwind",
15
+ "model-selection"
16
+ ],
17
+ "main": "./dist/index.cjs",
18
+ "module": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js",
24
+ "require": "./dist/index.cjs"
25
+ }
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "README.md"
30
+ ],
31
+ "peerDependencies": {
32
+ "react": ">=18",
33
+ "react-dom": ">=18"
34
+ },
35
+ "dependencies": {
36
+ "@nucleux/utils": "0.7.0",
37
+ "@nucleux/badge": "0.7.0",
38
+ "@nucleux/radio-group": "0.7.0",
39
+ "@nucleux/switch": "0.7.0",
40
+ "lucide-react": "^0.469.0"
41
+ },
42
+ "devDependencies": {
43
+ "tsup": "^8.3.5",
44
+ "typescript": "^5.7.2",
45
+ "@types/react": "^18.3.18",
46
+ "react": "^18.3.1"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "license": "MIT",
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/intelligaia-open-labs/nucleux.git",
55
+ "directory": "packages/model-selection"
56
+ },
57
+ "homepage": "https://github.com/intelligaia-open-labs/nucleux#readme",
58
+ "scripts": {
59
+ "build": "tsup"
60
+ }
6
61
  }