@hh.ru/magritte-ui-chat-input 1.2.1 → 1.2.3

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.
Files changed (66) hide show
  1. package/index.css +117 -117
  2. package/index.d.ts +10 -10
  3. package/index.js +1 -1
  4. package/index.mock.d.ts +9 -9
  5. package/index.mock.js +37 -36
  6. package/index.mock.js.map +1 -1
  7. package/internal/ChatInputContext.d.ts +1 -1
  8. package/internal/ChatInputContext.js.map +1 -1
  9. package/internal/ChatInputSlotButton.js +13 -13
  10. package/internal/ChatInputSlotButton.js.map +1 -1
  11. package/internal/FocusTarget.js +3 -10
  12. package/internal/FocusTarget.js.map +1 -1
  13. package/internal/ModeTransition.js +41 -16
  14. package/internal/ModeTransition.js.map +1 -1
  15. package/internal/TextInput.d.ts +1 -1
  16. package/internal/TextInput.js +146 -126
  17. package/internal/TextInput.js.map +1 -1
  18. package/internal/VoiceRecord.d.ts +1 -1
  19. package/internal/VoiceRecord.js +66 -24
  20. package/internal/VoiceRecord.js.map +1 -1
  21. package/internal/VoiceRecordIndicator.js +22 -9
  22. package/internal/VoiceRecordIndicator.js.map +1 -1
  23. package/internal/flattenFragments.js +9 -13
  24. package/internal/flattenFragments.js.map +1 -1
  25. package/internal/focusGroup.js +18 -28
  26. package/internal/focusGroup.js.map +1 -1
  27. package/internal/useHasSound.js +49 -64
  28. package/internal/useHasSound.js.map +1 -1
  29. package/internal/useObjectUrl.js +15 -21
  30. package/internal/useObjectUrl.js.map +1 -1
  31. package/internal/useRovingFocusGroup.js +102 -137
  32. package/internal/useRovingFocusGroup.js.map +1 -1
  33. package/internal/useSendOnEnter.js +44 -46
  34. package/internal/useSendOnEnter.js.map +1 -1
  35. package/internal/useVoiceRecordSession.d.ts +1 -1
  36. package/internal/useVoiceRecordSession.js +185 -219
  37. package/internal/useVoiceRecordSession.js.map +1 -1
  38. package/internal/voiceSlotRegistry.d.ts +1 -1
  39. package/internal/voiceSlotRegistry.js +3 -3
  40. package/internal/voiceSlotRegistry.js.map +1 -1
  41. package/package.json +11 -11
  42. package/public/ChatInput.d.ts +6 -6
  43. package/public/ChatInput.js +129 -73
  44. package/public/ChatInput.js.map +1 -1
  45. package/public/ChatInputChips.js +4 -4
  46. package/public/ChatInputChips.js.map +1 -1
  47. package/public/ChatInputFile.d.ts +1 -1
  48. package/public/ChatInputFile.js +107 -47
  49. package/public/ChatInputFile.js.map +1 -1
  50. package/public/ChatInputFiles.js +10 -3
  51. package/public/ChatInputFiles.js.map +1 -1
  52. package/public/ChatInputPreview.js +14 -9
  53. package/public/ChatInputPreview.js.map +1 -1
  54. package/public/ChatInputSendButton.d.ts +1 -1
  55. package/public/ChatInputSendButton.js +34 -23
  56. package/public/ChatInputSendButton.js.map +1 -1
  57. package/public/ChatInputVoiceButton.d.ts +1 -1
  58. package/public/ChatInputVoiceButton.js +87 -91
  59. package/public/ChatInputVoiceButton.js.map +1 -1
  60. package/public/getChatInputFileType.d.ts +1 -1
  61. package/public/getChatInputFileType.js +7 -10
  62. package/public/getChatInputFileType.js.map +1 -1
  63. package/styles-hEUo5BXU.js +5 -0
  64. package/styles-hEUo5BXU.js.map +1 -0
  65. package/styles-B3o1JdaK.js +0 -5
  66. package/styles-B3o1JdaK.js.map +0 -1
@@ -1,244 +1,210 @@
1
1
  import './../index.css';
2
2
  import { useState, useRef, useCallback, useEffect } from 'react';
3
3
 
4
- const RECORDING_MIME_FALLBACK = 'audio/webm';
5
- const RECORDING_FILE_BASENAME = 'voice-message';
6
- // расширение файла из фактического MIME записи: браузеры выдают разные форматы (webm в
7
- // Chrome/Firefox, mp4 в Safari), а бэкенд обычно смотрит на расширение — оно должно совпадать
8
- // с содержимым, иначе mp4 уедет под именем .webm
4
+ const RECORDING_MIME_FALLBACK = "audio/webm";
5
+ const RECORDING_FILE_BASENAME = "voice-message";
9
6
  const MIME_SUBTYPE_TO_EXTENSION = {
10
- webm: 'webm',
11
- ogg: 'ogg',
12
- mp4: 'm4a',
13
- 'x-m4a': 'm4a',
14
- mpeg: 'mp3',
15
- wav: 'wav',
16
- 'x-wav': 'wav',
7
+ webm: "webm",
8
+ ogg: "ogg",
9
+ mp4: "m4a",
10
+ "x-m4a": "m4a",
11
+ mpeg: "mp3",
12
+ wav: "wav",
13
+ "x-wav": "wav"
17
14
  };
18
15
  const fileNameForMime = (mimeType) => {
19
- // «audio/webm;codecs=opus» -> «webm»
20
- const subtype = mimeType.split(';')[0].trim().split('/')[1] ?? '';
21
- const extension = MIME_SUBTYPE_TO_EXTENSION[subtype] ?? (subtype || 'webm');
22
- return `${RECORDING_FILE_BASENAME}.${extension}`;
16
+ const subtype = mimeType.split(";")[0].trim().split("/")[1] ?? "";
17
+ const extension = MIME_SUBTYPE_TO_EXTENSION[subtype] ?? (subtype || "webm");
18
+ return `${RECORDING_FILE_BASENAME}.${extension}`;
23
19
  };
24
- // первый поддерживаемый браузером тип из предпочтений; null — писать в дефолтном кодеке браузера
25
20
  const toMimeTypeList = (preferred) => {
26
- if (Array.isArray(preferred)) {
27
- return preferred;
28
- }
29
- return preferred ? [preferred] : [];
21
+ if (Array.isArray(preferred)) {
22
+ return preferred;
23
+ }
24
+ return preferred ? [preferred] : [];
30
25
  };
31
26
  const pickSupportedMimeType = (preferred) => {
32
- if (typeof MediaRecorder === 'undefined' || !MediaRecorder.isTypeSupported) {
33
- return null;
34
- }
35
- return toMimeTypeList(preferred).find((type) => MediaRecorder.isTypeSupported(type)) ?? null;
27
+ if (typeof MediaRecorder === "undefined" || !MediaRecorder.isTypeSupported) {
28
+ return null;
29
+ }
30
+ return toMimeTypeList(preferred).find((type) => MediaRecorder.isTypeSupported(type)) ?? null;
36
31
  };
