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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/index.css +185 -93
  2. package/index.d.ts +3 -0
  3. package/index.js +8 -1
  4. package/index.js.map +1 -1
  5. package/index.mock.d.ts +3 -0
  6. package/index.mock.js +5 -0
  7. package/index.mock.js.map +1 -1
  8. package/internal/ModeTransition.js +1 -1
  9. package/internal/TextInput.js +2 -2
  10. package/internal/TextInput.js.map +1 -1
  11. package/internal/VoiceRecord.js +1 -1
  12. package/internal/VoiceRecordIndicator.js +1 -1
  13. package/internal/flattenFragments.d.ts +6 -0
  14. package/internal/flattenFragments.js +20 -0
  15. package/internal/flattenFragments.js.map +1 -0
  16. package/internal/useHasSound.js +1 -1
  17. package/internal/useHasSound.js.map +1 -1
  18. package/internal/useObjectUrl.d.ts +6 -0
  19. package/internal/useObjectUrl.js +30 -0
  20. package/internal/useObjectUrl.js.map +1 -0
  21. package/package.json +17 -15
  22. package/public/ChatInput.d.ts +4 -0
  23. package/public/ChatInput.js +9 -1
  24. package/public/ChatInput.js.map +1 -1
  25. package/public/ChatInputChips.js +3 -5
  26. package/public/ChatInputChips.js.map +1 -1
  27. package/public/ChatInputFile.d.ts +43 -0
  28. package/public/ChatInputFile.js +74 -0
  29. package/public/ChatInputFile.js.map +1 -0
  30. package/public/ChatInputFiles.d.ts +6 -0
  31. package/public/ChatInputFiles.js +12 -0
  32. package/public/ChatInputFiles.js.map +1 -0
  33. package/public/ChatInputPreview.js +7 -2
  34. package/public/ChatInputPreview.js.map +1 -1
  35. package/public/ChatInputVoiceButton.js +1 -1
  36. package/public/ChatInputVoiceButton.js.map +1 -1
  37. package/public/getChatInputFileType.d.ts +5 -0
  38. package/public/getChatInputFileType.js +16 -0
  39. package/public/getChatInputFileType.js.map +1 -0
  40. package/styles-B3o1JdaK.js +5 -0
  41. package/styles-B3o1JdaK.js.map +1 -0
  42. package/styles-CXscNVWO.js +0 -5
  43. package/styles-CXscNVWO.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"TextInput.js","sources":["src/internal/TextInput.tsx"],"sourcesContent":["import {\n type ForwardedRef,\n forwardRef,\n type RefObject,\n type TextareaHTMLAttributes,\n type UIEvent,\n type FocusEventHandler,\n useCallback,\n useLayoutEffect,\n useRef,\n useState,\n} from 'react';\nimport classNames from 'classnames';\n\nimport { useMultipleRefs } from '@hh.ru/magritte-common-use-multiple-refs';\nimport { useSendOnEnter } from '@hh.ru/magritte-ui-chat-input/internal/useSendOnEnter';\nimport { Scrollbar, useScrollbar } from '@hh.ru/magritte-ui-scrollbar';\n\nimport styles from '../styles.less';\n\ntype TextInputProps = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'value'> & {\n value: string;\n maxHeight?: number;\n layout: 'vertical' | 'horizontal';\n onSend?: VoidFunction;\n /** Ширина контейнера; её изменение пере-измеряет layout (перенос текста зависит от ширины) */\n containerWidth?: number;\n /** Есть центральный слот - ввод всегда на своём ряду, паддинги spacious */\n hasCentral: boolean;\n /** Контролы с обеих сторон - в вертикальном лейауте ввод уезжает на свой ряд, паддинги spacious */\n hasBothSideControls: boolean;\n};\n\ninterface TextInputState {\n layout: 'vertical' | 'horizontal';\n height: number;\n}\n\nconst MIN_HEIGHT = 20;\n\n// допуск на субпиксельные значения скролла\nconst HIDDEN_CONTENT_THRESHOLD = 4;\n\n// вертикальные паддинги ввода - источник правды здесь, ставятся инлайном (в LESS только\n// горизонтальные): фейд и раскладка считают по одним и тем же значениям без чтения стилей из DOM.\n// spacious - ввод на своём ряду (не в нижнем, где контролы), base - все остальные раскладки\nconst VERTICAL_PADDING = {\n base: { top: 8, bottom: 8 },\n spacious: { top: 12, bottom: 16 },\n} as const;\n\nconst clamp = (value: number, min: number, max: number) => Math.max(Math.min(max, value), min);\n\nconst useTextInputHeight = ({\n inputRef,\n layout,\n maxHeight,\n containerWidth,\n value,\n onScrollHeightChange,\n}: Pick<TextInputProps, 'layout' | 'maxHeight' | 'value' | 'containerWidth'> & {\n inputRef: RefObject<HTMLTextAreaElement>;\n onScrollHeightChange?: VoidFunction;\n}) => {\n const [currentState, setCurrentState] = useState<TextInputState>(() => ({\n layout,\n height: MIN_HEIGHT,\n }));\n const prevScrollHeightRef = useRef<number | null>(null);\n\n useLayoutEffect(() => {\n const input = inputRef.current;\n if (!input) {\n return;\n }\n\n // изменение реального scrollHeight (например, рост контента при упоре в maxHeight) —\n // повод пересчитать зависимое (скроллбар); читаем до измерительных тоглов\n if (prevScrollHeightRef.current !== null && prevScrollHeightRef.current !== input.scrollHeight) {\n onScrollHeightChange?.();\n }\n prevScrollHeightRef.current = input.scrollHeight;\n\n // высота контента на ширине конкретного слота: .measure схлопывает высоту и паддинги\n // (scrollHeight = чистая высота контента), layout-vertical выбирает вертикальный/горизонтальный слот\n const measureContentHeight = (targetVertical: boolean) => {\n input.classList.add(styles.measure);\n input.classList.toggle(styles.layoutVertical, targetVertical);\n const height = input.scrollHeight;\n input.classList.toggle(styles.layoutVertical, currentState.layout === 'vertical');\n input.classList.remove(styles.measure);\n return height;\n };\n\n // лейаут определяется по горизонтальному слоту (влезает ли контент в одну строку). Замер\n // горизонтального слота — форс-рефлоу, поэтому сначала снимаем заведомо известные случаи:\n // внешний layout='vertical' и перенос строки в значении гарантируют вертикаль, пустое\n // значение — горизонталь; замеряем только неоднозначный однострочный случай (перенос по ширине).\n // Высоту всегда берём на ширине целевого слота — иначе размер меряется на старом месте.\n let isVertical: boolean;\n if (layout === 'vertical' || value.includes('\\n')) {\n isVertical = true;\n } else if (value === '') {\n isVertical = false;\n } else {\n isVertical = measureContentHeight(false) > MIN_HEIGHT;\n }\n const contentHeight = isVertical ? measureContentHeight(true) : MIN_HEIGHT;\n\n const newState: TextInputState = {\n layout: isVertical ? 'vertical' : 'horizontal',\n height: clamp(contentHeight, MIN_HEIGHT, maxHeight ?? contentHeight),\n };\n\n if (\n (Object.keys(currentState) as Array<keyof TextInputState>).some(\n (key) => currentState[key] !== newState[key]\n )\n ) {\n setCurrentState(newState);\n }\n // currentState не должно быть в зависимостях, иначе зациклится обновление\n // eslint-disable-next-line disable-autofix/react-hooks/exhaustive-deps\n }, [layout, maxHeight, value, inputRef, containerWidth]);\n\n return currentState;\n};\n\nexport const useFocus = <T extends HTMLTextAreaElement = HTMLTextAreaElement>(): {\n focusHandler: FocusEventHandler<T>;\n blurHandler: FocusEventHandler<T>;\n isFocused: boolean;\n} => {\n const [isFocused, setFocused] = useState(false);\n const focusHandler = useCallback<FocusEventHandler<T>>((event) => {\n setFocused(event.target.classList.contains('focus-visible'));\n }, []);\n\n const blurHandler = useCallback<FocusEventHandler<T>>(() => {\n setFocused(false);\n }, []);\n\n return { focusHandler, blurHandler, isFocused };\n};\n\nconst TextInputComponent = (\n {\n value,\n maxHeight,\n layout,\n onSend,\n containerWidth,\n hasCentral,\n hasBothSideControls,\n onKeyDown,\n onScroll,\n className,\n style,\n ...textareaProps\n }: TextInputProps,\n forwardedRef: ForwardedRef<HTMLTextAreaElement>\n) => {\n const inputRef = useRef<HTMLTextAreaElement>(null);\n const { hasVerticalScroll, verticalScrollbarProps, scrollContainerRef, resizeCallbackRef, hoverContainerCssClass } =\n useScrollbar({ axis: 'vertical' });\n const refCallback = useMultipleRefs(inputRef, scrollContainerRef, forwardedRef);\n\n // ResizeObserver внутри useScrollbar видит только изменения размера textarea:\n // после упора в maxHeight растет лишь scrollHeight - пересчитываем трек по нему\n const handleScrollHeightChange = useCallback(() => {\n resizeCallbackRef.current?.();\n }, [resizeCallbackRef]);\n\n const inputParams = useTextInputHeight({\n inputRef,\n layout,\n maxHeight,\n value,\n containerWidth,\n onScrollHeightChange: handleScrollHeightChange,\n });\n\n const verticalPadding =\n hasCentral || (hasBothSideControls && inputParams.layout === 'vertical')\n ? VERTICAL_PADDING.spacious\n : VERTICAL_PADDING.base;\n\n // фейд у краёв показываем, только когда за соответствующим краем есть непроскролленный контент\n const [hasHiddenTop, setHasHiddenTop] = useState(false);\n const [hasHiddenBottom, setHasHiddenBottom] = useState(false);\n const updateFades = useCallback(() => {\n const input = inputRef.current;\n if (!input) {\n return;\n }\n // верхний и нижний паддинги - часть скроллируемой области, но контента в них нет:\n // остаток скролла в их пределах не значит, что текст скрыт\n const hiddenTop = input.scrollTop - verticalPadding.top;\n const hiddenBottom = input.scrollHeight - input.scrollTop - input.clientHeight - verticalPadding.bottom;\n setHasHiddenTop(hiddenTop > HIDDEN_CONTENT_THRESHOLD);\n setHasHiddenBottom(hiddenBottom > HIDDEN_CONTENT_THRESHOLD);\n }, [verticalPadding.top, verticalPadding.bottom]);\n\n // позиция скролла отслеживается через onScroll, здесь - изменения контента, высоты и ширины\n // контейнера: при упоре в maxHeight высота не меняется, но текст перетекает и за краями\n // может появиться/исчезнуть скрытый контент\n useLayoutEffect(() => {\n updateFades();\n }, [value, inputParams.height, containerWidth, updateFades]);\n\n const handleKeyDown = useSendOnEnter({ onSend, onKeyDown });\n const handleScroll = useCallback(\n (event: UIEvent<HTMLTextAreaElement>) => {\n updateFades();\n onScroll?.(event);\n },\n [updateFades, onScroll]\n );\n\n const { isFocused, focusHandler, blurHandler } = useFocus();\n\n return (\n <div className={classNames(styles.textInputWrapper, hoverContainerCssClass)}>\n <textarea\n enterKeyHint=\"enter\"\n {...textareaProps}\n onFocus={focusHandler}\n onBlur={blurHandler}\n className={classNames(\n styles.textInput,\n {\n [styles.layoutVertical]: inputParams.layout === 'vertical',\n [styles.withTopFade]: hasHiddenTop,\n [styles.withBottomFade]: hasHiddenBottom,\n [styles.textInputFocused]: isFocused,\n },\n className\n )}\n style={{\n ...style,\n height: inputParams.height,\n paddingTop: verticalPadding.top,\n paddingBottom: verticalPadding.bottom,\n }}\n ref={refCallback}\n value={value}\n onKeyDown={handleKeyDown}\n onScroll={handleScroll}\n data-qa=\"text-input\"\n />\n {hasVerticalScroll && (\n <div className={styles.scrollbarCell}>\n <Scrollbar {...verticalScrollbarProps} />\n </div>\n )}\n </div>\n );\n};\n\nexport const TextInput = forwardRef(TextInputComponent);\n"],"names":["_jsxs","_jsx"],"mappings":"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"}
