@epicbagel/saas-kit 1.29.0 → 1.31.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.
@@ -458,6 +458,7 @@ function ChatInput({
458
458
  compactOnBlur = false,
459
459
  header,
460
460
  helperText,
461
+ tools,
461
462
  privacyNote
462
463
  }) {
463
464
  const internalRef = _react.useRef.call(void 0, null);
@@ -608,6 +609,7 @@ function ChatInput({
608
609
  addFilesLabel
609
610
  ] }) })
610
611
  ] }),
612
+ tools ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { "data-slot": "chat-input-tools", className: "flex shrink-0 items-center gap-1", children: tools }) : null,
611
613
  helperText ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
612
614
  "span",
613
615
  {
@@ -1048,6 +1050,7 @@ var Chat = _react.forwardRef.call(void 0, function Chat2({
1048
1050
  composerAppearance = "default",
1049
1051
  mobileCompactComposer = false,
1050
1052
  composerHelperText,
1053
+ composerTools,
1051
1054
  composerPrivacyNote,
1052
1055
  emptyFileDropzone = false,
1053
1056
  emptyFileDropzoneLabel = "Drop files here, or choose them",
@@ -1142,6 +1145,7 @@ var Chat = _react.forwardRef.call(void 0, function Chat2({
1142
1145
  const workspace = layout === "workspace";
1143
1146
  const shortViewport = useIsShortViewport();
1144
1147
  const compactSuggestions = mobileCompactSuggestions && (isMobile || shortViewport);
1148
+ const compactComposer = mobileCompactComposer && (isMobile || shortViewport);
1145
1149
  const showEmptyFileDropzone = emptyFileDropzone && Boolean(onFilesSelected) && attachments.length === 0;
1146
1150
  const liveVoiceActive = voiceSession && voiceSession.state !== "disconnected" && voiceSession.state !== "error";
1147
1151
  const showEmptyPending = !conversationStarted && pending && Boolean(emptyPendingContent) && !liveVoiceActive;
@@ -1261,6 +1265,7 @@ var Chat = _react.forwardRef.call(void 0, function Chat2({
1261
1265
  uploadingLabel,
1262
1266
  appearance: composerAppearance,
1263
1267
  helperText: composerHelperText,
1268
+ tools: composerTools,
1264
1269
  privacyNote: composerPrivacyNote,
1265
1270
  className: workspace ? "rounded-3xl shadow-sm" : void 0
1266
1271
  }
@@ -1327,7 +1332,9 @@ var Chat = _react.forwardRef.call(void 0, function Chat2({
1327
1332
  "pt-3",
1328
1333
  application && "px-4 py-3 md:bg-transparent md:px-0 md:pb-0 md:backdrop-blur-none md:supports-backdrop-filter:bg-transparent",
1329
1334
  workspace && "border-0 bg-gradient-to-t from-background via-background to-transparent px-4 pb-[max(1.25rem,env(safe-area-inset-bottom))] pt-8 backdrop-blur-none supports-[backdrop-filter]:bg-gradient-to-t",
1330
- workspace && mobileCompactComposer && "max-md:pb-[max(0.75rem,env(safe-area-inset-bottom))] max-md:pt-3"
1335
+ workspace && mobileCompactComposer && "max-md:pb-[max(0.75rem,env(safe-area-inset-bottom))] max-md:pt-3",
1336
+ // A short window's dock is as tight as a phone's.
1337
+ workspace && compactComposer && "pb-[max(0.75rem,env(safe-area-inset-bottom))] pt-3"
1331
1338
  ),
1332
1339
  children: [
1333
1340
  !compactSuggestions && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
@@ -1375,8 +1382,9 @@ var Chat = _react.forwardRef.call(void 0, function Chat2({
1375
1382
  addFilesLabel,
1376
1383
  uploadingLabel,
1377
1384
  appearance: composerAppearance,
1378
- compactOnBlur: mobileCompactComposer && isMobile,
1385
+ compactOnBlur: compactComposer,
1379
1386
  helperText: composerHelperText,
1387
+ tools: composerTools,
1380
1388
  privacyNote: composerPrivacyNote,
1381
1389
  className: workspace ? "mx-auto max-w-3xl rounded-3xl shadow-sm" : void 0
1382
1390
  }
@@ -232,13 +232,20 @@ interface ChatInputProps {
232
232
  /**
233
233
  * Fold an empty, unfocused composer into a short text row and control bar. Focusing its textarea
234
234
  * expands it; drafts and staged files always keep it expanded. Chat enables
235
- * this only for an active mobile conversation with `mobileCompactComposer`.
235
+ * this only for an active conversation on a phone or a short window, with
236
+ * `mobileCompactComposer`.
236
237
  */
237
238
  compactOnBlur?: boolean;
238
239
  /** Content inside the composer surface, above its files and text field. */
239
240
  header?: ReactNode;
240
241
  /** Quiet contextual copy between the composer controls. */
241
242
  helperText?: ReactNode;
243
+ /**
244
+ * Controls for the message, in the composer's control row after the `+`
245
+ * menu: a setting the next send uses, such as an `OptionPicker`. Each
246
+ * control keeps a 44px touch target; they stay in the folded composer too.
247
+ */
248
+ tools?: ReactNode;
242
249
  /** Privacy or visibility reassurance rendered below the composer. */
243
250
  privacyNote?: ReactNode;
244
251
  }
@@ -259,7 +266,7 @@ interface ChatInputProps {
259
266
  * `<Chat>` instead of composing `<ChatInput>` directly if you want
260
267
  * device-aware default modes.
261
268
  */
262
- declare function ChatInput({ value, onChange, onSend, pending, disabled, placeholder, size, uploadingLabel, voice, voiceSession, voiceTitle, voiceLang, submitLabel, autoFocus, className, inputRef, attachments, onFilesSelected, onRemoveAttachment, accept, multiple, addFilesLabel, appearance, compactOnBlur, header, helperText, privacyNote, }: ChatInputProps): react_jsx_runtime.JSX.Element;
269
+ declare function ChatInput({ value, onChange, onSend, pending, disabled, placeholder, size, uploadingLabel, voice, voiceSession, voiceTitle, voiceLang, submitLabel, autoFocus, className, inputRef, attachments, onFilesSelected, onRemoveAttachment, accept, multiple, addFilesLabel, appearance, compactOnBlur, header, helperText, tools, privacyNote, }: ChatInputProps): react_jsx_runtime.JSX.Element;
263
270
  /** A next step offered in the empty state, under the composer (`emptyActions`). */
264
271
  interface ChatEmptyAction {
265
272
  /** Stable key. */
@@ -414,13 +421,20 @@ interface ChatProps {
414
421
  /** Visual treatment for both empty and active text composers. */
415
422
  composerAppearance?: ChatInputProps["appearance"];
416
423
  /**
417
- * On phones (below 768px), fold an active conversation's empty composer
418
- * into a short text row above its controls until the textarea is focused. Drafts and files stay
419
- * expanded. Desktop and the new-conversation composer are unchanged.
424
+ * On phones (below 768px wide) and short windows (below 900px tall), fold an
425
+ * active conversation's empty composer into a short text row above its
426
+ * controls until the textarea is focused. Drafts and files stay expanded.
427
+ * Taller desktop windows and the new-conversation composer are unchanged.
420
428
  */
421
429
  mobileCompactComposer?: boolean;
422
430
  /** Quiet contextual copy inside the composer action row. */
423
431
  composerHelperText?: ReactNode;
432
+ /**
433
+ * Controls in the composer's control row, after the `+` menu, on the new
434
+ * conversation's composer and the conversation's alike: a setting the next
435
+ * message uses, such as an `OptionPicker`.
436
+ */
437
+ composerTools?: ReactNode;
424
438
  /** Privacy or visibility reassurance below the composer. */
425
439
  composerPrivacyNote?: ReactNode;
426
440
  /** Show a prominent file drop target before the empty-state composer. */
@@ -232,13 +232,20 @@ interface ChatInputProps {
232
232
  /**
233
233
  * Fold an empty, unfocused composer into a short text row and control bar. Focusing its textarea
234
234
  * expands it; drafts and staged files always keep it expanded. Chat enables
235
- * this only for an active mobile conversation with `mobileCompactComposer`.
235
+ * this only for an active conversation on a phone or a short window, with
236
+ * `mobileCompactComposer`.
236
237
  */
237
238
  compactOnBlur?: boolean;
238
239
  /** Content inside the composer surface, above its files and text field. */
239
240
  header?: ReactNode;
240
241
  /** Quiet contextual copy between the composer controls. */
241
242
  helperText?: ReactNode;
243
+ /**
244
+ * Controls for the message, in the composer's control row after the `+`
245
+ * menu: a setting the next send uses, such as an `OptionPicker`. Each
246
+ * control keeps a 44px touch target; they stay in the folded composer too.
247
+ */
248
+ tools?: ReactNode;
242
249
  /** Privacy or visibility reassurance rendered below the composer. */
243
250
  privacyNote?: ReactNode;
244
251
  }
@@ -259,7 +266,7 @@ interface ChatInputProps {
259
266
  * `<Chat>` instead of composing `<ChatInput>` directly if you want
260
267
  * device-aware default modes.
261
268
  */
262
- declare function ChatInput({ value, onChange, onSend, pending, disabled, placeholder, size, uploadingLabel, voice, voiceSession, voiceTitle, voiceLang, submitLabel, autoFocus, className, inputRef, attachments, onFilesSelected, onRemoveAttachment, accept, multiple, addFilesLabel, appearance, compactOnBlur, header, helperText, privacyNote, }: ChatInputProps): react_jsx_runtime.JSX.Element;
269
+ declare function ChatInput({ value, onChange, onSend, pending, disabled, placeholder, size, uploadingLabel, voice, voiceSession, voiceTitle, voiceLang, submitLabel, autoFocus, className, inputRef, attachments, onFilesSelected, onRemoveAttachment, accept, multiple, addFilesLabel, appearance, compactOnBlur, header, helperText, tools, privacyNote, }: ChatInputProps): react_jsx_runtime.JSX.Element;
263
270
  /** A next step offered in the empty state, under the composer (`emptyActions`). */
264
271
  interface ChatEmptyAction {
265
272
  /** Stable key. */
@@ -414,13 +421,20 @@ interface ChatProps {
414
421
  /** Visual treatment for both empty and active text composers. */
415
422
  composerAppearance?: ChatInputProps["appearance"];
416
423
  /**
417
- * On phones (below 768px), fold an active conversation's empty composer
418
- * into a short text row above its controls until the textarea is focused. Drafts and files stay
419
- * expanded. Desktop and the new-conversation composer are unchanged.
424
+ * On phones (below 768px wide) and short windows (below 900px tall), fold an
425
+ * active conversation's empty composer into a short text row above its
426
+ * controls until the textarea is focused. Drafts and files stay expanded.
427
+ * Taller desktop windows and the new-conversation composer are unchanged.
420
428
  */
421
429
  mobileCompactComposer?: boolean;
422
430
  /** Quiet contextual copy inside the composer action row. */
423
431
  composerHelperText?: ReactNode;
432
+ /**
433
+ * Controls in the composer's control row, after the `+` menu, on the new
434
+ * conversation's composer and the conversation's alike: a setting the next
435
+ * message uses, such as an `OptionPicker`.
436
+ */
437
+ composerTools?: ReactNode;
424
438
  /** Privacy or visibility reassurance below the composer. */
425
439
  composerPrivacyNote?: ReactNode;
426
440
  /** Show a prominent file drop target before the empty-state composer. */
@@ -458,6 +458,7 @@ function ChatInput({
458
458
  compactOnBlur = false,
459
459
  header,
460
460
  helperText,
461
+ tools,
461
462
  privacyNote
462
463
  }) {
463
464
  const internalRef = useRef(null);
@@ -608,6 +609,7 @@ function ChatInput({
608
609
  addFilesLabel
609
610
  ] }) })
610
611
  ] }),
612
+ tools ? /* @__PURE__ */ jsx("div", { "data-slot": "chat-input-tools", className: "flex shrink-0 items-center gap-1", children: tools }) : null,
611
613
  helperText ? /* @__PURE__ */ jsx(
612
614
  "span",
613
615
  {
@@ -1048,6 +1050,7 @@ var Chat = forwardRef(function Chat2({
1048
1050
  composerAppearance = "default",
1049
1051
  mobileCompactComposer = false,
1050
1052
  composerHelperText,
1053
+ composerTools,
1051
1054
  composerPrivacyNote,
1052
1055
  emptyFileDropzone = false,
1053
1056
  emptyFileDropzoneLabel = "Drop files here, or choose them",
@@ -1142,6 +1145,7 @@ var Chat = forwardRef(function Chat2({
1142
1145
  const workspace = layout === "workspace";
1143
1146
  const shortViewport = useIsShortViewport();
1144
1147
  const compactSuggestions = mobileCompactSuggestions && (isMobile || shortViewport);
1148
+ const compactComposer = mobileCompactComposer && (isMobile || shortViewport);
1145
1149
  const showEmptyFileDropzone = emptyFileDropzone && Boolean(onFilesSelected) && attachments.length === 0;
1146
1150
  const liveVoiceActive = voiceSession && voiceSession.state !== "disconnected" && voiceSession.state !== "error";
1147
1151
  const showEmptyPending = !conversationStarted && pending && Boolean(emptyPendingContent) && !liveVoiceActive;
@@ -1261,6 +1265,7 @@ var Chat = forwardRef(function Chat2({
1261
1265
  uploadingLabel,
1262
1266
  appearance: composerAppearance,
1263
1267
  helperText: composerHelperText,
1268
+ tools: composerTools,
1264
1269
  privacyNote: composerPrivacyNote,
1265
1270
  className: workspace ? "rounded-3xl shadow-sm" : void 0
1266
1271
  }
@@ -1327,7 +1332,9 @@ var Chat = forwardRef(function Chat2({
1327
1332
  "pt-3",
1328
1333
  application && "px-4 py-3 md:bg-transparent md:px-0 md:pb-0 md:backdrop-blur-none md:supports-backdrop-filter:bg-transparent",
1329
1334
  workspace && "border-0 bg-gradient-to-t from-background via-background to-transparent px-4 pb-[max(1.25rem,env(safe-area-inset-bottom))] pt-8 backdrop-blur-none supports-[backdrop-filter]:bg-gradient-to-t",
1330
- workspace && mobileCompactComposer && "max-md:pb-[max(0.75rem,env(safe-area-inset-bottom))] max-md:pt-3"
1335
+ workspace && mobileCompactComposer && "max-md:pb-[max(0.75rem,env(safe-area-inset-bottom))] max-md:pt-3",
1336
+ // A short window's dock is as tight as a phone's.
1337
+ workspace && compactComposer && "pb-[max(0.75rem,env(safe-area-inset-bottom))] pt-3"
1331
1338
  ),
1332
1339
  children: [
1333
1340
  !compactSuggestions && /* @__PURE__ */ jsx(
@@ -1375,8 +1382,9 @@ var Chat = forwardRef(function Chat2({
1375
1382
  addFilesLabel,
1376
1383
  uploadingLabel,
1377
1384
  appearance: composerAppearance,
1378
- compactOnBlur: mobileCompactComposer && isMobile,
1385
+ compactOnBlur: compactComposer,
1379
1386
  helperText: composerHelperText,
1387
+ tools: composerTools,
1380
1388
  privacyNote: composerPrivacyNote,
1381
1389
  className: workspace ? "mx-auto max-w-3xl rounded-3xl shadow-sm" : void 0
1382
1390
  }
@@ -0,0 +1,74 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }"use client";
2
+ "use client";
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+ var _chunkZAITUDYDcjs = require('../chunk-ZAITUDYD.cjs');
11
+
12
+
13
+ var _chunkJDEXZGIYcjs = require('../chunk-JDEXZGIY.cjs');
14
+
15
+
16
+ var _chunkKWLSBTB2cjs = require('../chunk-KWLSBTB2.cjs');
17
+
18
+ // src/primitives/option-picker.tsx
19
+ var _lucidereact = require('lucide-react');
20
+ var _react = require('react');
21
+ var _jsxruntime = require('react/jsx-runtime');
22
+ function OptionPicker({
23
+ value,
24
+ onValueChange,
25
+ options,
26
+ label,
27
+ icon,
28
+ disabled = false,
29
+ side = "top",
30
+ align = "start",
31
+ className
32
+ }) {
33
+ const headingId = _react.useId.call(void 0, );
34
+ const current = options.find((option) => option.value === value);
35
+ return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkZAITUDYDcjs.DropdownMenu, { children: [
36
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkZAITUDYDcjs.DropdownMenuTrigger, { asChild: true, disabled, children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
37
+ _chunkJDEXZGIYcjs.Button,
38
+ {
39
+ type: "button",
40
+ variant: "ghost",
41
+ "aria-label": `${label}: ${_nullishCoalesce(_optionalChain([current, 'optionalAccess', _ => _.label]), () => ( value))}`,
42
+ "data-slot": "option-picker-trigger",
43
+ className: _chunkKWLSBTB2cjs.cn.call(void 0, "min-h-[44px] shrink-0 gap-1.5 px-3 text-sm font-medium", className),
44
+ children: [
45
+ icon,
46
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { children: _nullishCoalesce(_optionalChain([current, 'optionalAccess', _2 => _2.label]), () => ( value)) }),
47
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.ChevronUp, { "aria-hidden": true, className: _chunkKWLSBTB2cjs.cn.call(void 0, "h-3.5 w-3.5 opacity-60", side === "bottom" && "rotate-180") })
48
+ ]
49
+ }
50
+ ) }),
51
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkZAITUDYDcjs.DropdownMenuContent, { side, align, className: "w-80 max-w-[calc(100vw-2rem)]", "aria-labelledby": headingId, children: [
52
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkZAITUDYDcjs.DropdownMenuLabel, { id: headingId, className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: label }),
53
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkZAITUDYDcjs.DropdownMenuRadioGroup, { value, onValueChange: (next) => onValueChange(next), children: options.map((option) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
54
+ _chunkZAITUDYDcjs.DropdownMenuRadioItem,
55
+ {
56
+ value: option.value,
57
+ "data-slot": "option-picker-option",
58
+ className: "items-start py-2.5 sm:py-2.5 [&>span:first-child]:top-3.5",
59
+ children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "span", { className: "flex min-w-0 flex-col gap-0.5", children: [
60
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "span", { className: "font-medium", children: [
61
+ option.label,
62
+ option.tag ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "ml-1.5 text-xs font-normal text-muted-foreground", children: option.tag }) : null
63
+ ] }),
64
+ option.description ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "text-xs leading-relaxed text-muted-foreground", children: option.description }) : null
65
+ ] })
66
+ },
67
+ option.value
68
+ )) })
69
+ ] })
70
+ ] });
71
+ }
72
+
73
+
74
+ exports.OptionPicker = OptionPicker;
@@ -0,0 +1,35 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ interface OptionPickerOption<TValue extends string = string> {
5
+ value: TValue;
6
+ /** The option's name, also shown on the button once picked. */
7
+ label: string;
8
+ /** One line on what picking it means. */
9
+ description?: ReactNode;
10
+ /** A quiet note after the name, such as "default". */
11
+ tag?: ReactNode;
12
+ }
13
+ interface OptionPickerProps<TValue extends string = string> {
14
+ value: TValue;
15
+ onValueChange: (value: TValue) => void;
16
+ options: ReadonlyArray<OptionPickerOption<TValue>>;
17
+ /** Names the setting: the list's heading, and the button's accessible name with the value. */
18
+ label: string;
19
+ /** Shown before the value on the button. */
20
+ icon?: ReactNode;
21
+ disabled?: boolean;
22
+ /** Where the list opens. Defaults to `top`, for a control in a composer. */
23
+ side?: "top" | "bottom";
24
+ align?: "start" | "end";
25
+ className?: string;
26
+ }
27
+ /**
28
+ * A setting picked from a short list, the way a chat composer picks its effort:
29
+ * a button showing the current value opens a list of options, each a name and
30
+ * one line on what it means, one of them picked. A controlled radio group in a
31
+ * menu, so arrow keys move between options and Enter picks one.
32
+ */
33
+ declare function OptionPicker<TValue extends string = string>({ value, onValueChange, options, label, icon, disabled, side, align, className, }: OptionPickerProps<TValue>): react_jsx_runtime.JSX.Element;
34
+
35
+ export { OptionPicker, type OptionPickerOption, type OptionPickerProps };
@@ -0,0 +1,35 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ interface OptionPickerOption<TValue extends string = string> {
5
+ value: TValue;
6
+ /** The option's name, also shown on the button once picked. */
7
+ label: string;
8
+ /** One line on what picking it means. */
9
+ description?: ReactNode;
10
+ /** A quiet note after the name, such as "default". */
11
+ tag?: ReactNode;
12
+ }
13
+ interface OptionPickerProps<TValue extends string = string> {
14
+ value: TValue;
15
+ onValueChange: (value: TValue) => void;
16
+ options: ReadonlyArray<OptionPickerOption<TValue>>;
17
+ /** Names the setting: the list's heading, and the button's accessible name with the value. */
18
+ label: string;
19
+ /** Shown before the value on the button. */
20
+ icon?: ReactNode;
21
+ disabled?: boolean;
22
+ /** Where the list opens. Defaults to `top`, for a control in a composer. */
23
+ side?: "top" | "bottom";
24
+ align?: "start" | "end";
25
+ className?: string;
26
+ }
27
+ /**
28
+ * A setting picked from a short list, the way a chat composer picks its effort:
29
+ * a button showing the current value opens a list of options, each a name and
30
+ * one line on what it means, one of them picked. A controlled radio group in a
31
+ * menu, so arrow keys move between options and Enter picks one.
32
+ */
33
+ declare function OptionPicker<TValue extends string = string>({ value, onValueChange, options, label, icon, disabled, side, align, className, }: OptionPickerProps<TValue>): react_jsx_runtime.JSX.Element;
34
+
35
+ export { OptionPicker, type OptionPickerOption, type OptionPickerProps };
@@ -0,0 +1,74 @@
1
+ "use client";
2
+ "use client";
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuContent,
6
+ DropdownMenuLabel,
7
+ DropdownMenuRadioGroup,
8
+ DropdownMenuRadioItem,
9
+ DropdownMenuTrigger
10
+ } from "../chunk-V4LE2HWP.js";
11
+ import {
12
+ Button
13
+ } from "../chunk-W326GKML.js";
14
+ import {
15
+ cn
16
+ } from "../chunk-G6SRSWXB.js";
17
+
18
+ // src/primitives/option-picker.tsx
19
+ import { ChevronUp } from "lucide-react";
20
+ import { useId } from "react";
21
+ import { jsx, jsxs } from "react/jsx-runtime";
22
+ function OptionPicker({
23
+ value,
24
+ onValueChange,
25
+ options,
26
+ label,
27
+ icon,
28
+ disabled = false,
29
+ side = "top",
30
+ align = "start",
31
+ className
32
+ }) {
33
+ const headingId = useId();
34
+ const current = options.find((option) => option.value === value);
35
+ return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
36
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, disabled, children: /* @__PURE__ */ jsxs(
37
+ Button,
38
+ {
39
+ type: "button",
40
+ variant: "ghost",
41
+ "aria-label": `${label}: ${current?.label ?? value}`,
42
+ "data-slot": "option-picker-trigger",
43
+ className: cn("min-h-[44px] shrink-0 gap-1.5 px-3 text-sm font-medium", className),
44
+ children: [
45
+ icon,
46
+ /* @__PURE__ */ jsx("span", { children: current?.label ?? value }),
47
+ /* @__PURE__ */ jsx(ChevronUp, { "aria-hidden": true, className: cn("h-3.5 w-3.5 opacity-60", side === "bottom" && "rotate-180") })
48
+ ]
49
+ }
50
+ ) }),
51
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { side, align, className: "w-80 max-w-[calc(100vw-2rem)]", "aria-labelledby": headingId, children: [
52
+ /* @__PURE__ */ jsx(DropdownMenuLabel, { id: headingId, className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: label }),
53
+ /* @__PURE__ */ jsx(DropdownMenuRadioGroup, { value, onValueChange: (next) => onValueChange(next), children: options.map((option) => /* @__PURE__ */ jsx(
54
+ DropdownMenuRadioItem,
55
+ {
56
+ value: option.value,
57
+ "data-slot": "option-picker-option",
58
+ className: "items-start py-2.5 sm:py-2.5 [&>span:first-child]:top-3.5",
59
+ children: /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col gap-0.5", children: [
60
+ /* @__PURE__ */ jsxs("span", { className: "font-medium", children: [
61
+ option.label,
62
+ option.tag ? /* @__PURE__ */ jsx("span", { className: "ml-1.5 text-xs font-normal text-muted-foreground", children: option.tag }) : null
63
+ ] }),
64
+ option.description ? /* @__PURE__ */ jsx("span", { className: "text-xs leading-relaxed text-muted-foreground", children: option.description }) : null
65
+ ] })
66
+ },
67
+ option.value
68
+ )) })
69
+ ] })
70
+ ] });
71
+ }
72
+ export {
73
+ OptionPicker
74
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epicbagel/saas-kit",
3
- "version": "1.29.0",
3
+ "version": "1.31.0",
4
4
  "description": "Accessible React primitives, layout components and design tokens for SaaS apps, built on Radix UI and Tailwind CSS v4.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -790,6 +790,16 @@
790
790
  "default": "./dist/primitives/notes.cjs"
791
791
  }
792
792
  },
793
+ "./primitives/option-picker": {
794
+ "import": {
795
+ "types": "./dist/primitives/option-picker.d.ts",
796
+ "default": "./dist/primitives/option-picker.js"
797
+ },
798
+ "require": {
799
+ "types": "./dist/primitives/option-picker.d.cts",
800
+ "default": "./dist/primitives/option-picker.cjs"
801
+ }
802
+ },
793
803
  "./primitives/page-header-actions": {
794
804
  "import": {
795
805
  "types": "./dist/primitives/page-header-actions.d.ts",