@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.
- package/index.css +117 -117
- package/index.d.ts +10 -10
- package/index.js +1 -1
- package/index.mock.d.ts +9 -9
- package/index.mock.js +37 -36
- package/index.mock.js.map +1 -1
- package/internal/ChatInputContext.d.ts +1 -1
- package/internal/ChatInputContext.js.map +1 -1
- package/internal/ChatInputSlotButton.js +13 -13
- package/internal/ChatInputSlotButton.js.map +1 -1
- package/internal/FocusTarget.js +3 -10
- package/internal/FocusTarget.js.map +1 -1
- package/internal/ModeTransition.js +41 -16
- package/internal/ModeTransition.js.map +1 -1
- package/internal/TextInput.d.ts +1 -1
- package/internal/TextInput.js +146 -126
- package/internal/TextInput.js.map +1 -1
- package/internal/VoiceRecord.d.ts +1 -1
- package/internal/VoiceRecord.js +66 -24
- package/internal/VoiceRecord.js.map +1 -1
- package/internal/VoiceRecordIndicator.js +22 -9
- package/internal/VoiceRecordIndicator.js.map +1 -1
- package/internal/flattenFragments.js +9 -13
- package/internal/flattenFragments.js.map +1 -1
- package/internal/focusGroup.js +18 -28
- package/internal/focusGroup.js.map +1 -1
- package/internal/useHasSound.js +49 -64
- package/internal/useHasSound.js.map +1 -1
- package/internal/useObjectUrl.js +15 -21
- package/internal/useObjectUrl.js.map +1 -1
- package/internal/useRovingFocusGroup.js +102 -137
- package/internal/useRovingFocusGroup.js.map +1 -1
- package/internal/useSendOnEnter.js +44 -46
- package/internal/useSendOnEnter.js.map +1 -1
- package/internal/useVoiceRecordSession.d.ts +1 -1
- package/internal/useVoiceRecordSession.js +185 -219
- package/internal/useVoiceRecordSession.js.map +1 -1
- package/internal/voiceSlotRegistry.d.ts +1 -1
- package/internal/voiceSlotRegistry.js +3 -3
- package/internal/voiceSlotRegistry.js.map +1 -1
- package/package.json +11 -11
- package/public/ChatInput.d.ts +6 -6
- package/public/ChatInput.js +129 -73
- package/public/ChatInput.js.map +1 -1
- package/public/ChatInputChips.js +4 -4
- package/public/ChatInputChips.js.map +1 -1
- package/public/ChatInputFile.d.ts +1 -1
- package/public/ChatInputFile.js +107 -47
- package/public/ChatInputFile.js.map +1 -1
- package/public/ChatInputFiles.js +10 -3
- package/public/ChatInputFiles.js.map +1 -1
- package/public/ChatInputPreview.js +14 -9
- package/public/ChatInputPreview.js.map +1 -1
- package/public/ChatInputSendButton.d.ts +1 -1
- package/public/ChatInputSendButton.js +34 -23
- package/public/ChatInputSendButton.js.map +1 -1
- package/public/ChatInputVoiceButton.d.ts +1 -1
- package/public/ChatInputVoiceButton.js +87 -91
- package/public/ChatInputVoiceButton.js.map +1 -1
- package/public/getChatInputFileType.d.ts +1 -1
- package/public/getChatInputFileType.js +7 -10
- package/public/getChatInputFileType.js.map +1 -1
- package/styles-hEUo5BXU.js +5 -0
- package/styles-hEUo5BXU.js.map +1 -0
- package/styles-B3o1JdaK.js +0 -5
- package/styles-B3o1JdaK.js.map +0 -1
package/public/ChatInput.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { type ChangeEventHandler, type ForwardedRef, type ForwardRefExoticComponent, type ReactNode, type RefAttributes, type TextareaHTMLAttributes } from 'react';
|
|
2
|
-
import { ChatInputChips } from '
|
|
3
|
-
import { ChatInputFile } from '
|
|
4
|
-
import { ChatInputFiles } from '
|
|
5
|
-
import { ChatInputPreview } from '
|
|
6
|
-
import { ChatInputSendButton } from '
|
|
7
|
-
import { ChatInputVoiceButton } from '
|
|
2
|
+
import { ChatInputChips } from '../public/ChatInputChips';
|
|
3
|
+
import { ChatInputFile } from '../public/ChatInputFile';
|
|
4
|
+
import { ChatInputFiles } from '../public/ChatInputFiles';
|
|
5
|
+
import { ChatInputPreview } from '../public/ChatInputPreview';
|
|
6
|
+
import { ChatInputSendButton } from '../public/ChatInputSendButton';
|
|
7
|
+
import { ChatInputVoiceButton } from '../public/ChatInputVoiceButton';
|
|
8
8
|
interface ChatInputOwnProps {
|
|
9
9
|
/** Левый слот */
|
|
10
10
|
leftActions?: ReactNode;
|
package/public/ChatInput.js
CHANGED
|
@@ -19,7 +19,7 @@ import { ChatInputPreview } from './ChatInputPreview.js';
|
|
|
19
19
|
import { ChatInputSendButton } from './ChatInputSendButton.js';
|
|
20
20
|
import { ChatInputVoiceButton } from './ChatInputVoiceButton.js';
|
|
21
21
|
import { IconDefaultPropsContext } from '@hh.ru/magritte-ui-icon';
|
|
22
|
-
import { s as styles } from '../styles-
|
|
22
|
+
import { s as styles } from '../styles-hEUo5BXU.js';
|
|
23
23
|
import 'motion/react';
|
|
24
24
|
import '@hh.ru/magritte-design-tokens/core-tokens';
|
|
25
25
|
import '../internal/useSendOnEnter.js';
|
|
@@ -42,83 +42,139 @@ import '@hh.ru/magritte-ui-link';
|
|
|
42
42
|
import '../internal/ChatInputSlotButton.js';
|
|
43
43
|
|
|
44
44
|
const DEFAULT_ICON_PROPS = {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
borderRadius: 12,
|
|
46
|
+
padding: 8,
|
|
47
|
+
initialColor: "secondary"
|
|
48
48
|
};
|
|
49
|
-
const ChatInputInner = ({
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
49
|
+
const ChatInputInner = ({
|
|
50
|
+
leftActions,
|
|
51
|
+
rightActions,
|
|
52
|
+
centralActions,
|
|
53
|
+
preview,
|
|
54
|
+
maxHeight,
|
|
55
|
+
layout = "horizontal",
|
|
56
|
+
onSend,
|
|
57
|
+
wrapperRef,
|
|
58
|
+
textareaRef,
|
|
59
|
+
noBackground = false,
|
|
60
|
+
disabled = false,
|
|
61
|
+
"data-qa": dataQa = "chat-input",
|
|
62
|
+
...textareaProps
|
|
63
|
+
}) => {
|
|
64
|
+
const outerDisabled = useDisabled();
|
|
65
|
+
const isDisabled = disabled || outerDisabled;
|
|
66
|
+
const rootRef = useRef(null);
|
|
67
|
+
const rootRefCallback = useMultipleRefs(rootRef, wrapperRef);
|
|
68
|
+
const textareaLocalRef = useRef(null);
|
|
69
|
+
const textareaRefCallback = useMultipleRefs(textareaLocalRef, textareaRef);
|
|
70
|
+
const [containerWidth, setContainerWidth] = useState();
|
|
71
|
+
useLayoutEffect(() => {
|
|
72
|
+
const root = rootRef.current;
|
|
73
|
+
if (!root) {
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
const observer = new ResizeObserver((entries) => {
|
|
77
|
+
const width = Math.round(entries[0]?.contentRect.width ?? 0);
|
|
78
|
+
setContainerWidth((prev) => prev === width ? prev : width);
|
|
79
|
+
});
|
|
80
|
+
observer.observe(root);
|
|
81
|
+
return () => observer.disconnect();
|
|
82
|
+
}, []);
|
|
83
|
+
const voiceConfig = useVoiceConfig();
|
|
84
|
+
const voiceSession = useVoiceRecordSession(voiceConfig ?? {});
|
|
85
|
+
const contextValue = useMemo(
|
|
86
|
+
() => ({
|
|
87
|
+
onSend,
|
|
88
|
+
startVoiceRecord: voiceSession.start,
|
|
89
|
+
voiceRecordStatus: voiceSession.status
|
|
90
|
+
}),
|
|
91
|
+
[onSend, voiceSession.start, voiceSession.status]
|
|
92
|
+
);
|
|
93
|
+
const isVoiceMode = voiceSession.status === "recording" || voiceSession.status === "processing";
|
|
94
|
+
const { focusStop } = useRovingFocusGroup(rootRef);
|
|
95
|
+
const prevVoiceModeRef = useRef(isVoiceMode);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
const wasVoiceMode = prevVoiceModeRef.current;
|
|
98
|
+
prevVoiceModeRef.current = isVoiceMode;
|
|
99
|
+
if (wasVoiceMode === isVoiceMode) {
|
|
100
|
+
return void 0;
|
|
101
|
+
}
|
|
102
|
+
const frameId = requestAnimationFrame(() => {
|
|
103
|
+
if (isVoiceMode) {
|
|
104
|
+
focusStop();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const textarea = textareaLocalRef.current;
|
|
108
|
+
if (textarea) {
|
|
109
|
+
textarea.focus();
|
|
110
|
+
const caret = textarea.value.length;
|
|
111
|
+
textarea.setSelectionRange(caret, caret);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
return () => cancelAnimationFrame(frameId);
|
|
115
|
+
}, [isVoiceMode, focusStop]);
|
|
116
|
+
return /* @__PURE__ */ jsx(DisabledContext.Provider, { value: isDisabled, children: /* @__PURE__ */ jsx(ChatInputContextProvider, { value: contextValue, children: /* @__PURE__ */ jsx(
|
|
117
|
+
"div",
|
|
118
|
+
{
|
|
119
|
+
ref: rootRefCallback,
|
|
120
|
+
className: classNames(styles.chatInput, { [styles.noBackground]: noBackground }),
|
|
121
|
+
"data-qa": dataQa,
|
|
122
|
+
children: /* @__PURE__ */ jsx(
|
|
123
|
+
ModeTransition,
|
|
124
|
+
{
|
|
125
|
+
mode: isVoiceMode ? "voice" : "text",
|
|
126
|
+
voice: /* @__PURE__ */ jsx(
|
|
127
|
+
VoiceRecord,
|
|
128
|
+
{
|
|
129
|
+
mode: voiceSession.status === "processing" ? "processing" : "recording",
|
|
130
|
+
stream: voiceSession.stream,
|
|
131
|
+
startedAt: voiceSession.startedAt,
|
|
132
|
+
processingLabel: voiceConfig?.processingLabel,
|
|
133
|
+
onComplete: voiceSession.stop,
|
|
134
|
+
onCancel: voiceSession.cancel,
|
|
135
|
+
completeButtonProps: voiceConfig?.completeButtonProps,
|
|
136
|
+
cancelButtonProps: voiceConfig?.cancelButtonProps,
|
|
137
|
+
"data-qa": "chat-input-voice-record"
|
|
100
138
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
139
|
+
),
|
|
140
|
+
text: /* @__PURE__ */ jsxs(
|
|
141
|
+
"div",
|
|
142
|
+
{
|
|
143
|
+
className: classNames(styles.contentGrid, {
|
|
144
|
+
[styles.noLeftControls]: !leftActions,
|
|
145
|
+
[styles.noRightControls]: !rightActions,
|
|
146
|
+
[styles.hasCentral]: !!centralActions
|
|
147
|
+
}),
|
|
148
|
+
children: [
|
|
149
|
+
/* @__PURE__ */ jsx(
|
|
150
|
+
TextInput,
|
|
151
|
+
{
|
|
152
|
+
...textareaProps,
|
|
153
|
+
ref: textareaRefCallback,
|
|
154
|
+
layout,
|
|
155
|
+
onSend,
|
|
156
|
+
maxHeight,
|
|
157
|
+
containerWidth,
|
|
158
|
+
hasCentral: !!centralActions,
|
|
159
|
+
disabled: isDisabled,
|
|
160
|
+
hasBothSideControls: !!leftActions && !!rightActions
|
|
161
|
+
}
|
|
162
|
+
),
|
|
163
|
+
!!rightActions && /* @__PURE__ */ jsx(IconDefaultPropsContext, { props: DEFAULT_ICON_PROPS, children: /* @__PURE__ */ jsx("div", { className: styles.right, ...focusPriorityProps(FOCUS_PRIORITY.rightSlot), children: rightActions }) }),
|
|
164
|
+
!!leftActions && /* @__PURE__ */ jsx(IconDefaultPropsContext, { props: DEFAULT_ICON_PROPS, children: /* @__PURE__ */ jsx("div", { className: styles.left, ...focusPriorityProps(FOCUS_PRIORITY.leftSlot), children: leftActions }) }),
|
|
165
|
+
!!centralActions && /* @__PURE__ */ jsx("div", { className: styles.central, ...focusPriorityProps(FOCUS_PRIORITY.centralSlot), children: centralActions }),
|
|
166
|
+
!!preview && /* @__PURE__ */ jsx("div", { className: styles.preview, ...focusPriorityProps(FOCUS_PRIORITY.previewSlot), children: preview })
|
|
167
|
+
]
|
|
106
168
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
[styles.noRightControls]: !rightActions,
|
|
113
|
-
[styles.hasCentral]: !!centralActions,
|
|
114
|
-
}), 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 }))] }) }) }) }) }));
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
) }) });
|
|
115
174
|
};
|
|
116
|
-
|
|
117
|
-
// конфиг, ChatInputInner (ниже провайдера) читает его. forwardRef-реф на textarea прокидываем
|
|
118
|
-
// вниз обычным пропом
|
|
119
|
-
const ChatInputComponent = (props, ref) => (jsx(ChatInputVoiceSlotProvider, { children: jsx(ChatInputInner, { ...props, textareaRef: ref }) }));
|
|
175
|
+
const ChatInputComponent = (props, ref) => /* @__PURE__ */ jsx(ChatInputVoiceSlotProvider, { children: /* @__PURE__ */ jsx(ChatInputInner, { ...props, textareaRef: ref }) });
|
|
120
176
|
const ChatInput = forwardRef(ChatInputComponent);
|
|
121
|
-
ChatInput.displayName =
|
|
177
|
+
ChatInput.displayName = "ChatInput";
|
|
122
178
|
ChatInput.Chips = ChatInputChips;
|
|
123
179
|
ChatInput.File = ChatInputFile;
|
|
124
180
|
ChatInput.Files = ChatInputFiles;
|
package/public/ChatInput.js.map
CHANGED
|
@@ -1 +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 { ChatInputFile } from '@hh.ru/magritte-ui-chat-input/public/ChatInputFile';\nimport { ChatInputFiles } from '@hh.ru/magritte-ui-chat-input/public/ChatInputFiles';\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 File: typeof ChatInputFile;\n Files: typeof ChatInputFiles;\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.File = ChatInputFile;\nChatInput.Files = ChatInputFiles;\nChatInput.Preview = ChatInputPreview;\nChatInput.SendButton = ChatInputSendButton;\nChatInput.VoiceButton = ChatInputVoiceButton;\n\nexport { ChatInput };\n"],"names":["_jsx","_jsxs"],"mappings":"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"}
|
|
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 { ChatInputFile } from '@hh.ru/magritte-ui-chat-input/public/ChatInputFile';\nimport { ChatInputFiles } from '@hh.ru/magritte-ui-chat-input/public/ChatInputFiles';\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 File: typeof ChatInputFile;\n Files: typeof ChatInputFiles;\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.File = ChatInputFile;\nChatInput.Files = ChatInputFiles;\nChatInput.Preview = ChatInputPreview;\nChatInput.SendButton = ChatInputSendButton;\nChatInput.VoiceButton = ChatInputVoiceButton;\n\nexport { ChatInput };\n"],"names":[],"mappings":"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"}
|
package/public/ChatInputChips.js
CHANGED
|
@@ -4,11 +4,11 @@ import { isValidElement } from 'react';
|
|
|
4
4
|
import { flattenFragments } from '../internal/flattenFragments.js';
|
|
5
5
|
import { ChipDefaultPropsContext } from '@hh.ru/magritte-ui-chips';
|
|
6
6
|
import { ScrollableContainer } from '@hh.ru/magritte-ui-scrollable-container';
|
|
7
|
-
import { s as styles } from '../styles-
|
|
7
|
+
import { s as styles } from '../styles-hEUo5BXU.js';
|
|
8
8
|
|
|
9
|
-
const CHIP_DEFAULT_PROPS = { size:
|
|
10
|
-
const ChatInputChips = ({ children }) =>
|
|
11
|
-
ChatInputChips.displayName =
|
|
9
|
+
const CHIP_DEFAULT_PROPS = { size: "extra-small" };
|
|
10
|
+
const ChatInputChips = ({ children }) => /* @__PURE__ */ jsx("div", { className: styles.chipsContainer, children: /* @__PURE__ */ jsx(ChipDefaultPropsContext, { props: CHIP_DEFAULT_PROPS, children: /* @__PURE__ */ jsx(ScrollableContainer, { children: /* @__PURE__ */ jsx(ScrollableContainer.DefaultLayout, { size: "small", fadeOnMobile: true, children: /* @__PURE__ */ jsx(ScrollableContainer.Gallery, { children: /* @__PURE__ */ jsx("div", { className: styles.chips, children: flattenFragments(children).map((child, index) => /* @__PURE__ */ jsx(ScrollableContainer.Item, { children: child }, isValidElement(child) && child.key || index)) }) }) }) }) }) });
|
|
11
|
+
ChatInputChips.displayName = "ChatInputChips";
|
|
12
12
|
|
|
13
13
|
export { ChatInputChips };
|
|
14
14
|
//# sourceMappingURL=ChatInputChips.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatInputChips.js","sources":["src/public/ChatInputChips.tsx"],"sourcesContent":["import { isValidElement, type FC, type ReactNode } from 'react';\n\nimport { flattenFragments } from '@hh.ru/magritte-ui-chat-input/internal/flattenFragments';\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\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":[
|
|
1
|
+
{"version":3,"file":"ChatInputChips.js","sources":["src/public/ChatInputChips.tsx"],"sourcesContent":["import { isValidElement, type FC, type ReactNode } from 'react';\n\nimport { flattenFragments } from '@hh.ru/magritte-ui-chat-input/internal/flattenFragments';\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\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":[],"mappings":";;;;;;;AAaA,MAAM,kBAAA,GAAqB,EAAE,IAAA,EAAM,aAAc,EAAA,CAAA;AAEpC,MAAA,cAAA,GAA0C,CAAC,EAAE,QAAS,EAAA,qBAC9D,GAAA,CAAA,KAAA,EAAA,EAAI,SAAW,EAAA,MAAA,CAAO,cACnB,EAAA,QAAA,kBAAA,GAAA,CAAC,uBAAwB,EAAA,EAAA,KAAA,EAAO,oBAC5B,QAAC,kBAAA,GAAA,CAAA,mBAAA,EAAA,EACG,QAAC,kBAAA,GAAA,CAAA,mBAAA,CAAoB,aAApB,EAAA,EAAkC,IAAK,EAAA,OAAA,EAAQ,YAAY,EAAA,IAAA,EACxD,QAAC,kBAAA,GAAA,CAAA,mBAAA,CAAoB,OAApB,EAAA,EACG,8BAAC,KAAI,EAAA,EAAA,SAAA,EAAW,MAAO,CAAA,KAAA,EAGlB,QAAiB,EAAA,gBAAA,CAAA,QAAQ,CAAE,CAAA,GAAA,CAAI,CAAC,KAAA,EAAO,KACpC,qBAAA,GAAA,CAAC,mBAAoB,CAAA,IAAA,EAApB,EACI,QAAA,EAAA,KAAA,EAAA,EAD2B,cAAe,CAAA,KAAK,CAAK,IAAA,KAAA,CAAM,GAAQ,IAAA,KAEvE,CACH,CAAA,EACL,CACJ,EAAA,CAAA,EACJ,CACJ,EAAA,CAAA,EACJ,CACJ,EAAA,EAAA;AAGJ,cAAA,CAAe,WAAc,GAAA,gBAAA;;;;"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type HTMLAttributes, type MouseEventHandler, type ReactNode } from 'react';
|
|
2
|
-
import { type ChatInputButtonAria } from '
|
|
2
|
+
import { type ChatInputButtonAria } from '../types';
|
|
3
3
|
export type ChatInputFileState = 'loading' | 'loaded' | 'invalid';
|
|
4
4
|
export type ChatInputFileType = 'doc' | 'video' | 'image';
|
|
5
5
|
export interface ChatInputFileProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'className'> {
|
package/public/ChatInputFile.js
CHANGED
|
@@ -5,70 +5,130 @@ import classNames from 'classnames';
|
|
|
5
5
|
import { useTranslation } from '@hh.ru/magritte-common-use-translation';
|
|
6
6
|
import { Cell, CellText } from '@hh.ru/magritte-ui-cell';
|
|
7
7
|
import { useObjectUrl } from '../internal/useObjectUrl.js';
|
|
8
|
-
import { CheckCircleOutlinedSize16, ExclamationTriangleOutlinedSize16, ImageOutlinedSize24,
|
|
8
|
+
import { CrossOutlinedSize16, CheckCircleOutlinedSize16, ExclamationTriangleOutlinedSize16, ImageOutlinedSize24, VideocameraOutlinedSize24, DocumentOutlinedSize24 } from '@hh.ru/magritte-ui-icon/icon';
|
|
9
9
|
import { LinkDefaultPropsContext } from '@hh.ru/magritte-ui-link';
|
|
10
10
|
import { Loader } from '@hh.ru/magritte-ui-loader';
|
|
11
11
|
import { TextDefaultPropsContext } from '@hh.ru/magritte-ui-typography';
|
|
12
|
-
import { s as styles } from '../styles-
|
|
12
|
+
import { s as styles } from '../styles-hEUo5BXU.js';
|
|
13
13
|
|
|
14
14
|
const splitFileName = (name) => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return { base: name.slice(0, dotIndex), extension: name.slice(dotIndex) };
|
|
15
|
+
const dotIndex = name.lastIndexOf(".");
|
|
16
|
+
if (dotIndex <= 0 || dotIndex === name.length - 1) {
|
|
17
|
+
return { base: name, extension: "" };
|
|
18
|
+
}
|
|
19
|
+
return { base: name.slice(0, dotIndex), extension: name.slice(dotIndex) };
|
|
21
20
|
};
|
|
22
21
|
const FileName = ({ name }) => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
if (typeof name !== "string") {
|
|
23
|
+
return /* @__PURE__ */ jsx(Fragment, { children: name });
|
|
24
|
+
}
|
|
25
|
+
const { base, extension } = splitFileName(name);
|
|
26
|
+
return /* @__PURE__ */ jsxs("span", { className: styles.fileName, children: [
|
|
27
|
+
/* @__PURE__ */ jsx("span", { className: styles.fileNameBase, children: base }),
|
|
28
|
+
extension && /* @__PURE__ */ jsx("span", { className: styles.fileNameExtension, children: extension })
|
|
29
|
+
] });
|
|
30
30
|
};
|
|
31
31
|
const STATUS_ICON = {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
loading: /* @__PURE__ */ jsx(Loader, { size: 16 }),
|
|
33
|
+
loaded: /* @__PURE__ */ jsx(CheckCircleOutlinedSize16, { initialColor: "positive" }),
|
|
34
|
+
invalid: /* @__PURE__ */ jsx(ExclamationTriangleOutlinedSize16, { initialColor: "negative" })
|
|
35
35
|
};
|
|
36
|
-
const LINK_DEFAULT_PROPS = { typography:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
const LINK_DEFAULT_PROPS = { typography: "label-3-regular", inline: true };
|
|
37
|
+
const NAME_TEXT_PROPS = { typography: "label-3-regular", style: "primary", ellipsis: true };
|
|
38
|
+
const DESCRIPTION_TEXT_PROPS = { typography: "label-3-regular", style: "secondary", ellipsis: true };
|
|
39
|
+
const ChatInputFile = forwardRef(
|
|
40
|
+
({
|
|
41
|
+
"data-qa": dataQa = "chat-input-file",
|
|
42
|
+
description,
|
|
43
|
+
name,
|
|
44
|
+
onRemove,
|
|
45
|
+
preview,
|
|
46
|
+
previewAlt,
|
|
47
|
+
removeButtonProps,
|
|
48
|
+
state = "loaded",
|
|
49
|
+
type = "doc",
|
|
50
|
+
...props
|
|
51
|
+
}, ref) => {
|
|
52
|
+
const removeLabel = useTranslation("ChatInput", "removeFile");
|
|
53
|
+
const isImage = type === "image";
|
|
54
|
+
const previewSrc = useObjectUrl(isImage ? preview : void 0);
|
|
49
55
|
const [failedSrc, setFailedSrc] = useState();
|
|
50
|
-
if (state !==
|
|
51
|
-
|
|
56
|
+
if (state !== "loaded" && failedSrc !== void 0) {
|
|
57
|
+
setFailedSrc(void 0);
|
|
52
58
|
}
|
|
53
59
|
let content;
|
|
54
60
|
if (isImage) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
const showPreview = state === "loaded" && !!previewSrc && previewSrc !== failedSrc;
|
|
62
|
+
let placeholder = null;
|
|
63
|
+
if (state === "loading") {
|
|
64
|
+
placeholder = /* @__PURE__ */ jsx(Loader, { size: 24 });
|
|
65
|
+
} else if (!showPreview) {
|
|
66
|
+
placeholder = /* @__PURE__ */ jsx(ImageOutlinedSize24, { initialColor: "tertiary" });
|
|
67
|
+
}
|
|
68
|
+
content = /* @__PURE__ */ jsx("div", { className: classNames(styles.fileTile, { [styles.fileTilePlaceholder]: !showPreview }), children: showPreview ? /* @__PURE__ */ jsx(
|
|
69
|
+
"img",
|
|
70
|
+
{
|
|
71
|
+
className: styles.fileTileImage,
|
|
72
|
+
src: previewSrc,
|
|
73
|
+
alt: previewAlt ?? (typeof name === "string" ? name : ""),
|
|
74
|
+
onError: () => setFailedSrc(previewSrc)
|
|
59
75
|
}
|
|
60
|
-
|
|
61
|
-
|
|
76
|
+
) : placeholder });
|
|
77
|
+
} else {
|
|
78
|
+
const Icon = type === "video" ? VideocameraOutlinedSize24 : DocumentOutlinedSize24;
|
|
79
|
+
content = /* @__PURE__ */ jsx("div", { className: classNames(styles.fileCard, { [styles.fileCardRemovable]: !!onRemove }), children: /* @__PURE__ */ jsxs(
|
|
80
|
+
Cell,
|
|
81
|
+
{
|
|
82
|
+
align: "center",
|
|
83
|
+
horPadding: false,
|
|
84
|
+
vertPadding: false,
|
|
85
|
+
left: /* @__PURE__ */ jsx(Icon, { initialColor: "secondary", backgroundStyle: "secondary", padding: 12, borderRadius: 8 }),
|
|
86
|
+
children: [
|
|
87
|
+
!!name && /* @__PURE__ */ jsx(
|
|
88
|
+
CellText,
|
|
89
|
+
{
|
|
90
|
+
type: "title",
|
|
91
|
+
typography: "label-3-regular",
|
|
92
|
+
style: "primary",
|
|
93
|
+
maxLines: 1,
|
|
94
|
+
"data-qa": "chat-input-file-name",
|
|
95
|
+
children: /* @__PURE__ */ jsx(TextDefaultPropsContext, { props: NAME_TEXT_PROPS, children: /* @__PURE__ */ jsx(FileName, { name }) })
|
|
96
|
+
}
|
|
97
|
+
),
|
|
98
|
+
!!description && /* @__PURE__ */ jsx(
|
|
99
|
+
CellText,
|
|
100
|
+
{
|
|
101
|
+
type: "subtitle",
|
|
102
|
+
style: "secondary",
|
|
103
|
+
maxLines: 1,
|
|
104
|
+
left: STATUS_ICON[state],
|
|
105
|
+
"data-qa": "chat-input-file-description",
|
|
106
|
+
children: /* @__PURE__ */ jsx(TextDefaultPropsContext, { props: DESCRIPTION_TEXT_PROPS, children: /* @__PURE__ */ jsx(LinkDefaultPropsContext, { props: LINK_DEFAULT_PROPS, children: description }) })
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
]
|
|
62
110
|
}
|
|
63
|
-
|
|
111
|
+
) });
|
|
64
112
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
113
|
+
return /* @__PURE__ */ jsxs("div", { ...props, className: styles.file, "data-qa": dataQa, ref, children: [
|
|
114
|
+
content,
|
|
115
|
+
onRemove && /* @__PURE__ */ jsx("span", { className: styles.fileRemove, children: /* @__PURE__ */ jsx(
|
|
116
|
+
CrossOutlinedSize16,
|
|
117
|
+
{
|
|
118
|
+
"data-qa": "chat-input-file-remove",
|
|
119
|
+
...removeButtonProps,
|
|
120
|
+
"aria-label": removeButtonProps?.["aria-label"] ?? removeLabel,
|
|
121
|
+
initialColor: "primary",
|
|
122
|
+
backgroundStyle: "secondary",
|
|
123
|
+
borderRadius: 8,
|
|
124
|
+
padding: 2,
|
|
125
|
+
onClick: onRemove
|
|
126
|
+
}
|
|
127
|
+
) })
|
|
128
|
+
] });
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
ChatInputFile.displayName = "ChatInputFile";
|
|
72
132
|
|
|
73
133
|
export { ChatInputFile };
|
|
74
134
|
//# sourceMappingURL=ChatInputFile.js.map
|