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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/index.css +117 -117
  2. package/index.d.ts +10 -10
  3. package/index.js +1 -1
  4. package/index.mock.d.ts +9 -9
  5. package/index.mock.js +37 -36
  6. package/index.mock.js.map +1 -1
  7. package/internal/ChatInputContext.d.ts +1 -1
  8. package/internal/ChatInputContext.js.map +1 -1
  9. package/internal/ChatInputSlotButton.js +13 -13
  10. package/internal/ChatInputSlotButton.js.map +1 -1
  11. package/internal/FocusTarget.js +3 -10
  12. package/internal/FocusTarget.js.map +1 -1
  13. package/internal/ModeTransition.js +41 -11
  14. package/internal/ModeTransition.js.map +1 -1
  15. package/internal/TextInput.d.ts +1 -1
  16. package/internal/TextInput.js +146 -126
  17. package/internal/TextInput.js.map +1 -1
  18. package/internal/VoiceRecord.d.ts +1 -1
  19. package/internal/VoiceRecord.js +66 -24
  20. package/internal/VoiceRecord.js.map +1 -1
  21. package/internal/VoiceRecordIndicator.js +22 -9
  22. package/internal/VoiceRecordIndicator.js.map +1 -1
  23. package/internal/flattenFragments.js +9 -13
  24. package/internal/flattenFragments.js.map +1 -1
  25. package/internal/focusGroup.js +18 -28
  26. package/internal/focusGroup.js.map +1 -1
  27. package/internal/useHasSound.js +49 -64
  28. package/internal/useHasSound.js.map +1 -1
  29. package/internal/useObjectUrl.js +15 -21
  30. package/internal/useObjectUrl.js.map +1 -1
  31. package/internal/useRovingFocusGroup.js +102 -137
  32. package/internal/useRovingFocusGroup.js.map +1 -1
  33. package/internal/useSendOnEnter.js +44 -46
  34. package/internal/useSendOnEnter.js.map +1 -1
  35. package/internal/useVoiceRecordSession.d.ts +1 -1
  36. package/internal/useVoiceRecordSession.js +185 -219
  37. package/internal/useVoiceRecordSession.js.map +1 -1
  38. package/internal/voiceSlotRegistry.d.ts +1 -1
  39. package/internal/voiceSlotRegistry.js +3 -3
  40. package/internal/voiceSlotRegistry.js.map +1 -1
  41. package/package.json +11 -11
  42. package/public/ChatInput.d.ts +6 -6
  43. package/public/ChatInput.js +129 -73
  44. package/public/ChatInput.js.map +1 -1
  45. package/public/ChatInputChips.js +4 -4
  46. package/public/ChatInputChips.js.map +1 -1
  47. package/public/ChatInputFile.d.ts +1 -1
  48. package/public/ChatInputFile.js +107 -47
  49. package/public/ChatInputFile.js.map +1 -1
  50. package/public/ChatInputFiles.js +10 -3
  51. package/public/ChatInputFiles.js.map +1 -1
  52. package/public/ChatInputPreview.js +14 -9
  53. package/public/ChatInputPreview.js.map +1 -1
  54. package/public/ChatInputSendButton.d.ts +1 -1
  55. package/public/ChatInputSendButton.js +34 -23
  56. package/public/ChatInputSendButton.js.map +1 -1
  57. package/public/ChatInputVoiceButton.d.ts +1 -1
  58. package/public/ChatInputVoiceButton.js +87 -91
  59. package/public/ChatInputVoiceButton.js.map +1 -1
  60. package/public/getChatInputFileType.d.ts +1 -1
  61. package/public/getChatInputFileType.js +7 -10
  62. package/public/getChatInputFileType.js.map +1 -1
  63. package/styles-hEUo5BXU.js +5 -0
  64. package/styles-hEUo5BXU.js.map +1 -0
  65. package/styles-CHMi0EYL.js +0 -5
  66. package/styles-CHMi0EYL.js.map +0 -1
@@ -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 '@hh.ru/magritte-ui-chat-input/public/ChatInputChips';
3
- import { ChatInputFile } from '@hh.ru/magritte-ui-chat-input/public/ChatInputFile';
4
- import { ChatInputFiles } from '@hh.ru/magritte-ui-chat-input/public/ChatInputFiles';
5
- import { ChatInputPreview } from '@hh.ru/magritte-ui-chat-input/public/ChatInputPreview';
6
- import { ChatInputSendButton } from '@hh.ru/magritte-ui-chat-input/public/ChatInputSendButton';
7
- import { ChatInputVoiceButton } from '@hh.ru/magritte-ui-chat-input/public/ChatInputVoiceButton';
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;
@@ -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-CHMi0EYL.js';
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
- borderRadius: 12,
46
- padding: 8,
47
- initialColor: 'secondary',
45
+ borderRadius: 12,
46
+ padding: 8,
47
+ initialColor: "secondary"
48
48
  };
