@corti/dictation-web 0.6.0 → 0.6.1-rc.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.
@@ -1,19 +1,23 @@
1
1
  /**
2
- * Requests access to the microphone.
2
+ * Primes the document with an active microphone stream.
3
3
  *
4
- * This function checks if the microphone permission is in "prompt" state, then requests
5
- * access and stops any active tracks immediately.
4
+ * Opens (and immediately stops) a media stream so the document holds an
5
+ * active mic permission for this session. In Firefox this is what makes
6
+ * real deviceIds and labels appear in subsequent enumerateDevices() calls.
7
+ * Throws early if the user has already denied permission.
6
8
  *
7
- * @returns A promise that resolves when the permission request is complete.
9
+ * @returns A promise that resolves once the stream has been opened and stopped.
8
10
  * @throws Error if microphone access is denied or unavailable.
9
11
  */
10
- export declare function requestMicAccess(): Promise<void>;
12
+ export declare function primeMicStream(): Promise<void>;
11
13
  /**
12
14
  * Retrieves available audio input devices.
13
15
  *
14
- * This function uses the mediaDevices API to enumerate devices and filters out those
15
- * which are audio inputs. In some browsers, you may need to request user media before
16
- * device labels are populated.
16
+ * Enumerates devices first; if the result looks like Firefox's pre-permission
17
+ * placeholder (an entry with empty deviceId/label even though the Permissions
18
+ * API reports access), primes the document with getUserMedia and re-enumerates.
19
+ * Browsers that already return populated entries (Chrome, Safari) skip the
20
+ * priming call entirely.
17
21
  *
18
22
  * @returns A promise that resolves with an object containing:
19
23
  * - `devices`: an array of MediaDeviceInfo objects for audio inputs.
@@ -1,39 +1,36 @@
1
1
  /**
2
- * Requests access to the microphone.
2
+ * Primes the document with an active microphone stream.
3
3
  *
4
- * This function checks if the microphone permission is in "prompt" state, then requests
5
- * access and stops any active tracks immediately.
4
+ * Opens (and immediately stops) a media stream so the document holds an
5
+ * active mic permission for this session. In Firefox this is what makes
6
+ * real deviceIds and labels appear in subsequent enumerateDevices() calls.
7
+ * Throws early if the user has already denied permission.
6
8
  *
7
- * @returns A promise that resolves when the permission request is complete.
9
+ * @returns A promise that resolves once the stream has been opened and stopped.
8
10
  * @throws Error if microphone access is denied or unavailable.
9
11
  */