1
+ {"version":3,"file":"TextInput.js","sources":["src/internal/TextInput.tsx"],"sourcesContent":["import {\n type ForwardedRef,\n forwardRef,\n type RefObject,\n type TextareaHTMLAttributes,\n type UIEvent,\n type FocusEventHandler,\n useCallback,\n useLayoutEffect,\n useRef,\n useState,\n} from 'react';\nimport classNames from 'classnames';\n\nimport { useMultipleRefs } from '@hh.ru/magritte-common-use-multiple-refs';\nimport { useSendOnEnter } from '@hh.ru/magritte-ui-chat-input/internal/useSendOnEnter';\nimport { Scrollbar, useScrollbar } from '@hh.ru/magritte-ui-scrollbar';\n\nimport styles from '../styles.less';\n\ntype TextInputProps = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'value'> & {\n value: string;\n maxHeight?: number;\n layout: 'vertical' | 'horizontal';\n onSend?: VoidFunction;\n /** Ширина контейнера; её изменение пере-измеряет layout (перенос текста зависит от ширины) */\n containerWidth?: number;\n /** Есть центральный слот - ввод всегда на своём ряду, паддинги spacious */\n hasCentral: boolean;\n /** Контролы с обеих сторон - в вертикальном лейауте ввод уезжает на свой ряд, паддинги spacious */\n hasBothSideControls: boolean;\n};\n\ninterface TextInputState {\n layout: 'vertical' | 'horizontal';\n height: number;\n}\n\nconst MIN_HEIGHT = 20;\n\n// допуск на субпиксельные значения скролла\nconst HIDDEN_CONTENT_THRESHOLD = 4;\n\n// вертикальные паддинги ввода - источник правды здесь, ставятся инлайном (в LESS только\n// горизонтальные): фейд и раскладка считают по одним и тем же значениям без чтения стилей из DOM.\n// spacious - ввод на своём ряду (не в нижнем, где контролы), base - все остальные раскладки\nconst VERTICAL_PADDING = {\n base: { top: 8, bottom: 8 },\n spacious: { top: 12, bottom: 16 },\n} as const;\n\nconst clamp = (value: number, min: number, max: number) => Math.max(Math.min(max, value), min);\n\nconst useTextInputHeight = ({\n inputRef,\n layout,\n maxHeight,\n containerWidth,\n value,\n onScrollHeightChange,\n}: Pick<TextInputProps, 'layout' | 'maxHeight' | 'value' | 'containerWidth'> & {\n inputRef: RefObject<HTMLTextAreaElement>;\n onScrollHeightChange?: VoidFunction;\n}) => {\n const [currentState, setCurrentState] = useState<TextInputState>(() => ({\n layout,\n height: MIN_HEIGHT,\n }));\n const prevScrollHeightRef = useRef<number | null>(null);\n\n useLayoutEffect(() => {\n const input = inputRef.current;\n if (!input) {\n return;\n }\n\n // изменение реального scrollHeight (например, рост контента при упоре в maxHeight) —\n // повод пересчитать зависимое (скроллбар); читаем до измерительных тоглов\n if (prevScrollHeightRef.current !== null && prevScrollHeightRef.current !== input.scrollHeight) {\n onScrollHeightChange?.();\n }\n prevScrollHeightRef.current = input.scrollHeight;\n\n // высота контента на ширине конкретного слота: .measure схлопывает высоту и паддинги\n // (scrollHeight = чистая высота контента), layout-vertical выбирает вертикальный/горизонтальный слот\n const measureContentHeight = (targetVertical: boolean) => {\n input.classList.add(styles.measure);\n input.classList.toggle(styles.layoutVertical, targetVertical);\n const height = input.scrollHeight;\n input.classList.toggle(styles.layoutVertical, currentState.layout === 'vertical');\n input.classList.remove(styles.measure);\n return height;\n };\n\n // лейаут определяется по горизонтальному слоту (влезает ли контент в одну строку). Замер\n // горизонтального слота — форс-рефлоу, поэтому сначала снимаем заведомо известные случаи:\n // внешний layout='vertical' и перенос строки в значении гарантируют вертикаль, пустое\n // значение — горизонталь; замеряем только неоднозначный однострочный случай (перенос по ширине).\n // Высоту всегда берём на ширине целевого слота — иначе размер меряется на старом месте.\n let isVertical: boolean;\n if (layout === 'vertical' || value.includes('\\n')) {\n isVertical = true;\n } else if (value === '') {\n isVertical = false;\n } else {\n isVertical = measureContentHeight(false) > MIN_HEIGHT;\n }\n const contentHeight = isVertical ? measureContentHeight(true) : MIN_HEIGHT;\n\n const newState: TextInputState = {\n layout: isVertical ? 'vertical' : 'horizontal',\n height: clamp(contentHeight, MIN_HEIGHT, maxHeight ?? contentHeight),\n };\n\n if (\n (Object.keys(currentState) as Array<keyof TextInputState>).some(\n (key) => currentState[key] !== newState[key]\n )\n ) {\n setCurrentState(newState);\n }\n // currentState не должно быть в зависимостях, иначе зациклится обновление\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [layout, maxHeight, value, inputRef, containerWidth]);\n\n return currentState;\n};\n\nexport const useFocus = <T extends HTMLTextAreaElement = HTMLTextAreaElement>(): {\n focusHandler: FocusEventHandler<T>;\n blurHandler: FocusEventHandler<T>;\n isFocused: boolean;\n} => {\n const [isFocused, setFocused] = useState(false);\n const focusHandler = useCallback<FocusEventHandler<T>>((event) => {\n setFocused(event.target.classList.contains('focus-visible'));\n }, []);\n\n const blurHandler = useCallback<FocusEventHandler<T>>(() => {\n setFocused(false);\n }, []);\n\n return { focusHandler, blurHandler, isFocused };\n};\n\nconst TextInputComponent = (\n {\n value,\n maxHeight,\n layout,\n onSend,\n containerWidth,\n hasCentral,\n hasBothSideControls,\n onKeyDown,\n onScroll,\n className,\n style,\n ...textareaProps\n }: TextInputProps,\n forwardedRef: ForwardedRef<HTMLTextAreaElement>\n) => {\n const inputRef = useRef<HTMLTextAreaElement>(null);\n const { hasVerticalScroll, verticalScrollbarProps, scrollContainerRef, resizeCallbackRef, hoverContainerCssClass } =\n useScrollbar({ axis: 'vertical' });\n const refCallback = useMultipleRefs(inputRef, scrollContainerRef, forwardedRef);\n\n // ResizeObserver внутри useScrollbar видит только изменения размера textarea:\n // после упора в maxHeight растет лишь scrollHeight - пересчитываем трек по нему\n const handleScrollHeightChange = useCallback(() => {\n resizeCallbackRef.current?.();\n }, [resizeCallbackRef]);\n\n const inputParams = useTextInputHeight({\n inputRef,\n layout,\n maxHeight,\n value,\n containerWidth,\n onScrollHeightChange: handleScrollHeightChange,\n });\n\n const verticalPadding =\n hasCentral || (hasBothSideControls && inputParams.layout === 'vertical')\n ? VERTICAL_PADDING.spacious\n : VERTICAL_PADDING.base;\n\n // фейд у краёв показываем, только когда за соответствующим краем есть непроскролленный контент\n const [hasHiddenTop, setHasHiddenTop] = useState(false);\n const [hasHiddenBottom, setHasHiddenBottom] = useState(false);\n const updateFades = useCallback(() => {\n const input = inputRef.current;\n if (!input) {\n return;\n }\n // верхний и нижний паддинги - часть скроллируемой области, но контента в них нет:\n // остаток скролла в их пределах не значит, что текст скрыт\n const hiddenTop = input.scrollTop - verticalPadding.top;\n const hiddenBottom = input.scrollHeight - input.scrollTop - input.clientHeight - verticalPadding.bottom;\n setHasHiddenTop(hiddenTop > HIDDEN_CONTENT_THRESHOLD);\n setHasHiddenBottom(hiddenBottom > HIDDEN_CONTENT_THRESHOLD);\n }, [verticalPadding.top, verticalPadding.bottom]);\n\n // позиция скролла отслеживается через onScroll, здесь - изменения контента, высоты и ширины\n // контейнера: при упоре в maxHeight высота не меняется, но текст перетекает и за краями\n // может появиться/исчезнуть скрытый контент\n useLayoutEffect(() => {\n updateFades();\n }, [value, inputParams.height, containerWidth, updateFades]);\n\n const handleKeyDown = useSendOnEnter({ onSend, onKeyDown });\n const handleScroll = useCallback(\n (event: UIEvent<HTMLTextAreaElement>) => {\n updateFades();\n onScroll?.(event);\n },\n [updateFades, onScroll]\n );\n\n const { isFocused, focusHandler, blurHandler } = useFocus();\n\n return (\n <div className={classNames(styles.textInputWrapper, hoverContainerCssClass)}>\n <textarea\n enterKeyHint=\"enter\"\n {...textareaProps}\n onFocus={focusHandler}\n onBlur={blurHandler}\n className={classNames(\n styles.textInput,\n {\n [styles.layoutVertical]: inputParams.layout === 'vertical',\n [styles.withTopFade]: hasHiddenTop,\n [styles.withBottomFade]: hasHiddenBottom,\n [styles.textInputFocused]: isFocused,\n },\n className\n )}\n style={{\n ...style,\n height: inputParams.height,\n paddingTop: verticalPadding.top,\n paddingBottom: verticalPadding.bottom,\n }}\n ref={refCallback}\n value={value}\n onKeyDown={handleKeyDown}\n onScroll={handleScroll}\n data-qa=\"text-input\"\n />\n {hasVerticalScroll && (\n <div className={styles.scrollbarCell}>\n <Scrollbar {...verticalScrollbarProps} />\n </div>\n )}\n </div>\n );\n};\n\nexport const TextInput = forwardRef(TextInputComponent);\n"],"names":["_jsxs","_jsx"],"mappings":"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"}
@@ -9,7 +9,7 @@ import { FOCUS_PRIORITY } from './focusGroup.js';
9
9
  import { CrossOutlinedSize24, CheckOutlinedSize24 } from '@hh.ru/magritte-ui-icon/icon';