49
- const ChatInputInner = ({ leftActions, rightActions, centralActions, preview, maxHeight, layout = 'horizontal', onSend, wrapperRef, textareaRef, noBackground = false, disabled = false, 'data-qa': dataQa = 'chat-input', ...textareaProps }) => {
50
- const outerDisabled = useDisabled();
51
- const isDisabled = disabled || outerDisabled;
52
- const rootRef = useRef(null);
53
- const rootRefCallback = useMultipleRefs(rootRef, wrapperRef);
54
- // реф textarea — для возврата фокуса при выходе из режима записи
55
- const textareaLocalRef = useRef(null);
56
- const textareaRefCallback = useMultipleRefs(textareaLocalRef, textareaRef);
57
- const [containerWidth, setContainerWidth] = useState();
58
- useLayoutEffect(() => {
59
- const root = rootRef.current;
60
- if (!root) {
61
- return undefined;
62
- }
63
- const observer = new ResizeObserver((entries) => {
64
- const width = Math.round(entries[0]?.contentRect.width ?? 0);
65
- setContainerWidth((prev) => (prev === width ? prev : width));
66
- });
67
- observer.observe(root);
68
- return () => observer.disconnect();
69
- }, []);
70
- // конфиг записи регистрирует ChatInput.VoiceButton через slot-registry. Кнопка живёт в
71
- // text-гриде, который на время записи не размонтируется (ModeTransition лишь прячет его), —
72
- // поэтому регистрация и колбэки остаются живыми всю сессию, латч не нужен
73
- const voiceConfig = useVoiceConfig();
74
- const voiceSession = useVoiceRecordSession(voiceConfig ?? {});
75
- const contextValue = useMemo(() => ({
76
- onSend,
77
- startVoiceRecord: voiceSession.start,
78
- voiceRecordStatus: voiceSession.status,
79
- }), [onSend, voiceSession.start, voiceSession.status]);
80
- // режим ведём по статусу сессии, а не по наличию конфига: даже если приложение уберёт кнопку
81
- // по ходу записи, запись продолжается. Без кнопки запись не стартует, так что статус сам остаётся idle
82
- const isVoiceMode = voiceSession.status === 'recording' || voiceSession.status === 'processing';
83
- // roving-группа кнопок: один tab stop на все кнопки, внутри — стрелки по приоритетам
84
- const { focusStop } = useRovingFocusGroup(rootRef);
85
- // при входе в запись фокус на стоп группы (после смены слоя это галочка — первый приоритет
86
- // слоя записи), при возврате в текст — в textarea с курсором в конце. Фокус ставим в rAF:
87
- // слой режима к этому моменту уже видим (motion применил visibility), иначе focus()
88
- // на visibility:hidden не сработает
89
- const prevVoiceModeRef = useRef(isVoiceMode);
90
- useEffect(() => {
91
- const wasVoiceMode = prevVoiceModeRef.current;
92
- prevVoiceModeRef.current = isVoiceMode;
93
- if (wasVoiceMode === isVoiceMode) {
94
- return undefined;
95
- }
96
- const frameId = requestAnimationFrame(() => {
97
- if (isVoiceMode) {
98
- focusStop();
99
- return;
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
- const textarea = textareaLocalRef.current;
102
- if (textarea) {
103
- textarea.focus();
104
- const caret = textarea.value.length;
105
- textarea.setSelectionRange(caret, caret);
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
- return () => cancelAnimationFrame(frameId);
109
- }, [isVoiceMode, focusStop]);
110
- 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, {
111
- [styles.noLeftControls]: !leftActions,
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
- // внешняя обёртка держит slot-registry для конфига записи: кнопка (внутри слота) регистрирует
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 = 'ChatInput';
177
+ ChatInput.displayName = "ChatInput";
122
178
  ChatInput.Chips = ChatInputChips;
123
179
  ChatInput.File = ChatInputFile;
124
180
  ChatInput.Files = ChatInputFiles;
@@ -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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"}
@@ -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-CHMi0EYL.js';
7
+ import { s as styles } from '../styles-hEUo5BXU.js';
8
8
 
9
- const CHIP_DEFAULT_PROPS = { size: 'extra-small' };
10
- 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))) }) }) }) }) }) }));
11
- ChatInputChips.displayName = 'ChatInputChips';
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":["_jsx"],"mappings":";;;;;;;AAaA,MAAM,kBAAkB,GAAG,EAAE,IAAI,EAAE,aAAa,EAAW,CAAC;MAE/C,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;;;;"}
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 '@hh.ru/magritte-ui-chat-input/types';
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'> {
@@ -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, CrossOutlinedSize16, VideocameraOutlinedSize24, DocumentOutlinedSize24 } from '@hh.ru/magritte-ui-icon/icon';
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-CHMi0EYL.js';
12
+ import { s as styles } from '../styles-hEUo5BXU.js';
13
13
 
