@hh.ru/magritte-ui-chat-input 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.css +286 -0
- package/index.d.ts +7 -0
- package/index.js +38 -0
- package/index.js.map +1 -0
- package/index.mock.d.ts +7 -0
- package/index.mock.js +47 -0
- package/index.mock.js.map +1 -0
- package/internal/ChatInputContext.d.ts +13 -0
- package/internal/ChatInputContext.js +9 -0
- package/internal/ChatInputContext.js.map +1 -0
- package/internal/ChatInputSlotButton.d.ts +16 -0
- package/internal/ChatInputSlotButton.js +25 -0
- package/internal/ChatInputSlotButton.js.map +1 -0
- package/internal/FocusTarget.d.ts +14 -0
- package/internal/FocusTarget.js +17 -0
- package/internal/FocusTarget.js.map +1 -0
- package/internal/ModeTransition.d.ts +12 -0
- package/internal/ModeTransition.js +29 -0
- package/internal/ModeTransition.js.map +1 -0
- package/internal/TextInput.d.ts +18 -0
- package/internal/TextInput.js +149 -0
- package/internal/TextInput.js.map +1 -0
- package/internal/VoiceRecord.d.ts +24 -0
- package/internal/VoiceRecord.js +45 -0
- package/internal/VoiceRecord.js.map +1 -0
- package/internal/VoiceRecordIndicator.d.ts +4 -0
- package/internal/VoiceRecordIndicator.js +22 -0
- package/internal/VoiceRecordIndicator.js.map +1 -0
- package/internal/focusGroup.d.ts +22 -0
- package/internal/focusGroup.js +36 -0
- package/internal/focusGroup.js.map +1 -0
- package/internal/useHasSound.d.ts +7 -0
- package/internal/useHasSound.js +77 -0
- package/internal/useHasSound.js.map +1 -0
- package/internal/useRovingFocusGroup.d.ts +19 -0
- package/internal/useRovingFocusGroup.js +151 -0
- package/internal/useRovingFocusGroup.js.map +1 -0
- package/internal/useSendOnEnter.d.ts +13 -0
- package/internal/useSendOnEnter.js +42 -0
- package/internal/useSendOnEnter.js.map +1 -0
- package/internal/useVoiceRecordSession.d.ts +51 -0
- package/internal/useVoiceRecordSession.js +245 -0
- package/internal/useVoiceRecordSession.js.map +1 -0
- package/internal/voiceSlotRegistry.d.ts +6 -0
- package/internal/voiceSlotRegistry.js +10 -0
- package/internal/voiceSlotRegistry.js.map +1 -0
- package/package.json +44 -0
- package/public/ChatInput.d.ts +47 -0
- package/public/ChatInput.js +122 -0
- package/public/ChatInput.js.map +1 -0
- package/public/ChatInputChips.d.ts +6 -0
- package/public/ChatInputChips.js +16 -0
- package/public/ChatInputChips.js.map +1 -0
- package/public/ChatInputPreview.d.ts +12 -0
- package/public/ChatInputPreview.js +14 -0
- package/public/ChatInputPreview.js.map +1 -0
- package/public/ChatInputSendButton.d.ts +14 -0
- package/public/ChatInputSendButton.js +38 -0
- package/public/ChatInputSendButton.js.map +1 -0
- package/public/ChatInputVoiceButton.d.ts +17 -0
- package/public/ChatInputVoiceButton.js +91 -0
- package/public/ChatInputVoiceButton.js.map +1 -0
- package/styles-CQQdqOFh.js +5 -0
- package/styles-CQQdqOFh.js.map +1 -0
- package/types.d.ts +46 -0
- package/types.js +3 -0
- package/types.js.map +1 -0
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hh.ru/magritte-ui-chat-input",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"types": "index.d.ts",
|
|
6
|
+
"sideEffects": [
|
|
7
|
+
"index.css"
|
|
8
|
+
],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "yarn root:build $(pwd)",
|
|
11
|
+
"build-test-branch": "yarn root:build-test-branch $(pwd)",
|
|
12
|
+
"prepack": "yarn root:prepack $(pwd)",
|
|
13
|
+
"postpublish": "yarn root:postpublish $(pwd)",
|
|
14
|
+
"stylelint-check": "yarn root:stylelint-check $(pwd)",
|
|
15
|
+
"eslint-check": "yarn root:eslint-check $(pwd)",
|
|
16
|
+
"ts-config": "yarn root:ts-config $(pwd)",
|
|
17
|
+
"ts-check": "yarn root:ts-check $(pwd)",
|
|
18
|
+
"test": "yarn root:test $(pwd)",
|
|
19
|
+
"watch": "yarn root:watch $(pwd)"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"classnames": ">=2.3.2",
|
|
23
|
+
"motion": "^12.23.12",
|
|
24
|
+
"react": ">=18.2.0"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@hh.ru/magritte-common-keyboard": "4.3.4",
|
|
31
|
+
"@hh.ru/magritte-common-use-disabled": "1.0.15",
|
|
32
|
+
"@hh.ru/magritte-common-use-multiple-refs": "1.1.11",
|
|
33
|
+
"@hh.ru/magritte-common-use-translation": "3.2.0",
|
|
34
|
+
"@hh.ru/magritte-design-tokens": "24.8.0",
|
|
35
|
+
"@hh.ru/magritte-internal-slot-registry": "1.1.0",
|
|
36
|
+
"@hh.ru/magritte-ui-chips": "7.0.25",
|
|
37
|
+
"@hh.ru/magritte-ui-icon": "14.2.15",
|
|
38
|
+
"@hh.ru/magritte-ui-loader": "4.0.3",
|
|
39
|
+
"@hh.ru/magritte-ui-scrollable-container": "6.3.0",
|
|
40
|
+
"@hh.ru/magritte-ui-scrollbar": "1.0.30",
|
|
41
|
+
"@hh.ru/magritte-ui-typography": "5.3.8"
|
|
42
|
+
},
|
|
43
|
+
"gitHead": "24217e8377aee28baaafb5f7492141f1674776df"
|
|
44
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type ChangeEventHandler, type ForwardedRef, type ForwardRefExoticComponent, type ReactNode, type RefAttributes, type TextareaHTMLAttributes } from 'react';
|
|
2
|
+
import { ChatInputChips } from '@hh.ru/magritte-ui-chat-input/public/ChatInputChips';
|
|
3
|
+
import { ChatInputPreview } from '@hh.ru/magritte-ui-chat-input/public/ChatInputPreview';
|
|
4
|
+
import { ChatInputSendButton } from '@hh.ru/magritte-ui-chat-input/public/ChatInputSendButton';
|
|
5
|
+
import { ChatInputVoiceButton } from '@hh.ru/magritte-ui-chat-input/public/ChatInputVoiceButton';
|
|
6
|
+
interface ChatInputOwnProps {
|
|
7
|
+
/** Левый слот */
|
|
8
|
+
leftActions?: ReactNode;
|
|
9
|
+
/** Правый слот */
|
|
10
|
+
rightActions?: ReactNode;
|
|
11
|
+
/** Центральный нижний слот */
|
|
12
|
+
centralActions?: ReactNode;
|
|
13
|
+
/** Верхний слот для блока превью */
|
|
14
|
+
preview?: ReactNode;
|
|
15
|
+
/** Максимальная высота поля ввода в пикселях. Если не задан — высота не ограничена. */
|
|
16
|
+
maxHeight?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Раскладка поля ввода.
|
|
19
|
+
* `horizontal` — ввод стоит в ряду контролов, пока содержимое помещается в одну строку,
|
|
20
|
+
* и сам переезжает на свой ряд, когда перестаёт влезать.
|
|
21
|
+
* `vertical` — ввод всегда на своём ряду; в горизонтальный слот его не ставим.
|
|
22
|
+
* @default horizontal
|
|
23
|
+
*/
|
|
24
|
+
layout?: 'vertical' | 'horizontal';
|
|
25
|
+
/** Хендлер отправки: вызывается по Enter (Shift+Enter — перенос строки) */
|
|
26
|
+
onSend?: VoidFunction;
|
|
27
|
+
/** Значение поля ввода. Компонент работает только в controlled-режиме */
|
|
28
|
+
value: string;
|
|
29
|
+
/** Обработчик изменения значения */
|
|
30
|
+
onChange: ChangeEventHandler<HTMLTextAreaElement>;
|
|
31
|
+
/** Реф на корневую обёртку — для измерения размеров/позиционирования.
|
|
32
|
+
* Реф на textarea (фокус и т.п.) прокидывается обычным `ref`, как у Input/TextArea */
|
|
33
|
+
wrapperRef?: ForwardedRef<HTMLDivElement>;
|
|
34
|
+
/** Отключает фон и обводку компонента */
|
|
35
|
+
noBackground?: boolean;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
'data-qa'?: string;
|
|
38
|
+
}
|
|
39
|
+
export type ChatInputProps = ChatInputOwnProps & Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, keyof ChatInputOwnProps | 'defaultValue'>;
|
|
40
|
+
interface ChatInputComponents {
|
|
41
|
+
Chips: typeof ChatInputChips;
|
|
42
|
+
Preview: typeof ChatInputPreview;
|
|
43
|
+
SendButton: typeof ChatInputSendButton;
|
|
44
|
+
VoiceButton: typeof ChatInputVoiceButton;
|
|
45
|
+
}
|
|
46
|
+
declare const ChatInput: ForwardRefExoticComponent<ChatInputProps & RefAttributes<HTMLTextAreaElement>> & ChatInputComponents;
|
|
47
|
+
export { ChatInput };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import './../index.css';
|
|
2
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
3
|
+
import { forwardRef, useRef, useState, useLayoutEffect, useMemo, useEffect } from 'react';
|
|
4
|
+
import classNames from 'classnames';
|
|
5
|
+
import { useDisabled, DisabledContext } from '@hh.ru/magritte-common-use-disabled';
|
|
6
|
+
import { useMultipleRefs } from '@hh.ru/magritte-common-use-multiple-refs';
|
|
7
|
+
import { ChatInputContextProvider } from '../internal/ChatInputContext.js';
|
|
8
|
+
import { ModeTransition } from '../internal/ModeTransition.js';
|
|
9
|
+
import { TextInput } from '../internal/TextInput.js';
|
|
10
|
+
import { VoiceRecord } from '../internal/VoiceRecord.js';
|
|
11
|
+
import { focusPriorityProps, FOCUS_PRIORITY } from '../internal/focusGroup.js';
|
|
12
|
+
import { useRovingFocusGroup } from '../internal/useRovingFocusGroup.js';
|
|
13
|
+
import { useVoiceRecordSession } from '../internal/useVoiceRecordSession.js';
|
|
14
|
+
import { ChatInputVoiceSlotProvider, useVoiceConfig } from '../internal/voiceSlotRegistry.js';
|
|
15
|
+
import { ChatInputChips } from './ChatInputChips.js';
|
|
16
|
+
import { ChatInputPreview } from './ChatInputPreview.js';
|
|
17
|
+
import { ChatInputSendButton } from './ChatInputSendButton.js';
|
|
18
|
+
import { ChatInputVoiceButton } from './ChatInputVoiceButton.js';
|
|
19
|
+
import { IconDefaultPropsContext } from '@hh.ru/magritte-ui-icon';
|
|
20
|
+
import { s as styles } from '../styles-CQQdqOFh.js';
|
|
21
|
+
import 'motion/react';
|
|
22
|
+
import '@hh.ru/magritte-design-tokens/types';
|
|
23
|
+
import '../internal/useSendOnEnter.js';
|
|
24
|
+
import '@hh.ru/magritte-common-keyboard';
|
|
25
|
+
import '@hh.ru/magritte-ui-scrollbar';
|
|
26
|
+
import '@hh.ru/magritte-common-use-translation';
|
|
27
|
+
import '../internal/FocusTarget.js';
|
|
28
|
+
import '../internal/VoiceRecordIndicator.js';
|
|
29
|
+
import '../internal/useHasSound.js';
|
|
30
|
+
import '@hh.ru/magritte-ui-icon/icon';
|
|
31
|
+
import '@hh.ru/magritte-ui-loader';
|
|
32
|
+
import '@hh.ru/magritte-ui-typography';
|
|
33
|
+
import '@hh.ru/magritte-internal-slot-registry';
|
|
34
|
+
import '@hh.ru/magritte-ui-chips';
|
|
35
|
+
import '@hh.ru/magritte-ui-scrollable-container';
|
|
36
|
+
import '../internal/ChatInputSlotButton.js';
|
|
37
|
+
|
|
38
|
+
const DEFAULT_ICON_PROPS = {
|
|
39
|
+
borderRadius: 12,
|
|
40
|
+
padding: 8,
|
|
41
|
+
initialColor: 'secondary',
|
|
42
|
+
};
|
|
43
|
+
const ChatInputInner = ({ leftActions, rightActions, centralActions, preview, maxHeight, layout = 'horizontal', onSend, wrapperRef, textareaRef, noBackground = false, disabled = false, 'data-qa': dataQa = 'chat-input', ...textareaProps }) => {
|
|
44
|
+
const outerDisabled = useDisabled();
|
|
45
|
+
const isDisabled = disabled || outerDisabled;
|
|
46
|
+
const rootRef = useRef(null);
|
|
47
|
+
const rootRefCallback = useMultipleRefs(rootRef, wrapperRef);
|
|
48
|
+
// реф textarea — для возврата фокуса при выходе из режима записи
|
|
49
|
+
const textareaLocalRef = useRef(null);
|
|
50
|
+
const textareaRefCallback = useMultipleRefs(textareaLocalRef, textareaRef);
|
|
51
|
+
const [containerWidth, setContainerWidth] = useState();
|
|
52
|
+
useLayoutEffect(() => {
|
|
53
|
+
const root = rootRef.current;
|
|
54
|
+
if (!root) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const observer = new ResizeObserver((entries) => {
|
|
58
|
+
const width = Math.round(entries[0]?.contentRect.width ?? 0);
|
|
59
|
+
setContainerWidth((prev) => (prev === width ? prev : width));
|
|
60
|
+
});
|
|
61
|
+
observer.observe(root);
|
|
62
|
+
return () => observer.disconnect();
|
|
63
|
+
}, []);
|
|
64
|
+
// конфиг записи регистрирует ChatInput.VoiceButton через slot-registry. Кнопка живёт в
|
|
65
|
+
// text-гриде, который на время записи не размонтируется (ModeTransition лишь прячет его), —
|
|
66
|
+
// поэтому регистрация и колбэки остаются живыми всю сессию, латч не нужен
|
|
67
|
+
const voiceConfig = useVoiceConfig();
|
|
68
|
+
const voiceSession = useVoiceRecordSession(voiceConfig ?? {});
|
|
69
|
+
const contextValue = useMemo(() => ({
|
|
70
|
+
onSend,
|
|
71
|
+
startVoiceRecord: voiceSession.start,
|
|
72
|
+
voiceRecordStatus: voiceSession.status,
|
|
73
|
+
}), [onSend, voiceSession.start, voiceSession.status]);
|
|
74
|
+
// режим ведём по статусу сессии, а не по наличию конфига: даже если приложение уберёт кнопку
|
|
75
|
+
// по ходу записи, запись продолжается. Без кнопки запись не стартует, так что статус сам остаётся idle
|
|
76
|
+
const isVoiceMode = voiceSession.status === 'recording' || voiceSession.status === 'processing';
|
|
77
|
+
// roving-группа кнопок: один tab stop на все кнопки, внутри — стрелки по приоритетам
|
|
78
|
+
const { focusStop } = useRovingFocusGroup(rootRef);
|
|
79
|
+
// при входе в запись фокус на стоп группы (после смены слоя это галочка — первый приоритет
|
|
80
|
+
// слоя записи), при возврате в текст — в textarea с курсором в конце. Фокус ставим в rAF:
|
|
81
|
+
// слой режима к этому моменту уже видим (motion применил visibility), иначе focus()
|
|
82
|
+
// на visibility:hidden не сработает
|
|
83
|
+
const prevVoiceModeRef = useRef(isVoiceMode);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
const wasVoiceMode = prevVoiceModeRef.current;
|
|
86
|
+
prevVoiceModeRef.current = isVoiceMode;
|
|
87
|
+
if (wasVoiceMode === isVoiceMode) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
const frameId = requestAnimationFrame(() => {
|
|
91
|
+
if (isVoiceMode) {
|
|
92
|
+
focusStop();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const textarea = textareaLocalRef.current;
|
|
96
|
+
if (textarea) {
|
|
97
|
+
textarea.focus();
|
|
98
|
+
const caret = textarea.value.length;
|
|
99
|
+
textarea.setSelectionRange(caret, caret);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
return () => cancelAnimationFrame(frameId);
|
|
103
|
+
}, [isVoiceMode, focusStop]);
|
|
104
|
+
return (jsx(DisabledContext.Provider, { value: isDisabled, children: jsx(ChatInputContextProvider, { value: contextValue, children: jsx("div", { ref: rootRefCallback, className: classNames(styles.chatInput, { [styles.noBackground]: noBackground }), "data-qa": dataQa, children: jsx(ModeTransition, { mode: isVoiceMode ? 'voice' : 'text', voice: jsx(VoiceRecord, { mode: voiceSession.status === 'processing' ? 'processing' : 'recording', stream: voiceSession.stream, startedAt: voiceSession.startedAt, processingLabel: voiceConfig?.processingLabel, onComplete: voiceSession.stop, onCancel: voiceSession.cancel, completeButtonProps: voiceConfig?.completeButtonProps, cancelButtonProps: voiceConfig?.cancelButtonProps, "data-qa": "chat-input-voice-record" }), text: jsxs("div", { className: classNames(styles.contentGrid, {
|
|
105
|
+
[styles.noLeftControls]: !leftActions,
|
|
106
|
+
[styles.noRightControls]: !rightActions,
|
|
107
|
+
[styles.hasCentral]: !!centralActions,
|
|
108
|
+
}), children: [jsx(TextInput, { ...textareaProps, ref: textareaRefCallback, layout: layout, onSend: onSend, maxHeight: maxHeight, containerWidth: containerWidth, hasCentral: !!centralActions, disabled: isDisabled, hasBothSideControls: !!leftActions && !!rightActions }), !!rightActions && (jsx(IconDefaultPropsContext, { props: DEFAULT_ICON_PROPS, children: jsx("div", { className: styles.right, ...focusPriorityProps(FOCUS_PRIORITY.rightSlot), children: rightActions }) })), !!leftActions && (jsx(IconDefaultPropsContext, { props: DEFAULT_ICON_PROPS, children: jsx("div", { className: styles.left, ...focusPriorityProps(FOCUS_PRIORITY.leftSlot), children: leftActions }) })), !!centralActions && (jsx("div", { className: styles.central, ...focusPriorityProps(FOCUS_PRIORITY.centralSlot), children: centralActions })), !!preview && (jsx("div", { className: styles.preview, ...focusPriorityProps(FOCUS_PRIORITY.previewSlot), children: preview }))] }) }) }) }) }));
|
|
109
|
+
};
|
|
110
|
+
// внешняя обёртка держит slot-registry для конфига записи: кнопка (внутри слота) регистрирует
|
|
111
|
+
// конфиг, ChatInputInner (ниже провайдера) читает его. forwardRef-реф на textarea прокидываем
|
|
112
|
+
// вниз обычным пропом
|
|
113
|
+
const ChatInputComponent = (props, ref) => (jsx(ChatInputVoiceSlotProvider, { children: jsx(ChatInputInner, { ...props, textareaRef: ref }) }));
|
|
114
|
+
const ChatInput = forwardRef(ChatInputComponent);
|
|
115
|
+
ChatInput.displayName = 'ChatInput';
|
|
116
|
+
ChatInput.Chips = ChatInputChips;
|
|
117
|
+
ChatInput.Preview = ChatInputPreview;
|
|
118
|
+
ChatInput.SendButton = ChatInputSendButton;
|
|
119
|
+
ChatInput.VoiceButton = ChatInputVoiceButton;
|
|
120
|
+
|
|
121
|
+
export { ChatInput };
|
|
122
|
+
//# sourceMappingURL=ChatInput.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatInput.js","sources":["src/public/ChatInput.tsx"],"sourcesContent":["import {\n type ChangeEventHandler,\n type FC,\n type ForwardedRef,\n forwardRef,\n type ForwardRefExoticComponent,\n type ReactNode,\n type RefAttributes,\n type TextareaHTMLAttributes,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport classNames from 'classnames';\n\nimport { DisabledContext, useDisabled } from '@hh.ru/magritte-common-use-disabled';\nimport { useMultipleRefs } from '@hh.ru/magritte-common-use-multiple-refs';\nimport { ChatInputContextProvider } from '@hh.ru/magritte-ui-chat-input/internal/ChatInputContext';\nimport { ModeTransition } from '@hh.ru/magritte-ui-chat-input/internal/ModeTransition';\nimport { TextInput } from '@hh.ru/magritte-ui-chat-input/internal/TextInput';\nimport { VoiceRecord } from '@hh.ru/magritte-ui-chat-input/internal/VoiceRecord';\nimport { FOCUS_PRIORITY, focusPriorityProps } from '@hh.ru/magritte-ui-chat-input/internal/focusGroup';\nimport { useRovingFocusGroup } from '@hh.ru/magritte-ui-chat-input/internal/useRovingFocusGroup';\nimport { useVoiceRecordSession } from '@hh.ru/magritte-ui-chat-input/internal/useVoiceRecordSession';\nimport { ChatInputVoiceSlotProvider, useVoiceConfig } from '@hh.ru/magritte-ui-chat-input/internal/voiceSlotRegistry';\nimport { ChatInputChips } from '@hh.ru/magritte-ui-chat-input/public/ChatInputChips';\nimport { ChatInputPreview } from '@hh.ru/magritte-ui-chat-input/public/ChatInputPreview';\nimport { ChatInputSendButton } from '@hh.ru/magritte-ui-chat-input/public/ChatInputSendButton';\nimport { ChatInputVoiceButton } from '@hh.ru/magritte-ui-chat-input/public/ChatInputVoiceButton';\nimport { IconDefaultPropsContext } from '@hh.ru/magritte-ui-icon';\n\nimport styles from '../styles.less';\n\ninterface ChatInputOwnProps {\n /** Левый слот */\n leftActions?: ReactNode;\n /** Правый слот */\n rightActions?: ReactNode;\n /** Центральный нижний слот */\n centralActions?: ReactNode;\n /** Верхний слот для блока превью */\n preview?: ReactNode;\n /** Максимальная высота поля ввода в пикселях. Если не задан — высота не ограничена. */\n maxHeight?: number;\n /**\n * Раскладка поля ввода.\n * `horizontal` — ввод стоит в ряду контролов, пока содержимое помещается в одну строку,\n * и сам переезжает на свой ряд, когда перестаёт влезать.\n * `vertical` — ввод всегда на своём ряду; в горизонтальный слот его не ставим.\n * @default horizontal\n */\n layout?: 'vertical' | 'horizontal';\n /** Хендлер отправки: вызывается по Enter (Shift+Enter — перенос строки) */\n onSend?: VoidFunction;\n /** Значение поля ввода. Компонент работает только в controlled-режиме */\n value: string;\n /** Обработчик изменения значения */\n onChange: ChangeEventHandler<HTMLTextAreaElement>;\n /** Реф на корневую обёртку — для измерения размеров/позиционирования.\n * Реф на textarea (фокус и т.п.) прокидывается обычным `ref`, как у Input/TextArea */\n wrapperRef?: ForwardedRef<HTMLDivElement>;\n /** Отключает фон и обводку компонента */\n noBackground?: boolean;\n disabled?: boolean;\n 'data-qa'?: string;\n}\n\n// defaultValue исключён: компонент только controlled, uncontrolled-режим не поддерживается\nexport type ChatInputProps = ChatInputOwnProps &\n Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, keyof ChatInputOwnProps | 'defaultValue'>;\n\nconst DEFAULT_ICON_PROPS = {\n borderRadius: 12,\n padding: 8,\n initialColor: 'secondary',\n} as const;\n\ninterface ChatInputComponents {\n Chips: typeof ChatInputChips;\n Preview: typeof ChatInputPreview;\n SendButton: typeof ChatInputSendButton;\n VoiceButton: typeof ChatInputVoiceButton;\n}\n\ntype ChatInputInnerProps = ChatInputProps & {\n /** Реф на textarea, переданный через forwardRef внешней обёртки */\n textareaRef: ForwardedRef<HTMLTextAreaElement>;\n};\n\nconst ChatInputInner: FC<ChatInputInnerProps> = ({\n leftActions,\n rightActions,\n centralActions,\n preview,\n maxHeight,\n layout = 'horizontal',\n onSend,\n wrapperRef,\n textareaRef,\n noBackground = false,\n disabled = false,\n 'data-qa': dataQa = 'chat-input',\n ...textareaProps\n}) => {\n const outerDisabled = useDisabled();\n const isDisabled = disabled || outerDisabled;\n const rootRef = useRef<HTMLDivElement>(null);\n const rootRefCallback = useMultipleRefs(rootRef, wrapperRef);\n // реф textarea — для возврата фокуса при выходе из режима записи\n const textareaLocalRef = useRef<HTMLTextAreaElement>(null);\n const textareaRefCallback = useMultipleRefs(textareaLocalRef, textareaRef);\n const [containerWidth, setContainerWidth] = useState<number>();\n useLayoutEffect(() => {\n const root = rootRef.current;\n if (!root) {\n return undefined;\n }\n const observer = new ResizeObserver((entries) => {\n const width = Math.round(entries[0]?.contentRect.width ?? 0);\n setContainerWidth((prev) => (prev === width ? prev : width));\n });\n observer.observe(root);\n return () => observer.disconnect();\n }, []);\n\n // конфиг записи регистрирует ChatInput.VoiceButton через slot-registry. Кнопка живёт в\n // text-гриде, который на время записи не размонтируется (ModeTransition лишь прячет его), —\n // поэтому регистрация и колбэки остаются живыми всю сессию, латч не нужен\n const voiceConfig = useVoiceConfig();\n\n const voiceSession = useVoiceRecordSession(voiceConfig ?? {});\n\n const contextValue = useMemo(\n () => ({\n onSend,\n startVoiceRecord: voiceSession.start,\n voiceRecordStatus: voiceSession.status,\n }),\n [onSend, voiceSession.start, voiceSession.status]\n );\n\n // режим ведём по статусу сессии, а не по наличию конфига: даже если приложение уберёт кнопку\n // по ходу записи, запись продолжается. Без кнопки запись не стартует, так что статус сам остаётся idle\n const isVoiceMode = voiceSession.status === 'recording' || voiceSession.status === 'processing';\n\n // roving-группа кнопок: один tab stop на все кнопки, внутри — стрелки по приоритетам\n const { focusStop } = useRovingFocusGroup(rootRef);\n\n // при входе в запись фокус на стоп группы (после смены слоя это галочка — первый приоритет\n // слоя записи), при возврате в текст — в textarea с курсором в конце. Фокус ставим в rAF:\n // слой режима к этому моменту уже видим (motion применил visibility), иначе focus()\n // на visibility:hidden не сработает\n const prevVoiceModeRef = useRef(isVoiceMode);\n useEffect(() => {\n const wasVoiceMode = prevVoiceModeRef.current;\n prevVoiceModeRef.current = isVoiceMode;\n if (wasVoiceMode === isVoiceMode) {\n return undefined;\n }\n const frameId = requestAnimationFrame(() => {\n if (isVoiceMode) {\n focusStop();\n return;\n }\n const textarea = textareaLocalRef.current;\n if (textarea) {\n textarea.focus();\n const caret = textarea.value.length;\n textarea.setSelectionRange(caret, caret);\n }\n });\n return () => cancelAnimationFrame(frameId);\n }, [isVoiceMode, focusStop]);\n\n return (\n <DisabledContext.Provider value={isDisabled}>\n <ChatInputContextProvider value={contextValue}>\n <div\n ref={rootRefCallback}\n className={classNames(styles.chatInput, { [styles.noBackground]: noBackground })}\n data-qa={dataQa}\n >\n <ModeTransition\n mode={isVoiceMode ? 'voice' : 'text'}\n voice={\n <VoiceRecord\n mode={voiceSession.status === 'processing' ? 'processing' : 'recording'}\n stream={voiceSession.stream}\n startedAt={voiceSession.startedAt}\n processingLabel={voiceConfig?.processingLabel}\n onComplete={voiceSession.stop}\n onCancel={voiceSession.cancel}\n completeButtonProps={voiceConfig?.completeButtonProps}\n cancelButtonProps={voiceConfig?.cancelButtonProps}\n data-qa=\"chat-input-voice-record\"\n />\n }\n text={\n <div\n className={classNames(styles.contentGrid, {\n [styles.noLeftControls]: !leftActions,\n [styles.noRightControls]: !rightActions,\n [styles.hasCentral]: !!centralActions,\n })}\n >\n {/* textarea в DOM первым намеренно: Tab снаружи входит сначала в него.\n Раскладку держит grid-area (от порядка не зависит). Кнопки слотов —\n одна roving-группа (useRovingFocusGroup): Tab видит только текущий\n стоп, внутри группы фокус ходит стрелками по приоритетам слотов */}\n <TextInput\n {...textareaProps}\n ref={textareaRefCallback}\n layout={layout}\n onSend={onSend}\n maxHeight={maxHeight}\n containerWidth={containerWidth}\n hasCentral={!!centralActions}\n disabled={isDisabled}\n hasBothSideControls={!!leftActions && !!rightActions}\n />\n {!!rightActions && (\n <IconDefaultPropsContext props={DEFAULT_ICON_PROPS}>\n <div className={styles.right} {...focusPriorityProps(FOCUS_PRIORITY.rightSlot)}>\n {rightActions}\n </div>\n </IconDefaultPropsContext>\n )}\n {!!leftActions && (\n <IconDefaultPropsContext props={DEFAULT_ICON_PROPS}>\n <div className={styles.left} {...focusPriorityProps(FOCUS_PRIORITY.leftSlot)}>\n {leftActions}\n </div>\n </IconDefaultPropsContext>\n )}\n {!!centralActions && (\n <div className={styles.central} {...focusPriorityProps(FOCUS_PRIORITY.centralSlot)}>\n {centralActions}\n </div>\n )}\n {!!preview && (\n <div className={styles.preview} {...focusPriorityProps(FOCUS_PRIORITY.previewSlot)}>\n {preview}\n </div>\n )}\n </div>\n }\n />\n </div>\n </ChatInputContextProvider>\n </DisabledContext.Provider>\n );\n};\n\n// внешняя обёртка держит slot-registry для конфига записи: кнопка (внутри слота) регистрирует\n// конфиг, ChatInputInner (ниже провайдера) читает его. forwardRef-реф на textarea прокидываем\n// вниз обычным пропом\nconst ChatInputComponent = (props: ChatInputProps, ref: ForwardedRef<HTMLTextAreaElement>) => (\n <ChatInputVoiceSlotProvider>\n <ChatInputInner {...props} textareaRef={ref} />\n </ChatInputVoiceSlotProvider>\n);\n\nconst ChatInput = forwardRef(ChatInputComponent) as ForwardRefExoticComponent<\n ChatInputProps & RefAttributes<HTMLTextAreaElement>\n> &\n ChatInputComponents;\n\nChatInput.displayName = 'ChatInput';\n\nChatInput.Chips = ChatInputChips;\nChatInput.Preview = ChatInputPreview;\nChatInput.SendButton = ChatInputSendButton;\nChatInput.VoiceButton = ChatInputVoiceButton;\n\nexport { ChatInput };\n"],"names":["_jsx","_jsxs"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import './../index.css';
|
|
2
|
+
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
import { isValidElement, Children, Fragment } from 'react';
|
|
4
|
+
import { ChipDefaultPropsContext } from '@hh.ru/magritte-ui-chips';
|
|
5
|
+
import { ScrollableContainer } from '@hh.ru/magritte-ui-scrollable-container';
|
|
6
|
+
import { s as styles } from '../styles-CQQdqOFh.js';
|
|
7
|
+
|
|
8
|
+
const CHIP_DEFAULT_PROPS = { size: 'extra-small' };
|
|
9
|
+
const flattenFragments = (children) => Children.toArray(children).flatMap((child) => isValidElement(child) && child.type === Fragment
|
|
10
|
+
? flattenFragments(child.props.children)
|
|
11
|
+
: child);
|
|
12
|
+
const ChatInputChips = ({ children }) => (jsx("div", { className: styles.chipsContainer, children: jsx(ChipDefaultPropsContext, { props: CHIP_DEFAULT_PROPS, children: jsx(ScrollableContainer, { children: jsx(ScrollableContainer.DefaultLayout, { size: "small", fadeOnMobile: true, children: jsx(ScrollableContainer.Gallery, { children: jsx("div", { className: styles.chips, children: flattenFragments(children).map((child, index) => (jsx(ScrollableContainer.Item, { children: child }, (isValidElement(child) && child.key) || index))) }) }) }) }) }) }));
|
|
13
|
+
ChatInputChips.displayName = 'ChatInputChips';
|
|
14
|
+
|
|
15
|
+
export { ChatInputChips };
|
|
16
|
+
//# sourceMappingURL=ChatInputChips.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatInputChips.js","sources":["src/public/ChatInputChips.tsx"],"sourcesContent":["import { Children, Fragment, isValidElement, type FC, type PropsWithChildren, type ReactNode } from 'react';\n\nimport { ChipDefaultPropsContext } from '@hh.ru/magritte-ui-chips';\nimport { ScrollableContainer } from '@hh.ru/magritte-ui-scrollable-container';\n\nimport styles from '../styles.less';\n\nexport interface ChatInputChipsProps {\n /** Чипсы, размещаются в один ряд с возможностью горизонтального скролла */\n children: ReactNode;\n}\n\nconst CHIP_DEFAULT_PROPS = { size: 'extra-small' } as const;\n\nconst flattenFragments = (children: ReactNode): ReactNode[] =>\n Children.toArray(children).flatMap((child) =>\n isValidElement(child) && child.type === Fragment\n ? flattenFragments((child.props as PropsWithChildren).children)\n : child\n );\n\nexport const ChatInputChips: FC<ChatInputChipsProps> = ({ children }) => (\n <div className={styles.chipsContainer}>\n <ChipDefaultPropsContext props={CHIP_DEFAULT_PROPS}>\n <ScrollableContainer>\n <ScrollableContainer.DefaultLayout size=\"small\" fadeOnMobile>\n <ScrollableContainer.Gallery>\n <div className={styles.chips}>\n {/* Children.toArray сохраняет ключи детей — переносим их на обёртку,\n чтобы удаление чипса из середины не сдвигало стейт соседей */}\n {flattenFragments(children).map((child, index) => (\n <ScrollableContainer.Item key={(isValidElement(child) && child.key) || index}>\n {child}\n </ScrollableContainer.Item>\n ))}\n </div>\n </ScrollableContainer.Gallery>\n </ScrollableContainer.DefaultLayout>\n </ScrollableContainer>\n </ChipDefaultPropsContext>\n </div>\n);\n\nChatInputChips.displayName = 'ChatInputChips';\n"],"names":["_jsx"],"mappings":";;;;;;AAYA,MAAM,kBAAkB,GAAG,EAAE,IAAI,EAAE,aAAa,EAAW,CAAC;AAE5D,MAAM,gBAAgB,GAAG,CAAC,QAAmB,KACzC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,KACrC,cAAc,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ;MAC1C,gBAAgB,CAAE,KAAK,CAAC,KAA2B,CAAC,QAAQ,CAAC;MAC7D,KAAK,CACd,CAAC;MAEO,cAAc,GAA4B,CAAC,EAAE,QAAQ,EAAE,MAChEA,GAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAE,MAAM,CAAC,cAAc,EAAA,QAAA,EACjCA,GAAC,CAAA,uBAAuB,EAAC,EAAA,KAAK,EAAE,kBAAkB,EAAA,QAAA,EAC9CA,IAAC,mBAAmB,EAAA,EAAA,QAAA,EAChBA,IAAC,mBAAmB,CAAC,aAAa,EAAC,EAAA,IAAI,EAAC,OAAO,EAAC,YAAY,EACxD,IAAA,EAAA,QAAA,EAAAA,GAAA,CAAC,mBAAmB,CAAC,OAAO,EACxB,EAAA,QAAA,EAAAA,GAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAE,MAAM,CAAC,KAAK,YAGvB,gBAAgB,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,MACzCA,GAAC,CAAA,mBAAmB,CAAC,IAAI,EACpB,EAAA,QAAA,EAAA,KAAK,IADqB,CAAC,cAAc,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,GAAG,KAAK,KAAK,CAEjD,CAC9B,CAAC,EACA,CAAA,EAAA,CACoB,GACE,EAClB,CAAA,EAAA,CACA,EACxB,CAAA,EACR;AAEF,cAAc,CAAC,WAAW,GAAG,gBAAgB;;;;"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type FC, type ReactNode } from 'react';
|
|
2
|
+
export interface ChatInputPreviewProps {
|
|
3
|
+
/** Заголовок в центральной секции, типографика subtitle-3-semibold */
|
|
4
|
+
title?: ReactNode;
|
|
5
|
+
/** Подзаголовок под title, типографика label-3-regular; обрезается многоточием */
|
|
6
|
+
subtitle?: ReactNode;
|
|
7
|
+
/** Слот левой секции, обычно иконка */
|
|
8
|
+
leftActions?: ReactNode;
|
|
9
|
+
/** Слот правой секции, обычно иконка */
|
|
10
|
+
rightActions?: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
export declare const ChatInputPreview: FC<ChatInputPreviewProps>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import './../index.css';
|
|
2
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
3
|
+
import { IconDefaultPropsContext } from '@hh.ru/magritte-ui-icon';
|
|
4
|
+
import { s as styles } from '../styles-CQQdqOFh.js';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_ICON_PROPS = {
|
|
7
|
+
initialColor: 'secondary',
|
|
8
|
+
padding: 8,
|
|
9
|
+
};
|
|
10
|
+
const ChatInputPreview = ({ title, subtitle, leftActions, rightActions }) => (jsx(IconDefaultPropsContext, { props: DEFAULT_ICON_PROPS, children: jsxs("div", { className: styles.previewBlock, children: [!!leftActions && jsx("div", { className: styles.previewSide, children: leftActions }), jsxs("div", { className: styles.previewContent, children: [!!title && jsx("div", { className: styles.previewTitle, children: title }), !!subtitle && jsx("div", { className: styles.previewSubtitle, children: subtitle })] }), !!rightActions && jsx("div", { className: styles.previewSide, children: rightActions })] }) }));
|
|
11
|
+
ChatInputPreview.displayName = 'ChatInputPreview';
|
|
12
|
+
|
|
13
|
+
export { ChatInputPreview };
|
|
14
|
+
//# sourceMappingURL=ChatInputPreview.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatInputPreview.js","sources":["src/public/ChatInputPreview.tsx"],"sourcesContent":["import { type FC, type ReactNode } from 'react';\n\nimport { IconDefaultPropsContext } from '@hh.ru/magritte-ui-icon';\n\nimport styles from '../styles.less';\n\nexport interface ChatInputPreviewProps {\n /** Заголовок в центральной секции, типографика subtitle-3-semibold */\n title?: ReactNode;\n /** Подзаголовок под title, типографика label-3-regular; обрезается многоточием */\n subtitle?: ReactNode;\n /** Слот левой секции, обычно иконка */\n leftActions?: ReactNode;\n /** Слот правой секции, обычно иконка */\n rightActions?: ReactNode;\n}\n\nconst DEFAULT_ICON_PROPS = {\n initialColor: 'secondary',\n padding: 8,\n} as const;\n\nexport const ChatInputPreview: FC<ChatInputPreviewProps> = ({ title, subtitle, leftActions, rightActions }) => (\n <IconDefaultPropsContext props={DEFAULT_ICON_PROPS}>\n <div className={styles.previewBlock}>\n {!!leftActions && <div className={styles.previewSide}>{leftActions}</div>}\n <div className={styles.previewContent}>\n {!!title && <div className={styles.previewTitle}>{title}</div>}\n {!!subtitle && <div className={styles.previewSubtitle}>{subtitle}</div>}\n </div>\n {!!rightActions && <div className={styles.previewSide}>{rightActions}</div>}\n </div>\n </IconDefaultPropsContext>\n);\n\nChatInputPreview.displayName = 'ChatInputPreview';\n"],"names":["_jsx","_jsxs"],"mappings":";;;;AAiBA,MAAM,kBAAkB,GAAG;AACvB,IAAA,YAAY,EAAE,WAAW;AACzB,IAAA,OAAO,EAAE,CAAC;CACJ,CAAC;MAEE,gBAAgB,GAA8B,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAE,MACtGA,GAAA,CAAC,uBAAuB,EAAA,EAAC,KAAK,EAAE,kBAAkB,EAC9C,QAAA,EAAAC,IAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAE,MAAM,CAAC,YAAY,EAAA,QAAA,EAAA,CAC9B,CAAC,CAAC,WAAW,IAAID,aAAK,SAAS,EAAE,MAAM,CAAC,WAAW,YAAG,WAAW,EAAA,CAAO,EACzEC,IAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAE,MAAM,CAAC,cAAc,aAChC,CAAC,CAAC,KAAK,IAAID,GAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAE,MAAM,CAAC,YAAY,EAAA,QAAA,EAAG,KAAK,EAAO,CAAA,EAC7D,CAAC,CAAC,QAAQ,IAAIA,GAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAE,MAAM,CAAC,eAAe,YAAG,QAAQ,EAAA,CAAO,IACrE,EACL,CAAC,CAAC,YAAY,IAAIA,GAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAE,MAAM,CAAC,WAAW,EAAA,QAAA,EAAG,YAAY,EAAO,CAAA,CAAA,EAAA,CACzE,EACgB,CAAA,EAC5B;AAEF,gBAAgB,CAAC,WAAW,GAAG,kBAAkB;;;;"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type FC, type MouseEventHandler, type ReactNode } from 'react';
|
|
2
|
+
import { type ChatInputButtonAria } from '@hh.ru/magritte-ui-chat-input/types';
|
|
3
|
+
export interface ChatInputSendButtonProps extends ChatInputButtonAria {
|
|
4
|
+
/** Доп. обработчик клика — вызывается вместе с `onSend`, а не вместо него */
|
|
5
|
+
onClick?: MouseEventHandler<HTMLButtonElement>;
|
|
6
|
+
/** Кастомный контент вместо иконки отправки. Иконку в children оформляем как встроенную
|
|
7
|
+
* (через IconDefaultPropsContext); клик по любому content ловится в любом случае */
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Кнопка отправки для слота ChatInput. Обёртка над иконкой с фиксированным оформлением;
|
|
12
|
+
* по клику дёргает `onSend` из контекста ChatInput вместе с собственным `onClick`.
|
|
13
|
+
*/
|
|
14
|
+
export declare const ChatInputSendButton: FC<ChatInputSendButtonProps>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import './../index.css';
|
|
2
|
+
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
import { useTranslation } from '@hh.ru/magritte-common-use-translation';
|
|
4
|
+
import { useChatInputContext } from '../internal/ChatInputContext.js';
|
|
5
|
+
import { ChatInputSlotButton } from '../internal/ChatInputSlotButton.js';
|
|
6
|
+
import { FocusTarget } from '../internal/FocusTarget.js';
|
|
7
|
+
import { FOCUS_PRIORITY } from '../internal/focusGroup.js';
|
|
8
|
+
import { ArrowUpOutlinedSize24 } from '@hh.ru/magritte-ui-icon/icon';
|
|
9
|
+
import 'react';
|
|
10
|
+
import '@hh.ru/magritte-common-use-disabled';
|
|
11
|
+
import '@hh.ru/magritte-ui-icon';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Кнопка отправки для слота ChatInput. Обёртка над иконкой с фиксированным оформлением;
|
|
15
|
+
* по клику дёргает `onSend` из контекста ChatInput вместе с собственным `onClick`.
|
|
16
|
+
*/
|
|
17
|
+
const ChatInputSendButton = ({ onClick, children, 'aria-label': ariaLabel, ...ariaProps }) => {
|
|
18
|
+
const { onSend } = useChatInputContext();
|
|
19
|
+
const sendLabel = useTranslation('ChatInput', 'send');
|
|
20
|
+
const handleClick = (event) => {
|
|
21
|
+
onClick?.(event);
|
|
22
|
+
onSend?.();
|
|
23
|
+
};
|
|
24
|
+
const iconProps = {
|
|
25
|
+
// дефолтный data-qa до спреда — пользовательский data-qa из ariaProps его перекрывает
|
|
26
|
+
'data-qa': 'send',
|
|
27
|
+
...ariaProps,
|
|
28
|
+
'aria-label': ariaLabel ?? sendLabel,
|
|
29
|
+
initialColor: 'inverse',
|
|
30
|
+
backgroundStyle: 'inverse',
|
|
31
|
+
onClick: handleClick,
|
|
32
|
+
};
|
|
33
|
+
return (jsx(FocusTarget, { priority: FOCUS_PRIORITY.send, children: jsx(ChatInputSlotButton, { icon: jsx(ArrowUpOutlinedSize24, { ...iconProps }), iconProps: iconProps, onClick: handleClick, children: children }) }));
|
|
34
|
+
};
|
|
35
|
+
ChatInputSendButton.displayName = 'ChatInputSendButton';
|
|
36
|
+
|
|
37
|
+
export { ChatInputSendButton };
|
|
38
|
+
//# sourceMappingURL=ChatInputSendButton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatInputSendButton.js","sources":["src/public/ChatInputSendButton.tsx"],"sourcesContent":["import { type FC, type MouseEvent, type MouseEventHandler, type ReactNode } from 'react';\n\nimport { useTranslation } from '@hh.ru/magritte-common-use-translation';\nimport { useChatInputContext } from '@hh.ru/magritte-ui-chat-input/internal/ChatInputContext';\nimport { ChatInputSlotButton } from '@hh.ru/magritte-ui-chat-input/internal/ChatInputSlotButton';\nimport { FocusTarget } from '@hh.ru/magritte-ui-chat-input/internal/FocusTarget';\nimport { FOCUS_PRIORITY } from '@hh.ru/magritte-ui-chat-input/internal/focusGroup';\nimport { type ChatInputButtonAria } from '@hh.ru/magritte-ui-chat-input/types';\nimport { ArrowUpOutlinedSize24 } from '@hh.ru/magritte-ui-icon/icon';\n\nexport interface ChatInputSendButtonProps extends ChatInputButtonAria {\n /** Доп. обработчик клика — вызывается вместе с `onSend`, а не вместо него */\n onClick?: MouseEventHandler<HTMLButtonElement>;\n /** Кастомный контент вместо иконки отправки. Иконку в children оформляем как встроенную\n * (через IconDefaultPropsContext); клик по любому content ловится в любом случае */\n children?: ReactNode;\n}\n\n/**\n * Кнопка отправки для слота ChatInput. Обёртка над иконкой с фиксированным оформлением;\n * по клику дёргает `onSend` из контекста ChatInput вместе с собственным `onClick`.\n */\nexport const ChatInputSendButton: FC<ChatInputSendButtonProps> = ({\n onClick,\n children,\n 'aria-label': ariaLabel,\n ...ariaProps\n}) => {\n const { onSend } = useChatInputContext();\n const sendLabel = useTranslation('ChatInput', 'send');\n\n const handleClick = (event: MouseEvent<HTMLButtonElement>) => {\n onClick?.(event);\n onSend?.();\n };\n\n const iconProps = {\n // дефолтный data-qa до спреда — пользовательский data-qa из ariaProps его перекрывает\n 'data-qa': 'send',\n ...ariaProps,\n 'aria-label': ariaLabel ?? sendLabel,\n initialColor: 'inverse',\n backgroundStyle: 'inverse',\n onClick: handleClick,\n } as const;\n\n return (\n <FocusTarget priority={FOCUS_PRIORITY.send}>\n <ChatInputSlotButton\n icon={<ArrowUpOutlinedSize24 {...iconProps} />}\n iconProps={iconProps}\n onClick={handleClick}\n >\n {children}\n </ChatInputSlotButton>\n </FocusTarget>\n );\n};\n\nChatInputSendButton.displayName = 'ChatInputSendButton';\n"],"names":["_jsx"],"mappings":";;;;;;;;;;;AAkBA;;;AAGG;AACU,MAAA,mBAAmB,GAAiC,CAAC,EAC9D,OAAO,EACP,QAAQ,EACR,YAAY,EAAE,SAAS,EACvB,GAAG,SAAS,EACf,KAAI;AACD,IAAA,MAAM,EAAE,MAAM,EAAE,GAAG,mBAAmB,EAAE,CAAC;IACzC,MAAM,SAAS,GAAG,cAAc,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;AAEtD,IAAA,MAAM,WAAW,GAAG,CAAC,KAAoC,KAAI;AACzD,QAAA,OAAO,GAAG,KAAK,CAAC,CAAC;QACjB,MAAM,IAAI,CAAC;AACf,KAAC,CAAC;AAEF,IAAA,MAAM,SAAS,GAAG;;AAEd,QAAA,SAAS,EAAE,MAAM;AACjB,QAAA,GAAG,SAAS;QACZ,YAAY,EAAE,SAAS,IAAI,SAAS;AACpC,QAAA,YAAY,EAAE,SAAS;AACvB,QAAA,eAAe,EAAE,SAAS;AAC1B,QAAA,OAAO,EAAE,WAAW;KACd,CAAC;AAEX,IAAA,QACIA,GAAA,CAAC,WAAW,EAAA,EAAC,QAAQ,EAAE,cAAc,CAAC,IAAI,EACtC,QAAA,EAAAA,GAAA,CAAC,mBAAmB,EAAA,EAChB,IAAI,EAAEA,GAAA,CAAC,qBAAqB,EAAA,EAAA,GAAK,SAAS,EAAA,CAAI,EAC9C,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,WAAW,EAEnB,QAAA,EAAA,QAAQ,EACS,CAAA,EAAA,CACZ,EAChB;AACN,EAAE;AAEF,mBAAmB,CAAC,WAAW,GAAG,qBAAqB;;;;"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type FC, type MouseEventHandler, type ReactNode } from 'react';
|
|
2
|
+
import { type ChatInputButtonAria, type ChatInputVoiceRecordConfig } from '@hh.ru/magritte-ui-chat-input/types';
|
|
3
|
+
export interface ChatInputVoiceButtonProps extends ChatInputButtonAria, ChatInputVoiceRecordConfig {
|
|
4
|
+
/** Доп. обработчик клика — вызывается вместе со стартом записи, а не вместо него */
|
|
5
|
+
onClick?: MouseEventHandler<HTMLButtonElement>;
|
|
6
|
+
/** Кастомный контент вместо иконки микрофона. Иконку в children оформляем как встроенную
|
|
7
|
+
* (через IconDefaultPropsContext); клик по любому content ловится в любом случае */
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Кнопка записи голоса для слота ChatInput. Несёт на себе конфигурацию записи (колбэки,
|
|
12
|
+
* `maxDuration`, `processingLabel`) — она регистрируется в slot-registry и оттуда доезжает до
|
|
13
|
+
* сессии в корне ChatInput. По клику запускает запись через контекст ChatInput. Пока запрашивается
|
|
14
|
+
* доступ к микрофону — показывает лоадер; оформление (паддинги, цвет) берёт из дефолтов слота.
|
|
15
|
+
* Вне ChatInput кнопка неактивна.
|
|
16
|
+
*/
|
|
17
|
+
export declare const ChatInputVoiceButton: FC<ChatInputVoiceButtonProps>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import './../index.css';
|
|
2
|
+
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
import { useMemo, useRef, useCallback } from 'react';
|
|
4
|
+
import { useTranslation } from '@hh.ru/magritte-common-use-translation';
|
|
5
|
+
import { useChatInputContext } from '../internal/ChatInputContext.js';
|
|
6
|
+
import { ChatInputSlotButton } from '../internal/ChatInputSlotButton.js';
|
|
7
|
+
import { FocusTarget } from '../internal/FocusTarget.js';
|
|
8
|
+
import { FOCUS_PRIORITY } from '../internal/focusGroup.js';
|
|
9
|
+
import { useRegisterVoiceConfig } from '../internal/voiceSlotRegistry.js';
|
|
10
|
+
import { MicOutlinedSize24 } from '@hh.ru/magritte-ui-icon/icon';
|
|
11
|
+
import '@hh.ru/magritte-common-use-disabled';
|
|
12
|
+
import '@hh.ru/magritte-ui-icon';
|
|
13
|
+
import '@hh.ru/magritte-internal-slot-registry';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Обёртка над колбэком из пропов со стабильной по ссылке идентичностью. Приложение обычно
|
|
17
|
+
* пересоздаёт хендлеры на каждый рендер, а в slot-registry нужны стабильные ссылки (иначе конфиг
|
|
18
|
+
* перерегистрируется на каждый рендер). Актуальную версию колбэка держим в рефе, наружу отдаём
|
|
19
|
+
* фиксированную обёртку, которая зовёт последний ref. Возвращает `undefined` из вызова, если
|
|
20
|
+
* исходный колбэк не передан.
|
|
21
|
+
*/
|
|
22
|
+
const useStableFn = (fn) => {
|
|
23
|
+
const fnRef = useRef(fn);
|
|
24
|
+
fnRef.current = fn;
|
|
25
|
+
return useCallback((...args) => fnRef.current?.(...args), []);
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Кнопка записи голоса для слота ChatInput. Несёт на себе конфигурацию записи (колбэки,
|
|
29
|
+
* `maxDuration`, `processingLabel`) — она регистрируется в slot-registry и оттуда доезжает до
|
|
30
|
+
* сессии в корне ChatInput. По клику запускает запись через контекст ChatInput. Пока запрашивается
|
|
31
|
+
* доступ к микрофону — показывает лоадер; оформление (паддинги, цвет) берёт из дефолтов слота.
|
|
32
|
+
* Вне ChatInput кнопка неактивна.
|
|
33
|
+
*/
|
|
34
|
+
const ChatInputVoiceButton = ({ maxDuration, mimeType, processingLabel, onRecordStart, onComplete, onError, onCancel, completeButtonProps, cancelButtonProps, onClick, children, 'aria-label': ariaLabel, ...ariaProps }) => {
|
|
35
|
+
const { startVoiceRecord, voiceRecordStatus } = useChatInputContext();
|
|
36
|
+
const recordVoiceLabel = useTranslation('ChatInput', 'recordVoice');
|
|
37
|
+
// список mimeType приложение часто передаёт инлайн-массивом (новая ссылка на каждый рендер);
|
|
38
|
+
// мемоизуем по стабильному строковому ключу, чтобы не перерегистрировать конфиг зря
|
|
39
|
+
const mimeTypeKey = Array.isArray(mimeType) ? mimeType.join(',') : mimeType;
|
|
40
|
+
// колбэки из пропов оборачиваем в стабильные ссылки, тогда конфиг для slot-registry
|
|
41
|
+
// меняет идентичность только при смене примитивов (maxDuration/processingLabel), а не на
|
|
42
|
+
// каждый рендер из-за инлайновых хендлеров приложения — регистрация не дёргается
|
|
43
|
+
const stableRecordStart = useStableFn(onRecordStart);
|
|
44
|
+
const stableComplete = useStableFn(onComplete);
|
|
45
|
+
const stableError = useStableFn(onError);
|
|
46
|
+
const stableCancel = useStableFn(onCancel);
|
|
47
|
+
const config = useMemo(() => ({
|
|
48
|
+
maxDuration,
|
|
49
|
+
mimeType,
|
|
50
|
+
processingLabel,
|
|
51
|
+
onRecordStart: stableRecordStart,
|
|
52
|
+
onComplete: stableComplete,
|
|
53
|
+
onError: stableError,
|
|
54
|
+
onCancel: stableCancel,
|
|
55
|
+
completeButtonProps,
|
|
56
|
+
cancelButtonProps,
|
|
57
|
+
}),
|
|
58
|
+
// mimeType намеренно не в deps — его влияние учтено через стабильный mimeTypeKey
|
|
59
|
+
// eslint-disable-next-line disable-autofix/react-hooks/exhaustive-deps
|
|
60
|
+
[
|
|
61
|
+
maxDuration,
|
|
62
|
+
mimeTypeKey,
|
|
63
|
+
processingLabel,
|
|
64
|
+
stableRecordStart,
|
|
65
|
+
stableComplete,
|
|
66
|
+
stableError,
|
|
67
|
+
stableCancel,
|
|
68
|
+
completeButtonProps,
|
|
69
|
+
cancelButtonProps,
|
|
70
|
+
]);
|
|
71
|
+
useRegisterVoiceConfig(config);
|
|
72
|
+
const handleClick = (event) => {
|
|
73
|
+
onClick?.(event);
|
|
74
|
+
startVoiceRecord?.();
|
|
75
|
+
};
|
|
76
|
+
const iconProps = {
|
|
77
|
+
// дефолтный data-qa до спреда — пользовательский data-qa из ariaProps его перекрывает
|
|
78
|
+
'data-qa': 'record',
|
|
79
|
+
...ariaProps,
|
|
80
|
+
'aria-label': ariaLabel ?? recordVoiceLabel,
|
|
81
|
+
disabled: !startVoiceRecord,
|
|
82
|
+
loading: voiceRecordStatus === 'starting',
|
|
83
|
+
onClick: handleClick,
|
|
84
|
+
// цвета тут нет, потому что в ChatInput он уже пробрасывается на левый и правый слоты
|
|
85
|
+
};
|
|
86
|
+
return (jsx(FocusTarget, { priority: FOCUS_PRIORITY.voice, children: jsx(ChatInputSlotButton, { icon: jsx(MicOutlinedSize24, { ...iconProps }), iconProps: iconProps, onClick: handleClick, children: children }) }));
|
|
87
|
+
};
|
|
88
|
+
ChatInputVoiceButton.displayName = 'ChatInputVoiceButton';
|
|
89
|
+
|
|
90
|
+
export { ChatInputVoiceButton };
|
|
91
|
+
//# sourceMappingURL=ChatInputVoiceButton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatInputVoiceButton.js","sources":["src/public/ChatInputVoiceButton.tsx"],"sourcesContent":["import { type FC, type MouseEvent, type MouseEventHandler, type ReactNode, useCallback, useMemo, useRef } from 'react';\n\nimport { useTranslation } from '@hh.ru/magritte-common-use-translation';\nimport { useChatInputContext } from '@hh.ru/magritte-ui-chat-input/internal/ChatInputContext';\nimport { ChatInputSlotButton } from '@hh.ru/magritte-ui-chat-input/internal/ChatInputSlotButton';\nimport { FocusTarget } from '@hh.ru/magritte-ui-chat-input/internal/FocusTarget';\nimport { FOCUS_PRIORITY } from '@hh.ru/magritte-ui-chat-input/internal/focusGroup';\nimport { useRegisterVoiceConfig } from '@hh.ru/magritte-ui-chat-input/internal/voiceSlotRegistry';\nimport { type ChatInputButtonAria, type ChatInputVoiceRecordConfig } from '@hh.ru/magritte-ui-chat-input/types';\nimport { MicOutlinedSize24 } from '@hh.ru/magritte-ui-icon/icon';\n\n/**\n * Обёртка над колбэком из пропов со стабильной по ссылке идентичностью. Приложение обычно\n * пересоздаёт хендлеры на каждый рендер, а в slot-registry нужны стабильные ссылки (иначе конфиг\n * перерегистрируется на каждый рендер). Актуальную версию колбэка держим в рефе, наружу отдаём\n * фиксированную обёртку, которая зовёт последний ref. Возвращает `undefined` из вызова, если\n * исходный колбэк не передан.\n */\nconst useStableFn = <Args extends unknown[], Return>(\n fn: ((...args: Args) => Return) | undefined\n): ((...args: Args) => Return | undefined) => {\n const fnRef = useRef(fn);\n fnRef.current = fn;\n return useCallback((...args: Args) => fnRef.current?.(...args), []);\n};\n\nexport interface ChatInputVoiceButtonProps extends ChatInputButtonAria, ChatInputVoiceRecordConfig {\n /** Доп. обработчик клика — вызывается вместе со стартом записи, а не вместо него */\n onClick?: MouseEventHandler<HTMLButtonElement>;\n /** Кастомный контент вместо иконки микрофона. Иконку в children оформляем как встроенную\n * (через IconDefaultPropsContext); клик по любому content ловится в любом случае */\n children?: ReactNode;\n}\n\n/**\n * Кнопка записи голоса для слота ChatInput. Несёт на себе конфигурацию записи (колбэки,\n * `maxDuration`, `processingLabel`) — она регистрируется в slot-registry и оттуда доезжает до\n * сессии в корне ChatInput. По клику запускает запись через контекст ChatInput. Пока запрашивается\n * доступ к микрофону — показывает лоадер; оформление (паддинги, цвет) берёт из дефолтов слота.\n * Вне ChatInput кнопка неактивна.\n */\nexport const ChatInputVoiceButton: FC<ChatInputVoiceButtonProps> = ({\n maxDuration,\n mimeType,\n processingLabel,\n onRecordStart,\n onComplete,\n onError,\n onCancel,\n completeButtonProps,\n cancelButtonProps,\n onClick,\n children,\n 'aria-label': ariaLabel,\n ...ariaProps\n}) => {\n const { startVoiceRecord, voiceRecordStatus } = useChatInputContext();\n const recordVoiceLabel = useTranslation('ChatInput', 'recordVoice');\n\n // список mimeType приложение часто передаёт инлайн-массивом (новая ссылка на каждый рендер);\n // мемоизуем по стабильному строковому ключу, чтобы не перерегистрировать конфиг зря\n const mimeTypeKey = Array.isArray(mimeType) ? mimeType.join(',') : mimeType;\n\n // колбэки из пропов оборачиваем в стабильные ссылки, тогда конфиг для slot-registry\n // меняет идентичность только при смене примитивов (maxDuration/processingLabel), а не на\n // каждый рендер из-за инлайновых хендлеров приложения — регистрация не дёргается\n const stableRecordStart = useStableFn(onRecordStart);\n const stableComplete = useStableFn(onComplete);\n const stableError = useStableFn(onError);\n const stableCancel = useStableFn(onCancel);\n\n const config = useMemo<ChatInputVoiceRecordConfig>(\n () => ({\n maxDuration,\n mimeType,\n processingLabel,\n onRecordStart: stableRecordStart,\n onComplete: stableComplete,\n onError: stableError,\n onCancel: stableCancel,\n completeButtonProps,\n cancelButtonProps,\n }),\n // mimeType намеренно не в deps — его влияние учтено через стабильный mimeTypeKey\n // eslint-disable-next-line disable-autofix/react-hooks/exhaustive-deps\n [\n maxDuration,\n mimeTypeKey,\n processingLabel,\n stableRecordStart,\n stableComplete,\n stableError,\n stableCancel,\n completeButtonProps,\n cancelButtonProps,\n ]\n );\n useRegisterVoiceConfig(config);\n\n const handleClick = (event: MouseEvent<HTMLButtonElement>) => {\n onClick?.(event);\n startVoiceRecord?.();\n };\n\n const iconProps = {\n // дефолтный data-qa до спреда — пользовательский data-qa из ariaProps его перекрывает\n 'data-qa': 'record',\n ...ariaProps,\n 'aria-label': ariaLabel ?? recordVoiceLabel,\n disabled: !startVoiceRecord,\n loading: voiceRecordStatus === 'starting',\n onClick: handleClick,\n // цвета тут нет, потому что в ChatInput он уже пробрасывается на левый и правый слоты\n };\n\n return (\n <FocusTarget priority={FOCUS_PRIORITY.voice}>\n <ChatInputSlotButton\n icon={<MicOutlinedSize24 {...iconProps} />}\n iconProps={iconProps}\n onClick={handleClick}\n >\n {children}\n </ChatInputSlotButton>\n </FocusTarget>\n );\n};\n\nChatInputVoiceButton.displayName = 'ChatInputVoiceButton';\n"],"names":["_jsx"],"mappings":";;;;;;;;;;;;;AAWA;;;;;;AAMG;AACH,MAAM,WAAW,GAAG,CAChB,EAA2C,KACF;AACzC,IAAA,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC;AACzB,IAAA,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC;AACnB,IAAA,OAAO,WAAW,CAAC,CAAC,GAAG,IAAU,KAAK,KAAK,CAAC,OAAO,GAAG,GAAG,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;AACxE,CAAC,CAAC;AAUF;;;;;;AAMG;AACU,MAAA,oBAAoB,GAAkC,CAAC,EAChE,WAAW,EACX,QAAQ,EACR,eAAe,EACf,aAAa,EACb,UAAU,EACV,OAAO,EACP,QAAQ,EACR,mBAAmB,EACnB,iBAAiB,EACjB,OAAO,EACP,QAAQ,EACR,YAAY,EAAE,SAAS,EACvB,GAAG,SAAS,EACf,KAAI;IACD,MAAM,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,GAAG,mBAAmB,EAAE,CAAC;IACtE,MAAM,gBAAgB,GAAG,cAAc,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;;;IAIpE,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC;;;;AAK5E,IAAA,MAAM,iBAAiB,GAAG,WAAW,CAAC,aAAa,CAAC,CAAC;AACrD,IAAA,MAAM,cAAc,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;AAC/C,IAAA,MAAM,WAAW,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;AACzC,IAAA,MAAM,YAAY,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;AAE3C,IAAA,MAAM,MAAM,GAAG,OAAO,CAClB,OAAO;QACH,WAAW;QACX,QAAQ;QACR,eAAe;AACf,QAAA,aAAa,EAAE,iBAAiB;AAChC,QAAA,UAAU,EAAE,cAAc;AAC1B,QAAA,OAAO,EAAE,WAAW;AACpB,QAAA,QAAQ,EAAE,YAAY;QACtB,mBAAmB;QACnB,iBAAiB;KACpB,CAAC;;;AAGF,IAAA;QACI,WAAW;QACX,WAAW;QACX,eAAe;QACf,iBAAiB;QACjB,cAAc;QACd,WAAW;QACX,YAAY;QACZ,mBAAmB;QACnB,iBAAiB;AACpB,KAAA,CACJ,CAAC;IACF,sBAAsB,CAAC,MAAM,CAAC,CAAC;AAE/B,IAAA,MAAM,WAAW,GAAG,CAAC,KAAoC,KAAI;AACzD,QAAA,OAAO,GAAG,KAAK,CAAC,CAAC;QACjB,gBAAgB,IAAI,CAAC;AACzB,KAAC,CAAC;AAEF,IAAA,MAAM,SAAS,GAAG;;AAEd,QAAA,SAAS,EAAE,QAAQ;AACnB,QAAA,GAAG,SAAS;QACZ,YAAY,EAAE,SAAS,IAAI,gBAAgB;QAC3C,QAAQ,EAAE,CAAC,gBAAgB;QAC3B,OAAO,EAAE,iBAAiB,KAAK,UAAU;AACzC,QAAA,OAAO,EAAE,WAAW;;KAEvB,CAAC;AAEF,IAAA,QACIA,GAAA,CAAC,WAAW,EAAA,EAAC,QAAQ,EAAE,cAAc,CAAC,KAAK,EACvC,QAAA,EAAAA,GAAA,CAAC,mBAAmB,EAAA,EAChB,IAAI,EAAEA,GAAA,CAAC,iBAAiB,EAAA,EAAA,GAAK,SAAS,EAAA,CAAI,EAC1C,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,WAAW,EAEnB,QAAA,EAAA,QAAQ,EACS,CAAA,EAAA,CACZ,EAChB;AACN,EAAE;AAEF,oBAAoB,CAAC,WAAW,GAAG,sBAAsB;;;;"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import './index.css';
|
|
2
|
+
var styles = {"chat-input":"magritte-chat-input___P69oN_1-0-1","chatInput":"magritte-chat-input___P69oN_1-0-1","no-background":"magritte-no-background___eiIY1_1-0-1","noBackground":"magritte-no-background___eiIY1_1-0-1","text-input-focused":"magritte-text-input-focused___TfyLu_1-0-1","textInputFocused":"magritte-text-input-focused___TfyLu_1-0-1","content-grid":"magritte-content-grid___g03sT_1-0-1","contentGrid":"magritte-content-grid___g03sT_1-0-1","preview":"magritte-preview___baXKi_1-0-1","left":"magritte-left___7JZYp_1-0-1","central":"magritte-central___zkguj_1-0-1","right":"magritte-right___-tGcO_1-0-1","scrollbar-cell":"magritte-scrollbar-cell___--M2x_1-0-1","scrollbarCell":"magritte-scrollbar-cell___--M2x_1-0-1","text-input":"magritte-text-input___3AFO4_1-0-1","textInput":"magritte-text-input___3AFO4_1-0-1","layout-vertical":"magritte-layout-vertical___mVTr-_1-0-1","layoutVertical":"magritte-layout-vertical___mVTr-_1-0-1","has-central":"magritte-has-central___VRZik_1-0-1","hasCentral":"magritte-has-central___VRZik_1-0-1","no-left-controls":"magritte-no-left-controls___7r6yy_1-0-1","noLeftControls":"magritte-no-left-controls___7r6yy_1-0-1","no-right-controls":"magritte-no-right-controls___HBSag_1-0-1","noRightControls":"magritte-no-right-controls___HBSag_1-0-1","chips":"magritte-chips___gLao5_1-0-1","chips-container":"magritte-chips-container___-bS5O_1-0-1","chipsContainer":"magritte-chips-container___-bS5O_1-0-1","preview-block":"magritte-preview-block___iiPdS_1-0-1","previewBlock":"magritte-preview-block___iiPdS_1-0-1","preview-side":"magritte-preview-side___ymR-u_1-0-1","previewSide":"magritte-preview-side___ymR-u_1-0-1","preview-content":"magritte-preview-content___ZgyBD_1-0-1","previewContent":"magritte-preview-content___ZgyBD_1-0-1","preview-title":"magritte-preview-title___SkFeZ_1-0-1","previewTitle":"magritte-preview-title___SkFeZ_1-0-1","preview-subtitle":"magritte-preview-subtitle___58qfR_1-0-1","previewSubtitle":"magritte-preview-subtitle___58qfR_1-0-1","text-input-wrapper":"magritte-text-input-wrapper___TPbwC_1-0-1","textInputWrapper":"magritte-text-input-wrapper___TPbwC_1-0-1","with-top-fade":"magritte-with-top-fade___okgqo_1-0-1","withTopFade":"magritte-with-top-fade___okgqo_1-0-1","with-bottom-fade":"magritte-with-bottom-fade___82gr8_1-0-1","withBottomFade":"magritte-with-bottom-fade___82gr8_1-0-1","measure":"magritte-measure___xNKc0_1-0-1","voice-record":"magritte-voice-record___p64PV_1-0-1","voiceRecord":"magritte-voice-record___p64PV_1-0-1","voice-record-left":"magritte-voice-record-left___njEt-_1-0-1","voiceRecordLeft":"magritte-voice-record-left___njEt-_1-0-1","voice-record-right":"magritte-voice-record-right___UTv-U_1-0-1","voiceRecordRight":"magritte-voice-record-right___UTv-U_1-0-1","voice-processing":"magritte-voice-processing___C0xnX_1-0-1","voiceProcessing":"magritte-voice-processing___C0xnX_1-0-1","voice-record-indicator":"magritte-voice-record-indicator___V8ybb_1-0-1","voiceRecordIndicator":"magritte-voice-record-indicator___V8ybb_1-0-1","voice-record-indicator-bar":"magritte-voice-record-indicator-bar___jgyqz_1-0-1","voiceRecordIndicatorBar":"magritte-voice-record-indicator-bar___jgyqz_1-0-1","voice-record-indicator-wave":"magritte-voice-record-indicator-wave___4VsyE_1-0-1","voiceRecordIndicatorWave":"magritte-voice-record-indicator-wave___4VsyE_1-0-1","voice-record-indicator-has-sound":"magritte-voice-record-indicator-has-sound___67t5g_1-0-1","voiceRecordIndicatorHasSound":"magritte-voice-record-indicator-has-sound___67t5g_1-0-1","mode-container":"magritte-mode-container___nATVD_1-0-1","modeContainer":"magritte-mode-container___nATVD_1-0-1","voice-overlay":"magritte-voice-overlay___LbxAI_1-0-1","voiceOverlay":"magritte-voice-overlay___LbxAI_1-0-1"};
|
|
3
|
+
|
|
4
|
+
export { styles as s };
|
|
5
|
+
//# sourceMappingURL=styles-CQQdqOFh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"styles-CQQdqOFh.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;"}
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type AriaAttributes, type AriaRole } from 'react';
|
|
2
|
+
/** aria/тестовые пропсы для икон-кнопок ChatInput (микрофон, отправка, подтверждение и отмена записи). */
|
|
3
|
+
export interface ChatInputButtonAria extends Omit<AriaAttributes, 'aria-hidden'> {
|
|
4
|
+
'aria-hidden'?: boolean;
|
|
5
|
+
role?: AriaRole;
|
|
6
|
+
'data-qa'?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Статус сессии записи голоса. */
|
|
9
|
+
export type VoiceRecordStatus = 'idle' | 'starting' | 'recording' | 'processing';
|
|
10
|
+
/**
|
|
11
|
+
* Конфигурация записи голоса. Задаётся прямо на `ChatInput.VoiceButton` (конфиг без кнопки
|
|
12
|
+
* бессмыслен и наоборот), а до сессии в корне доезжает через slot-registry. Поля — исходные
|
|
13
|
+
* колбэки/параметры сессии записи плюс aria кнопок блока записи.
|
|
14
|
+
*/
|
|
15
|
+
export interface ChatInputVoiceRecordConfig {
|
|
16
|
+
/** Максимальная длительность записи в секундах; по достижении запись финализируется,
|
|
17
|
+
* как по кнопке подтверждения. Не задана — без ограничения */
|
|
18
|
+
maxDuration?: number;
|
|
19
|
+
/** Предпочитаемый MIME-тип записи (можно списком по убыванию приоритета, например
|
|
20
|
+
* `['audio/webm;codecs=opus', 'audio/mp4']`). Берётся первый поддерживаемый браузером
|
|
21
|
+
* (`MediaRecorder.isTypeSupported`); если ни один не подошёл или проп не задан — кодек
|
|
22
|
+
* выбирает браузер (в Safari это `audio/mp4`, в Chrome/Firefox — `audio/webm`). Имя и
|
|
23
|
+
* расширение итогового файла выводятся из фактического типа записи */
|
|
24
|
+
mimeType?: string | string[];
|
|
25
|
+
/** Текст в состоянии обработки записи, например «Обрабатываю запись» */
|
|
26
|
+
processingLabel?: string;
|
|
27
|
+
/** Запись фактически началась (микрофон получен, контент подменился блоком записи).
|
|
28
|
+
* Точка подключения к живому звуку: анализатор для визуализации, стриминговое распознавание
|
|
29
|
+
* и т.п. Стримом владеет ChatInput — останавливать его треки нельзя, только читать;
|
|
30
|
+
* для независимого времени жизни — `stream.clone()` */
|
|
31
|
+
onRecordStart?: (stream: MediaStream) => void;
|
|
32
|
+
/** Готовая запись — по кнопке подтверждения или `maxDuration`. Если вернуть промис,
|
|
33
|
+
* состояние «обработка» держится до его завершения; после ChatInput сам вернётся
|
|
34
|
+
* к текстовому вводу. Ошибки обработки — зона ответственности приложения */
|
|
35
|
+
onComplete: (file: File) => void | Promise<void>;
|
|
36
|
+
/** Не удалось получить микрофон; ChatInput остался в обычном режиме */
|
|
37
|
+
onError?: (error: unknown) => void;
|
|
38
|
+
/** Запись отменена крестиком; файл выброшен */
|
|
39
|
+
onCancel?: VoidFunction;
|
|
40
|
+
/** aria/data-qa пропсы кнопки подтверждения записи (галочка) в блоке записи.
|
|
41
|
+
* Своя `aria-label` перекрывает дефолтную */
|
|
42
|
+
completeButtonProps?: ChatInputButtonAria;
|
|
43
|
+
/** aria/data-qa пропсы кнопки отмены записи (крестик) в блоке записи.
|
|
44
|
+
* Своя `aria-label` перекрывает дефолтную */
|
|
45
|
+
cancelButtonProps?: ChatInputButtonAria;
|
|
46
|
+
}
|
package/types.js
ADDED
package/types.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|