10
10
  import { Loader } from '@hh.ru/magritte-ui-loader';
11
11
  import { Text } from '@hh.ru/magritte-ui-typography';
12
- import { s as styles } from '../styles-CXscNVWO.js';
12
+ import { s as styles } from '../styles-B3o1JdaK.js';
13
13
  import './useHasSound.js';
14
14
 
15
15
  const formatDuration = (totalSeconds) => {
@@ -2,7 +2,7 @@ import './../index.css';
2
2
  import { jsx } from 'react/jsx-runtime';
3
3
  import classNames from 'classnames';
4
4
  import { useHasSound } from './useHasSound.js';
5
- import { s as styles } from '../styles-CXscNVWO.js';
5
+ import { s as styles } from '../styles-B3o1JdaK.js';
6
6
  import 'react';
7
7
 
8
8
  const TARGET_HEIGHT = [2, 11, 22, 22, 22, 11, 22, 11, 6, 2];
@@ -0,0 +1,6 @@
1
+ import { type ReactNode } from 'react';
2
+ /** Раскрывает фрагменты в плоский список детей. Children.toArray сохраняет ключи детей —
3
+ * их можно перенести на обёртку, чтобы удаление элемента из середины не сдвигало стейт соседей.
4
+ * Внутренний Children.toArray нумерует детей фрагмента заново (.0, .1), поэтому к их ключам
5
+ * добавляется ключ фрагмента — иначе они совпадут с ключами соседей фрагмента */
6
+ export declare const flattenFragments: (children: ReactNode, keyPrefix?: string) => ReactNode[];
@@ -0,0 +1,20 @@
1
+ import './../index.css';
2
+ import { Children, isValidElement, Fragment, cloneElement } from 'react';
3
+
4
+ /** Раскрывает фрагменты в плоский список детей. Children.toArray сохраняет ключи детей —
5
+ * их можно перенести на обёртку, чтобы удаление элемента из середины не сдвигало стейт соседей.
6
+ * Внутренний Children.toArray нумерует детей фрагмента заново (.0, .1), поэтому к их ключам
7
+ * добавляется ключ фрагмента — иначе они совпадут с ключами соседей фрагмента */
8
+ const flattenFragments = (children, keyPrefix = '') => Children.toArray(children).flatMap((child) => {
9
+ if (!isValidElement(child)) {
10
+ return child;
11
+ }
12
+ const key = `${keyPrefix}${child.key}`;
13
+ if (child.type === Fragment) {
14
+ return flattenFragments(child.props.children, key);
15
+ }
16
+ return keyPrefix ? cloneElement(child, { key }) : child;
17
+ });
18
+
19
+ export { flattenFragments };
20
+ //# sourceMappingURL=flattenFragments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flattenFragments.js","sources":["src/internal/flattenFragments.ts"],"sourcesContent":["import { Children, cloneElement, Fragment, isValidElement, type PropsWithChildren, type ReactNode } from 'react';\n\n/** Раскрывает фрагменты в плоский список детей. Children.toArray сохраняет ключи детей —\n * их можно перенести на обёртку, чтобы удаление элемента из середины не сдвигало стейт соседей.\n * Внутренний Children.toArray нумерует детей фрагмента заново (.0, .1), поэтому к их ключам\n * добавляется ключ фрагмента — иначе они совпадут с ключами соседей фрагмента */\nexport const flattenFragments = (children: ReactNode, keyPrefix = ''): ReactNode[] =>\n Children.toArray(children).flatMap((child) => {\n if (!isValidElement(child)) {\n return child;\n }\n const key = `${keyPrefix}${child.key}`;\n if (child.type === Fragment) {\n return flattenFragments((child.props as PropsWithChildren).children, key);\n }\n return keyPrefix ? cloneElement(child, { key }) : child;\n });\n"],"names":[],"mappings":";;AAEA;;;AAGiF;AACpE,MAAA,gBAAgB,GAAG,CAAC,QAAmB,EAAE,SAAS,GAAG,EAAE,KAChE,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,KAAI;AACzC,IAAA,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE;AACxB,QAAA,OAAO,KAAK,CAAC;KAChB;IACD,MAAM,GAAG,GAAG,CAAG,EAAA,SAAS,GAAG,KAAK,CAAC,GAAG,CAAA,CAAE,CAAC;AACvC,IAAA,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE;QACzB,OAAO,gBAAgB,CAAE,KAAK,CAAC,KAA2B,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;KAC7E;AACD,IAAA,OAAO,SAAS,GAAG,YAAY,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,CAAC,GAAG,KAAK,CAAC;AAC5D,CAAC;;;;"}
@@ -38,7 +38,7 @@ const useHasSound = (stream) => {
38
38
  analyser.fftSize = 256;
39
39
  source.connect(analyser);
40
40
  // Web Audio API принимает данные только в типизированный массив
41
- // eslint-disable-next-line no-restricted-syntax
41
+ // oxlint-disable-next-line magritte/no-restricted-syntax
42
42
  const buffer = new Uint8Array(analyser.fftSize);
43
43
  let lastLoudAt = 0;
44
44
  let current = false;
@@ -1 +1 @@
1
- {"version":3,"file":"useHasSound.js","sources":["src/internal/useHasSound.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\n\n// Порог RMS, выше которого считаем, что звук есть (речь по RMS обычно ~0.1+, шумовой пол — ниже)\nconst SOUND_RMS_THRESHOLD = 0.02;\n// Сколько держим флаг «звук есть» после последнего громкого кадра — чтобы паузы между словами\n// не роняли индикатор в тишину и он не мигал\nconst RELEASE_MS = 350;\n// Период опроса громкости: флаг булев с гистерезисом RELEASE_MS, детализация кадра дисплея тут\n// не нужна — 100 мс дают запас в ~3.5 замера на окно удержания при кратно меньшей нагрузке, чем rAF\nconst SAMPLE_INTERVAL_MS = 100;\n\ntype AudioContextConstructor = typeof AudioContext;\n\nconst getAudioContextConstructor = (): AudioContextConstructor | null => {\n if (typeof window === 'undefined') {\n return null;\n }\n const scoped = window as unknown as {\n AudioContext?: AudioContextConstructor;\n webkitAudioContext?: AudioContextConstructor;\n };\n return scoped.AudioContext ?? scoped.webkitAudioContext ?? null;\n};\n\n/**\n * Флаг «в стриме микрофона сейчас есть звук». Пока стрим есть — держит rAF-цикл и читает\n * RMS через AnalyserNode, сравнивая с порогом (флаг ещё RELEASE_MS удерживается после последнего\n * громкого кадра, чтобы не мигать на паузах между словами). Ререндер только на смене флага.\n * Треки стрима не трогаем — ими владеет сессия записи (`useVoiceRecordSession`), только читаем.\n */\nexport const useHasSound = (stream: MediaStream | null): boolean => {\n const [hasSound, setHasSound] = useState(false);\n\n useEffect(() => {\n const AudioCtx = getAudioContextConstructor();\n if (!stream || !AudioCtx || stream.getAudioTracks().length === 0) {\n return undefined;\n }\n\n const audioContext = new AudioCtx();\n // ctx может стартовать в suspended (autoplay policy); старт всегда из клика по микрофону,\n // так что resume безопасен и обычно сразу переводит в running\n void audioContext.resume();\n\n const source = audioContext.createMediaStreamSource(stream);\n const analyser = audioContext.createAnalyser();\n analyser.fftSize = 256;\n source.connect(analyser);\n\n // Web Audio API принимает данные только в типизированный массив\n // eslint-disable-next-line no-restricted-syntax\n const buffer = new Uint8Array(analyser.fftSize);\n let lastLoudAt = 0;\n let current = false;\n\n const tick = () => {\n analyser.getByteTimeDomainData(buffer);\n let sumSquares = 0;\n for (let i = 0; i < buffer.length; i += 1) {\n const sample = (buffer[i] - 128) / 128;\n sumSquares += sample * sample;\n }\n const rms = Math.sqrt(sumSquares / buffer.length);\n const now = performance.now();\n if (rms > SOUND_RMS_THRESHOLD) {\n lastLoudAt = now;\n }\n const next = now - lastLoudAt < RELEASE_MS;\n if (next !== current) {\n current = next;\n setHasSound(next);\n }\n };\n tick();\n const intervalId = setInterval(tick, SAMPLE_INTERVAL_MS);\n\n return () => {\n clearInterval(intervalId);\n source.disconnect();\n analyser.disconnect();\n void audioContext.close();\n setHasSound(false);\n };\n }, [stream]);\n\n return hasSound;\n};\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useHasSound.js","sources":["src/internal/useHasSound.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\n\n// Порог RMS, выше которого считаем, что звук есть (речь по RMS обычно ~0.1+, шумовой пол — ниже)\nconst SOUND_RMS_THRESHOLD = 0.02;\n// Сколько держим флаг «звук есть» после последнего громкого кадра — чтобы паузы между словами\n// не роняли индикатор в тишину и он не мигал\nconst RELEASE_MS = 350;\n// Период опроса громкости: флаг булев с гистерезисом RELEASE_MS, детализация кадра дисплея тут\n// не нужна — 100 мс дают запас в ~3.5 замера на окно удержания при кратно меньшей нагрузке, чем rAF\nconst SAMPLE_INTERVAL_MS = 100;\n\ntype AudioContextConstructor = typeof AudioContext;\n\nconst getAudioContextConstructor = (): AudioContextConstructor | null => {\n if (typeof window === 'undefined') {\n return null;\n }\n const scoped = window as unknown as {\n AudioContext?: AudioContextConstructor;\n webkitAudioContext?: AudioContextConstructor;\n };\n return scoped.AudioContext ?? scoped.webkitAudioContext ?? null;\n};\n\n/**\n * Флаг «в стриме микрофона сейчас есть звук». Пока стрим есть — держит rAF-цикл и читает\n * RMS через AnalyserNode, сравнивая с порогом (флаг ещё RELEASE_MS удерживается после последнего\n * громкого кадра, чтобы не мигать на паузах между словами). Ререндер только на смене флага.\n * Треки стрима не трогаем — ими владеет сессия записи (`useVoiceRecordSession`), только читаем.\n */\nexport const useHasSound = (stream: MediaStream | null): boolean => {\n const [hasSound, setHasSound] = useState(false);\n\n useEffect(() => {\n const AudioCtx = getAudioContextConstructor();\n if (!stream || !AudioCtx || stream.getAudioTracks().length === 0) {\n return undefined;\n }\n\n const audioContext = new AudioCtx();\n // ctx может стартовать в suspended (autoplay policy); старт всегда из клика по микрофону,\n // так что resume безопасен и обычно сразу переводит в running\n void audioContext.resume();\n\n const source = audioContext.createMediaStreamSource(stream);\n const analyser = audioContext.createAnalyser();\n analyser.fftSize = 256;\n source.connect(analyser);\n\n // Web Audio API принимает данные только в типизированный массив\n // oxlint-disable-next-line magritte/no-restricted-syntax\n const buffer = new Uint8Array(analyser.fftSize);\n let lastLoudAt = 0;\n let current = false;\n\n const tick = () => {\n analyser.getByteTimeDomainData(buffer);\n let sumSquares = 0;\n for (let i = 0; i < buffer.length; i += 1) {\n const sample = (buffer[i] - 128) / 128;\n sumSquares += sample * sample;\n }\n const rms = Math.sqrt(sumSquares / buffer.length);\n const now = performance.now();\n if (rms > SOUND_RMS_THRESHOLD) {\n lastLoudAt = now;\n }\n const next = now - lastLoudAt < RELEASE_MS;\n if (next !== current) {\n current = next;\n setHasSound(next);\n }\n };\n tick();\n const intervalId = setInterval(tick, SAMPLE_INTERVAL_MS);\n\n return () => {\n clearInterval(intervalId);\n source.disconnect();\n analyser.disconnect();\n void audioContext.close();\n setHasSound(false);\n };\n }, [stream]);\n\n return hasSound;\n};\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Url для отображения картинки: строку возвращает как есть, для Blob/File создаёт локальный object URL
3
+ * и отзывает его при смене источника и размонтировании. Blob с известным не-image типом пропускаем.
4
+ * URL создаётся в эффекте (не в рендере), поэтому StrictMode и SSR его не теряют и не плодят
5
+ */
6
+ export declare const useObjectUrl: (source: string | Blob | undefined) => string | undefined;
@@ -0,0 +1,30 @@
1
+ import './../index.css';
2
+ import { useState, useEffect } from 'react';
3
+
4
+ const isImageBlob = (blob) => !blob.type || blob.type.startsWith('image/');
5
+ /**
6
+ * Url для отображения картинки: строку возвращает как есть, для Blob/File создаёт локальный object URL
7
+ * и отзывает его при смене источника и размонтировании. Blob с известным не-image типом пропускаем.
8
+ * URL создаётся в эффекте (не в рендере), поэтому StrictMode и SSR его не теряют и не плодят
9
+ */
10
+ const useObjectUrl = (source) => {
11
+ const blob = source instanceof Blob && isImageBlob(source) ? source : undefined;
12
+ const [objectUrl, setObjectUrl] = useState();
13
+ useEffect(() => {
14
+ if (!blob) {
15
+ setObjectUrl(undefined);
16
+ return undefined;
17
+ }
18
+ const url = URL.createObjectURL(blob);
19
+ setObjectUrl({ blob, url });
20
+ return () => URL.revokeObjectURL(url);
21
+ }, [blob]);
22
+ if (typeof source === 'string') {
23
+ return source;
24
+ }
25
+ // url от предыдущего Blob уже отозван — до эффекта для нового ничего не отдаём
26
+ return objectUrl && objectUrl.blob === blob ? objectUrl.url : undefined;
27
+ };
28
+
29
+ export { useObjectUrl };
30
+ //# sourceMappingURL=useObjectUrl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useObjectUrl.js","sources":["src/internal/useObjectUrl.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\n\nconst isImageBlob = (blob: Blob): boolean => !blob.type || blob.type.startsWith('image/');\n\n/**\n * Url для отображения картинки: строку возвращает как есть, для Blob/File создаёт локальный object URL\n * и отзывает его при смене источника и размонтировании. Blob с известным не-image типом пропускаем.\n * URL создаётся в эффекте (не в рендере), поэтому StrictMode и SSR его не теряют и не плодят\n */\nexport const useObjectUrl = (source: string | Blob | undefined): string | undefined => {\n const blob = source instanceof Blob && isImageBlob(source) ? source : undefined;\n const [objectUrl, setObjectUrl] = useState<{ blob: Blob; url: string }>();\n\n useEffect(() => {\n if (!blob) {\n setObjectUrl(undefined);\n return undefined;\n }\n const url = URL.createObjectURL(blob);\n setObjectUrl({ blob, url });\n return () => URL.revokeObjectURL(url);\n }, [blob]);\n\n if (typeof source === 'string') {\n return source;\n }\n // url от предыдущего Blob уже отозван — до эффекта для нового ничего не отдаём\n return objectUrl && objectUrl.blob === blob ? objectUrl.url : undefined;\n};\n"],"names":[],"mappings":";;AAEA,MAAM,WAAW,GAAG,CAAC,IAAU,KAAc,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;AAE1F;;;;AAIG;AACU,MAAA,YAAY,GAAG,CAAC,MAAiC,KAAwB;AAClF,IAAA,MAAM,IAAI,GAAG,MAAM,YAAY,IAAI,IAAI,WAAW,CAAC,MAAM,CAAC,GAAG,MAAM,GAAG,SAAS,CAAC;IAChF,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,EAA+B,CAAC;IAE1E,SAAS,CAAC,MAAK;QACX,IAAI,CAAC,IAAI,EAAE;YACP,YAAY,CAAC,SAAS,CAAC,CAAC;AACxB,YAAA,OAAO,SAAS,CAAC;SACpB;QACD,MAAM,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;AACtC,QAAA,YAAY,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;QAC5B,OAAO,MAAM,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;AAC1C,KAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;AAEX,IAAA,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE;AAC5B,QAAA,OAAO,MAAM,CAAC;KACjB;;AAED,IAAA,OAAO,SAAS,IAAI,SAAS,CAAC,IAAI,KAAK,IAAI,GAAG,SAAS,CAAC,GAAG,GAAG,SAAS,CAAC;AAC5E;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hh.ru/magritte-ui-chat-input",
3
- "version": "1.1.19",
3
+ "version": "1.2.1",
4
4
  "main": "index.js",
5
5
  "types": "index.d.ts",
6
6
  "sideEffects": [
@@ -12,7 +12,7 @@
12
12
  "prepack": "yarn root:prepack $(pwd)",
13
13
  "postpublish": "yarn root:postpublish $(pwd)",
14
14
  "stylelint-check": "yarn root:stylelint-check $(pwd)",
15
- "eslint-check": "yarn root:eslint-check $(pwd)",
15
+ "lint-check": "yarn root:lint-check $(pwd)",
16
16
  "ts-config": "yarn root:ts-config $(pwd)",
17
17
  "ts-check": "yarn root:ts-check $(pwd)",
18
18
  "test": "yarn root:test $(pwd)",
@@ -27,18 +27,20 @@
27
27
  "access": "public"
28
28
  },
29
29
  "dependencies": {
30
- "@hh.ru/magritte-common-keyboard": "4.3.5",
31
- "@hh.ru/magritte-common-use-disabled": "1.0.15",
32
- "@hh.ru/magritte-common-use-multiple-refs": "1.1.11",
33
- "@hh.ru/magritte-common-use-translation": "3.5.0",
34
- "@hh.ru/magritte-design-tokens": "25.1.0",
35
- "@hh.ru/magritte-internal-slot-registry": "1.1.0",
36
- "@hh.ru/magritte-ui-chips": "7.0.43",
37
- "@hh.ru/magritte-ui-icon": "15.1.2",
38
- "@hh.ru/magritte-ui-loader": "4.1.3",
39
- "@hh.ru/magritte-ui-scrollable-container": "6.4.10",
40
- "@hh.ru/magritte-ui-scrollbar": "1.0.36",
41
- "@hh.ru/magritte-ui-typography": "5.3.13"
30
+ "@hh.ru/magritte-common-keyboard": "4.3.6",
31
+ "@hh.ru/magritte-common-use-disabled": "1.0.16",
32
+ "@hh.ru/magritte-common-use-multiple-refs": "1.1.12",
33
+ "@hh.ru/magritte-common-use-translation": "3.6.1",
34
+ "@hh.ru/magritte-design-tokens": "25.1.1",
35
+ "@hh.ru/magritte-internal-slot-registry": "1.1.1",
36
+ "@hh.ru/magritte-ui-cell": "7.3.1",
37
+ "@hh.ru/magritte-ui-chips": "7.0.45",
38
+ "@hh.ru/magritte-ui-icon": "15.1.3",
39
+ "@hh.ru/magritte-ui-link": "7.2.4",
40
+ "@hh.ru/magritte-ui-loader": "4.1.4",
41
+ "@hh.ru/magritte-ui-scrollable-container": "6.5.1",
42
+ "@hh.ru/magritte-ui-scrollbar": "1.0.37",
43
+ "@hh.ru/magritte-ui-typography": "5.3.14"
42
44
  },
43
- "gitHead": "cb4ef069c994e3e4a1e5c76a31a6f35749190ad1"
45
+ "gitHead": "46d606980abbb523a41afba452c0a852ee902864"
44
46
  }
@@ -1,5 +1,7 @@
1
1
  import { type ChangeEventHandler, type ForwardedRef, type ForwardRefExoticComponent, type ReactNode, type RefAttributes, type TextareaHTMLAttributes } from 'react';
2
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';
3
5
  import { ChatInputPreview } from '@hh.ru/magritte-ui-chat-input/public/ChatInputPreview';
4
6
  import { ChatInputSendButton } from '@hh.ru/magritte-ui-chat-input/public/ChatInputSendButton';
5
7
  import { ChatInputVoiceButton } from '@hh.ru/magritte-ui-chat-input/public/ChatInputVoiceButton';
@@ -39,6 +41,8 @@ interface ChatInputOwnProps {
39
41
  export type ChatInputProps = ChatInputOwnProps & Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, keyof ChatInputOwnProps | 'defaultValue'>;
40
42
  interface ChatInputComponents {
41
43
  Chips: typeof ChatInputChips;
44
+ File: typeof ChatInputFile;
45
+ Files: typeof ChatInputFiles;
42
46
  Preview: typeof ChatInputPreview;
43
47
  SendButton: typeof ChatInputSendButton;
44
48
  VoiceButton: typeof ChatInputVoiceButton;
@@ -13,11 +13,13 @@ import { useRovingFocusGroup } from '../internal/useRovingFocusGroup.js';
13
13
  import { useVoiceRecordSession } from '../internal/useVoiceRecordSession.js';
14
14
  import { ChatInputVoiceSlotProvider, useVoiceConfig } from '../internal/voiceSlotRegistry.js';
15
15
  import { ChatInputChips } from './ChatInputChips.js';
16
+ import { ChatInputFile } from './ChatInputFile.js';
17
+ import { ChatInputFiles } from './ChatInputFiles.js';
16
18
  import { ChatInputPreview } from './ChatInputPreview.js';
17
19
  import { ChatInputSendButton } from './ChatInputSendButton.js';
18
20
  import { ChatInputVoiceButton } from './ChatInputVoiceButton.js';
19
21
  import { IconDefaultPropsContext } from '@hh.ru/magritte-ui-icon';
20
- import { s as styles } from '../styles-CXscNVWO.js';
22
+ import { s as styles } from '../styles-B3o1JdaK.js';
21
23
  import 'motion/react';
22
24
  import '@hh.ru/magritte-design-tokens/core-tokens';
23
25
  import '../internal/useSendOnEnter.js';
@@ -31,8 +33,12 @@ import '@hh.ru/magritte-ui-icon/icon';
31
33
  import '@hh.ru/magritte-ui-loader';
32
34
  import '@hh.ru/magritte-ui-typography';
33
35
  import '@hh.ru/magritte-internal-slot-registry';
36
+ import '../internal/flattenFragments.js';
34
37
  import '@hh.ru/magritte-ui-chips';
35
38
  import '@hh.ru/magritte-ui-scrollable-container';
39
+ import '@hh.ru/magritte-ui-cell';
40
+ import '../internal/useObjectUrl.js';
41
+ import '@hh.ru/magritte-ui-link';
36
42
  import '../internal/ChatInputSlotButton.js';
37
43
 
38
44
  const DEFAULT_ICON_PROPS = {
@@ -114,6 +120,8 @@ const ChatInputComponent = (props, ref) => (jsx(ChatInputVoiceSlotProvider, { ch
114
120
  const ChatInput = forwardRef(ChatInputComponent);
115
121
  ChatInput.displayName = 'ChatInput';
116
122
  ChatInput.Chips = ChatInputChips;
123
+ ChatInput.File = ChatInputFile;
124
+ ChatInput.Files = ChatInputFiles;
117
125
  ChatInput.Preview = ChatInputPreview;
118
126
  ChatInput.SendButton = ChatInputSendButton;
119
127
  ChatInput.VoiceButton = ChatInputVoiceButton;
@@ -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 { ChatInputPreview } from '@hh.ru/magritte-ui-chat-input/public/ChatInputPreview';\nimport { ChatInputSendButton } from '@hh.ru/magritte-ui-chat-input/public/ChatInputSendButton';\nimport { ChatInputVoiceButton } from '@hh.ru/magritte-ui-chat-input/public/ChatInputVoiceButton';\nimport { IconDefaultPropsContext } from '@hh.ru/magritte-ui-icon';\n\nimport styles from '../styles.less';\n\ninterface ChatInputOwnProps {\n /** Левый слот */\n leftActions?: ReactNode;\n /** Правый слот */\n rightActions?: ReactNode;\n /** Центральный нижний слот */\n centralActions?: ReactNode;\n /** Верхний слот для блока превью */\n preview?: ReactNode;\n /** Максимальная высота поля ввода в пикселях. Если не задан — высота не ограничена. */\n maxHeight?: number;\n /**\n * Раскладка поля ввода.\n * `horizontal` — ввод стоит в ряду контролов, пока содержимое помещается в одну строку,\n * и сам переезжает на свой ряд, когда перестаёт влезать.\n * `vertical` — ввод всегда на своём ряду; в горизонтальный слот его не ставим.\n * @default horizontal\n */\n layout?: 'vertical' | 'horizontal';\n /** Хендлер отправки: вызывается по Enter (Shift+Enter — перенос строки) */\n onSend?: VoidFunction;\n /** Значение поля ввода. Компонент работает только в controlled-режиме */\n value: string;\n /** Обработчик изменения значения */\n onChange: ChangeEventHandler<HTMLTextAreaElement>;\n /** Реф на корневую обёртку — для измерения размеров/позиционирования.\n * Реф на textarea (фокус и т.п.) прокидывается обычным `ref`, как у Input/TextArea */\n wrapperRef?: ForwardedRef<HTMLDivElement>;\n /** Отключает фон и обводку компонента */\n noBackground?: boolean;\n disabled?: boolean;\n 'data-qa'?: string;\n}\n\n// defaultValue исключён: компонент только controlled, uncontrolled-режим не поддерживается\nexport type ChatInputProps = ChatInputOwnProps &\n Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, keyof ChatInputOwnProps | 'defaultValue'>;\n\nconst DEFAULT_ICON_PROPS = {\n borderRadius: 12,\n padding: 8,\n initialColor: 'secondary',\n} as const;\n\ninterface ChatInputComponents {\n Chips: typeof ChatInputChips;\n Preview: typeof ChatInputPreview;\n SendButton: typeof ChatInputSendButton;\n VoiceButton: typeof ChatInputVoiceButton;\n}\n\ntype ChatInputInnerProps = ChatInputProps & {\n /** Реф на textarea, переданный через forwardRef внешней обёртки */\n textareaRef: ForwardedRef<HTMLTextAreaElement>;\n};\n\nconst ChatInputInner: FC<ChatInputInnerProps> = ({\n leftActions,\n rightActions,\n centralActions,\n preview,\n maxHeight,\n layout = 'horizontal',\n onSend,\n wrapperRef,\n textareaRef,\n noBackground = false,\n disabled = false,\n 'data-qa': dataQa = 'chat-input',\n ...textareaProps\n}) => {\n const outerDisabled = useDisabled();\n const isDisabled = disabled || outerDisabled;\n const rootRef = useRef<HTMLDivElement>(null);\n const rootRefCallback = useMultipleRefs(rootRef, wrapperRef);\n // реф textarea — для возврата фокуса при выходе из режима записи\n const textareaLocalRef = useRef<HTMLTextAreaElement>(null);\n const textareaRefCallback = useMultipleRefs(textareaLocalRef, textareaRef);\n const [containerWidth, setContainerWidth] = useState<number>();\n useLayoutEffect(() => {\n const root = rootRef.current;\n if (!root) {\n return undefined;\n }\n const observer = new ResizeObserver((entries) => {\n const width = Math.round(entries[0]?.contentRect.width ?? 0);\n setContainerWidth((prev) => (prev === width ? prev : width));\n });\n observer.observe(root);\n return () => observer.disconnect();\n }, []);\n\n // конфиг записи регистрирует ChatInput.VoiceButton через slot-registry. Кнопка живёт в\n // text-гриде, который на время записи не размонтируется (ModeTransition лишь прячет его), —\n // поэтому регистрация и колбэки остаются живыми всю сессию, латч не нужен\n const voiceConfig = useVoiceConfig();\n\n const voiceSession = useVoiceRecordSession(voiceConfig ?? {});\n\n const contextValue = useMemo(\n () => ({\n onSend,\n startVoiceRecord: voiceSession.start,\n voiceRecordStatus: voiceSession.status,\n }),\n [onSend, voiceSession.start, voiceSession.status]\n );\n\n // режим ведём по статусу сессии, а не по наличию конфига: даже если приложение уберёт кнопку\n // по ходу записи, запись продолжается. Без кнопки запись не стартует, так что статус сам остаётся idle\n const isVoiceMode = voiceSession.status === 'recording' || voiceSession.status === 'processing';\n\n // roving-группа кнопок: один tab stop на все кнопки, внутри — стрелки по приоритетам\n const { focusStop } = useRovingFocusGroup(rootRef);\n\n // при входе в запись фокус на стоп группы (после смены слоя это галочка — первый приоритет\n // слоя записи), при возврате в текст — в textarea с курсором в конце. Фокус ставим в rAF:\n // слой режима к этому моменту уже видим (motion применил visibility), иначе focus()\n // на visibility:hidden не сработает\n const prevVoiceModeRef = useRef(isVoiceMode);\n useEffect(() => {\n const wasVoiceMode = prevVoiceModeRef.current;\n prevVoiceModeRef.current = isVoiceMode;\n if (wasVoiceMode === isVoiceMode) {\n return undefined;\n }\n const frameId = requestAnimationFrame(() => {\n if (isVoiceMode) {\n focusStop();\n return;\n }\n const textarea = textareaLocalRef.current;\n if (textarea) {\n textarea.focus();\n const caret = textarea.value.length;\n textarea.setSelectionRange(caret, caret);\n }\n });\n return () => cancelAnimationFrame(frameId);\n }, [isVoiceMode, focusStop]);\n\n return (\n <DisabledContext.Provider value={isDisabled}>\n <ChatInputContextProvider value={contextValue}>\n <div\n ref={rootRefCallback}\n className={classNames(styles.chatInput, { [styles.noBackground]: noBackground })}\n data-qa={dataQa}\n >\n <ModeTransition\n mode={isVoiceMode ? 'voice' : 'text'}\n voice={\n <VoiceRecord\n mode={voiceSession.status === 'processing' ? 'processing' : 'recording'}\n stream={voiceSession.stream}\n startedAt={voiceSession.startedAt}\n processingLabel={voiceConfig?.processingLabel}\n onComplete={voiceSession.stop}\n onCancel={voiceSession.cancel}\n completeButtonProps={voiceConfig?.completeButtonProps}\n cancelButtonProps={voiceConfig?.cancelButtonProps}\n data-qa=\"chat-input-voice-record\"\n />\n }\n text={\n <div\n className={classNames(styles.contentGrid, {\n [styles.noLeftControls]: !leftActions,\n [styles.noRightControls]: !rightActions,\n [styles.hasCentral]: !!centralActions,\n })}\n >\n {/* textarea в DOM первым намеренно: Tab снаружи входит сначала в него.\n Раскладку держит grid-area (от порядка не зависит). Кнопки слотов —\n одна roving-группа (useRovingFocusGroup): Tab видит только текущий\n стоп, внутри группы фокус ходит стрелками по приоритетам слотов */}\n <TextInput\n {...textareaProps}\n ref={textareaRefCallback}\n layout={layout}\n onSend={onSend}\n maxHeight={maxHeight}\n containerWidth={containerWidth}\n hasCentral={!!centralActions}\n disabled={isDisabled}\n hasBothSideControls={!!leftActions && !!rightActions}\n />\n {!!rightActions && (\n <IconDefaultPropsContext props={DEFAULT_ICON_PROPS}>\n <div className={styles.right} {...focusPriorityProps(FOCUS_PRIORITY.rightSlot)}>\n {rightActions}\n </div>\n </IconDefaultPropsContext>\n )}\n {!!leftActions && (\n <IconDefaultPropsContext props={DEFAULT_ICON_PROPS}>\n <div className={styles.left} {...focusPriorityProps(FOCUS_PRIORITY.leftSlot)}>\n {leftActions}\n </div>\n </IconDefaultPropsContext>\n )}\n {!!centralActions && (\n <div className={styles.central} {...focusPriorityProps(FOCUS_PRIORITY.centralSlot)}>\n {centralActions}\n </div>\n )}\n {!!preview && (\n <div className={styles.preview} {...focusPriorityProps(FOCUS_PRIORITY.previewSlot)}>\n {preview}\n </div>\n )}\n </div>\n }\n />\n </div>\n </ChatInputContextProvider>\n </DisabledContext.Provider>\n );\n};\n\n// внешняя обёртка держит slot-registry для конфига записи: кнопка (внутри слота) регистрирует\n// конфиг, ChatInputInner (ниже провайдера) читает его. forwardRef-реф на textarea прокидываем\n// вниз обычным пропом\nconst ChatInputComponent = (props: ChatInputProps, ref: ForwardedRef<HTMLTextAreaElement>) => (\n <ChatInputVoiceSlotProvider>\n <ChatInputInner {...props} textareaRef={ref} />\n </ChatInputVoiceSlotProvider>\n);\n\nconst ChatInput = forwardRef(ChatInputComponent) as ForwardRefExoticComponent<\n ChatInputProps & RefAttributes<HTMLTextAreaElement>\n> &\n ChatInputComponents;\n\nChatInput.displayName = 'ChatInput';\n\nChatInput.Chips = ChatInputChips;\nChatInput.Preview = ChatInputPreview;\nChatInput.SendButton = ChatInputSendButton;\nChatInput.VoiceButton = ChatInputVoiceButton;\n\nexport { ChatInput };\n"],"names":["_jsx","_jsxs"],"mappings":"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"}
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,14 +1,12 @@
1
1
  import './../index.css';
2
2
  import { jsx } from 'react/jsx-runtime';
3
- import { isValidElement, Children, Fragment } from 'react';
3
+ import { isValidElement } from 'react';
4
+ import { flattenFragments } from '../internal/flattenFragments.js';
4
5
  import { ChipDefaultPropsContext } from '@hh.ru/magritte-ui-chips';
5
6
  import { ScrollableContainer } from '@hh.ru/magritte-ui-scrollable-container';
6
- import { s as styles } from '../styles-CXscNVWO.js';
7
+ import { s as styles } from '../styles-B3o1JdaK.js';
7
8
 
8
9
  const CHIP_DEFAULT_PROPS = { size: 'extra-small' };
9
- const flattenFragments = (children) => Children.toArray(children).flatMap((child) => isValidElement(child) && child.type === Fragment
10
- ? flattenFragments(child.props.children)
11
- : child);
12
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))) }) }) }) }) }) }));
13
11
  ChatInputChips.displayName = 'ChatInputChips';
