@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.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-y-auto rounded-lg border bg-background p-6 shadow-lg",
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("div", { className: cn("mt-4 flex justify-end gap-2", className), ...props });
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] overflow-y-auto transition-shadow duration-150 ease-out",
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: "0 2px 10px color-mix(in oklch, var(--color-primary) 18%, transparent)"
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 useEffect6, useRef as useRef6 } from "react";
3042
+ import { useEffect as useEffect7, useRef as useRef7 } from "react";
2705
3043
  import { Check as Check5, Plus } from "lucide-react";
2706
- import { jsx as jsx41, jsxs as jsxs23 } from "react/jsx-runtime";
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 = useRef6(/* @__PURE__ */ new Map());
2719
- useEffect6(() => {
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__ */ jsxs23("div", { className: "flex w-full shrink-0 items-center gap-2", children: [
2728
- /* @__PURE__ */ jsx41("nav", { "aria-label": navLabel, className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx41(
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__ */ jsx41("ol", { className: "flex min-w-[700px] flex-1 items-start", children: steps.map(({ n, label, phase }, i) => {
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__ */ jsxs23("li", { className: cn("flex items-start", !last && "flex-1"), children: [
2739
- /* @__PURE__ */ jsxs23(
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__ */ jsx41(
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__ */ jsx41(Check5, { className: "h-3 w-3" }) : n
3100
+ children: done ? /* @__PURE__ */ jsx45(Check5, { className: "h-3 w-3" }) : n
2763
3101
  }
2764
3102
  ),
2765
- /* @__PURE__ */ jsx41(
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__ */ jsx41("span", { className: "text-[9px] tracking-[0.5px] text-muted-foreground/50 uppercase", children: phase })
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__ */ jsx41(
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__ */ jsxs23(
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__ */ jsx41(Plus, { className: "h-3.5 w-3.5" }),
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