@bmsuisse/ui 0.15.0 → 0.16.1
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/dist/index.d.ts +207 -2
- package/dist/index.js +386 -40
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -29,7 +29,16 @@ var buttonVariants = cva(
|
|
|
29
29
|
// Tailwind theme (this package doesn't define them) — same pattern as AlertBox's
|
|
30
30
|
// warning/info/success colors, which assume tokens the shared theme doesn't have.
|
|
31
31
|
"swiss-primary": "bg-swiss-primary text-swiss-primary-foreground font-semibold shadow-sm hover:bg-swiss-primary-hover hover:-translate-y-px active:translate-y-px",
|
|
32
|
-
"swiss-secondary": "border border-input bg-background font-semibold text-foreground shadow-sm hover:bg-accent hover:-translate-y-px active:translate-y-px"
|
|
32
|
+
"swiss-secondary": "border border-input bg-background font-semibold text-foreground shadow-sm hover:bg-accent hover:-translate-y-px active:translate-y-px",
|
|
33
|
+
// Violet "AI action" accent, in the same three weights as default/outline/ghost.
|
|
34
|
+
// Fixed Tailwind palette rather than a token — same choice as Badge's `warning`
|
|
35
|
+
// and AlertBox/StatusBadge's warning/info/success tones: the shared theme has no
|
|
36
|
+
// `ai` color, and unlike `swiss-primary` these should work in a consuming app with
|
|
37
|
+
// zero setup. Tints go through `violet-500/N` (not per-mode `violet-50`/`-950`
|
|
38
|
+
// shades) so one class list reads correctly on both light and dark surfaces.
|
|
39
|
+
ai: "bg-violet-600 text-white shadow-sm hover:bg-violet-600/90",
|
|
40
|
+
"ai-subtle": "border border-violet-500/30 bg-violet-500/10 text-violet-700 hover:bg-violet-500/20 dark:text-violet-300",
|
|
41
|
+
"ai-ghost": "text-violet-700 hover:bg-violet-500/10 dark:text-violet-300"
|
|
33
42
|
},
|
|
34
43
|
size: {
|
|
35
44
|
default: "h-9 px-3",
|
|
@@ -179,7 +188,7 @@ var DialogContent = forwardRef5(({ className, children, showCloseButton = true,
|
|
|
179
188
|
{
|
|
180
189
|
ref,
|
|
181
190
|
className: cn(
|
|
182
|
-
"fixed left-1/2 top-1/2 z-50 max-h-[85vh] w-full max-w-md -translate-x-1/2 -translate-y-1/2 overflow-
|
|
191
|
+
"fixed left-1/2 top-1/2 z-50 flex max-h-[85vh] w-full max-w-md -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-lg border bg-background shadow-lg",
|
|
183
192
|
className
|
|
184
193
|
),
|
|
185
194
|
...props,
|
|
@@ -189,7 +198,7 @@ var DialogContent = forwardRef5(({ className, children, showCloseButton = true,
|
|
|
189
198
|
DialogPrimitive.Close,
|
|
190
199
|
{
|
|
191
200
|
"data-testid": closeButtonTestId,
|
|
192
|
-
className: "absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100",
|
|
201
|
+
className: "absolute right-4 top-4 z-20 rounded-sm opacity-70 hover:opacity-100",
|
|
193
202
|
children: [
|
|
194
203
|
/* @__PURE__ */ jsx7(X, { className: "h-4 w-4" }),
|
|
195
204
|
/* @__PURE__ */ jsx7("span", { className: "sr-only", children: "Close" })
|
|
@@ -204,7 +213,7 @@ DialogContent.displayName = "DialogContent";
|
|
|
204
213
|
var DialogHeader = ({
|
|
205
214
|
className,
|
|
206
215
|
...props
|
|
207
|
-
}) => /* @__PURE__ */ jsx7("div", { className: cn("flex flex-col gap-1.5", className), ...props });
|
|
216
|
+
}) => /* @__PURE__ */ jsx7("div", { className: cn("flex shrink-0 flex-col gap-1.5 px-6 pt-6", className), ...props });
|
|
208
217
|
var DialogTitle = forwardRef5(({ className, ...props }, ref) => /* @__PURE__ */ jsx7(DialogPrimitive.Title, { ref, className: cn("text-lg font-semibold", className), ...props }));
|
|
209
218
|
DialogTitle.displayName = "DialogTitle";
|
|
210
219
|
var DialogDescription = forwardRef5(({ className, ...props }, ref) => /* @__PURE__ */ jsx7(
|
|
@@ -216,10 +225,20 @@ var DialogDescription = forwardRef5(({ className, ...props }, ref) => /* @__PURE
|
|
|
216
225
|
}
|
|
217
226
|
));
|
|
218
227
|
DialogDescription.displayName = "DialogDescription";
|
|
228
|
+
var DialogBody = ({
|
|
229
|
+
className,
|
|
230
|
+
...props
|
|
231
|
+
}) => /* @__PURE__ */ jsx7("div", { className: cn("min-h-0 flex-1 overflow-y-auto px-6 pb-6", className), ...props });
|
|
219
232
|
var DialogFooter = ({
|
|
220
233
|
className,
|
|
221
234
|
...props
|
|
222
|
-
}) => /* @__PURE__ */ jsx7(
|
|
235
|
+
}) => /* @__PURE__ */ jsx7(
|
|
236
|
+
"div",
|
|
237
|
+
{
|
|
238
|
+
className: cn("-mt-6 flex shrink-0 justify-end gap-2 px-6 pb-6 pt-4", className),
|
|
239
|
+
...props
|
|
240
|
+
}
|
|
241
|
+
);
|
|
223
242
|
|
|
224
243
|
// src/primitives/popover.tsx
|
|
225
244
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
@@ -641,7 +660,7 @@ var SheetOverlay = forwardRef15(({ className, ...props }, ref) => /* @__PURE__ *
|
|
|
641
660
|
));
|
|
642
661
|
SheetOverlay.displayName = "SheetOverlay";
|
|
643
662
|
var sheetVariants = cva3(
|
|
644
|
-
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition-[transform,box-shadow] duration-300 ease-in-out",
|
|
663
|
+
"fixed z-50 flex flex-col gap-4 overflow-hidden bg-background p-6 shadow-lg transition-[transform,box-shadow] duration-300 ease-in-out",
|
|
645
664
|
{
|
|
646
665
|
variants: {
|
|
647
666
|
side: {
|
|
@@ -744,11 +763,15 @@ SheetContent.displayName = "SheetContent";
|
|
|
744
763
|
var SheetHeader = ({
|
|
745
764
|
className,
|
|
746
765
|
...props
|
|
747
|
-
}) => /* @__PURE__ */ jsx18("div", { className: cn("flex flex-col gap-1.5", className), ...props });
|
|
766
|
+
}) => /* @__PURE__ */ jsx18("div", { className: cn("flex shrink-0 flex-col gap-1.5", className), ...props });
|
|
767
|
+
var SheetBody = ({
|
|
768
|
+
className,
|
|
769
|
+
...props
|
|
770
|
+
}) => /* @__PURE__ */ jsx18("div", { className: cn("min-h-0 flex-1 overflow-y-auto", className), ...props });
|
|
748
771
|
var SheetFooter = ({
|
|
749
772
|
className,
|
|
750
773
|
...props
|
|
751
|
-
}) => /* @__PURE__ */ jsx18("div", { className: cn("flex justify-end gap-2", className), ...props });
|
|
774
|
+
}) => /* @__PURE__ */ jsx18("div", { className: cn("flex shrink-0 justify-end gap-2", className), ...props });
|
|
752
775
|
var SheetTitle = forwardRef15(({ className, ...props }, ref) => /* @__PURE__ */ jsx18(DialogPrimitive2.Title, { ref, className: cn("text-lg font-semibold", className), ...props }));
|
|
753
776
|
SheetTitle.displayName = "SheetTitle";
|
|
754
777
|
var SheetDescription = forwardRef15(({ className, ...props }, ref) => /* @__PURE__ */ jsx18(
|
|
@@ -797,7 +820,7 @@ var Modal = ({
|
|
|
797
820
|
/* @__PURE__ */ jsx20(DialogTitle, { children: title }),
|
|
798
821
|
description ? /* @__PURE__ */ jsx20(DialogDescription, { children: description }) : null
|
|
799
822
|
] }),
|
|
800
|
-
children,
|
|
823
|
+
/* @__PURE__ */ jsx20(DialogBody, { children }),
|
|
801
824
|
footer ? /* @__PURE__ */ jsx20(DialogFooter, { children: footer }) : null
|
|
802
825
|
] }) });
|
|
803
826
|
|
|
@@ -919,12 +942,12 @@ var FormModal = ({
|
|
|
919
942
|
const handleCancel = () => {
|
|
920
943
|
onOpenChange(false);
|
|
921
944
|
};
|
|
922
|
-
return /* @__PURE__ */ jsx23(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsx23(DialogContent, { className, children: /* @__PURE__ */ jsxs8("form", { onSubmit: handleSubmit, children: [
|
|
945
|
+
return /* @__PURE__ */ jsx23(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsx23(DialogContent, { className, children: /* @__PURE__ */ jsxs8("form", { onSubmit: handleSubmit, className: "flex min-h-0 flex-1 flex-col", children: [
|
|
923
946
|
/* @__PURE__ */ jsxs8(DialogHeader, { children: [
|
|
924
947
|
/* @__PURE__ */ jsx23(DialogTitle, { children: title }),
|
|
925
948
|
description ? /* @__PURE__ */ jsx23(DialogDescription, { children: description }) : null
|
|
926
949
|
] }),
|
|
927
|
-
children,
|
|
950
|
+
/* @__PURE__ */ jsx23(DialogBody, { children }),
|
|
928
951
|
/* @__PURE__ */ jsxs8(DialogFooter, { children: [
|
|
929
952
|
/* @__PURE__ */ jsx23(
|
|
930
953
|
Button,
|
|
@@ -1095,7 +1118,7 @@ var ResponsivePanel = ({
|
|
|
1095
1118
|
onInteractOutside,
|
|
1096
1119
|
style: resizable || draggable ? geometry.style : void 0,
|
|
1097
1120
|
className: cn(
|
|
1098
|
-
"max-h-[85vh]
|
|
1121
|
+
"max-h-[85vh] transition-shadow duration-150 ease-out",
|
|
1099
1122
|
geometry.isDragging && "shadow-2xl ring-2 ring-primary/30",
|
|
1100
1123
|
desktopSizeClasses[size],
|
|
1101
1124
|
className
|
|
@@ -1140,7 +1163,7 @@ var ResponsivePanel = ({
|
|
|
1140
1163
|
]
|
|
1141
1164
|
}
|
|
1142
1165
|
),
|
|
1143
|
-
children,
|
|
1166
|
+
/* @__PURE__ */ jsx24(DialogBody, { children }),
|
|
1144
1167
|
footer ? /* @__PURE__ */ jsx24(DialogFooter, { children: footer }) : null
|
|
1145
1168
|
]
|
|
1146
1169
|
}
|
|
@@ -1152,17 +1175,13 @@ var ResponsivePanel = ({
|
|
|
1152
1175
|
side: "bottom",
|
|
1153
1176
|
resizable,
|
|
1154
1177
|
onInteractOutside,
|
|
1155
|
-
className: cn(
|
|
1156
|
-
"flex flex-col overflow-y-auto rounded-t-2xl",
|
|
1157
|
-
drawerSizeClasses[size],
|
|
1158
|
-
className
|
|
1159
|
-
),
|
|
1178
|
+
className: cn("rounded-t-2xl", drawerSizeClasses[size], className),
|
|
1160
1179
|
children: [
|
|
1161
1180
|
/* @__PURE__ */ jsxs9(SheetHeader, { children: [
|
|
1162
1181
|
/* @__PURE__ */ jsx24(SheetTitle, { children: title }),
|
|
1163
1182
|
description ? /* @__PURE__ */ jsx24(SheetDescription, { children: description }) : null
|
|
1164
1183
|
] }),
|
|
1165
|
-
children,
|
|
1184
|
+
/* @__PURE__ */ jsx24(SheetBody, { children }),
|
|
1166
1185
|
footer ? /* @__PURE__ */ jsx24(SheetFooter, { children: footer }) : null
|
|
1167
1186
|
]
|
|
1168
1187
|
}
|
|
@@ -2187,8 +2206,10 @@ var KpiCard = memo(function KpiCard2({
|
|
|
2187
2206
|
progressLabel,
|
|
2188
2207
|
segments,
|
|
2189
2208
|
centerValue,
|
|
2209
|
+
background,
|
|
2190
2210
|
testId
|
|
2191
2211
|
}) {
|
|
2212
|
+
const bgStyle = background ? { background } : void 0;
|
|
2192
2213
|
if (variant === "hero") {
|
|
2193
2214
|
return /* @__PURE__ */ jsxs18(
|
|
2194
2215
|
"div",
|
|
@@ -2196,8 +2217,8 @@ var KpiCard = memo(function KpiCard2({
|
|
|
2196
2217
|
"data-testid": testId,
|
|
2197
2218
|
className: "relative col-span-2 flex min-w-0 flex-1 flex-col gap-1.5 overflow-hidden rounded-2xl p-4 md:p-3 lg:col-span-1 lg:max-w-[340px] lg:min-w-[260px]",
|
|
2198
2219
|
style: {
|
|
2199
|
-
background: "var(--color-primary)",
|
|
2200
|
-
boxShadow:
|
|
2220
|
+
background: background ?? "var(--color-primary)",
|
|
2221
|
+
boxShadow: `0 2px 10px color-mix(in oklch, ${background ?? "var(--color-primary)"} 18%, transparent)`
|
|
2201
2222
|
},
|
|
2202
2223
|
children: [
|
|
2203
2224
|
/* @__PURE__ */ jsxs18("div", { className: "relative flex items-start justify-between", children: [
|
|
@@ -2241,6 +2262,7 @@ var KpiCard = memo(function KpiCard2({
|
|
|
2241
2262
|
"div",
|
|
2242
2263
|
{
|
|
2243
2264
|
"data-testid": testId,
|
|
2265
|
+
style: bgStyle,
|
|
2244
2266
|
className: cn(
|
|
2245
2267
|
"flex h-full min-w-0 flex-1 flex-col gap-1.5 overflow-hidden rounded-2xl border border-border bg-card p-3 transition-shadow md:p-2.5",
|
|
2246
2268
|
href && "hover:border-primary/20 hover:shadow-md"
|
|
@@ -2282,7 +2304,7 @@ var KpiCard = memo(function KpiCard2({
|
|
|
2282
2304
|
}
|
|
2283
2305
|
if (variant === "donut") {
|
|
2284
2306
|
const total = segments?.reduce((sum, s) => sum + s.value, 0) ?? 0;
|
|
2285
|
-
return /* @__PURE__ */ jsxs18("div", { "data-testid": testId, className: "rounded-xl border border-border bg-card p-5", children: [
|
|
2307
|
+
return /* @__PURE__ */ jsxs18("div", { "data-testid": testId, style: bgStyle, className: "rounded-xl border border-border bg-card p-5", children: [
|
|
2286
2308
|
/* @__PURE__ */ jsxs18("div", { className: rowBetween, children: [
|
|
2287
2309
|
/* @__PURE__ */ jsx35("p", { className: "text-[11px] font-bold tracking-[0.12em] text-muted-foreground uppercase", children: label }),
|
|
2288
2310
|
Icon2 && /* @__PURE__ */ jsx35("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-xl bg-primary/8 text-primary", children: /* @__PURE__ */ jsx35(Icon2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) })
|
|
@@ -2319,7 +2341,7 @@ var KpiCard = memo(function KpiCard2({
|
|
|
2319
2341
|
const prefix = isSuffix ? null : parts?.[0];
|
|
2320
2342
|
const number = isSuffix ? parts[0] : parts?.[1] ?? parts?.[0];
|
|
2321
2343
|
const suffix = isSuffix ? parts[1] : null;
|
|
2322
|
-
return /* @__PURE__ */ jsxs18("div", { "data-testid": testId, className: "rounded-xl border border-border bg-card p-5", children: [
|
|
2344
|
+
return /* @__PURE__ */ jsxs18("div", { "data-testid": testId, style: bgStyle, className: "rounded-xl border border-border bg-card p-5", children: [
|
|
2323
2345
|
/* @__PURE__ */ jsxs18("div", { className: rowBetween, children: [
|
|
2324
2346
|
/* @__PURE__ */ jsx35("p", { className: "text-[11px] font-bold tracking-[0.12em] text-muted-foreground uppercase", children: label }),
|
|
2325
2347
|
Icon2 && /* @__PURE__ */ jsx35("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-xl bg-primary/8 text-primary", children: /* @__PURE__ */ jsx35(Icon2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) })
|
|
@@ -2700,10 +2722,326 @@ function SidebarNav({ children, className, ...rest }) {
|
|
|
2700
2722
|
] });
|
|
2701
2723
|
}
|
|
2702
2724
|
|
|
2725
|
+
// src/patterns/ai/AiButton.tsx
|
|
2726
|
+
import { Sparkles } from "lucide-react";
|
|
2727
|
+
import { forwardRef as forwardRef17 } from "react";
|
|
2728
|
+
import { jsx as jsx41, jsxs as jsxs23 } from "react/jsx-runtime";
|
|
2729
|
+
var AiButton = forwardRef17(
|
|
2730
|
+
({ variant = "ai-subtle", loading = false, icon: Icon2 = Sparkles, disabled, children, ...props }, ref) => /* @__PURE__ */ jsxs23(
|
|
2731
|
+
Button,
|
|
2732
|
+
{
|
|
2733
|
+
ref,
|
|
2734
|
+
variant,
|
|
2735
|
+
disabled: disabled || loading,
|
|
2736
|
+
"aria-busy": loading || void 0,
|
|
2737
|
+
...props,
|
|
2738
|
+
children: [
|
|
2739
|
+
loading ? /* @__PURE__ */ jsx41(LoadingSpinner, { size: "sm", className: "[&_svg]:text-current" }) : /* @__PURE__ */ jsx41(Icon2, { className: "h-4 w-4", "aria-hidden": "true" }),
|
|
2740
|
+
children
|
|
2741
|
+
]
|
|
2742
|
+
}
|
|
2743
|
+
)
|
|
2744
|
+
);
|
|
2745
|
+
AiButton.displayName = "AiButton";
|
|
2746
|
+
|
|
2747
|
+
// src/patterns/ai/AiExplainButton.tsx
|
|
2748
|
+
import { RefreshCw, Sparkles as Sparkles2 } from "lucide-react";
|
|
2749
|
+
import { useState as useState12 } from "react";
|
|
2750
|
+
import { jsx as jsx42, jsxs as jsxs24 } from "react/jsx-runtime";
|
|
2751
|
+
function AiExplainButton({
|
|
2752
|
+
onExplain,
|
|
2753
|
+
label = "Explain",
|
|
2754
|
+
title,
|
|
2755
|
+
align = "start",
|
|
2756
|
+
contentClassName = "w-80",
|
|
2757
|
+
size = "sm",
|
|
2758
|
+
...props
|
|
2759
|
+
}) {
|
|
2760
|
+
const [open, setOpen] = useState12(false);
|
|
2761
|
+
const [loading, setLoading] = useState12(false);
|
|
2762
|
+
const [content, setContent] = useState12(null);
|
|
2763
|
+
const [error, setError] = useState12(null);
|
|
2764
|
+
const load = async () => {
|
|
2765
|
+
setLoading(true);
|
|
2766
|
+
setError(null);
|
|
2767
|
+
try {
|
|
2768
|
+
setContent(await onExplain());
|
|
2769
|
+
} catch (err) {
|
|
2770
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
2771
|
+
} finally {
|
|
2772
|
+
setLoading(false);
|
|
2773
|
+
}
|
|
2774
|
+
};
|
|
2775
|
+
const handleOpenChange = (next) => {
|
|
2776
|
+
setOpen(next);
|
|
2777
|
+
if (next && content === null && error === null && !loading) void load();
|
|
2778
|
+
};
|
|
2779
|
+
return /* @__PURE__ */ jsxs24(Popover, { open, onOpenChange: handleOpenChange, children: [
|
|
2780
|
+
/* @__PURE__ */ jsx42(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx42(AiButton, { size, ...props, children: label }) }),
|
|
2781
|
+
/* @__PURE__ */ jsxs24(PopoverContent, { align, className: contentClassName, children: [
|
|
2782
|
+
/* @__PURE__ */ jsxs24("p", { className: "mb-2 flex items-center gap-1.5 text-sm font-semibold", children: [
|
|
2783
|
+
/* @__PURE__ */ jsx42(Sparkles2, { className: "h-4 w-4 shrink-0 text-muted-foreground", "aria-hidden": "true" }),
|
|
2784
|
+
title ?? label
|
|
2785
|
+
] }),
|
|
2786
|
+
loading ? /* @__PURE__ */ jsx42(LoadingSpinner, { size: "sm", label: "Thinking\u2026", className: "text-sm text-muted-foreground" }) : error ? /* @__PURE__ */ jsxs24("div", { className: "flex flex-col items-start gap-2", children: [
|
|
2787
|
+
/* @__PURE__ */ jsx42("div", { role: "alert", className: "w-full", children: /* @__PURE__ */ jsx42(AlertBox, { variant: "error", children: error }) }),
|
|
2788
|
+
/* @__PURE__ */ jsxs24(Button, { type: "button", variant: "outline", size: "sm", onClick: () => void load(), children: [
|
|
2789
|
+
/* @__PURE__ */ jsx42(RefreshCw, { className: "h-4 w-4", "aria-hidden": "true" }),
|
|
2790
|
+
"Try again"
|
|
2791
|
+
] })
|
|
2792
|
+
] }) : /* @__PURE__ */ jsx42("div", { className: "max-h-80 overflow-y-auto text-sm leading-relaxed", children: content })
|
|
2793
|
+
] })
|
|
2794
|
+
] });
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
// src/patterns/ai/VoiceInputButton.tsx
|
|
2798
|
+
import { Mic, MicOff } from "lucide-react";
|
|
2799
|
+
import { forwardRef as forwardRef18 } from "react";
|
|
2800
|
+
|
|
2801
|
+
// src/patterns/ai/useSpeechRecognition.ts
|
|
2802
|
+
import { useCallback as useCallback2, useEffect as useEffect6, useRef as useRef6, useState as useState13 } from "react";
|
|
2803
|
+
function getCtor() {
|
|
2804
|
+
if (typeof window === "undefined") return void 0;
|
|
2805
|
+
const w = window;
|
|
2806
|
+
return w.SpeechRecognition ?? w.webkitSpeechRecognition;
|
|
2807
|
+
}
|
|
2808
|
+
function browserEngine() {
|
|
2809
|
+
const Ctor = getCtor();
|
|
2810
|
+
return Ctor ? new Ctor() : null;
|
|
2811
|
+
}
|
|
2812
|
+
function describeSpeechError(code) {
|
|
2813
|
+
switch (code) {
|
|
2814
|
+
case "aborted":
|
|
2815
|
+
return null;
|
|
2816
|
+
case "not-allowed":
|
|
2817
|
+
case "service-not-allowed":
|
|
2818
|
+
return "Microphone access is blocked. Allow it in the browser's site settings and try again.";
|
|
2819
|
+
case "audio-capture":
|
|
2820
|
+
return "No microphone was found.";
|
|
2821
|
+
case "no-speech":
|
|
2822
|
+
return "No speech was detected.";
|
|
2823
|
+
case "network":
|
|
2824
|
+
return "Speech recognition needs a network connection.";
|
|
2825
|
+
default:
|
|
2826
|
+
return `Speech recognition failed (${code}).`;
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
function useSpeechRecognition({
|
|
2830
|
+
lang,
|
|
2831
|
+
onResult,
|
|
2832
|
+
onError,
|
|
2833
|
+
engine
|
|
2834
|
+
} = {}) {
|
|
2835
|
+
const [listening, setListening] = useState13(false);
|
|
2836
|
+
const [interim, setInterim] = useState13("");
|
|
2837
|
+
const [error, setError] = useState13(null);
|
|
2838
|
+
const recognitionRef = useRef6(null);
|
|
2839
|
+
const onResultRef = useRef6(onResult);
|
|
2840
|
+
const onErrorRef = useRef6(onError);
|
|
2841
|
+
const engineRef = useRef6(engine);
|
|
2842
|
+
onResultRef.current = onResult;
|
|
2843
|
+
onErrorRef.current = onError;
|
|
2844
|
+
engineRef.current = engine;
|
|
2845
|
+
const supported = engine !== void 0 || getCtor() !== void 0;
|
|
2846
|
+
const stop = useCallback2(() => {
|
|
2847
|
+
recognitionRef.current?.stop();
|
|
2848
|
+
}, []);
|
|
2849
|
+
const start = useCallback2(() => {
|
|
2850
|
+
if (recognitionRef.current) return;
|
|
2851
|
+
const recognition = engineRef.current ? engineRef.current() : browserEngine();
|
|
2852
|
+
if (!recognition) return;
|
|
2853
|
+
recognition.lang = lang ?? (typeof document !== "undefined" ? document.documentElement.lang : "") ?? "";
|
|
2854
|
+
if (!recognition.lang) recognition.lang = "en-US";
|
|
2855
|
+
recognition.continuous = true;
|
|
2856
|
+
recognition.interimResults = true;
|
|
2857
|
+
recognition.onresult = (event) => {
|
|
2858
|
+
let pending = "";
|
|
2859
|
+
for (let i = event.resultIndex; i < event.results.length; i += 1) {
|
|
2860
|
+
const result = event.results[i];
|
|
2861
|
+
if (!result) continue;
|
|
2862
|
+
if (result.isFinal) onResultRef.current?.(result[0].transcript.trim());
|
|
2863
|
+
else pending += result[0].transcript;
|
|
2864
|
+
}
|
|
2865
|
+
setInterim(pending);
|
|
2866
|
+
};
|
|
2867
|
+
recognition.onerror = (event) => {
|
|
2868
|
+
setError(event.error);
|
|
2869
|
+
onErrorRef.current?.(event.error);
|
|
2870
|
+
};
|
|
2871
|
+
recognition.onend = () => {
|
|
2872
|
+
recognitionRef.current = null;
|
|
2873
|
+
setListening(false);
|
|
2874
|
+
setInterim("");
|
|
2875
|
+
};
|
|
2876
|
+
recognitionRef.current = recognition;
|
|
2877
|
+
setError(null);
|
|
2878
|
+
setInterim("");
|
|
2879
|
+
setListening(true);
|
|
2880
|
+
recognition.start();
|
|
2881
|
+
}, [lang]);
|
|
2882
|
+
const toggle = useCallback2(() => {
|
|
2883
|
+
if (recognitionRef.current) stop();
|
|
2884
|
+
else start();
|
|
2885
|
+
}, [start, stop]);
|
|
2886
|
+
useEffect6(() => () => recognitionRef.current?.abort(), []);
|
|
2887
|
+
return { supported, listening, interim, error, start, stop, toggle };
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
// src/patterns/ai/VoiceInputButton.tsx
|
|
2891
|
+
import { jsx as jsx43, jsxs as jsxs25 } from "react/jsx-runtime";
|
|
2892
|
+
var VoiceInputButton = forwardRef18(
|
|
2893
|
+
({
|
|
2894
|
+
onTranscript,
|
|
2895
|
+
onError,
|
|
2896
|
+
lang,
|
|
2897
|
+
engine,
|
|
2898
|
+
label,
|
|
2899
|
+
startLabel = "Start dictation",
|
|
2900
|
+
stopLabel = "Stop dictation",
|
|
2901
|
+
showInterim = false,
|
|
2902
|
+
size,
|
|
2903
|
+
disabled,
|
|
2904
|
+
className,
|
|
2905
|
+
...props
|
|
2906
|
+
}, ref) => {
|
|
2907
|
+
const { supported, listening, interim, toggle } = useSpeechRecognition({
|
|
2908
|
+
lang,
|
|
2909
|
+
onResult: onTranscript,
|
|
2910
|
+
onError,
|
|
2911
|
+
engine
|
|
2912
|
+
});
|
|
2913
|
+
const unsupportedTitle = "Speech recognition isn't available in this browser";
|
|
2914
|
+
return (
|
|
2915
|
+
// `title` lives on the wrapper, not the button: buttonVariants sets
|
|
2916
|
+
// `disabled:pointer-events-none`, so a disabled button never shows its own.
|
|
2917
|
+
/* @__PURE__ */ jsxs25("span", { className: "inline-flex min-w-0 items-center gap-2", title: supported ? void 0 : unsupportedTitle, children: [
|
|
2918
|
+
/* @__PURE__ */ jsxs25(
|
|
2919
|
+
Button,
|
|
2920
|
+
{
|
|
2921
|
+
ref,
|
|
2922
|
+
type: "button",
|
|
2923
|
+
variant: "outline",
|
|
2924
|
+
size: size ?? (label ? "default" : "icon"),
|
|
2925
|
+
onClick: toggle,
|
|
2926
|
+
disabled: disabled || !supported,
|
|
2927
|
+
"aria-label": label ? void 0 : supported ? listening ? stopLabel : startLabel : unsupportedTitle,
|
|
2928
|
+
"aria-pressed": supported ? listening : void 0,
|
|
2929
|
+
className: cn(
|
|
2930
|
+
listening && "border-destructive bg-destructive/10 text-red-800 hover:bg-destructive/15 hover:text-red-800 dark:text-red-300 dark:hover:text-red-300",
|
|
2931
|
+
className
|
|
2932
|
+
),
|
|
2933
|
+
...props,
|
|
2934
|
+
children: [
|
|
2935
|
+
supported ? /* @__PURE__ */ jsx43(Mic, { className: cn("h-4 w-4", listening && "animate-pulse motion-reduce:animate-none"), "aria-hidden": "true" }) : /* @__PURE__ */ jsx43(MicOff, { className: "h-4 w-4", "aria-hidden": "true" }),
|
|
2936
|
+
label
|
|
2937
|
+
]
|
|
2938
|
+
}
|
|
2939
|
+
),
|
|
2940
|
+
showInterim && interim ? /* @__PURE__ */ jsx43("span", { "aria-live": "polite", className: "truncate text-sm text-muted-foreground italic", children: interim }) : null
|
|
2941
|
+
] })
|
|
2942
|
+
);
|
|
2943
|
+
}
|
|
2944
|
+
);
|
|
2945
|
+
VoiceInputButton.displayName = "VoiceInputButton";
|
|
2946
|
+
|
|
2947
|
+
// src/patterns/ai/VoiceTranscript.tsx
|
|
2948
|
+
import { Undo2, Wand2 } from "lucide-react";
|
|
2949
|
+
import { useState as useState14 } from "react";
|
|
2950
|
+
import { jsx as jsx44, jsxs as jsxs26 } from "react/jsx-runtime";
|
|
2951
|
+
function VoiceTranscript({
|
|
2952
|
+
value,
|
|
2953
|
+
onChange,
|
|
2954
|
+
onTransform,
|
|
2955
|
+
transformLabel = "Transform with AI",
|
|
2956
|
+
placeholder = "Speak or type\u2026",
|
|
2957
|
+
lang,
|
|
2958
|
+
engine,
|
|
2959
|
+
rows = 4,
|
|
2960
|
+
disabled,
|
|
2961
|
+
className,
|
|
2962
|
+
...props
|
|
2963
|
+
}) {
|
|
2964
|
+
const [loading, setLoading] = useState14(false);
|
|
2965
|
+
const [error, setError] = useState14(null);
|
|
2966
|
+
const [previous, setPrevious] = useState14(null);
|
|
2967
|
+
const append = (chunk) => {
|
|
2968
|
+
onChange(value ? `${value.trimEnd()} ${chunk}` : chunk);
|
|
2969
|
+
};
|
|
2970
|
+
const transform = async () => {
|
|
2971
|
+
if (!onTransform) return;
|
|
2972
|
+
const before = value;
|
|
2973
|
+
setLoading(true);
|
|
2974
|
+
setError(null);
|
|
2975
|
+
try {
|
|
2976
|
+
const next = await onTransform(before);
|
|
2977
|
+
setPrevious(before);
|
|
2978
|
+
onChange(next);
|
|
2979
|
+
} catch (err) {
|
|
2980
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
2981
|
+
} finally {
|
|
2982
|
+
setLoading(false);
|
|
2983
|
+
}
|
|
2984
|
+
};
|
|
2985
|
+
const undo = () => {
|
|
2986
|
+
if (previous === null) return;
|
|
2987
|
+
onChange(previous);
|
|
2988
|
+
setPrevious(null);
|
|
2989
|
+
};
|
|
2990
|
+
return /* @__PURE__ */ jsxs26("div", { className: cn("flex flex-col gap-2", className), ...props, children: [
|
|
2991
|
+
/* @__PURE__ */ jsx44(
|
|
2992
|
+
Textarea,
|
|
2993
|
+
{
|
|
2994
|
+
value,
|
|
2995
|
+
onChange: (event) => onChange(event.target.value),
|
|
2996
|
+
placeholder,
|
|
2997
|
+
rows,
|
|
2998
|
+
disabled: disabled || loading,
|
|
2999
|
+
"aria-busy": loading || void 0
|
|
3000
|
+
}
|
|
3001
|
+
),
|
|
3002
|
+
/* @__PURE__ */ jsxs26("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
3003
|
+
/* @__PURE__ */ jsx44(
|
|
3004
|
+
VoiceInputButton,
|
|
3005
|
+
{
|
|
3006
|
+
onTranscript: append,
|
|
3007
|
+
onError: (code) => setError(describeSpeechError(code)),
|
|
3008
|
+
lang,
|
|
3009
|
+
engine,
|
|
3010
|
+
label: "Dictate",
|
|
3011
|
+
size: "sm",
|
|
3012
|
+
showInterim: true,
|
|
3013
|
+
disabled: disabled || loading
|
|
3014
|
+
}
|
|
3015
|
+
),
|
|
3016
|
+
onTransform ? /* @__PURE__ */ jsx44(
|
|
3017
|
+
AiButton,
|
|
3018
|
+
{
|
|
3019
|
+
variant: "ai",
|
|
3020
|
+
size: "sm",
|
|
3021
|
+
icon: Wand2,
|
|
3022
|
+
loading,
|
|
3023
|
+
disabled: disabled || value.trim().length === 0,
|
|
3024
|
+
onClick: () => void transform(),
|
|
3025
|
+
children: transformLabel
|
|
3026
|
+
}
|
|
3027
|
+
) : null,
|
|
3028
|
+
previous !== null && !loading ? /* @__PURE__ */ jsxs26(Button, { type: "button", variant: "ghost", size: "sm", onClick: undo, children: [
|
|
3029
|
+
/* @__PURE__ */ jsx44(Undo2, { className: "h-4 w-4", "aria-hidden": "true" }),
|
|
3030
|
+
"Undo"
|
|
3031
|
+
] }) : null
|
|
3032
|
+
] }),
|
|
3033
|
+
error ? (
|
|
3034
|
+
// AlertBox has no `role` prop, so the live region wraps it; same shape
|
|
3035
|
+
// a consuming app's own form errors take.
|
|
3036
|
+
/* @__PURE__ */ jsx44("div", { role: "alert", children: /* @__PURE__ */ jsx44(AlertBox, { variant: "error", children: error }) })
|
|
3037
|
+
) : null
|
|
3038
|
+
] });
|
|
3039
|
+
}
|
|
3040
|
+
|
|
2703
3041
|
// src/patterns/stepper/Stepper.tsx
|
|
2704
|
-
import { useEffect as
|
|
3042
|
+
import { useEffect as useEffect7, useRef as useRef7 } from "react";
|
|
2705
3043
|
import { Check as Check5, Plus } from "lucide-react";
|
|
2706
|
-
import { jsx as
|
|
3044
|
+
import { jsx as jsx45, jsxs as jsxs27 } from "react/jsx-runtime";
|
|
2707
3045
|
function Stepper({
|
|
2708
3046
|
step,
|
|
2709
3047
|
maxStep,
|
|
@@ -2715,8 +3053,8 @@ function Stepper({
|
|
|
2715
3053
|
scrollTestId = "stepper-scroll",
|
|
2716
3054
|
newTestId
|
|
2717
3055
|
}) {
|
|
2718
|
-
const stepRefs =
|
|
2719
|
-
|
|
3056
|
+
const stepRefs = useRef7(/* @__PURE__ */ new Map());
|
|
3057
|
+
useEffect7(() => {
|
|
2720
3058
|
const activeStep = stepRefs.current.get(step);
|
|
2721
3059
|
activeStep?.scrollIntoView({ block: "nearest", inline: "center" });
|
|
2722
3060
|
}, [step]);
|
|
@@ -2724,19 +3062,19 @@ function Stepper({
|
|
|
2724
3062
|
// onNew is pinned outside the horizontally-scrolling region below (a sibling of
|
|
2725
3063
|
// it, not a child) so its own edge never scrolls off-screen along with the
|
|
2726
3064
|
// later steps once the scroller's min-width doesn't fit the viewport.
|
|
2727
|
-
/* @__PURE__ */
|
|
2728
|
-
/* @__PURE__ */
|
|
3065
|
+
/* @__PURE__ */ jsxs27("div", { className: "flex w-full shrink-0 items-center gap-2", children: [
|
|
3066
|
+
/* @__PURE__ */ jsx45("nav", { "aria-label": navLabel, className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx45(
|
|
2729
3067
|
"div",
|
|
2730
3068
|
{
|
|
2731
3069
|
"data-testid": scrollTestId,
|
|
2732
3070
|
className: "flex w-full items-center gap-0 overflow-x-auto px-2 py-1",
|
|
2733
|
-
children: /* @__PURE__ */
|
|
3071
|
+
children: /* @__PURE__ */ jsx45("ol", { className: "flex min-w-[700px] flex-1 items-start", children: steps.map(({ n, label, phase }, i) => {
|
|
2734
3072
|
const done = step > n;
|
|
2735
3073
|
const active = step === n;
|
|
2736
3074
|
const navigable = n <= maxStep && n !== step;
|
|
2737
3075
|
const last = i === steps.length - 1;
|
|
2738
|
-
return /* @__PURE__ */
|
|
2739
|
-
/* @__PURE__ */
|
|
3076
|
+
return /* @__PURE__ */ jsxs27("li", { className: cn("flex items-start", !last && "flex-1"), children: [
|
|
3077
|
+
/* @__PURE__ */ jsxs27(
|
|
2740
3078
|
"button",
|
|
2741
3079
|
{
|
|
2742
3080
|
ref: (element) => {
|
|
@@ -2752,17 +3090,17 @@ function Stepper({
|
|
|
2752
3090
|
navigable ? "cursor-pointer hover:opacity-80" : "cursor-default"
|
|
2753
3091
|
),
|
|
2754
3092
|
children: [
|
|
2755
|
-
/* @__PURE__ */
|
|
3093
|
+
/* @__PURE__ */ jsx45(
|
|
2756
3094
|
"div",
|
|
2757
3095
|
{
|
|
2758
3096
|
className: cn(
|
|
2759
3097
|
"flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[11px] font-bold transition-colors",
|
|
2760
3098
|
done ? "bg-primary/15 text-primary" : active ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"
|
|
2761
3099
|
),
|
|
2762
|
-
children: done ? /* @__PURE__ */
|
|
3100
|
+
children: done ? /* @__PURE__ */ jsx45(Check5, { className: "h-3 w-3" }) : n
|
|
2763
3101
|
}
|
|
2764
3102
|
),
|
|
2765
|
-
/* @__PURE__ */
|
|
3103
|
+
/* @__PURE__ */ jsx45(
|
|
2766
3104
|
"span",
|
|
2767
3105
|
{
|
|
2768
3106
|
className: cn(
|
|
@@ -2772,11 +3110,11 @@ function Stepper({
|
|
|
2772
3110
|
children: label
|
|
2773
3111
|
}
|
|
2774
3112
|
),
|
|
2775
|
-
/* @__PURE__ */
|
|
3113
|
+
/* @__PURE__ */ jsx45("span", { className: "text-[9px] tracking-[0.5px] text-muted-foreground/50 uppercase", children: phase })
|
|
2776
3114
|
]
|
|
2777
3115
|
}
|
|
2778
3116
|
),
|
|
2779
|
-
!last && /* @__PURE__ */
|
|
3117
|
+
!last && /* @__PURE__ */ jsx45(
|
|
2780
3118
|
"div",
|
|
2781
3119
|
{
|
|
2782
3120
|
className: cn(
|
|
@@ -2789,7 +3127,7 @@ function Stepper({
|
|
|
2789
3127
|
}) })
|
|
2790
3128
|
}
|
|
2791
3129
|
) }),
|
|
2792
|
-
onNew && /* @__PURE__ */
|
|
3130
|
+
onNew && /* @__PURE__ */ jsxs27(
|
|
2793
3131
|
"button",
|
|
2794
3132
|
{
|
|
2795
3133
|
"data-testid": newTestId,
|
|
@@ -2797,7 +3135,7 @@ function Stepper({
|
|
|
2797
3135
|
title: newLabel,
|
|
2798
3136
|
className: "flex shrink-0 items-center gap-1.5 rounded-lg border border-border px-3 py-1.5 text-[12px] font-semibold text-foreground transition-colors hover:bg-muted/50",
|
|
2799
3137
|
children: [
|
|
2800
|
-
/* @__PURE__ */
|
|
3138
|
+
/* @__PURE__ */ jsx45(Plus, { className: "h-3.5 w-3.5" }),
|
|
2801
3139
|
newLabel
|
|
2802
3140
|
]
|
|
2803
3141
|
}
|
|
@@ -2806,6 +3144,8 @@ function Stepper({
|
|
|
2806
3144
|
);
|
|
2807
3145
|
}
|
|
2808
3146
|
export {
|
|
3147
|
+
AiButton,
|
|
3148
|
+
AiExplainButton,
|
|
2809
3149
|
AlertBox,
|
|
2810
3150
|
Badge,
|
|
2811
3151
|
Button,
|
|
@@ -2821,6 +3161,7 @@ export {
|
|
|
2821
3161
|
ConfirmDialog,
|
|
2822
3162
|
Dialog,
|
|
2823
3163
|
DialogActions,
|
|
3164
|
+
DialogBody,
|
|
2824
3165
|
DialogContent,
|
|
2825
3166
|
DialogDescription,
|
|
2826
3167
|
DialogFooter,
|
|
@@ -2872,6 +3213,7 @@ export {
|
|
|
2872
3213
|
SelectValue,
|
|
2873
3214
|
Separator,
|
|
2874
3215
|
Sheet,
|
|
3216
|
+
SheetBody,
|
|
2875
3217
|
SheetClose,
|
|
2876
3218
|
SheetContent,
|
|
2877
3219
|
SheetDescription,
|
|
@@ -2905,14 +3247,18 @@ export {
|
|
|
2905
3247
|
TooltipContent,
|
|
2906
3248
|
TooltipProvider,
|
|
2907
3249
|
TooltipTrigger,
|
|
3250
|
+
VoiceInputButton,
|
|
3251
|
+
VoiceTranscript,
|
|
2908
3252
|
badgeVariants,
|
|
2909
3253
|
buttonVariants,
|
|
2910
3254
|
cn,
|
|
3255
|
+
describeSpeechError,
|
|
2911
3256
|
getConfirmButtonPlacement,
|
|
2912
3257
|
isWindowsPlatform,
|
|
2913
3258
|
loadingSpinnerIconVariants,
|
|
2914
3259
|
sheetVariants,
|
|
2915
3260
|
useMediaQuery,
|
|
2916
|
-
useSidebarCollapsed
|
|
3261
|
+
useSidebarCollapsed,
|
|
3262
|
+
useSpeechRecognition
|
|
2917
3263
|
};
|
|
2918
3264
|
//# sourceMappingURL=index.js.map
|