14
12
 
@@ -1 +1 @@
1
- {"version":3,"file":"ChatInputChips.js","sources":["src/public/ChatInputChips.tsx"],"sourcesContent":["import { Children, Fragment, isValidElement, type FC, type PropsWithChildren, type ReactNode } from 'react';\n\nimport { ChipDefaultPropsContext } from '@hh.ru/magritte-ui-chips';\nimport { ScrollableContainer } from '@hh.ru/magritte-ui-scrollable-container';\n\nimport styles from '../styles.less';\n\nexport interface ChatInputChipsProps {\n /** Чипсы, размещаются в один ряд с возможностью горизонтального скролла */\n children: ReactNode;\n}\n\nconst CHIP_DEFAULT_PROPS = { size: 'extra-small' } as const;\n\nconst flattenFragments = (children: ReactNode): ReactNode[] =>\n Children.toArray(children).flatMap((child) =>\n isValidElement(child) && child.type === Fragment\n ? flattenFragments((child.props as PropsWithChildren).children)\n : child\n );\n\nexport const ChatInputChips: FC<ChatInputChipsProps> = ({ children }) => (\n <div className={styles.chipsContainer}>\n <ChipDefaultPropsContext props={CHIP_DEFAULT_PROPS}>\n <ScrollableContainer>\n <ScrollableContainer.DefaultLayout size=\"small\" fadeOnMobile>\n <ScrollableContainer.Gallery>\n <div className={styles.chips}>\n {/* Children.toArray сохраняет ключи детей — переносим их на обёртку,\n чтобы удаление чипса из середины не сдвигало стейт соседей */}\n {flattenFragments(children).map((child, index) => (\n <ScrollableContainer.Item key={(isValidElement(child) && child.key) || index}>\n {child}\n </ScrollableContainer.Item>\n ))}\n </div>\n </ScrollableContainer.Gallery>\n </ScrollableContainer.DefaultLayout>\n </ScrollableContainer>\n </ChipDefaultPropsContext>\n </div>\n);\n\nChatInputChips.displayName = 'ChatInputChips';\n"],"names":["_jsx"],"mappings":";;;;;;AAYA,MAAM,kBAAkB,GAAG,EAAE,IAAI,EAAE,aAAa,EAAW,CAAC;AAE5D,MAAM,gBAAgB,GAAG,CAAC,QAAmB,KACzC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,KACrC,cAAc,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ;MAC1C,gBAAgB,CAAE,KAAK,CAAC,KAA2B,CAAC,QAAQ,CAAC;MAC7D,KAAK,CACd,CAAC;MAEO,cAAc,GAA4B,CAAC,EAAE,QAAQ,EAAE,MAChEA,GAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAE,MAAM,CAAC,cAAc,EAAA,QAAA,EACjCA,GAAC,CAAA,uBAAuB,EAAC,EAAA,KAAK,EAAE,kBAAkB,EAAA,QAAA,EAC9CA,IAAC,mBAAmB,EAAA,EAAA,QAAA,EAChBA,IAAC,mBAAmB,CAAC,aAAa,EAAC,EAAA,IAAI,EAAC,OAAO,EAAC,YAAY,EACxD,IAAA,EAAA,QAAA,EAAAA,GAAA,CAAC,mBAAmB,CAAC,OAAO,EACxB,EAAA,QAAA,EAAAA,GAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAE,MAAM,CAAC,KAAK,YAGvB,gBAAgB,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,MACzCA,GAAC,CAAA,mBAAmB,CAAC,IAAI,EACpB,EAAA,QAAA,EAAA,KAAK,IADqB,CAAC,cAAc,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,GAAG,KAAK,KAAK,CAEjD,CAC9B,CAAC,EACA,CAAA,EAAA,CACoB,GACE,EAClB,CAAA,EAAA,CACA,EACxB,CAAA,EACR;AAEF,cAAc,CAAC,WAAW,GAAG,gBAAgB;;;;"}
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;;;;"}
@@ -0,0 +1,43 @@
1
+ import { type HTMLAttributes, type MouseEventHandler, type ReactNode } from 'react';
2
+ import { type ChatInputButtonAria } from '@hh.ru/magritte-ui-chat-input/types';
3
+ export type ChatInputFileState = 'loading' | 'loaded' | 'invalid';
4
+ export type ChatInputFileType = 'doc' | 'video' | 'image';
5
+ export interface ChatInputFileProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'className'> {
6
+ /** Имя файла. Если передана строка, при нехватке места обрезается основа имени, а расширение
7
+ * остаётся видимым целиком */
8
+ name?: ReactNode;
9
+ /**
10
+ * Строка под именем (например, для размера файла или текста ошибки).
11
+ * Для `type="image"` не отображается
12
+ */
13
+ description?: ReactNode;
14
+ /**
15
+ * Состояние файла
16
+ * @default loaded
17
+ */
18
+ state?: ChatInputFileState;
19
+ /**
20
+ * Тип файла: `doc` и `video` — карточка с иконкой и именем, `image` — квадратное превью
21
+ * @default doc
22
+ */
23
+ type?: ChatInputFileType;
24
+ /**
25
+ * Превью, показывается для `type="image"` в состоянии `loaded`: url картинки или сам файл (`File`/`Blob`).
26
+ * Для файла компонент локально создаёт object URL и сам его отзывает — загружать файл на сервер
27
+ * ради превью не нужно
28
+ */
29
+ preview?: string | Blob;
30
+ /** Alt превью. По умолчанию — имя файла, если оно строка */
31
+ previewAlt?: string;
32
+ /** Обработчик удаления файла. Без него кнопка удаления файла не показывается */
33
+ onRemove?: MouseEventHandler<HTMLButtonElement>;
34
+ /** aria/data-qa пропсы кнопки удаления файла. Своя `aria-label` перекрывает дефолтную */
35
+ removeButtonProps?: ChatInputButtonAria;
36
+ /**
37
+ * Значение data-qa
38
+ * @default chat-input-file
39
+ */
40
+ 'data-qa'?: string;
41
+ }
42
+ /** Приложенный к сообщению файл */
43
+ export declare const ChatInputFile: import("react").ForwardRefExoticComponent<ChatInputFileProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,74 @@
1
+ import './../index.css';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+ import { forwardRef, useState } from 'react';
4
+ import classNames from 'classnames';
5
+ import { useTranslation } from '@hh.ru/magritte-common-use-translation';
6
+ import { Cell, CellText } from '@hh.ru/magritte-ui-cell';
7
+ import { useObjectUrl } from '../internal/useObjectUrl.js';
8
+ import { CheckCircleOutlinedSize16, ExclamationTriangleOutlinedSize16, ImageOutlinedSize24, CrossOutlinedSize16, VideocameraOutlinedSize24, DocumentOutlinedSize24 } from '@hh.ru/magritte-ui-icon/icon';
9
+ import { LinkDefaultPropsContext } from '@hh.ru/magritte-ui-link';
10
+ import { Loader } from '@hh.ru/magritte-ui-loader';
11
+ import { TextDefaultPropsContext } from '@hh.ru/magritte-ui-typography';
12
+ import { s as styles } from '../styles-B3o1JdaK.js';
13
+
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) };
21
+ };
22
+ 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 })] }));
30
+ };
31
+ const STATUS_ICON = {
32
+ loading: jsx(Loader, { size: 16 }),
33
+ loaded: jsx(CheckCircleOutlinedSize16, { initialColor: "positive" }),
34
+ invalid: jsx(ExclamationTriangleOutlinedSize16, { initialColor: "negative" }),
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 (например, ещё не готовое серверное превью) загрузился повторно
49
+ const [failedSrc, setFailedSrc] = useState();
50
+ if (state !== 'loaded' && failedSrc !== undefined) {
51
+ setFailedSrc(undefined);
52
+ }
53
+ let content;
54
+ if (isImage) {
55
+ const showPreview = state === 'loaded' && !!previewSrc && previewSrc !== failedSrc;
56
+ let placeholder = null;
57
+ if (state === 'loading') {
58
+ placeholder = jsx(Loader, { size: 24 });
59
+ }
60
+ else if (!showPreview) {
61
+ placeholder = jsx(ImageOutlinedSize24, { initialColor: "tertiary" });
62
+ }
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) }));
64
+ }
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';
72
+
73
+ export { ChatInputFile };
74
+ //# sourceMappingURL=ChatInputFile.js.map