@hex-core/components 1.6.0 → 1.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/dist/_tsup-dts-rollup.d.ts +36 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +140 -1
- package/dist/index.js.map +1 -1
- package/dist/schemas.js +3 -3
- package/dist/schemas.js.map +1 -1
- package/dist/speech-recognition.d.ts +2 -0
- package/dist/speech-recognition.js +152 -0
- package/dist/speech-recognition.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { clsx } from 'clsx';
|
|
4
|
+
import { twMerge } from 'tailwind-merge';
|
|
5
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
6
|
+
|
|
7
|
+
function cn(...inputs) {
|
|
8
|
+
return twMerge(clsx(inputs));
|
|
9
|
+
}
|
|
10
|
+
function getSpeechRecognitionCtor() {
|
|
11
|
+
if (typeof window === "undefined") return null;
|
|
12
|
+
const w = window;
|
|
13
|
+
return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
|
|
14
|
+
}
|
|
15
|
+
function SpeechRecognition({
|
|
16
|
+
isListening,
|
|
17
|
+
onListeningChange,
|
|
18
|
+
onTranscript,
|
|
19
|
+
onError,
|
|
20
|
+
lang = "en-US",
|
|
21
|
+
continuous = true,
|
|
22
|
+
interimResults = true,
|
|
23
|
+
startLabel = "Start dictation",
|
|
24
|
+
stopLabel = "Stop dictation",
|
|
25
|
+
notSupportedLabel = "Speech recognition not supported in this browser",
|
|
26
|
+
disabled,
|
|
27
|
+
className,
|
|
28
|
+
...rest
|
|
29
|
+
}) {
|
|
30
|
+
const recognitionRef = React.useRef(null);
|
|
31
|
+
const [isSupported, setIsSupported] = React.useState(true);
|
|
32
|
+
const onTranscriptRef = React.useRef(onTranscript);
|
|
33
|
+
const onListeningChangeRef = React.useRef(onListeningChange);
|
|
34
|
+
const onErrorRef = React.useRef(onError);
|
|
35
|
+
onTranscriptRef.current = onTranscript;
|
|
36
|
+
onListeningChangeRef.current = onListeningChange;
|
|
37
|
+
onErrorRef.current = onError;
|
|
38
|
+
const mountedRef = React.useRef(true);
|
|
39
|
+
React.useEffect(
|
|
40
|
+
() => () => {
|
|
41
|
+
mountedRef.current = false;
|
|
42
|
+
},
|
|
43
|
+
[]
|
|
44
|
+
);
|
|
45
|
+
const rebuildingRef = React.useRef(false);
|
|
46
|
+
const isListeningRef = React.useRef(isListening);
|
|
47
|
+
isListeningRef.current = isListening;
|
|
48
|
+
React.useEffect(() => {
|
|
49
|
+
const Ctor = getSpeechRecognitionCtor();
|
|
50
|
+
setIsSupported(Ctor !== null);
|
|
51
|
+
}, []);
|
|
52
|
+
React.useEffect(() => {
|
|
53
|
+
if (!isListening) return;
|
|
54
|
+
const Ctor = getSpeechRecognitionCtor();
|
|
55
|
+
if (!Ctor) return;
|
|
56
|
+
const instance = new Ctor();
|
|
57
|
+
instance.continuous = continuous;
|
|
58
|
+
instance.interimResults = interimResults;
|
|
59
|
+
instance.lang = lang;
|
|
60
|
+
instance.onresult = (event) => {
|
|
61
|
+
if (!mountedRef.current) return;
|
|
62
|
+
for (let i = event.resultIndex; i < event.results.length; i++) {
|
|
63
|
+
const result = event.results[i];
|
|
64
|
+
const transcript = result[0]?.transcript ?? "";
|
|
65
|
+
if (transcript) onTranscriptRef.current(transcript, result.isFinal);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
instance.onerror = (event) => {
|
|
69
|
+
if (!mountedRef.current) return;
|
|
70
|
+
onErrorRef.current?.(event.error, event.message);
|
|
71
|
+
if (event.error !== "aborted") onListeningChangeRef.current(false);
|
|
72
|
+
};
|
|
73
|
+
instance.onend = () => {
|
|
74
|
+
if (!mountedRef.current) return;
|
|
75
|
+
if (rebuildingRef.current) return;
|
|
76
|
+
onListeningChangeRef.current(false);
|
|
77
|
+
};
|
|
78
|
+
recognitionRef.current = instance;
|
|
79
|
+
try {
|
|
80
|
+
instance.start();
|
|
81
|
+
} catch (err) {
|
|
82
|
+
onErrorRef.current?.("start-failed", err instanceof Error ? err.message : String(err));
|
|
83
|
+
onListeningChangeRef.current(false);
|
|
84
|
+
}
|
|
85
|
+
return () => {
|
|
86
|
+
rebuildingRef.current = isListeningRef.current;
|
|
87
|
+
instance.onresult = null;
|
|
88
|
+
instance.onerror = null;
|
|
89
|
+
instance.onend = null;
|
|
90
|
+
try {
|
|
91
|
+
instance.abort();
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
recognitionRef.current = null;
|
|
95
|
+
queueMicrotask(() => {
|
|
96
|
+
rebuildingRef.current = false;
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
}, [isListening, continuous, interimResults, lang]);
|
|
100
|
+
const tooltip = !isSupported ? notSupportedLabel : isListening ? stopLabel : startLabel;
|
|
101
|
+
const accessibleName = isSupported ? startLabel : notSupportedLabel;
|
|
102
|
+
const isDisabled = disabled || !isSupported;
|
|
103
|
+
return /* @__PURE__ */ jsx(
|
|
104
|
+
"button",
|
|
105
|
+
{
|
|
106
|
+
type: "button",
|
|
107
|
+
...rest,
|
|
108
|
+
disabled: isDisabled,
|
|
109
|
+
"aria-label": accessibleName,
|
|
110
|
+
"aria-pressed": isListening,
|
|
111
|
+
title: tooltip,
|
|
112
|
+
onClick: (event) => {
|
|
113
|
+
rest.onClick?.(event);
|
|
114
|
+
if (event.defaultPrevented || isDisabled) return;
|
|
115
|
+
onListeningChange(!isListening);
|
|
116
|
+
},
|
|
117
|
+
className: cn(
|
|
118
|
+
"inline-flex h-9 w-9 items-center justify-center rounded-md border bg-background",
|
|
119
|
+
"text-foreground transition-colors duration-[var(--duration-normal,200ms)] ease-out",
|
|
120
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
121
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
122
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
123
|
+
isListening && "animate-pulse border-destructive text-destructive",
|
|
124
|
+
className
|
|
125
|
+
),
|
|
126
|
+
children: /* @__PURE__ */ jsxs(
|
|
127
|
+
"svg",
|
|
128
|
+
{
|
|
129
|
+
"aria-hidden": true,
|
|
130
|
+
viewBox: "0 0 16 16",
|
|
131
|
+
width: "14",
|
|
132
|
+
height: "14",
|
|
133
|
+
fill: "none",
|
|
134
|
+
stroke: "currentColor",
|
|
135
|
+
strokeWidth: "1.5",
|
|
136
|
+
strokeLinecap: "round",
|
|
137
|
+
strokeLinejoin: "round",
|
|
138
|
+
children: [
|
|
139
|
+
/* @__PURE__ */ jsx("rect", { x: "6", y: "2", width: "4", height: "8", rx: "2" }),
|
|
140
|
+
/* @__PURE__ */ jsx("path", { d: "M3.5 7.5a4.5 4.5 0 0 0 9 0" }),
|
|
141
|
+
/* @__PURE__ */ jsx("path", { d: "M8 12v2" }),
|
|
142
|
+
/* @__PURE__ */ jsx("path", { d: "M5.5 14h5" })
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export { SpeechRecognition };
|
|
151
|
+
//# sourceMappingURL=speech-recognition.js.map
|
|
152
|
+
//# sourceMappingURL=speech-recognition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/utils.ts","../src/ai/speech-recognition/speech-recognition.tsx"],"names":[],"mappings":"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","file":"speech-recognition.js","sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names with Tailwind CSS conflict resolution.\n * @param inputs - Class values (strings, arrays, objects) to merge\n * @returns A single merged class string with Tailwind conflicts resolved\n */\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n","\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../../lib/utils.js\";\n\n/**\n * Browser SpeechRecognition wrapper. Renders a mic toggle button that\n * starts/stops the Web Speech API and emits transcript chunks.\n *\n * Headless on data: `isListening` + `onListeningChange` are required so\n * the consumer keeps state where it fits (a `useChat` hook, redux,\n * local state). `onTranscript` fires per result with `isFinal` so the\n * consumer can append finalized phrases and replace interim ones.\n *\n * Falls back to a disabled button labeled `notSupportedLabel` when the\n * browser lacks `SpeechRecognition` (Firefox as of 2026, older Safari).\n *\n * @example\n * const [listening, setListening] = useState(false);\n * const [text, setText] = useState(\"\");\n * <SpeechRecognition\n * isListening={listening}\n * onListeningChange={setListening}\n * onTranscript={(chunk, isFinal) => {\n * if (isFinal) setText((t) => t + chunk);\n * }}\n * />\n */\ntype SpeechRecognitionConstructor = new () => SpeechRecognitionInstance;\n\ninterface SpeechRecognitionResultLike {\n\treadonly isFinal: boolean;\n\treadonly length: number;\n\treadonly [index: number]: { readonly transcript: string };\n}\n\ninterface SpeechRecognitionResultListLike {\n\treadonly length: number;\n\treadonly [index: number]: SpeechRecognitionResultLike;\n}\n\ninterface SpeechRecognitionEventLike {\n\treadonly resultIndex: number;\n\treadonly results: SpeechRecognitionResultListLike;\n}\n\ninterface SpeechRecognitionErrorEventLike {\n\treadonly error: string;\n\treadonly message?: string;\n}\n\ninterface SpeechRecognitionInstance {\n\tcontinuous: boolean;\n\tinterimResults: boolean;\n\tlang: string;\n\tonresult: ((event: SpeechRecognitionEventLike) => void) | null;\n\tonerror: ((event: SpeechRecognitionErrorEventLike) => void) | null;\n\tonend: (() => void) | null;\n\tstart(): void;\n\tstop(): void;\n\tabort(): void;\n}\n\nfunction getSpeechRecognitionCtor(): SpeechRecognitionConstructor | null {\n\tif (typeof window === \"undefined\") return null;\n\tconst w = window as unknown as {\n\t\tSpeechRecognition?: SpeechRecognitionConstructor;\n\t\twebkitSpeechRecognition?: SpeechRecognitionConstructor;\n\t};\n\treturn w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;\n}\n\nexport interface SpeechRecognitionProps\n\textends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"onError\"> {\n\t/** Controlled listening state. */\n\tisListening: boolean;\n\t/** Called when listening starts/stops (user toggle or browser auto-end). */\n\tonListeningChange: (listening: boolean) => void;\n\t/** Called per transcript chunk. `isFinal` indicates a finalized phrase. */\n\tonTranscript: (text: string, isFinal: boolean) => void;\n\t/** Called on browser error (e.g. \"not-allowed\", \"no-speech\", \"network\"). */\n\tonError?: (error: string, message?: string) => void;\n\t/** BCP-47 language tag. Default `\"en-US\"`. */\n\tlang?: string;\n\t/** Keep listening across pauses. Default `true`. */\n\tcontinuous?: boolean;\n\t/** Emit interim (in-progress) results. Default `true`. */\n\tinterimResults?: boolean;\n\t/** Accessible name when idle. Default `\"Start dictation\"`. */\n\tstartLabel?: string;\n\t/** Accessible name when listening. Default `\"Stop dictation\"`. */\n\tstopLabel?: string;\n\t/** Accessible name + tooltip when the browser lacks the API. */\n\tnotSupportedLabel?: string;\n}\n\n/**\n * Renders a mic toggle button wired to the Web Speech API.\n * @param props - controlled listening state + transcript callback\n * @returns A button element that toggles speech recognition\n */\nfunction SpeechRecognition({\n\tisListening,\n\tonListeningChange,\n\tonTranscript,\n\tonError,\n\tlang = \"en-US\",\n\tcontinuous = true,\n\tinterimResults = true,\n\tstartLabel = \"Start dictation\",\n\tstopLabel = \"Stop dictation\",\n\tnotSupportedLabel = \"Speech recognition not supported in this browser\",\n\tdisabled,\n\tclassName,\n\t...rest\n}: SpeechRecognitionProps) {\n\tconst recognitionRef = React.useRef<SpeechRecognitionInstance | null>(null);\n\tconst [isSupported, setIsSupported] = React.useState(true);\n\n\t// \"Latest ref\" pattern, assigned synchronously in render so a Web Speech\n\t// callback firing between commit and a useEffect can never see stale\n\t// closures. React permits ref mutation during render when the assignment\n\t// is purely a latest-value mirror.\n\tconst onTranscriptRef = React.useRef(onTranscript);\n\tconst onListeningChangeRef = React.useRef(onListeningChange);\n\tconst onErrorRef = React.useRef(onError);\n\tonTranscriptRef.current = onTranscript;\n\tonListeningChangeRef.current = onListeningChange;\n\tonErrorRef.current = onError;\n\n\t// Mounted guard: the engine fires onend asynchronously, sometimes after\n\t// the React tree has been torn down. Without this, a stale handler can\n\t// invoke setState on an unmounted parent.\n\tconst mountedRef = React.useRef(true);\n\tReact.useEffect(\n\t\t() => () => {\n\t\t\tmountedRef.current = false;\n\t\t},\n\t\t[],\n\t);\n\n\t// Set when the lifecycle effect's cleanup runs because of a prop change\n\t// (lang/continuous/interimResults), not user-initiated stop. The about-\n\t// to-fire onend should NOT bubble back as `onListeningChange(false)` in\n\t// that case — the new effect run is about to re-create the engine.\n\tconst rebuildingRef = React.useRef(false);\n\t// Latest `isListening` mirror so cleanup can read the NEW value (closure\n\t// captures OLD). NEW=true means this is a prop-change rebuild; NEW=false\n\t// means the user stopped.\n\tconst isListeningRef = React.useRef(isListening);\n\tisListeningRef.current = isListening;\n\n\t// SSR: ctor lookup must run after mount.\n\tReact.useEffect(() => {\n\t\tconst Ctor = getSpeechRecognitionCtor();\n\t\tsetIsSupported(Ctor !== null);\n\t}, []);\n\n\t// Toggle the engine on `isListening` change. Recreate per session so a\n\t// stuck session can't leak — Chrome's recognition is single-use after\n\t// onend in some failure modes.\n\tReact.useEffect(() => {\n\t\tif (!isListening) return;\n\n\t\tconst Ctor = getSpeechRecognitionCtor();\n\t\tif (!Ctor) return;\n\n\t\tconst instance = new Ctor();\n\t\tinstance.continuous = continuous;\n\t\tinstance.interimResults = interimResults;\n\t\tinstance.lang = lang;\n\n\t\tinstance.onresult = (event) => {\n\t\t\tif (!mountedRef.current) return;\n\t\t\tfor (let i = event.resultIndex; i < event.results.length; i++) {\n\t\t\t\tconst result = event.results[i];\n\t\t\t\tconst transcript = result[0]?.transcript ?? \"\";\n\t\t\t\tif (transcript) onTranscriptRef.current(transcript, result.isFinal);\n\t\t\t}\n\t\t};\n\t\tinstance.onerror = (event) => {\n\t\t\tif (!mountedRef.current) return;\n\t\t\tonErrorRef.current?.(event.error, event.message);\n\t\t\t// \"aborted\" is a normal stop signal — don't toggle off twice.\n\t\t\tif (event.error !== \"aborted\") onListeningChangeRef.current(false);\n\t\t};\n\t\tinstance.onend = () => {\n\t\t\tif (!mountedRef.current) return;\n\t\t\t// Skip the off-toggle when cleanup is from a prop-change rebuild;\n\t\t\t// the next effect run will re-start with the new options.\n\t\t\tif (rebuildingRef.current) return;\n\t\t\tonListeningChangeRef.current(false);\n\t\t};\n\n\t\trecognitionRef.current = instance;\n\t\ttry {\n\t\t\tinstance.start();\n\t\t} catch (err) {\n\t\t\t// Chrome throws if start() is called twice; surface as an error\n\t\t\t// rather than letting it crash the React tree.\n\t\t\tonErrorRef.current?.(\"start-failed\", err instanceof Error ? err.message : String(err));\n\t\t\tonListeningChangeRef.current(false);\n\t\t}\n\n\t\treturn () => {\n\t\t\t// Mark this teardown as a rebuild iff isListening is STILL true\n\t\t\t// in the latest render (only lang/continuous/interimResults\n\t\t\t// changed). On a real user-stop the NEW isListening is false and\n\t\t\t// any synchronous onend-from-abort should toggle parent state.\n\t\t\trebuildingRef.current = isListeningRef.current;\n\t\t\tinstance.onresult = null;\n\t\t\tinstance.onerror = null;\n\t\t\tinstance.onend = null;\n\t\t\ttry {\n\t\t\t\tinstance.abort();\n\t\t\t} catch {\n\t\t\t\t// abort() throws if the engine never started; safe to ignore.\n\t\t\t}\n\t\t\trecognitionRef.current = null;\n\t\t\t// Reset on next microtask so a follow-up effect run sees a clean slate.\n\t\t\tqueueMicrotask(() => {\n\t\t\t\trebuildingRef.current = false;\n\t\t\t});\n\t\t};\n\t}, [isListening, continuous, interimResults, lang]);\n\n\tconst tooltip = !isSupported\n\t\t? notSupportedLabel\n\t\t: isListening\n\t\t\t? stopLabel\n\t\t\t: startLabel;\n\t// aria-label is stable when supported so screen readers don't re-announce\n\t// the entire button on each toggle. State is conveyed via aria-pressed.\n\t// Only the unsupported case swaps the accessible name.\n\tconst accessibleName = isSupported ? startLabel : notSupportedLabel;\n\tconst isDisabled = disabled || !isSupported;\n\n\treturn (\n\t\t<button\n\t\t\ttype=\"button\"\n\t\t\t{...rest}\n\t\t\tdisabled={isDisabled}\n\t\t\taria-label={accessibleName}\n\t\t\taria-pressed={isListening}\n\t\t\ttitle={tooltip}\n\t\t\tonClick={(event) => {\n\t\t\t\trest.onClick?.(event);\n\t\t\t\tif (event.defaultPrevented || isDisabled) return;\n\t\t\t\tonListeningChange(!isListening);\n\t\t\t}}\n\t\t\tclassName={cn(\n\t\t\t\t\"inline-flex h-9 w-9 items-center justify-center rounded-md border bg-background\",\n\t\t\t\t\"text-foreground transition-colors duration-[var(--duration-normal,200ms)] ease-out\",\n\t\t\t\t\"hover:bg-accent hover:text-accent-foreground\",\n\t\t\t\t\"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n\t\t\t\t\"disabled:cursor-not-allowed disabled:opacity-50\",\n\t\t\t\tisListening && \"animate-pulse border-destructive text-destructive\",\n\t\t\t\tclassName,\n\t\t\t)}\n\t\t>\n\t\t\t<svg\n\t\t\t\taria-hidden\n\t\t\t\tviewBox=\"0 0 16 16\"\n\t\t\t\twidth=\"14\"\n\t\t\t\theight=\"14\"\n\t\t\t\tfill=\"none\"\n\t\t\t\tstroke=\"currentColor\"\n\t\t\t\tstrokeWidth=\"1.5\"\n\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t>\n\t\t\t\t<rect x=\"6\" y=\"2\" width=\"4\" height=\"8\" rx=\"2\" />\n\t\t\t\t<path d=\"M3.5 7.5a4.5 4.5 0 0 0 9 0\" />\n\t\t\t\t<path d=\"M8 12v2\" />\n\t\t\t\t<path d=\"M5.5 14h5\" />\n\t\t\t</svg>\n\t\t</button>\n\t);\n}\n\nexport { SpeechRecognition };\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hex-core/components",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "AI-native React components for Hex UI — Radix UI + Tailwind CSS with machine-readable schemas. RSC-aware per-component bundles + tree-shakable.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"tsup": "^8.3.0",
|
|
102
102
|
"typescript": "^6.0.3",
|
|
103
103
|
"vaul": "^1.1.2",
|
|
104
|
-
"@hex-core/registry": "^0.3.
|
|
104
|
+
"@hex-core/registry": "^0.3.3"
|
|
105
105
|
},
|
|
106
106
|
"peerDependencies": {
|
|
107
107
|
"@hex-core/registry": "*",
|