10
- export async function requestMicAccess() {
11
- if (!navigator.permissions) {
12
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
13
- stream.getTracks().forEach((track) => {
14
- track.stop();
12
+ export async function primeMicStream() {
13
+ if (navigator.permissions) {
14
+ const permissionStatus = await navigator.permissions.query({
15
+ name: "microphone",
15
16
  });
16
- return;
17
+ if (permissionStatus.state === "denied") {
18
+ throw new Error("Microphone permission is denied");
19
+ }
17
20
  }
18
- const permissionStatus = await navigator.permissions.query({
19
- name: "microphone",
21
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
22
+ stream.getTracks().forEach((track) => {
23
+ track.stop();
20
24
  });
21
- if (permissionStatus.state === "denied") {
22
- throw new Error("Microphone permission is denied");
23
- }
24
- if (permissionStatus.state === "prompt") {
25
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
26
- stream.getTracks().forEach((track) => {
27
- track.stop();
28
- });
29
- }
30
25
  }
31
26
  /**
32
27
  * Retrieves available audio input devices.
33
28
  *
34
- * This function uses the mediaDevices API to enumerate devices and filters out those
35
- * which are audio inputs. In some browsers, you may need to request user media before
36
- * device labels are populated.
29
+ * Enumerates devices first; if the result looks like Firefox's pre-permission
30
+ * placeholder (an entry with empty deviceId/label even though the Permissions
31
+ * API reports access), primes the document with getUserMedia and re-enumerates.
32
+ * Browsers that already return populated entries (Chrome, Safari) skip the
33
+ * priming call entirely.
37
34
  *
38
35
  * @returns A promise that resolves with an object containing:
39
36
  * - `devices`: an array of MediaDeviceInfo objects for audio inputs.
@@ -44,10 +41,24 @@ export async function getAudioDevices() {
44
41
  if (!navigator.mediaDevices?.enumerateDevices) {
45
42
  throw new Error("MediaDevices API is not available");
46
43
  }
47
- await requestMicAccess();
44
+ let audioDevices = await listAudioInputs();
45
+ if (needsMicPriming(audioDevices)) {
46
+ await primeMicStream();
47
+ audioDevices = await listAudioInputs();
48
+ }
49
+ return {
50
+ defaultDevice: audioDevices[0],
51
+ devices: audioDevices,
52
+ };
53
+ }
54
+ async function listAudioInputs() {
48
55
  const devices = await navigator.mediaDevices.enumerateDevices();
49
- const audioDevices = devices.filter((device) => device.kind === "audioinput");
50
- const defaultDevice = audioDevices.length > 0 ? audioDevices[0] : undefined;
51
- return { defaultDevice, devices: audioDevices };
56
+ return devices.filter((device) => device.kind === "audioinput");
57
+ }
58
+ function needsMicPriming(audioInputs) {
59
+ if (audioInputs.length === 0) {
60
+ return false; // no mic hardware — don't trigger a permission prompt
61
+ }
62
+ return audioInputs.some((d) => d.deviceId === "" || d.label === "");
52
63
  }
53
64
  //# sourceMappingURL=devices.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"devices.js","sourceRoot":"","sources":["../../src/utils/devices.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,YAAY,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAE1E,MAAM,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE;YACnC,KAAK,CAAC,IAAI,EAAE,CAAC;QACf,CAAC,CAAC,CAAC;QAEH,OAAO;IACT,CAAC;IAED,MAAM,gBAAgB,GAAG,MAAM,SAAS,CAAC,WAAW,CAAC,KAAK,CAAC;QACzD,IAAI,EAAE,YAA8B;KACrC,CAAC,CAAC;IAEH,IAAI,gBAAgB,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;IACrD,CAAC;IAED,IAAI,gBAAgB,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;QACxC,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,YAAY,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAE1E,MAAM,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE;YACnC,KAAK,CAAC,IAAI,EAAE,CAAC;QACf,CAAC,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe;IAInC,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,gBAAgB,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;IACvD,CAAC;IAED,MAAM,gBAAgB,EAAE,CAAC;IAEzB,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,YAAY,CAAC,gBAAgB,EAAE,CAAC;IAChE,MAAM,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,YAAY,CAAC,CAAC;IAC9E,MAAM,aAAa,GAAG,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAE5E,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC;AAClD,CAAC","sourcesContent":["/**\n * Requests access to the microphone.\n *\n * This function checks if the microphone permission is in \"prompt\" state, then requests\n * access and stops any active tracks immediately.\n *\n * @returns A promise that resolves when the permission request is complete.\n * @throws Error if microphone access is denied or unavailable.\n */\nexport async function requestMicAccess(): Promise<void> {\n if (!navigator.permissions) {\n const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n\n stream.getTracks().forEach((track) => {\n track.stop();\n });\n\n return;\n }\n\n const permissionStatus = await navigator.permissions.query({\n name: \"microphone\" as PermissionName,\n });\n\n if (permissionStatus.state === \"denied\") {\n throw new Error(\"Microphone permission is denied\");\n }\n\n if (permissionStatus.state === \"prompt\") {\n const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n\n stream.getTracks().forEach((track) => {\n track.stop();\n });\n }\n}\n\n/**\n * Retrieves available audio input devices.\n *\n * This function uses the mediaDevices API to enumerate devices and filters out those\n * which are audio inputs. In some browsers, you may need to request user media before\n * device labels are populated.\n *\n * @returns A promise that resolves with an object containing:\n * - `devices`: an array of MediaDeviceInfo objects for audio inputs.\n * - `defaultDevice`: the first audio input device, if available.\n * @throws Error if mediaDevices API is unavailable or device enumeration fails.\n */\nexport async function getAudioDevices(): Promise<{\n devices: MediaDeviceInfo[];\n defaultDevice?: MediaDeviceInfo;\n}> {\n if (!navigator.mediaDevices?.enumerateDevices) {\n throw new Error(\"MediaDevices API is not available\");\n }\n\n await requestMicAccess();\n\n const devices = await navigator.mediaDevices.enumerateDevices();\n const audioDevices = devices.filter((device) => device.kind === \"audioinput\");\n const defaultDevice = audioDevices.length > 0 ? audioDevices[0] : undefined;\n\n return { defaultDevice, devices: audioDevices };\n}\n"]}
1
+ {"version":3,"file":"devices.js","sourceRoot":"","sources":["../../src/utils/devices.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc;IAClC,IAAI,SAAS,CAAC,WAAW,EAAE,CAAC;QAC1B,MAAM,gBAAgB,GAAG,MAAM,SAAS,CAAC,WAAW,CAAC,KAAK,CAAC;YACzD,IAAI,EAAE,YAA8B;SACrC,CAAC,CAAC;QAEH,IAAI,gBAAgB,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YACxC,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;QACrD,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,YAAY,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAE1E,MAAM,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE;QACnC,KAAK,CAAC,IAAI,EAAE,CAAC;IACf,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe;IAInC,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,gBAAgB,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;IACvD,CAAC;IAED,IAAI,YAAY,GAAG,MAAM,eAAe,EAAE,CAAC;IAE3C,IAAI,eAAe,CAAC,YAAY,CAAC,EAAE,CAAC;QAClC,MAAM,cAAc,EAAE,CAAC;QACvB,YAAY,GAAG,MAAM,eAAe,EAAE,CAAC;IACzC,CAAC;IAED,OAAO;QACL,aAAa,EAAE,YAAY,CAAC,CAAC,CAAC;QAC9B,OAAO,EAAE,YAAY;KACtB,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,eAAe;IAC5B,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,YAAY,CAAC,gBAAgB,EAAE,CAAC;IAChE,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,YAAY,CAAC,CAAC;AAClE,CAAC;AAED,SAAS,eAAe,CAAC,WAA8B;IACrD,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO,KAAK,CAAC,CAAC,sDAAsD;IACtE,CAAC;IACD,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,EAAE,IAAI,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC;AACtE,CAAC","sourcesContent":["/**\n * Primes the document with an active microphone stream.\n *\n * Opens (and immediately stops) a media stream so the document holds an\n * active mic permission for this session. In Firefox this is what makes\n * real deviceIds and labels appear in subsequent enumerateDevices() calls.\n * Throws early if the user has already denied permission.\n *\n * @returns A promise that resolves once the stream has been opened and stopped.\n * @throws Error if microphone access is denied or unavailable.\n */\nexport async function primeMicStream(): Promise<void> {\n if (navigator.permissions) {\n const permissionStatus = await navigator.permissions.query({\n name: \"microphone\" as PermissionName,\n });\n\n if (permissionStatus.state === \"denied\") {\n throw new Error(\"Microphone permission is denied\");\n }\n }\n\n const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n\n stream.getTracks().forEach((track) => {\n track.stop();\n });\n}\n\n/**\n * Retrieves available audio input devices.\n *\n * Enumerates devices first; if the result looks like Firefox's pre-permission\n * placeholder (an entry with empty deviceId/label even though the Permissions\n * API reports access), primes the document with getUserMedia and re-enumerates.\n * Browsers that already return populated entries (Chrome, Safari) skip the\n * priming call entirely.\n *\n * @returns A promise that resolves with an object containing:\n * - `devices`: an array of MediaDeviceInfo objects for audio inputs.\n * - `defaultDevice`: the first audio input device, if available.\n * @throws Error if mediaDevices API is unavailable or device enumeration fails.\n */\nexport async function getAudioDevices(): Promise<{\n devices: MediaDeviceInfo[];\n defaultDevice?: MediaDeviceInfo;\n}> {\n if (!navigator.mediaDevices?.enumerateDevices) {\n throw new Error(\"MediaDevices API is not available\");\n }\n\n let audioDevices = await listAudioInputs();\n\n if (needsMicPriming(audioDevices)) {\n await primeMicStream();\n audioDevices = await listAudioInputs();\n }\n\n return {\n defaultDevice: audioDevices[0],\n devices: audioDevices,\n };\n}\n\nasync function listAudioInputs(): Promise<MediaDeviceInfo[]> {\n const devices = await navigator.mediaDevices.enumerateDevices();\n return devices.filter((device) => device.kind === \"audioinput\");\n}\n\nfunction needsMicPriming(audioInputs: MediaDeviceInfo[]): boolean {\n if (audioInputs.length === 0) {\n return false; // no mic hardware — don't trigger a permission prompt\n }\n return audioInputs.some((d) => d.deviceId === \"\" || d.label === \"\");\n}\n"]}
@@ -5,8 +5,9 @@
5
5
  * @returns Normalized keybinding string or null if empty
6
6
  *
7
7
  * @example
8
- * normalizeKeybinding("k") // "k"
8
+ * normalizeKeybinding("k") // "K"
9
9
  * normalizeKeybinding("meta") // "Cmd" on Mac
10
+ * normalizeKeybinding(" ") // "Space"
10
11
  * normalizeKeybinding(" space ") // "Space"
11
12
  */
12
13
  export declare function normalizeKeybinding(keybinding: string | null | undefined): string | null;
@@ -27,7 +27,7 @@ function capitalize(str) {
27
27
  * Normalizes a key string to the keybinding format.
28
28
  * Handles platform-specific mappings and capitalization.
29
29
  *
30
- * @param key - Key string to normalize (will be trimmed and lowercased)
30
+ * @param key - Key string to normalize (will be trimmed and uppercased)
31
31
  * @returns Formatted key string for keybinding
32
32
  *
33
33
  * @example
@@ -35,7 +35,8 @@ function capitalize(str) {
35
35
  * normalizeKeyForKeybinding("META") // "Cmd" on Mac, "Meta" elsewhere
36
36
  * normalizeKeyForKeybinding(" alt ") // "Opt" on Mac, "Alt" elsewhere
37
37
  * normalizeKeyForKeybinding("shift") // "Shift"
38
- * normalizeKeyForKeybinding("k") // "k"
38
+ * normalizeKeyForKeybinding("k") // "K"
39
+ * normalizeKeyForKeybinding(" ") // "Space"
39
40
  */
40
41
  function normalizeKeyForKeybinding(key) {
41
42
  if (key === " ") {
@@ -54,6 +55,11 @@ function normalizeKeyForKeybinding(key) {
54
55
  if (normalized === "space") {
55
56
  return "Space";
56
57
  }
58
+ // TODO: Uncomment this for v1 to avoid breaking changes now
59
+ // Capitalize single letters (a-z) for consistent display
60
+ // if (/^[a-z]$/.test(normalized)) {
61
+ // return normalized.toUpperCase();
62
+ // }
57
63
  return normalized.length > 1 ? capitalize(normalized) : normalized;
58
64
  }
59
65
  /**
@@ -63,19 +69,19 @@ function normalizeKeyForKeybinding(key) {
63
69
  * @returns Normalized keybinding string or null if empty
64
70
  *
65
71
  * @example
66
- * normalizeKeybinding("k") // "k"
72
+ * normalizeKeybinding("k") // "K"
67
73
  * normalizeKeybinding("meta") // "Cmd" on Mac
74
+ * normalizeKeybinding(" ") // "Space"
68
75
  * normalizeKeybinding(" space ") // "Space"
69
76
  */
70
77
  export function normalizeKeybinding(keybinding) {
71
78
  if (!keybinding) {
72
79
  return null;
73
80
  }
74
- const trimmed = keybinding.trim();
75
- if (trimmed === "") {
81
+ if (keybinding !== " " && keybinding.trim() === "") {
76
82
  return null;
77
83
  }
78
- return normalizeKeyForKeybinding(trimmed);
84
+ return normalizeKeyForKeybinding(keybinding);
79
85
  }
80
86
  /**
81
87
  * Checks if a pressed key matches the keybinding.
@@ -1 +1 @@
1
- {"version":3,"file":"keybinding.js","sourceRoot":"","sources":["../../src/utils/keybinding.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Checks if the current platform is macOS.\n * Uses userAgent string for reliable cross-browser detection.\n *\n * @returns true if running on macOS\n */\nfunction isMac(): boolean {\n if (typeof navigator === \"undefined\") {\n return false;\n }\n\n // Check user agent for Mac patterns (most reliable method)\n return /Mac|iPhone|iPad|iPod/.test(navigator.userAgent);\n}\n\n/**\n * Capitalizes the first letter of a string.\n *\n * @param str - String to capitalize\n * @returns String with first letter capitalized\n */\nfunction capitalize(str: string): string {\n if (str.length === 0) {\n return str;\n }\n\n return str.charAt(0).toUpperCase() + str.slice(1);\n}\n\n/**\n * Normalizes a key string to the keybinding format.\n * Handles platform-specific mappings and capitalization.\n *\n * @param key - Key string to normalize (will be trimmed and lowercased)\n * @returns Formatted key string for keybinding\n *\n * @example\n * normalizeKeyForKeybinding(\"Control\") // \"Ctrl\"\n * normalizeKeyForKeybinding(\"META\") // \"Cmd\" on Mac, \"Meta\" elsewhere\n * normalizeKeyForKeybinding(\" alt \") // \"Opt\" on Mac, \"Alt\" elsewhere\n * normalizeKeyForKeybinding(\"shift\") // \"Shift\"\n * normalizeKeyForKeybinding(\"k\") // \"k\"\n */\nfunction normalizeKeyForKeybinding(key: string): string {\n if (key === \" \") {\n return \"Space\";\n }\n\n const normalized = key.trim().toLowerCase();\n\n if (normalized === \"control\") {\n return \"Ctrl\";\n }\n if (normalized === \"meta\" || normalized === \"cmd\") {\n return isMac() ? \"Cmd\" : \"Meta\";\n }\n if (normalized === \"alt\" || normalized === \"opt\") {\n return isMac() ? \"Opt\" : \"Alt\";\n }\n if (normalized === \"space\") {\n return \"Space\";\n }\n\n return normalized.length > 1 ? capitalize(normalized) : normalized;\n}\n\n/**\n * Normalizes a keybinding string.\n *\n * @param keybinding - Keybinding string to normalize\n * @returns Normalized keybinding string or null if empty\n *\n * @example\n * normalizeKeybinding(\"k\") // \"k\"\n * normalizeKeybinding(\"meta\") // \"Cmd\" on Mac\n * normalizeKeybinding(\" space \") // \"Space\"\n */\nexport function normalizeKeybinding(\n keybinding: string | null | undefined,\n): string | null {\n if (!keybinding) {\n return null;\n }\n\n const trimmed = keybinding.trim();\n\n if (trimmed === \"\") {\n return null;\n }\n\n return normalizeKeyForKeybinding(trimmed);\n}\n\n/**\n * Checks if a pressed key matches the keybinding.\n * Checks both event.key and event.code for better reliability.\n *\n * @param event - KeyboardEvent to check\n * @param keybinding - Keybinding string to match against\n * @returns true if either the key or code matches the keybinding\n *\n * @example\n * matchesKeybinding(event, \"k\") // true if event.key is \"k\" or event.code is \"KeyK\"\n * matchesKeybinding(event, \"`\") // true if event.key is \"`\" or event.code is \"Backquote\"\n */\nexport function matchesKeybinding(\n event: KeyboardEvent,\n keybinding: string | null | undefined,\n): boolean {\n const normalizedKeybinding = normalizeKeybinding(keybinding);\n\n if (!normalizedKeybinding) {\n return false;\n }\n\n const normalizedKey = normalizeKeyForKeybinding(event.key);\n const normalizedCode = normalizeKeyForKeybinding(event.code);\n\n return (\n normalizedKey === normalizedKeybinding ||\n normalizedCode === normalizedKeybinding\n );\n}\n\n/**\n * Checks if keybindings should be ignored for the current active element.\n * Returns true if the user is typing in an input field.\n *\n * @param element - Element to check\n * @returns true if keybindings should be ignored for this element\n *\n * @example\n * shouldIgnoreKeybinding(document.activeElement) // true if input/textarea/contenteditable\n */\nexport function shouldIgnoreKeybinding(element: Element | null): boolean {\n if (!element) {\n return false;\n }\n\n const tagName = element.tagName.toLowerCase();\n\n if (tagName === \"input\" || tagName === \"textarea\") {\n return true;\n }\n\n if (element instanceof HTMLElement && element.contentEditable === \"true\") {\n return true;\n }\n\n return false;\n}\n"]}
1
+ {"version":3,"file":"keybinding.js","sourceRoot":"","sources":["../../src/utils/keybinding.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Checks if the current platform is macOS.\n * Uses userAgent string for reliable cross-browser detection.\n *\n * @returns true if running on macOS\n */\nfunction isMac(): boolean {\n if (typeof navigator === \"undefined\") {\n return false;\n }\n\n // Check user agent for Mac patterns (most reliable method)\n return /Mac|iPhone|iPad|iPod/.test(navigator.userAgent);\n}\n\n/**\n * Capitalizes the first letter of a string.\n *\n * @param str - String to capitalize\n * @returns String with first letter capitalized\n */\nfunction capitalize(str: string): string {\n if (str.length === 0) {\n return str;\n }\n\n return str.charAt(0).toUpperCase() + str.slice(1);\n}\n\n/**\n * Normalizes a key string to the keybinding format.\n * Handles platform-specific mappings and capitalization.\n *\n * @param key - Key string to normalize (will be trimmed and uppercased)\n * @returns Formatted key string for keybinding\n *\n * @example\n * normalizeKeyForKeybinding(\"Control\") // \"Ctrl\"\n * normalizeKeyForKeybinding(\"META\") // \"Cmd\" on Mac, \"Meta\" elsewhere\n * normalizeKeyForKeybinding(\" alt \") // \"Opt\" on Mac, \"Alt\" elsewhere\n * normalizeKeyForKeybinding(\"shift\") // \"Shift\"\n * normalizeKeyForKeybinding(\"k\") // \"K\"\n * normalizeKeyForKeybinding(\" \") // \"Space\"\n */\nfunction normalizeKeyForKeybinding(key: string): string {\n if (key === \" \") {\n return \"Space\";\n }\n\n const normalized = key.trim().toLowerCase();\n\n if (normalized === \"control\") {\n return \"Ctrl\";\n }\n if (normalized === \"meta\" || normalized === \"cmd\") {\n return isMac() ? \"Cmd\" : \"Meta\";\n }\n if (normalized === \"alt\" || normalized === \"opt\") {\n return isMac() ? \"Opt\" : \"Alt\";\n }\n if (normalized === \"space\") {\n return \"Space\";\n }\n\n // TODO: Uncomment this for v1 to avoid breaking changes now\n // Capitalize single letters (a-z) for consistent display\n // if (/^[a-z]$/.test(normalized)) {\n // return normalized.toUpperCase();\n // }\n\n return normalized.length > 1 ? capitalize(normalized) : normalized;\n}\n\n/**\n * Normalizes a keybinding string.\n *\n * @param keybinding - Keybinding string to normalize\n * @returns Normalized keybinding string or null if empty\n *\n * @example\n * normalizeKeybinding(\"k\") // \"K\"\n * normalizeKeybinding(\"meta\") // \"Cmd\" on Mac\n * normalizeKeybinding(\" \") // \"Space\"\n * normalizeKeybinding(\" space \") // \"Space\"\n */\nexport function normalizeKeybinding(\n keybinding: string | null | undefined,\n): string | null {\n if (!keybinding) {\n return null;\n }\n if (keybinding !== \" \" && keybinding.trim() === \"\") {\n return null;\n }\n\n return normalizeKeyForKeybinding(keybinding);\n}\n\n/**\n * Checks if a pressed key matches the keybinding.\n * Checks both event.key and event.code for better reliability.\n *\n * @param event - KeyboardEvent to check\n * @param keybinding - Keybinding string to match against\n * @returns true if either the key or code matches the keybinding\n *\n * @example\n * matchesKeybinding(event, \"k\") // true if event.key is \"k\" or event.code is \"KeyK\"\n * matchesKeybinding(event, \"`\") // true if event.key is \"`\" or event.code is \"Backquote\"\n */\nexport function matchesKeybinding(\n event: KeyboardEvent,\n keybinding: string | null | undefined,\n): boolean {\n const normalizedKeybinding = normalizeKeybinding(keybinding);\n\n if (!normalizedKeybinding) {\n return false;\n }\n\n const normalizedKey = normalizeKeyForKeybinding(event.key);\n const normalizedCode = normalizeKeyForKeybinding(event.code);\n\n return (\n normalizedKey === normalizedKeybinding ||\n normalizedCode === normalizedKeybinding\n );\n}\n\n/**\n * Checks if keybindings should be ignored for the current active element.\n * Returns true if the user is typing in an input field.\n *\n * @param element - Element to check\n * @returns true if keybindings should be ignored for this element\n *\n * @example\n * shouldIgnoreKeybinding(document.activeElement) // true if input/textarea/contenteditable\n */\nexport function shouldIgnoreKeybinding(element: Element | null): boolean {\n if (!element) {\n return false;\n }\n\n const tagName = element.tagName.toLowerCase();\n\n if (tagName === \"input\" || tagName === \"textarea\") {\n return true;\n }\n\n if (element instanceof HTMLElement && element.contentEditable === \"true\") {\n return true;\n }\n\n return false;\n}\n"]}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@corti/dictation-web",
3
3
  "description": "Web component for Corti Dictation",
4
4
  "author": "Corti ApS",
5
- "version": "0.6.0",
5
+ "version": "0.6.1-rc.1",
6
6
  "license": "MIT",
7
7
  "type": "module",
8
8
  "main": "dist/index.js",