37
- /**
38
- * Запрашивает доступ к микрофону и возвращает промис с готовым потоком.
39
- *
40
- * Владение потоком — на стороне вызывающего: он сам решает, когда его отпустить
41
- * (`stream.getTracks().forEach((track) => track.stop())`). VoiceRecord получает уже готовый
42
- * `stream` и только пишет, а доступ и жизненный цикл потока остаются снаружи.
43
- */
44
32
  const requestMicrophone = () => {
45
- if (typeof navigator === 'undefined' || !navigator.mediaDevices?.getUserMedia) {
46
- return Promise.reject(new Error('navigator.mediaDevices.getUserMedia is not available'));
47
- }
48
- return navigator.mediaDevices.getUserMedia({ audio: true });
33
+ if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) {
34
+ return Promise.reject(new Error("navigator.mediaDevices.getUserMedia is not available"));
35
+ }
36
+ return navigator.mediaDevices.getUserMedia({ audio: true });
49
37
  };
50
- /**
51
- * Сессия записи голоса целиком: владеет микрофоном (getUserMedia в `start`, треки отпускаются
52
- * при завершении/отмене/анмаунте), MediaRecorder'ом и таймером. `start` вызывается из обработчика
53
- * клика, поэтому запроса доступа на монтировании (и проблем со StrictMode) нет. Актуальные колбэки
54
- * держим в рефах — императивные ручки стабильны.
55
- */
56
- const useVoiceRecordSession = ({ maxDuration, mimeType, onRecordStart, onComplete, onError, onCancel, }) => {
57
- const [status, setStatus] = useState('idle');
58
- // метка старта записи (Date.now) — по ней таймер VoiceRecord считает время у себя в листе;
59
- // меняется дважды за сессию (старт/конец), а не раз в секунду, поэтому корень не ререндерится
60
- const [startedAt, setStartedAt] = useState(null);
61
- // живой стрим наружу для визуализации/распознавания; выставляется только на время записи
62
- const [stream, setStream] = useState(null);
63
- const statusRef = useRef(status);
64
- const updateStatus = useCallback((next) => {
65
- statusRef.current = next;
66
- setStatus(next);
67
- }, []);
68
- const onRecordStartRef = useRef(onRecordStart);
69
- onRecordStartRef.current = onRecordStart;
70
- const onCompleteRef = useRef(onComplete);
71
- onCompleteRef.current = onComplete;
72
- const onErrorRef = useRef(onError);
73
- onErrorRef.current = onError;
74
- const onCancelRef = useRef(onCancel);
75
- onCancelRef.current = onCancel;
76
- const maxDurationRef = useRef(maxDuration);
77
- maxDurationRef.current = maxDuration;
78
- const mimeTypeRef = useRef(mimeType);
79
- mimeTypeRef.current = mimeType;
80
- const recordingRef = useRef(null);
81
- const unmountedRef = useRef(false);
82
- const stop = useCallback(() => {
83
- const recording = recordingRef.current;
84
- if (!recording || statusRef.current !== 'recording') {
85
- return;
86
- }
87
- updateStatus('processing');
88
- setStream(null);
89
- setStartedAt(null);
90
- clearInterval(recording.intervalId);
91
- recording.emitOnStop = true;
92
- recordingRef.current = null;
93
- if (recording.recorder.state !== 'inactive') {
94
- recording.recorder.stop();
38
+ const useVoiceRecordSession = ({
39
+ maxDuration,
40
+ mimeType,
41
+ onRecordStart,
42
+ onComplete,
43
+ onError,
44
+ onCancel
45
+ }) => {
46
+ const [status, setStatus] = useState("idle");
47
+ const [startedAt, setStartedAt] = useState(null);
48
+ const [stream, setStream] = useState(null);
49
+ const statusRef = useRef(status);
50
+ const updateStatus = useCallback((next) => {
51
+ statusRef.current = next;
52
+ setStatus(next);
53
+ }, []);
54
+ const onRecordStartRef = useRef(onRecordStart);
55
+ onRecordStartRef.current = onRecordStart;
56
+ const onCompleteRef = useRef(onComplete);
57
+ onCompleteRef.current = onComplete;
58
+ const onErrorRef = useRef(onError);
59
+ onErrorRef.current = onError;
60
+ const onCancelRef = useRef(onCancel);
61
+ onCancelRef.current = onCancel;
62
+ const maxDurationRef = useRef(maxDuration);
63
+ maxDurationRef.current = maxDuration;
64
+ const mimeTypeRef = useRef(mimeType);
65
+ mimeTypeRef.current = mimeType;
66
+ const recordingRef = useRef(null);
67
+ const unmountedRef = useRef(false);
68
+ const stop = useCallback(() => {
69
+ const recording = recordingRef.current;
70
+ if (!recording || statusRef.current !== "recording") {
71
+ return;
72
+ }
73
+ updateStatus("processing");
74
+ setStream(null);
75
+ setStartedAt(null);
76
+ clearInterval(recording.intervalId);
77
+ recording.emitOnStop = true;
78
+ recordingRef.current = null;
79
+ if (recording.recorder.state !== "inactive") {
80
+ recording.recorder.stop();
81
+ }
82
+ }, [updateStatus]);
83
+ const beginRecording = useCallback(
84
+ (stream2) => {
85
+ const picked = pickSupportedMimeType(mimeTypeRef.current);
86
+ const recorder = new MediaRecorder(stream2, picked ? { mimeType: picked } : void 0);
87
+ const chunks = [];
88
+ const startTime = Date.now();
89
+ const recording = { stream: stream2, recorder, emitOnStop: false };
90
+ recorder.ondataavailable = (event) => {
91
+ if (event.data.size > 0) {
92
+ chunks.push(event.data);
95
93
  }
96
- }, [updateStatus]);
97
- const beginRecording = useCallback((stream) => {
98
- const picked = pickSupportedMimeType(mimeTypeRef.current);
99
- const recorder = new MediaRecorder(stream, picked ? { mimeType: picked } : undefined);
100
- const chunks = [];
101
- const startTime = Date.now();
102
- const recording = { stream, recorder, emitOnStop: false };
103
- recorder.ondataavailable = (event) => {
104
- if (event.data.size > 0) {
105
- chunks.push(event.data);
106
- }
107
- };
108
- recorder.onstop = () => {
109
- stream.getTracks().forEach((track) => track.stop());
110
- if (recording.emitOnStop) {
111
- const type = recorder.mimeType || RECORDING_MIME_FALLBACK;
112
- const file = new File([new Blob(chunks, { type })], fileNameForMime(type), { type });
113
- // держим processing, пока приложение обрабатывает запись; ошибки обработки —
114
- // зона ответственности приложения, мы по ним только выходим из processing
115
- Promise.resolve()
116
- .then(() => onCompleteRef.current?.(file))
117
- .catch(() => undefined)
118
- .finally(() => {
119
- if (!unmountedRef.current && statusRef.current === 'processing') {
120
- updateStatus('idle');
121
- }
122
- });
123
- return;
124
- }
125
- // не эмитим: либо это наша отмена/анмаунт (там recordingRef уже очищен до stop()),
126
- // либо запись оборвалась сама — микрофон отключили/отозвали доступ. Во втором случае
127
- // recordingRef всё ещё указывает на нас: гасим сессию и сообщаем об ошибке, иначе
128
- // статус завис бы в recording с мёртвым стримом и без выхода
129
- if (recordingRef.current === recording) {
130
- recordingRef.current = null;
131
- clearInterval(recording.intervalId);
132
- if (!unmountedRef.current) {
133
- setStream(null);
134
- setStartedAt(null);
135
- updateStatus('idle');
136
- onErrorRef.current?.(new Error('Voice recording stopped unexpectedly'));
137
- }
138
- }
139
- };
140
- // ошибка рекордера — обрываем запись; чистка и переход в idle пойдут через onstop выше
141
- recorder.onerror = () => {
142
- if (recording.recorder.state !== 'inactive') {
143
- recording.recorder.stop();
144
- }
145
- };
146
- // микрофон физически пропал (отключили гарнитуру, отозвали доступ) — аудиотрек завершается;
147
- // сами останавливаем рекордер, дальше отрабатывает ветка неожиданной остановки в onstop
148
- stream.getAudioTracks().forEach((track) => {
149
- track.addEventListener('ended', () => {
150
- if (recordingRef.current === recording && recording.recorder.state !== 'inactive') {
151
- recording.recorder.stop();
152
- }
153
- });
154
- });
155
- // таймер живёт только ради авто-финализации по maxDuration и не трогает React-стейт;
156
- // отображаемое время считает VoiceRecord у себя по startedAt
157
- const tick = () => {
158
- const max = maxDurationRef.current;
159
- if (max !== undefined && Math.floor((Date.now() - startTime) / 1000) >= max) {
160
- stop();
94
+ };
95
+ recorder.onstop = () => {
96
+ stream2.getTracks().forEach((track) => track.stop());
97
+ if (recording.emitOnStop) {
98
+ const type = recorder.mimeType || RECORDING_MIME_FALLBACK;
99
+ const file = new File([new Blob(chunks, { type })], fileNameForMime(type), { type });
100
+ Promise.resolve().then(() => onCompleteRef.current?.(file)).catch(() => void 0).finally(() => {
101
+ if (!unmountedRef.current && statusRef.current === "processing") {
102
+ updateStatus("idle");
161
103
  }
162
- };
163
- recorder.start();
164
- recording.intervalId = setInterval(tick, 1000);
165
- recordingRef.current = recording;
166
- setStartedAt(startTime);
167
- setStream(stream);
168
- updateStatus('recording');
169
- onRecordStartRef.current?.(stream);
170
- }, [stop, updateStatus]);
171
- const start = useCallback(() => {
172
- if (statusRef.current !== 'idle') {
173
- return;
104
+ });
105
+ return;
174
106
  }
175
- updateStatus('starting');
176
- requestMicrophone()
177
- .then((stream) => {
178
- // пока ждали доступ, хук могли размонтировать — тогда сразу отпускаем треки
179
- if (unmountedRef.current || statusRef.current !== 'starting') {
180
- stream.getTracks().forEach((track) => track.stop());
181
- return;
182
- }
183
- try {
184
- beginRecording(stream);
185
- }
186
- catch (error) {
187
- // не удалось создать/запустить MediaRecorder (неподдержанный кодек, отсутствие API) —
188
- // отпускаем уже полученный микрофон, иначе индикатор «микрофон занят» останется гореть
189
- stream.getTracks().forEach((track) => track.stop());
190
- if (!unmountedRef.current) {
191
- updateStatus('idle');
192
- onErrorRef.current?.(error);
193
- }
194
- }
195
- })
196
- .catch((error) => {
197
- if (unmountedRef.current || statusRef.current !== 'starting') {
198
- return;
199
- }
200
- updateStatus('idle');
201
- onErrorRef.current?.(error);
107
+ if (recordingRef.current === recording) {
108
+ recordingRef.current = null;
109
+ clearInterval(recording.intervalId);
110
+ if (!unmountedRef.current) {
111
+ setStream(null);
112
+ setStartedAt(null);
113
+ updateStatus("idle");
114
+ onErrorRef.current?.(new Error("Voice recording stopped unexpectedly"));
115
+ }
116
+ }
117
+ };
118
+ recorder.onerror = () => {
119
+ if (recording.recorder.state !== "inactive") {
120
+ recording.recorder.stop();
121
+ }
122
+ };
123
+ stream2.getAudioTracks().forEach((track) => {
124
+ track.addEventListener("ended", () => {
125
+ if (recordingRef.current === recording && recording.recorder.state !== "inactive") {
126
+ recording.recorder.stop();
127
+ }
202
128
  });
203
- }, [beginRecording, updateStatus]);
204
- const cancel = useCallback(() => {
205
- // отмена во время ожидания микрофона: записи ещё нет, но pending-старт надо оборвать —
206
- // сбрасываем статус, и requestMicrophone().then увидит !== 'starting' и отпустит треки сам
207
- if (statusRef.current === 'starting') {
208
- updateStatus('idle');
209
- onCancelRef.current?.();
210
- return;
129
+ });
130
+ const tick = () => {
131
+ const max = maxDurationRef.current;
132
+ if (max !== void 0 && Math.floor((Date.now() - startTime) / 1e3) >= max) {
133
+ stop();
211
134
  }
212
- const recording = recordingRef.current;
213
- if (!recording || statusRef.current !== 'recording') {
214
- return;
135
+ };
136
+ recorder.start();
137
+ recording.intervalId = setInterval(tick, 1e3);
138
+ recordingRef.current = recording;
139
+ setStartedAt(startTime);
140
+ setStream(stream2);
141
+ updateStatus("recording");
142
+ onRecordStartRef.current?.(stream2);
143
+ },
144
+ [stop, updateStatus]
145
+ );
146
+ const start = useCallback(() => {
147
+ if (statusRef.current !== "idle") {
148
+ return;
149
+ }
150
+ updateStatus("starting");
151
+ requestMicrophone().then((stream2) => {
152
+ if (unmountedRef.current || statusRef.current !== "starting") {
153
+ stream2.getTracks().forEach((track) => track.stop());
154
+ return;
155
+ }
156
+ try {
157
+ beginRecording(stream2);
158
+ } catch (error) {
159
+ stream2.getTracks().forEach((track) => track.stop());
160
+ if (!unmountedRef.current) {
161
+ updateStatus("idle");
162
+ onErrorRef.current?.(error);
215
163
  }
216
- setStream(null);
164
+ }
165
+ }).catch((error) => {
166
+ if (unmountedRef.current || statusRef.current !== "starting") {
167
+ return;
168
+ }
169
+ updateStatus("idle");
170
+ onErrorRef.current?.(error);
171
+ });
172
+ }, [beginRecording, updateStatus]);
173
+ const cancel = useCallback(() => {
174
+ if (statusRef.current === "starting") {
175
+ updateStatus("idle");
176
+ onCancelRef.current?.();
177
+ return;
178
+ }
179
+ const recording = recordingRef.current;
180
+ if (!recording || statusRef.current !== "recording") {
181
+ return;
182
+ }
183
+ setStream(null);
184
+ clearInterval(recording.intervalId);
185
+ recordingRef.current = null;
186
+ if (recording.recorder.state !== "inactive") {
187
+ recording.recorder.stop();
188
+ }
189
+ updateStatus("idle");
190
+ setStartedAt(null);
191
+ onCancelRef.current?.();
192
+ }, [updateStatus]);
193
+ useEffect(() => {
194
+ unmountedRef.current = false;
195
+ return () => {
196
+ unmountedRef.current = true;
197
+ const recording = recordingRef.current;
198
+ if (recording) {
217
199
  clearInterval(recording.intervalId);
218
200
  recordingRef.current = null;
219
- // emitOnStop остался false: onstop только отпустит треки
220
- if (recording.recorder.state !== 'inactive') {
221
- recording.recorder.stop();
201
+ if (recording.recorder.state !== "inactive") {
202
+ recording.recorder.stop();
222
203
  }
223
- updateStatus('idle');
224
- setStartedAt(null);
225
- onCancelRef.current?.();
226
- }, [updateStatus]);
227
- useEffect(() => {
228
- unmountedRef.current = false;
229
- return () => {
230
- unmountedRef.current = true;
231
- const recording = recordingRef.current;
232
- if (recording) {
233
- clearInterval(recording.intervalId);
234
- recordingRef.current = null;
235
- if (recording.recorder.state !== 'inactive') {
236
- recording.recorder.stop();
237
- }
238
- }
239
- };
240
- }, []);
241
- return { status, startedAt, stream, start, stop, cancel };
204
+ }
205
+ };
206
+ }, []);
207
+ return { status, startedAt, stream, start, stop, cancel };
242
208
  };
243
209
 
244
210
  export { requestMicrophone, useVoiceRecordSession };
@@ -1 +1 @@
1
- {"version":3,"file":"useVoiceRecordSession.js","sources":["src/internal/useVoiceRecordSession.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { type VoiceRecordStatus } from '@hh.ru/magritte-ui-chat-input/types';\n\ninterface UseVoiceRecordSessionParams {\n /** Максимальная длительность записи в секундах; по достижении — авто-финализация, как по кнопке\n * подтверждения. Не задана — пишем без ограничения */\n maxDuration?: number;\n /** Предпочитаемый MIME-тип записи (можно списком по убыванию приоритета). Берётся первый\n * поддерживаемый `MediaRecorder.isTypeSupported`; иначе кодек выбирает браузер */\n mimeType?: string | string[];\n /** Запись фактически началась (микрофон получен). Стримом владеет сессия — останавливать\n * его треки нельзя, только читать (анализатор, стриминговое распознавание);\n * для независимого времени жизни — `stream.clone()` */\n onRecordStart?: (stream: MediaStream) => void;\n /** Готовая запись — по кнопке подтверждения или `maxDuration`. Если вернуть промис,\n * статус `processing` держится до его завершения (реджект тоже завершает) */\n onComplete?: (file: File) => void | Promise<void>;\n /** Не удалось получить микрофон; статус остался `idle` */\n onError?: (error: unknown) => void;\n /** Запись отменена; треки отпущены, файл выброшен */\n onCancel?: VoidFunction;\n}\n\ninterface VoiceRecordSession {\n status: VoiceRecordStatus;\n /** Метка старта записи (`Date.now()`); `null` вне записи. Отображаемое время по ней считает\n * потребитель (VoiceRecord) локально, чтобы посекундный тик не ререндерил корень */\n startedAt: number | null;\n /** Живой стрим микрофона, пока идёт запись (иначе null). Для чтения (визуализация,\n * распознавание); треки принадлежат сессии — останавливать их нельзя */\n stream: MediaStream | null;\n /** Запросить микрофон и начать запись. Вне `idle` — no-op */\n start: VoidFunction;\n /** Финализировать запись: собрать файл и отдать в `onComplete` */\n stop: VoidFunction;\n /** Отменить запись, ничего не эмитя */\n cancel: VoidFunction;\n}\n\ninterface ActiveRecording {\n stream: MediaStream;\n recorder: MediaRecorder;\n intervalId?: ReturnType<typeof setInterval>;\n /** Эмитить ли файл в onstop: true только при финализации (stop/maxDuration),\n * при отмене и анмаунте остаётся false */\n emitOnStop: boolean;\n}\n\nconst RECORDING_MIME_FALLBACK = 'audio/webm';\nconst RECORDING_FILE_BASENAME = 'voice-message';\n\n// расширение файла из фактического MIME записи: браузеры выдают разные форматы (webm в\n// Chrome/Firefox, mp4 в Safari), а бэкенд обычно смотрит на расширение — оно должно совпадать\n// с содержимым, иначе mp4 уедет под именем .webm\nconst MIME_SUBTYPE_TO_EXTENSION: Record<string, string> = {\n webm: 'webm',\n ogg: 'ogg',\n mp4: 'm4a',\n 'x-m4a': 'm4a',\n mpeg: 'mp3',\n wav: 'wav',\n 'x-wav': 'wav',\n};\n\nconst fileNameForMime = (mimeType: string): string => {\n // «audio/webm;codecs=opus» -> «webm»\n const subtype = mimeType.split(';')[0].trim().split('/')[1] ?? '';\n const extension = MIME_SUBTYPE_TO_EXTENSION[subtype] ?? (subtype || 'webm');\n return `${RECORDING_FILE_BASENAME}.${extension}`;\n};\n\n// первый поддерживаемый браузером тип из предпочтений; null — писать в дефолтном кодеке браузера\nconst toMimeTypeList = (preferred: string | string[] | undefined): string[] => {\n if (Array.isArray(preferred)) {\n return preferred;\n }\n return preferred ? [preferred] : [];\n};\n\nconst pickSupportedMimeType = (preferred: string | string[] | undefined): string | null => {\n if (typeof MediaRecorder === 'undefined' || !MediaRecorder.isTypeSupported) {\n return null;\n }\n return toMimeTypeList(preferred).find((type) => MediaRecorder.isTypeSupported(type)) ?? null;\n};\n\n/**\n * Запрашивает доступ к микрофону и возвращает промис с готовым потоком.\n *\n * Владение потоком — на стороне вызывающего: он сам решает, когда его отпустить\n * (`stream.getTracks().forEach((track) => track.stop())`). VoiceRecord получает уже готовый\n * `stream` и только пишет, а доступ и жизненный цикл потока остаются снаружи.\n */\nexport const requestMicrophone = (): Promise<MediaStream> => {\n if (typeof navigator === 'undefined' || !navigator.mediaDevices?.getUserMedia) {\n return Promise.reject(new Error('navigator.mediaDevices.getUserMedia is not available'));\n }\n\n return navigator.mediaDevices.getUserMedia({ audio: true });\n};\n\n/**\n * Сессия записи голоса целиком: владеет микрофоном (getUserMedia в `start`, треки отпускаются\n * при завершении/отмене/анмаунте), MediaRecorder'ом и таймером. `start` вызывается из обработчика\n * клика, поэтому запроса доступа на монтировании (и проблем со StrictMode) нет. Актуальные колбэки\n * держим в рефах — императивные ручки стабильны.\n */\nexport const useVoiceRecordSession = ({\n maxDuration,\n mimeType,\n onRecordStart,\n onComplete,\n onError,\n onCancel,\n}: UseVoiceRecordSessionParams): VoiceRecordSession => {\n const [status, setStatus] = useState<VoiceRecordStatus>('idle');\n // метка старта записи (Date.now) — по ней таймер VoiceRecord считает время у себя в листе;\n // меняется дважды за сессию (старт/конец), а не раз в секунду, поэтому корень не ререндерится\n const [startedAt, setStartedAt] = useState<number | null>(null);\n // живой стрим наружу для визуализации/распознавания; выставляется только на время записи\n const [stream, setStream] = useState<MediaStream | null>(null);\n\n const statusRef = useRef(status);\n const updateStatus = useCallback((next: VoiceRecordStatus) => {\n statusRef.current = next;\n setStatus(next);\n }, []);\n\n const onRecordStartRef = useRef(onRecordStart);\n onRecordStartRef.current = onRecordStart;\n const onCompleteRef = useRef(onComplete);\n onCompleteRef.current = onComplete;\n const onErrorRef = useRef(onError);\n onErrorRef.current = onError;\n const onCancelRef = useRef(onCancel);\n onCancelRef.current = onCancel;\n const maxDurationRef = useRef(maxDuration);\n maxDurationRef.current = maxDuration;\n const mimeTypeRef = useRef(mimeType);\n mimeTypeRef.current = mimeType;\n\n const recordingRef = useRef<ActiveRecording | null>(null);\n const unmountedRef = useRef(false);\n\n const stop = useCallback(() => {\n const recording = recordingRef.current;\n if (!recording || statusRef.current !== 'recording') {\n return;\n }\n updateStatus('processing');\n setStream(null);\n setStartedAt(null);\n clearInterval(recording.intervalId);\n recording.emitOnStop = true;\n recordingRef.current = null;\n if (recording.recorder.state !== 'inactive') {\n recording.recorder.stop();\n }\n }, [updateStatus]);\n\n const beginRecording = useCallback(\n (stream: MediaStream) => {\n const picked = pickSupportedMimeType(mimeTypeRef.current);\n const recorder = new MediaRecorder(stream, picked ? { mimeType: picked } : undefined);\n const chunks: Blob[] = [];\n const startTime = Date.now();\n const recording: ActiveRecording = { stream, recorder, emitOnStop: false };\n\n recorder.ondataavailable = (event) => {\n if (event.data.size > 0) {\n chunks.push(event.data);\n }\n };\n recorder.onstop = () => {\n stream.getTracks().forEach((track) => track.stop());\n if (recording.emitOnStop) {\n const type = recorder.mimeType || RECORDING_MIME_FALLBACK;\n const file = new File([new Blob(chunks, { type })], fileNameForMime(type), { type });\n // держим processing, пока приложение обрабатывает запись; ошибки обработки —\n // зона ответственности приложения, мы по ним только выходим из processing\n Promise.resolve()\n .then(() => onCompleteRef.current?.(file))\n .catch(() => undefined)\n .finally(() => {\n if (!unmountedRef.current && statusRef.current === 'processing') {\n updateStatus('idle');\n }\n });\n return;\n }\n // не эмитим: либо это наша отмена/анмаунт (там recordingRef уже очищен до stop()),\n // либо запись оборвалась сама — микрофон отключили/отозвали доступ. Во втором случае\n // recordingRef всё ещё указывает на нас: гасим сессию и сообщаем об ошибке, иначе\n // статус завис бы в recording с мёртвым стримом и без выхода\n if (recordingRef.current === recording) {\n recordingRef.current = null;\n clearInterval(recording.intervalId);\n if (!unmountedRef.current) {\n setStream(null);\n setStartedAt(null);\n updateStatus('idle');\n onErrorRef.current?.(new Error('Voice recording stopped unexpectedly'));\n }\n }\n };\n\n // ошибка рекордера — обрываем запись; чистка и переход в idle пойдут через onstop выше\n recorder.onerror = () => {\n if (recording.recorder.state !== 'inactive') {\n recording.recorder.stop();\n }\n };\n\n // микрофон физически пропал (отключили гарнитуру, отозвали доступ) — аудиотрек завершается;\n // сами останавливаем рекордер, дальше отрабатывает ветка неожиданной остановки в onstop\n stream.getAudioTracks().forEach((track) => {\n track.addEventListener('ended', () => {\n if (recordingRef.current === recording && recording.recorder.state !== 'inactive') {\n recording.recorder.stop();\n }\n });\n });\n\n // таймер живёт только ради авто-финализации по maxDuration и не трогает React-стейт;\n // отображаемое время считает VoiceRecord у себя по startedAt\n const tick = () => {\n const max = maxDurationRef.current;\n if (max !== undefined && Math.floor((Date.now() - startTime) / 1000) >= max) {\n stop();\n }\n };\n\n recorder.start();\n recording.intervalId = setInterval(tick, 1000);\n recordingRef.current = recording;\n setStartedAt(startTime);\n setStream(stream);\n updateStatus('recording');\n onRecordStartRef.current?.(stream);\n },\n [stop, updateStatus]\n );\n\n const start = useCallback(() => {\n if (statusRef.current !== 'idle') {\n return;\n }\n updateStatus('starting');\n requestMicrophone()\n .then((stream) => {\n // пока ждали доступ, хук могли размонтировать — тогда сразу отпускаем треки\n if (unmountedRef.current || statusRef.current !== 'starting') {\n stream.getTracks().forEach((track) => track.stop());\n return;\n }\n try {\n beginRecording(stream);\n } catch (error) {\n // не удалось создать/запустить MediaRecorder (неподдержанный кодек, отсутствие API) —\n // отпускаем уже полученный микрофон, иначе индикатор «микрофон занят» останется гореть\n stream.getTracks().forEach((track) => track.stop());\n if (!unmountedRef.current) {\n updateStatus('idle');\n onErrorRef.current?.(error);\n }\n }\n })\n .catch((error: unknown) => {\n if (unmountedRef.current || statusRef.current !== 'starting') {\n return;\n }\n updateStatus('idle');\n onErrorRef.current?.(error);\n });\n }, [beginRecording, updateStatus]);\n\n const cancel = useCallback(() => {\n // отмена во время ожидания микрофона: записи ещё нет, но pending-старт надо оборвать —\n // сбрасываем статус, и requestMicrophone().then увидит !== 'starting' и отпустит треки сам\n if (statusRef.current === 'starting') {\n updateStatus('idle');\n onCancelRef.current?.();\n return;\n }\n\n const recording = recordingRef.current;\n if (!recording || statusRef.current !== 'recording') {\n return;\n }\n setStream(null);\n clearInterval(recording.intervalId);\n recordingRef.current = null;\n // emitOnStop остался false: onstop только отпустит треки\n if (recording.recorder.state !== 'inactive') {\n recording.recorder.stop();\n }\n updateStatus('idle');\n setStartedAt(null);\n onCancelRef.current?.();\n }, [updateStatus]);\n\n useEffect(() => {\n unmountedRef.current = false;\n return () => {\n unmountedRef.current = true;\n const recording = recordingRef.current;\n if (recording) {\n clearInterval(recording.intervalId);\n recordingRef.current = null;\n if (recording.recorder.state !== 'inactive') {\n recording.recorder.stop();\n }\n }\n };\n }, []);\n\n return { status, startedAt, stream, start, stop, cancel };\n};\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useVoiceRecordSession.js","sources":["src/internal/useVoiceRecordSession.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { type VoiceRecordStatus } from '@hh.ru/magritte-ui-chat-input/types';\n\ninterface UseVoiceRecordSessionParams {\n /** Максимальная длительность записи в секундах; по достижении — авто-финализация, как по кнопке\n * подтверждения. Не задана — пишем без ограничения */\n maxDuration?: number;\n /** Предпочитаемый MIME-тип записи (можно списком по убыванию приоритета). Берётся первый\n * поддерживаемый `MediaRecorder.isTypeSupported`; иначе кодек выбирает браузер */\n mimeType?: string | string[];\n /** Запись фактически началась (микрофон получен). Стримом владеет сессия — останавливать\n * его треки нельзя, только читать (анализатор, стриминговое распознавание);\n * для независимого времени жизни — `stream.clone()` */\n onRecordStart?: (stream: MediaStream) => void;\n /** Готовая запись — по кнопке подтверждения или `maxDuration`. Если вернуть промис,\n * статус `processing` держится до его завершения (реджект тоже завершает) */\n onComplete?: (file: File) => void | Promise<void>;\n /** Не удалось получить микрофон; статус остался `idle` */\n onError?: (error: unknown) => void;\n /** Запись отменена; треки отпущены, файл выброшен */\n onCancel?: VoidFunction;\n}\n\ninterface VoiceRecordSession {\n status: VoiceRecordStatus;\n /** Метка старта записи (`Date.now()`); `null` вне записи. Отображаемое время по ней считает\n * потребитель (VoiceRecord) локально, чтобы посекундный тик не ререндерил корень */\n startedAt: number | null;\n /** Живой стрим микрофона, пока идёт запись (иначе null). Для чтения (визуализация,\n * распознавание); треки принадлежат сессии — останавливать их нельзя */\n stream: MediaStream | null;\n /** Запросить микрофон и начать запись. Вне `idle` — no-op */\n start: VoidFunction;\n /** Финализировать запись: собрать файл и отдать в `onComplete` */\n stop: VoidFunction;\n /** Отменить запись, ничего не эмитя */\n cancel: VoidFunction;\n}\n\ninterface ActiveRecording {\n stream: MediaStream;\n recorder: MediaRecorder;\n intervalId?: ReturnType<typeof setInterval>;\n /** Эмитить ли файл в onstop: true только при финализации (stop/maxDuration),\n * при отмене и анмаунте остаётся false */\n emitOnStop: boolean;\n}\n\nconst RECORDING_MIME_FALLBACK = 'audio/webm';\nconst RECORDING_FILE_BASENAME = 'voice-message';\n\n// расширение файла из фактического MIME записи: браузеры выдают разные форматы (webm в\n// Chrome/Firefox, mp4 в Safari), а бэкенд обычно смотрит на расширение — оно должно совпадать\n// с содержимым, иначе mp4 уедет под именем .webm\nconst MIME_SUBTYPE_TO_EXTENSION: Record<string, string> = {\n webm: 'webm',\n ogg: 'ogg',\n mp4: 'm4a',\n 'x-m4a': 'm4a',\n mpeg: 'mp3',\n wav: 'wav',\n 'x-wav': 'wav',\n};\n\nconst fileNameForMime = (mimeType: string): string => {\n // «audio/webm;codecs=opus» -> «webm»\n const subtype = mimeType.split(';')[0].trim().split('/')[1] ?? '';\n const extension = MIME_SUBTYPE_TO_EXTENSION[subtype] ?? (subtype || 'webm');\n return `${RECORDING_FILE_BASENAME}.${extension}`;\n};\n\n// первый поддерживаемый браузером тип из предпочтений; null — писать в дефолтном кодеке браузера\nconst toMimeTypeList = (preferred: string | string[] | undefined): string[] => {\n if (Array.isArray(preferred)) {\n return preferred;\n }\n return preferred ? [preferred] : [];\n};\n\nconst pickSupportedMimeType = (preferred: string | string[] | undefined): string | null => {\n if (typeof MediaRecorder === 'undefined' || !MediaRecorder.isTypeSupported) {\n return null;\n }\n return toMimeTypeList(preferred).find((type) => MediaRecorder.isTypeSupported(type)) ?? null;\n};\n\n/**\n * Запрашивает доступ к микрофону и возвращает промис с готовым потоком.\n *\n * Владение потоком — на стороне вызывающего: он сам решает, когда его отпустить\n * (`stream.getTracks().forEach((track) => track.stop())`). VoiceRecord получает уже готовый\n * `stream` и только пишет, а доступ и жизненный цикл потока остаются снаружи.\n */\nexport const requestMicrophone = (): Promise<MediaStream> => {\n if (typeof navigator === 'undefined' || !navigator.mediaDevices?.getUserMedia) {\n return Promise.reject(new Error('navigator.mediaDevices.getUserMedia is not available'));\n }\n\n return navigator.mediaDevices.getUserMedia({ audio: true });\n};\n\n/**\n * Сессия записи голоса целиком: владеет микрофоном (getUserMedia в `start`, треки отпускаются\n * при завершении/отмене/анмаунте), MediaRecorder'ом и таймером. `start` вызывается из обработчика\n * клика, поэтому запроса доступа на монтировании (и проблем со StrictMode) нет. Актуальные колбэки\n * держим в рефах — императивные ручки стабильны.\n */\nexport const useVoiceRecordSession = ({\n maxDuration,\n mimeType,\n onRecordStart,\n onComplete,\n onError,\n onCancel,\n}: UseVoiceRecordSessionParams): VoiceRecordSession => {\n const [status, setStatus] = useState<VoiceRecordStatus>('idle');\n // метка старта записи (Date.now) — по ней таймер VoiceRecord считает время у себя в листе;\n // меняется дважды за сессию (старт/конец), а не раз в секунду, поэтому корень не ререндерится\n const [startedAt, setStartedAt] = useState<number | null>(null);\n // живой стрим наружу для визуализации/распознавания; выставляется только на время записи\n const [stream, setStream] = useState<MediaStream | null>(null);\n\n const statusRef = useRef(status);\n const updateStatus = useCallback((next: VoiceRecordStatus) => {\n statusRef.current = next;\n setStatus(next);\n }, []);\n\n const onRecordStartRef = useRef(onRecordStart);\n onRecordStartRef.current = onRecordStart;\n const onCompleteRef = useRef(onComplete);\n onCompleteRef.current = onComplete;\n const onErrorRef = useRef(onError);\n onErrorRef.current = onError;\n const onCancelRef = useRef(onCancel);\n onCancelRef.current = onCancel;\n const maxDurationRef = useRef(maxDuration);\n maxDurationRef.current = maxDuration;\n const mimeTypeRef = useRef(mimeType);\n mimeTypeRef.current = mimeType;\n\n const recordingRef = useRef<ActiveRecording | null>(null);\n const unmountedRef = useRef(false);\n\n const stop = useCallback(() => {\n const recording = recordingRef.current;\n if (!recording || statusRef.current !== 'recording') {\n return;\n }\n updateStatus('processing');\n setStream(null);\n setStartedAt(null);\n clearInterval(recording.intervalId);\n recording.emitOnStop = true;\n recordingRef.current = null;\n if (recording.recorder.state !== 'inactive') {\n recording.recorder.stop();\n }\n }, [updateStatus]);\n\n const beginRecording = useCallback(\n (stream: MediaStream) => {\n const picked = pickSupportedMimeType(mimeTypeRef.current);\n const recorder = new MediaRecorder(stream, picked ? { mimeType: picked } : undefined);\n const chunks: Blob[] = [];\n const startTime = Date.now();\n const recording: ActiveRecording = { stream, recorder, emitOnStop: false };\n\n recorder.ondataavailable = (event) => {\n if (event.data.size > 0) {\n chunks.push(event.data);\n }\n };\n recorder.onstop = () => {\n stream.getTracks().forEach((track) => track.stop());\n if (recording.emitOnStop) {\n const type = recorder.mimeType || RECORDING_MIME_FALLBACK;\n const file = new File([new Blob(chunks, { type })], fileNameForMime(type), { type });\n // держим processing, пока приложение обрабатывает запись; ошибки обработки —\n // зона ответственности приложения, мы по ним только выходим из processing\n Promise.resolve()\n .then(() => onCompleteRef.current?.(file))\n .catch(() => undefined)\n .finally(() => {\n if (!unmountedRef.current && statusRef.current === 'processing') {\n updateStatus('idle');\n }\n });\n return;\n }\n // не эмитим: либо это наша отмена/анмаунт (там recordingRef уже очищен до stop()),\n // либо запись оборвалась сама — микрофон отключили/отозвали доступ. Во втором случае\n // recordingRef всё ещё указывает на нас: гасим сессию и сообщаем об ошибке, иначе\n // статус завис бы в recording с мёртвым стримом и без выхода\n if (recordingRef.current === recording) {\n recordingRef.current = null;\n clearInterval(recording.intervalId);\n if (!unmountedRef.current) {\n setStream(null);\n setStartedAt(null);\n updateStatus('idle');\n onErrorRef.current?.(new Error('Voice recording stopped unexpectedly'));\n }\n }\n };\n\n // ошибка рекордера — обрываем запись; чистка и переход в idle пойдут через onstop выше\n recorder.onerror = () => {\n if (recording.recorder.state !== 'inactive') {\n recording.recorder.stop();\n }\n };\n\n // микрофон физически пропал (отключили гарнитуру, отозвали доступ) — аудиотрек завершается;\n // сами останавливаем рекордер, дальше отрабатывает ветка неожиданной остановки в onstop\n stream.getAudioTracks().forEach((track) => {\n track.addEventListener('ended', () => {\n if (recordingRef.current === recording && recording.recorder.state !== 'inactive') {\n recording.recorder.stop();\n }\n });\n });\n\n // таймер живёт только ради авто-финализации по maxDuration и не трогает React-стейт;\n // отображаемое время считает VoiceRecord у себя по startedAt\n const tick = () => {\n const max = maxDurationRef.current;\n if (max !== undefined && Math.floor((Date.now() - startTime) / 1000) >= max) {\n stop();\n }\n };\n\n recorder.start();\n recording.intervalId = setInterval(tick, 1000);\n recordingRef.current = recording;\n setStartedAt(startTime);\n setStream(stream);\n updateStatus('recording');\n onRecordStartRef.current?.(stream);\n },\n [stop, updateStatus]\n );\n\n const start = useCallback(() => {\n if (statusRef.current !== 'idle') {\n return;\n }\n updateStatus('starting');\n requestMicrophone()\n .then((stream) => {\n // пока ждали доступ, хук могли размонтировать — тогда сразу отпускаем треки\n if (unmountedRef.current || statusRef.current !== 'starting') {\n stream.getTracks().forEach((track) => track.stop());\n return;\n }\n try {\n beginRecording(stream);\n } catch (error) {\n // не удалось создать/запустить MediaRecorder (неподдержанный кодек, отсутствие API) —\n // отпускаем уже полученный микрофон, иначе индикатор «микрофон занят» останется гореть\n stream.getTracks().forEach((track) => track.stop());\n if (!unmountedRef.current) {\n updateStatus('idle');\n onErrorRef.current?.(error);\n }\n }\n })\n .catch((error: unknown) => {\n if (unmountedRef.current || statusRef.current !== 'starting') {\n return;\n }\n updateStatus('idle');\n onErrorRef.current?.(error);\n });\n }, [beginRecording, updateStatus]);\n\n const cancel = useCallback(() => {\n // отмена во время ожидания микрофона: записи ещё нет, но pending-старт надо оборвать —\n // сбрасываем статус, и requestMicrophone().then увидит !== 'starting' и отпустит треки сам\n if (statusRef.current === 'starting') {\n updateStatus('idle');\n onCancelRef.current?.();\n return;\n }\n\n const recording = recordingRef.current;\n if (!recording || statusRef.current !== 'recording') {\n return;\n }\n setStream(null);\n clearInterval(recording.intervalId);\n recordingRef.current = null;\n // emitOnStop остался false: onstop только отпустит треки\n if (recording.recorder.state !== 'inactive') {\n recording.recorder.stop();\n }\n updateStatus('idle');\n setStartedAt(null);\n onCancelRef.current?.();\n }, [updateStatus]);\n\n useEffect(() => {\n unmountedRef.current = false;\n return () => {\n unmountedRef.current = true;\n const recording = recordingRef.current;\n if (recording) {\n clearInterval(recording.intervalId);\n recordingRef.current = null;\n if (recording.recorder.state !== 'inactive') {\n recording.recorder.stop();\n }\n }\n };\n }, []);\n\n return { status, startedAt, stream, start, stop, cancel };\n};\n"],"names":["stream"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { type ChatInputVoiceRecordConfig } from '@hh.ru/magritte-ui-chat-input/types';
1
+ import { type ChatInputVoiceRecordConfig } from '../types';
2
2
  export declare const ChatInputVoiceSlotProvider: import("react").FC<{
3
3
  children?: import("react").ReactNode | undefined;
4
4
  }>;
@@ -1,10 +1,10 @@
1
1
  import './../index.css';
2
2
  import { createSlotRegistry } from '@hh.ru/magritte-internal-slot-registry';
3
3
 
4
- const { SlotRegistryProvider, useRegisterSlot, useSlotRegistryValue } = createSlotRegistry('ChatInputVoice');
4
+ const { SlotRegistryProvider, useRegisterSlot, useSlotRegistryValue } = createSlotRegistry("ChatInputVoice");
5
5
  const ChatInputVoiceSlotProvider = SlotRegistryProvider;
6
- const useRegisterVoiceConfig = (config) => useRegisterSlot('voiceConfig', config);
7
- const useVoiceConfig = () => useSlotRegistryValue('voiceConfig');
6
+ const useRegisterVoiceConfig = (config) => useRegisterSlot("voiceConfig", config);
7
+ const useVoiceConfig = () => useSlotRegistryValue("voiceConfig");
8
8
 
9
9
  export { ChatInputVoiceSlotProvider, useRegisterVoiceConfig, useVoiceConfig };
10
10
  //# sourceMappingURL=voiceSlotRegistry.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"voiceSlotRegistry.js","sources":["src/internal/voiceSlotRegistry.ts"],"sourcesContent":["import { createSlotRegistry } from '@hh.ru/magritte-internal-slot-registry';\nimport { type ChatInputVoiceRecordConfig } from '@hh.ru/magritte-ui-chat-input/types';\n\ninterface ChatInputVoiceSlots {\n voiceConfig: ChatInputVoiceRecordConfig;\n}\n\nconst { SlotRegistryProvider, useRegisterSlot, useSlotRegistryValue } =\n createSlotRegistry<ChatInputVoiceSlots>('ChatInputVoice');\n\nexport const ChatInputVoiceSlotProvider = SlotRegistryProvider;\nexport const useRegisterVoiceConfig = (config: ChatInputVoiceRecordConfig | null | undefined): void =>\n useRegisterSlot('voiceConfig', config);\nexport const useVoiceConfig = (): ChatInputVoiceRecordConfig | undefined => useSlotRegistryValue('voiceConfig');\n"],"names":[],"mappings":";;AAOA,MAAM,EAAE,oBAAoB,EAAE,eAAe,EAAE,oBAAoB,EAAE,GACjE,kBAAkB,CAAsB,gBAAgB,CAAC,CAAC;AAEvD,MAAM,0BAA0B,GAAG,qBAAqB;AACxD,MAAM,sBAAsB,GAAG,CAAC,MAAqD,KACxF,eAAe,CAAC,aAAa,EAAE,MAAM,EAAE;AAC9B,MAAA,cAAc,GAAG,MAA8C,oBAAoB,CAAC,aAAa;;;;"}
1
+ {"version":3,"file":"voiceSlotRegistry.js","sources":["src/internal/voiceSlotRegistry.ts"],"sourcesContent":["import { createSlotRegistry } from '@hh.ru/magritte-internal-slot-registry';\nimport { type ChatInputVoiceRecordConfig } from '@hh.ru/magritte-ui-chat-input/types';\n\ninterface ChatInputVoiceSlots {\n voiceConfig: ChatInputVoiceRecordConfig;\n}\n\nconst { SlotRegistryProvider, useRegisterSlot, useSlotRegistryValue } =\n createSlotRegistry<ChatInputVoiceSlots>('ChatInputVoice');\n\nexport const ChatInputVoiceSlotProvider = SlotRegistryProvider;\nexport const useRegisterVoiceConfig = (config: ChatInputVoiceRecordConfig | null | undefined): void =>\n useRegisterSlot('voiceConfig', config);\nexport const useVoiceConfig = (): ChatInputVoiceRecordConfig | undefined => useSlotRegistryValue('voiceConfig');\n"],"names":[],"mappings":";;AAOA,MAAM,EAAE,oBAAsB,EAAA,eAAA,EAAiB,oBAAqB,EAAA,GAChE,mBAAwC,gBAAgB,CAAA,CAAA;AAErD,MAAM,0BAA6B,GAAA,qBAAA;AACnC,MAAM,sBAAyB,GAAA,CAAC,MACnC,KAAA,eAAA,CAAgB,eAAe,MAAM,EAAA;AAC5B,MAAA,cAAA,GAAiB,MAA8C,oBAAA,CAAqB,aAAa;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hh.ru/magritte-ui-chat-input",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "main": "index.js",
5
5
  "types": "index.d.ts",
6
6
  "sideEffects": [
@@ -31,16 +31,16 @@
31
31
  "@hh.ru/magritte-common-use-disabled": "1.0.16",
32
32
  "@hh.ru/magritte-common-use-multiple-refs": "1.1.12",
33
33
  "@hh.ru/magritte-common-use-translation": "3.6.1",
34
- "@hh.ru/magritte-design-tokens": "25.1.1",
34
+ "@hh.ru/magritte-design-tokens": "26.0.1",
35
35
  "@hh.ru/magritte-internal-slot-registry": "1.1.1",
36
- "@hh.ru/magritte-ui-cell": "7.3.1",
37
- "@hh.ru/magritte-ui-chips": "7.0.45",
38
- "@hh.ru/magritte-ui-icon": "15.1.3",
39
- "@hh.ru/magritte-ui-link": "7.2.4",
40
- "@hh.ru/magritte-ui-loader": "4.1.4",
41
- "@hh.ru/magritte-ui-scrollable-container": "6.5.1",
42
- "@hh.ru/magritte-ui-scrollbar": "1.0.37",
43
- "@hh.ru/magritte-ui-typography": "5.3.14"
36
+ "@hh.ru/magritte-ui-cell": "7.3.3",
37
+ "@hh.ru/magritte-ui-chips": "7.0.47",
38
+ "@hh.ru/magritte-ui-icon": "15.1.5",
39
+ "@hh.ru/magritte-ui-link": "7.2.6",
40
+ "@hh.ru/magritte-ui-loader": "4.1.6",
41
+ "@hh.ru/magritte-ui-scrollable-container": "6.5.3",
42
+ "@hh.ru/magritte-ui-scrollbar": "1.0.39",
43
+ "@hh.ru/magritte-ui-typography": "5.3.16"
44
44
  },
45
- "gitHead": "46d606980abbb523a41afba452c0a852ee902864"
45
+ "gitHead": "f6539d7a0e7cd42dd851458db1de04a748bbee03"
46
46
  }