14
14
  const splitFileName = (name) => {
15
- const dotIndex = name.lastIndexOf('.');
16
- // точка в начале (.gitignore) или в конце (file.) — расширения нет, имя целиком
17
- if (dotIndex <= 0 || dotIndex === name.length - 1) {
18
- return { base: name, extension: '' };
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
- if (typeof name !== 'string') {
24
- return jsx(Fragment, { children: name });
25
- }
26
- const { base, extension } = splitFileName(name);
27
- // основа сжимается с многоточием, расширение не сжимается; типографику даёт CellText.
28
- // имя без расширения обрезается так же, целиком
29
- return (jsxs("span", { className: styles.fileName, children: [jsx("span", { className: styles.fileNameBase, children: base }), extension && jsx("span", { className: styles.fileNameExtension, children: extension })] }));
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
- loading: jsx(Loader, { size: 16 }),
33
- loaded: jsx(CheckCircleOutlinedSize16, { initialColor: "positive" }),
34
- invalid: jsx(ExclamationTriangleOutlinedSize16, { initialColor: "negative" }),
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: 'label-3-regular', inline: true };
37
- // строку оформляет CellText, а Text, переданный пользователем в name/description, получает те же дефолты.
38
- // ellipsis — для Text: блочный элемент не обрезается text-overflow родителя
39
- const NAME_TEXT_PROPS = { typography: 'label-3-regular', style: 'primary', ellipsis: true };
40
- const DESCRIPTION_TEXT_PROPS = { typography: 'label-3-regular', style: 'secondary', ellipsis: true };
41
- /** Приложенный к сообщению файл */
42
- const ChatInputFile = forwardRef(({ 'data-qa': dataQa = 'chat-input-file', description, name, onRemove, preview, previewAlt, removeButtonProps, state = 'loaded', type = 'doc', ...props }, ref) => {
43
- const removeLabel = useTranslation('ChatInput', 'removeFile');
44
- const isImage = type === 'image';
45
- // object URL создаём только для плитки картинки — карточкам он не нужен
46
- const previewSrc = useObjectUrl(isImage ? preview : undefined);
47
- // битая картинка — показываем заглушку; сбрасывается сменой src или уходом из loaded,
48
- // чтобы тот же url (например, ещё не готовое серверное превью) загрузился повторно
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 !== 'loaded' && failedSrc !== undefined) {
51
- setFailedSrc(undefined);
56
+ if (state !== "loaded" && failedSrc !== void 0) {
57
+ setFailedSrc(void 0);
52
58
  }
53
59
  let content;
54
60
  if (isImage) {
55
- const showPreview = state === 'loaded' && !!previewSrc && previewSrc !== failedSrc;
56
- let placeholder = null;
57
- if (state === 'loading') {
58
- placeholder = jsx(Loader, { size: 24 });
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
- else if (!showPreview) {
61
- placeholder = jsx(ImageOutlinedSize24, { initialColor: "tertiary" });
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
- content = (jsx("div", { className: classNames(styles.fileTile, { [styles.fileTilePlaceholder]: !showPreview }), children: showPreview ? (jsx("img", { className: styles.fileTileImage, src: previewSrc, alt: previewAlt ?? (typeof name === 'string' ? name : ''), onError: () => setFailedSrc(previewSrc) })) : (placeholder) }));
111
+ ) });
64
112
  }
65
- else {
66
- const Icon = type === 'video' ? VideocameraOutlinedSize24 : DocumentOutlinedSize24;
67
- content = (jsx("div", { className: classNames(styles.fileCard, { [styles.fileCardRemovable]: !!onRemove }), children: jsxs(Cell, { align: "center", horPadding: false, vertPadding: false, left: jsx(Icon, { initialColor: "secondary", backgroundStyle: "secondary", padding: 12, borderRadius: 8 }), children: [!!name && (jsx(CellText, { type: "title", typography: "label-3-regular", style: "primary", maxLines: 1, "data-qa": "chat-input-file-name", children: jsx(TextDefaultPropsContext, { props: NAME_TEXT_PROPS, children: jsx(FileName, { name: name }) }) })), !!description && (jsx(CellText, { type: "subtitle", style: "secondary", maxLines: 1, left: STATUS_ICON[state], "data-qa": "chat-input-file-description", children: jsx(TextDefaultPropsContext, { props: DESCRIPTION_TEXT_PROPS, children: jsx(LinkDefaultPropsContext, { props: LINK_DEFAULT_PROPS, children: description }) }) }))] }) }));
68
- }
69
- return (jsxs("div", { ...props, className: styles.file, "data-qa": dataQa, ref: ref, children: [content, onRemove && (jsx("span", { className: styles.fileRemove, children: jsx(CrossOutlinedSize16, { "data-qa": "chat-input-file-remove", ...removeButtonProps, "aria-label": removeButtonProps?.['aria-label'] ?? removeLabel, initialColor: "primary", backgroundStyle: "secondary", borderRadius: 8, padding: 2, onClick: onRemove }) }))] }));
70
- });
71
- ChatInputFile.displayName = 'ChatInputFile';
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