@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.
- package/index.css +117 -117
- package/index.d.ts +10 -10
- package/index.js +1 -1
- package/index.mock.d.ts +9 -9
- package/index.mock.js +37 -36
- package/index.mock.js.map +1 -1
- package/internal/ChatInputContext.d.ts +1 -1
- package/internal/ChatInputContext.js.map +1 -1
- package/internal/ChatInputSlotButton.js +13 -13
- package/internal/ChatInputSlotButton.js.map +1 -1
- package/internal/FocusTarget.js +3 -10
- package/internal/FocusTarget.js.map +1 -1
- package/internal/ModeTransition.js +41 -11
- package/internal/ModeTransition.js.map +1 -1
- package/internal/TextInput.d.ts +1 -1
- package/internal/TextInput.js +146 -126
- package/internal/TextInput.js.map +1 -1
- package/internal/VoiceRecord.d.ts +1 -1
- package/internal/VoiceRecord.js +66 -24
- package/internal/VoiceRecord.js.map +1 -1
- package/internal/VoiceRecordIndicator.js +22 -9
- package/internal/VoiceRecordIndicator.js.map +1 -1
- package/internal/flattenFragments.js +9 -13
- package/internal/flattenFragments.js.map +1 -1
- package/internal/focusGroup.js +18 -28
- package/internal/focusGroup.js.map +1 -1
- package/internal/useHasSound.js +49 -64
- package/internal/useHasSound.js.map +1 -1
- package/internal/useObjectUrl.js +15 -21
- package/internal/useObjectUrl.js.map +1 -1
- package/internal/useRovingFocusGroup.js +102 -137
- package/internal/useRovingFocusGroup.js.map +1 -1
- package/internal/useSendOnEnter.js +44 -46
- package/internal/useSendOnEnter.js.map +1 -1
- package/internal/useVoiceRecordSession.d.ts +1 -1
- package/internal/useVoiceRecordSession.js +185 -219
- package/internal/useVoiceRecordSession.js.map +1 -1
- package/internal/voiceSlotRegistry.d.ts +1 -1
- package/internal/voiceSlotRegistry.js +3 -3
- package/internal/voiceSlotRegistry.js.map +1 -1
- package/package.json +11 -11
- package/public/ChatInput.d.ts +6 -6
- package/public/ChatInput.js +129 -73
- package/public/ChatInput.js.map +1 -1
- package/public/ChatInputChips.js +4 -4
- package/public/ChatInputChips.js.map +1 -1
- package/public/ChatInputFile.d.ts +1 -1
- package/public/ChatInputFile.js +107 -47
- package/public/ChatInputFile.js.map +1 -1
- package/public/ChatInputFiles.js +10 -3
- package/public/ChatInputFiles.js.map +1 -1
- package/public/ChatInputPreview.js +14 -9
- package/public/ChatInputPreview.js.map +1 -1
- package/public/ChatInputSendButton.d.ts +1 -1
- package/public/ChatInputSendButton.js +34 -23
- package/public/ChatInputSendButton.js.map +1 -1
- package/public/ChatInputVoiceButton.d.ts +1 -1
- package/public/ChatInputVoiceButton.js +87 -91
- package/public/ChatInputVoiceButton.js.map +1 -1
- package/public/getChatInputFileType.d.ts +1 -1
- package/public/getChatInputFileType.js +7 -10
- package/public/getChatInputFileType.js.map +1 -1
- package/styles-hEUo5BXU.js +5 -0
- package/styles-hEUo5BXU.js.map +1 -0
- package/styles-CHMi0EYL.js +0 -5
- package/styles-CHMi0EYL.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatInputFile.js","sources":["src/public/ChatInputFile.tsx"],"sourcesContent":["import { forwardRef, type HTMLAttributes, type MouseEventHandler, type ReactNode, useState } from 'react';\nimport classNames from 'classnames';\n\nimport { useTranslation } from '@hh.ru/magritte-common-use-translation';\nimport { Cell, CellText } from '@hh.ru/magritte-ui-cell';\nimport { useObjectUrl } from '@hh.ru/magritte-ui-chat-input/internal/useObjectUrl';\nimport { type ChatInputButtonAria } from '@hh.ru/magritte-ui-chat-input/types';\nimport {\n CheckCircleOutlinedSize16,\n CrossOutlinedSize16,\n DocumentOutlinedSize24,\n ExclamationTriangleOutlinedSize16,\n ImageOutlinedSize24,\n VideocameraOutlinedSize24,\n} from '@hh.ru/magritte-ui-icon/icon';\nimport { LinkDefaultPropsContext } from '@hh.ru/magritte-ui-link';\nimport { Loader } from '@hh.ru/magritte-ui-loader';\nimport { TextDefaultPropsContext } from '@hh.ru/magritte-ui-typography';\n\nimport styles from '../styles.less';\n\nexport type ChatInputFileState = 'loading' | 'loaded' | 'invalid';\n\nexport type ChatInputFileType = 'doc' | 'video' | 'image';\n\nexport interface ChatInputFileProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'className'> {\n /** Имя файла. Если передана строка, при нехватке места обрезается основа имени, а расширение\n * остаётся видимым целиком */\n name?: ReactNode;\n /**\n * Строка под именем (например, для размера файла или текста ошибки).\n * Для `type=\"image\"` не отображается\n */\n description?: ReactNode;\n /**\n * Состояние файла\n * @default loaded\n */\n state?: ChatInputFileState;\n /**\n * Тип файла: `doc` и `video` — карточка с иконкой и именем, `image` — квадратное превью\n * @default doc\n */\n type?: ChatInputFileType;\n /**\n * Превью, показывается для `type=\"image\"` в состоянии `loaded`: url картинки или сам файл (`File`/`Blob`).\n * Для файла компонент локально создаёт object URL и сам его отзывает — загружать файл на сервер\n * ради превью не нужно\n */\n preview?: string | Blob;\n /** Alt превью. По умолчанию — имя файла, если оно строка */\n previewAlt?: string;\n /** Обработчик удаления файла. Без него кнопка удаления файла не показывается */\n onRemove?: MouseEventHandler<HTMLButtonElement>;\n /** aria/data-qa пропсы кнопки удаления файла. Своя `aria-label` перекрывает дефолтную */\n removeButtonProps?: ChatInputButtonAria;\n /**\n * Значение data-qa\n * @default chat-input-file\n */\n 'data-qa'?: string;\n}\n\nconst splitFileName = (name: string): { base: string; extension: string } => {\n const dotIndex = name.lastIndexOf('.');\n // точка в начале (.gitignore) или в конце (file.) — расширения нет, имя целиком\n if (dotIndex <= 0 || dotIndex === name.length - 1) {\n return { base: name, extension: '' };\n }\n return { base: name.slice(0, dotIndex), extension: name.slice(dotIndex) };\n};\n\nconst FileName = ({ name }: { name: ReactNode }) => {\n if (typeof name !== 'string') {\n return <>{name}</>;\n }\n const { base, extension } = splitFileName(name);\n // основа сжимается с многоточием, расширение не сжимается; типографику даёт CellText.\n // имя без расширения обрезается так же, целиком\n return (\n <span className={styles.fileName}>\n <span className={styles.fileNameBase}>{base}</span>\n {extension && <span className={styles.fileNameExtension}>{extension}</span>}\n </span>\n );\n};\n\nconst STATUS_ICON: Record<ChatInputFileState, ReactNode> = {\n loading: <Loader size={16} />,\n loaded: <CheckCircleOutlinedSize16 initialColor=\"positive\" />,\n invalid: <ExclamationTriangleOutlinedSize16 initialColor=\"negative\" />,\n};\n\nconst LINK_DEFAULT_PROPS = { typography: 'label-3-regular', inline: true } as const;\n\n// строку оформляет CellText, а Text, переданный пользователем в name/description, получает те же дефолты.\n// ellipsis — для Text: блочный элемент не обрезается text-overflow родителя\nconst NAME_TEXT_PROPS = { typography: 'label-3-regular', style: 'primary', ellipsis: true } as const;\nconst DESCRIPTION_TEXT_PROPS = { typography: 'label-3-regular', style: 'secondary', ellipsis: true } as const;\n\n/** Приложенный к сообщению файл */\nexport const ChatInputFile = forwardRef<HTMLDivElement, ChatInputFileProps>(\n (\n {\n 'data-qa': dataQa = 'chat-input-file',\n description,\n name,\n onRemove,\n preview,\n previewAlt,\n removeButtonProps,\n state = 'loaded',\n type = 'doc',\n ...props\n },\n ref\n ) => {\n const removeLabel = useTranslation('ChatInput', 'removeFile');\n const isImage = type === 'image';\n // object URL создаём только для плитки картинки — карточкам он не нужен\n const previewSrc = useObjectUrl(isImage ? preview : undefined);\n // битая картинка — показываем заглушку; сбрасывается сменой src или уходом из loaded,\n // чтобы тот же url (например, ещё не готовое серверное превью) загрузился повторно\n const [failedSrc, setFailedSrc] = useState<string>();\n if (state !== 'loaded' && failedSrc !== undefined) {\n setFailedSrc(undefined);\n }\n\n let content: ReactNode;\n if (isImage) {\n const showPreview = state === 'loaded' && !!previewSrc && previewSrc !== failedSrc;\n let placeholder: ReactNode = null;\n if (state === 'loading') {\n placeholder = <Loader size={24} />;\n } else if (!showPreview) {\n placeholder = <ImageOutlinedSize24 initialColor=\"tertiary\" />;\n }\n content = (\n <div className={classNames(styles.fileTile, { [styles.fileTilePlaceholder]: !showPreview })}>\n {showPreview ? (\n <img\n className={styles.fileTileImage}\n src={previewSrc}\n alt={previewAlt ?? (typeof name === 'string' ? name : '')}\n onError={() => setFailedSrc(previewSrc)}\n />\n ) : (\n placeholder\n )}\n </div>\n );\n } else {\n const Icon = type === 'video' ? VideocameraOutlinedSize24 : DocumentOutlinedSize24;\n content = (\n <div className={classNames(styles.fileCard, { [styles.fileCardRemovable]: !!onRemove })}>\n <Cell\n align=\"center\"\n horPadding={false}\n vertPadding={false}\n left={\n <Icon initialColor=\"secondary\" backgroundStyle=\"secondary\" padding={12} borderRadius={8} />\n }\n >\n {!!name && (\n <CellText\n type=\"title\"\n typography=\"label-3-regular\"\n style=\"primary\"\n maxLines={1}\n data-qa=\"chat-input-file-name\"\n >\n <TextDefaultPropsContext props={NAME_TEXT_PROPS}>\n <FileName name={name} />\n </TextDefaultPropsContext>\n </CellText>\n )}\n {!!description && (\n <CellText\n type=\"subtitle\"\n style=\"secondary\"\n maxLines={1}\n left={STATUS_ICON[state]}\n data-qa=\"chat-input-file-description\"\n >\n <TextDefaultPropsContext props={DESCRIPTION_TEXT_PROPS}>\n <LinkDefaultPropsContext props={LINK_DEFAULT_PROPS}>\n {description}\n </LinkDefaultPropsContext>\n </TextDefaultPropsContext>\n </CellText>\n )}\n </Cell>\n </div>\n );\n }\n\n return (\n <div {...props} className={styles.file} data-qa={dataQa} ref={ref}>\n {content}\n {onRemove && (\n <span className={styles.fileRemove}>\n <CrossOutlinedSize16\n data-qa=\"chat-input-file-remove\"\n {...removeButtonProps}\n aria-label={removeButtonProps?.['aria-label'] ?? removeLabel}\n initialColor=\"primary\"\n backgroundStyle=\"secondary\"\n borderRadius={8}\n padding={2}\n onClick={onRemove}\n />\n </span>\n )}\n </div>\n );\n }\n);\n\nChatInputFile.displayName = 'ChatInputFile';\n"],"names":["_jsx","_Fragment","_jsxs"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ChatInputFile.js","sources":["src/public/ChatInputFile.tsx"],"sourcesContent":["import { forwardRef, type HTMLAttributes, type MouseEventHandler, type ReactNode, useState } from 'react';\nimport classNames from 'classnames';\n\nimport { useTranslation } from '@hh.ru/magritte-common-use-translation';\nimport { Cell, CellText } from '@hh.ru/magritte-ui-cell';\nimport { useObjectUrl } from '@hh.ru/magritte-ui-chat-input/internal/useObjectUrl';\nimport { type ChatInputButtonAria } from '@hh.ru/magritte-ui-chat-input/types';\nimport {\n CheckCircleOutlinedSize16,\n CrossOutlinedSize16,\n DocumentOutlinedSize24,\n ExclamationTriangleOutlinedSize16,\n ImageOutlinedSize24,\n VideocameraOutlinedSize24,\n} from '@hh.ru/magritte-ui-icon/icon';\nimport { LinkDefaultPropsContext } from '@hh.ru/magritte-ui-link';\nimport { Loader } from '@hh.ru/magritte-ui-loader';\nimport { TextDefaultPropsContext } from '@hh.ru/magritte-ui-typography';\n\nimport styles from '../styles.less';\n\nexport type ChatInputFileState = 'loading' | 'loaded' | 'invalid';\n\nexport type ChatInputFileType = 'doc' | 'video' | 'image';\n\nexport interface ChatInputFileProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'className'> {\n /** Имя файла. Если передана строка, при нехватке места обрезается основа имени, а расширение\n * остаётся видимым целиком */\n name?: ReactNode;\n /**\n * Строка под именем (например, для размера файла или текста ошибки).\n * Для `type=\"image\"` не отображается\n */\n description?: ReactNode;\n /**\n * Состояние файла\n * @default loaded\n */\n state?: ChatInputFileState;\n /**\n * Тип файла: `doc` и `video` — карточка с иконкой и именем, `image` — квадратное превью\n * @default doc\n */\n type?: ChatInputFileType;\n /**\n * Превью, показывается для `type=\"image\"` в состоянии `loaded`: url картинки или сам файл (`File`/`Blob`).\n * Для файла компонент локально создаёт object URL и сам его отзывает — загружать файл на сервер\n * ради превью не нужно\n */\n preview?: string | Blob;\n /** Alt превью. По умолчанию — имя файла, если оно строка */\n previewAlt?: string;\n /** Обработчик удаления файла. Без него кнопка удаления файла не показывается */\n onRemove?: MouseEventHandler<HTMLButtonElement>;\n /** aria/data-qa пропсы кнопки удаления файла. Своя `aria-label` перекрывает дефолтную */\n removeButtonProps?: ChatInputButtonAria;\n /**\n * Значение data-qa\n * @default chat-input-file\n */\n 'data-qa'?: string;\n}\n\nconst splitFileName = (name: string): { base: string; extension: string } => {\n const dotIndex = name.lastIndexOf('.');\n // точка в начале (.gitignore) или в конце (file.) — расширения нет, имя целиком\n if (dotIndex <= 0 || dotIndex === name.length - 1) {\n return { base: name, extension: '' };\n }\n return { base: name.slice(0, dotIndex), extension: name.slice(dotIndex) };\n};\n\nconst FileName = ({ name }: { name: ReactNode }) => {\n if (typeof name !== 'string') {\n return <>{name}</>;\n }\n const { base, extension } = splitFileName(name);\n // основа сжимается с многоточием, расширение не сжимается; типографику даёт CellText.\n // имя без расширения обрезается так же, целиком\n return (\n <span className={styles.fileName}>\n <span className={styles.fileNameBase}>{base}</span>\n {extension && <span className={styles.fileNameExtension}>{extension}</span>}\n </span>\n );\n};\n\nconst STATUS_ICON: Record<ChatInputFileState, ReactNode> = {\n loading: <Loader size={16} />,\n loaded: <CheckCircleOutlinedSize16 initialColor=\"positive\" />,\n invalid: <ExclamationTriangleOutlinedSize16 initialColor=\"negative\" />,\n};\n\nconst LINK_DEFAULT_PROPS = { typography: 'label-3-regular', inline: true } as const;\n\n// строку оформляет CellText, а Text, переданный пользователем в name/description, получает те же дефолты.\n// ellipsis — для Text: блочный элемент не обрезается text-overflow родителя\nconst NAME_TEXT_PROPS = { typography: 'label-3-regular', style: 'primary', ellipsis: true } as const;\nconst DESCRIPTION_TEXT_PROPS = { typography: 'label-3-regular', style: 'secondary', ellipsis: true } as const;\n\n/** Приложенный к сообщению файл */\nexport const ChatInputFile = forwardRef<HTMLDivElement, ChatInputFileProps>(\n (\n {\n 'data-qa': dataQa = 'chat-input-file',\n description,\n name,\n onRemove,\n preview,\n previewAlt,\n removeButtonProps,\n state = 'loaded',\n type = 'doc',\n ...props\n },\n ref\n ) => {\n const removeLabel = useTranslation('ChatInput', 'removeFile');\n const isImage = type === 'image';\n // object URL создаём только для плитки картинки — карточкам он не нужен\n const previewSrc = useObjectUrl(isImage ? preview : undefined);\n // битая картинка — показываем заглушку; сбрасывается сменой src или уходом из loaded,\n // чтобы тот же url (например, ещё не готовое серверное превью) загрузился повторно\n const [failedSrc, setFailedSrc] = useState<string>();\n if (state !== 'loaded' && failedSrc !== undefined) {\n setFailedSrc(undefined);\n }\n\n let content: ReactNode;\n if (isImage) {\n const showPreview = state === 'loaded' && !!previewSrc && previewSrc !== failedSrc;\n let placeholder: ReactNode = null;\n if (state === 'loading') {\n placeholder = <Loader size={24} />;\n } else if (!showPreview) {\n placeholder = <ImageOutlinedSize24 initialColor=\"tertiary\" />;\n }\n content = (\n <div className={classNames(styles.fileTile, { [styles.fileTilePlaceholder]: !showPreview })}>\n {showPreview ? (\n <img\n className={styles.fileTileImage}\n src={previewSrc}\n alt={previewAlt ?? (typeof name === 'string' ? name : '')}\n onError={() => setFailedSrc(previewSrc)}\n />\n ) : (\n placeholder\n )}\n </div>\n );\n } else {\n const Icon = type === 'video' ? VideocameraOutlinedSize24 : DocumentOutlinedSize24;\n content = (\n <div className={classNames(styles.fileCard, { [styles.fileCardRemovable]: !!onRemove })}>\n <Cell\n align=\"center\"\n horPadding={false}\n vertPadding={false}\n left={\n <Icon initialColor=\"secondary\" backgroundStyle=\"secondary\" padding={12} borderRadius={8} />\n }\n >\n {!!name && (\n <CellText\n type=\"title\"\n typography=\"label-3-regular\"\n style=\"primary\"\n maxLines={1}\n data-qa=\"chat-input-file-name\"\n >\n <TextDefaultPropsContext props={NAME_TEXT_PROPS}>\n <FileName name={name} />\n </TextDefaultPropsContext>\n </CellText>\n )}\n {!!description && (\n <CellText\n type=\"subtitle\"\n style=\"secondary\"\n maxLines={1}\n left={STATUS_ICON[state]}\n data-qa=\"chat-input-file-description\"\n >\n <TextDefaultPropsContext props={DESCRIPTION_TEXT_PROPS}>\n <LinkDefaultPropsContext props={LINK_DEFAULT_PROPS}>\n {description}\n </LinkDefaultPropsContext>\n </TextDefaultPropsContext>\n </CellText>\n )}\n </Cell>\n </div>\n );\n }\n\n return (\n <div {...props} className={styles.file} data-qa={dataQa} ref={ref}>\n {content}\n {onRemove && (\n <span className={styles.fileRemove}>\n <CrossOutlinedSize16\n data-qa=\"chat-input-file-remove\"\n {...removeButtonProps}\n aria-label={removeButtonProps?.['aria-label'] ?? removeLabel}\n initialColor=\"primary\"\n backgroundStyle=\"secondary\"\n borderRadius={8}\n padding={2}\n onClick={onRemove}\n />\n </span>\n )}\n </div>\n );\n }\n);\n\nChatInputFile.displayName = 'ChatInputFile';\n"],"names":[],"mappings":"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"}
|
package/public/ChatInputFiles.js
CHANGED
|
@@ -3,10 +3,17 @@ import { jsx } from 'react/jsx-runtime';
|
|
|
3
3
|
import { isValidElement } from 'react';
|
|
4
4
|
import { flattenFragments } from '../internal/flattenFragments.js';
|
|
5
5
|
import { ScrollableContainer } from '@hh.ru/magritte-ui-scrollable-container';
|
|
6
|
-
import { s as styles } from '../styles-
|
|
6
|
+
import { s as styles } from '../styles-hEUo5BXU.js';
|
|
7
7
|
|
|
8
|
-
const ChatInputFiles = ({ children }) =>
|
|
9
|
-
|
|
8
|
+
const ChatInputFiles = ({ children }) => /* @__PURE__ */ jsx("div", { className: styles.filesContainer, "data-qa": "chat-input-files", children: /* @__PURE__ */ jsx(ScrollableContainer, { correction: 11, children: /* @__PURE__ */ jsx(ScrollableContainer.DefaultLayout, { size: "small", noFadeOnDesktop: true, buttonsOffset: 8, buttonsOnHover: true, children: /* @__PURE__ */ jsx(ScrollableContainer.Gallery, { children: /* @__PURE__ */ jsx("div", { className: styles.files, children: flattenFragments(children).map((child, index) => /* @__PURE__ */ jsx(
|
|
9
|
+
ScrollableContainer.Item,
|
|
10
|
+
{
|
|
11
|
+
scrollToItemOnFocus: true,
|
|
12
|
+
children: child
|
|
13
|
+
},
|
|
14
|
+
isValidElement(child) && child.key || index
|
|
15
|
+
)) }) }) }) }) });
|
|
16
|
+
ChatInputFiles.displayName = "ChatInputFiles";
|
|
10
17
|
|
|
11
18
|
export { ChatInputFiles };
|
|
12
19
|
//# sourceMappingURL=ChatInputFiles.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatInputFiles.js","sources":["src/public/ChatInputFiles.tsx"],"sourcesContent":["import { isValidElement, type FC, type ReactNode } from 'react';\n\nimport { flattenFragments } from '@hh.ru/magritte-ui-chat-input/internal/flattenFragments';\nimport { ScrollableContainer } from '@hh.ru/magritte-ui-scrollable-container';\n\nimport styles from '../styles.less';\n\nexport interface ChatInputFilesProps {\n /** Файлы (`ChatInput.File`), размещаются в один ряд с возможностью горизонтального скролла */\n children: ReactNode;\n}\n\nexport const ChatInputFiles: FC<ChatInputFilesProps> = ({ children }) => (\n <div className={styles.filesContainer} data-qa=\"chat-input-files\">\n {/* область прокрутки выходит за паддинг ChatInput (12px) до его обводки (1px), чтобы файлы\n обрезались краем компонента, но не перекрывали обводку; в крайних положениях контент отступает\n от краёв на те же 12px */}\n <ScrollableContainer correction={11}>\n {/* в макете у ряда файлов нет краевых фейдов: на десктопе оставляем только кнопки листания,\n на мобильных фейды и так выключены по умолчанию. Кнопки — на 8px внутрь от края ряда\n (20px от края ChatInput) и появляются только при наведении на ряд */}\n <ScrollableContainer.DefaultLayout size=\"small\" noFadeOnDesktop buttonsOffset={8} buttonsOnHover>\n <ScrollableContainer.Gallery>\n <div className={styles.files}>\n {/* ключи детей переносим на обёртку, чтобы удаление файла из середины\n не сдвигало стейт соседей; при фокусе внутри файла (крестик, с клавиатуры)\n подскролливаем к нему */}\n {flattenFragments(children).map((child, index) => (\n <ScrollableContainer.Item\n key={(isValidElement(child) && child.key) || index}\n scrollToItemOnFocus\n >\n {child}\n </ScrollableContainer.Item>\n ))}\n </div>\n </ScrollableContainer.Gallery>\n </ScrollableContainer.DefaultLayout>\n </ScrollableContainer>\n </div>\n);\n\nChatInputFiles.displayName = 'ChatInputFiles';\n"],"names":[
|
|
1
|
+
{"version":3,"file":"ChatInputFiles.js","sources":["src/public/ChatInputFiles.tsx"],"sourcesContent":["import { isValidElement, type FC, type ReactNode } from 'react';\n\nimport { flattenFragments } from '@hh.ru/magritte-ui-chat-input/internal/flattenFragments';\nimport { ScrollableContainer } from '@hh.ru/magritte-ui-scrollable-container';\n\nimport styles from '../styles.less';\n\nexport interface ChatInputFilesProps {\n /** Файлы (`ChatInput.File`), размещаются в один ряд с возможностью горизонтального скролла */\n children: ReactNode;\n}\n\nexport const ChatInputFiles: FC<ChatInputFilesProps> = ({ children }) => (\n <div className={styles.filesContainer} data-qa=\"chat-input-files\">\n {/* область прокрутки выходит за паддинг ChatInput (12px) до его обводки (1px), чтобы файлы\n обрезались краем компонента, но не перекрывали обводку; в крайних положениях контент отступает\n от краёв на те же 12px */}\n <ScrollableContainer correction={11}>\n {/* в макете у ряда файлов нет краевых фейдов: на десктопе оставляем только кнопки листания,\n на мобильных фейды и так выключены по умолчанию. Кнопки — на 8px внутрь от края ряда\n (20px от края ChatInput) и появляются только при наведении на ряд */}\n <ScrollableContainer.DefaultLayout size=\"small\" noFadeOnDesktop buttonsOffset={8} buttonsOnHover>\n <ScrollableContainer.Gallery>\n <div className={styles.files}>\n {/* ключи детей переносим на обёртку, чтобы удаление файла из середины\n не сдвигало стейт соседей; при фокусе внутри файла (крестик, с клавиатуры)\n подскролливаем к нему */}\n {flattenFragments(children).map((child, index) => (\n <ScrollableContainer.Item\n key={(isValidElement(child) && child.key) || index}\n scrollToItemOnFocus\n >\n {child}\n </ScrollableContainer.Item>\n ))}\n </div>\n </ScrollableContainer.Gallery>\n </ScrollableContainer.DefaultLayout>\n </ScrollableContainer>\n </div>\n);\n\nChatInputFiles.displayName = 'ChatInputFiles';\n"],"names":[],"mappings":";;;;;;AAYO,MAAM,iBAA0C,CAAC,EAAE,UACtD,qBAAA,GAAA,CAAC,SAAI,SAAW,EAAA,MAAA,CAAO,gBAAgB,SAAQ,EAAA,kBAAA,EAI3C,8BAAC,mBAAoB,EAAA,EAAA,UAAA,EAAY,IAI7B,QAAC,kBAAA,GAAA,CAAA,mBAAA,CAAoB,eAApB,EAAkC,IAAA,EAAK,SAAQ,eAAe,EAAA,IAAA,EAAC,eAAe,CAAG,EAAA,cAAA,EAAc,MAC5F,QAAC,kBAAA,GAAA,CAAA,mBAAA,CAAoB,SAApB,EACG,QAAA,kBAAA,GAAA,CAAC,SAAI,SAAW,EAAA,MAAA,CAAO,OAIlB,QAAiB,EAAA,gBAAA,CAAA,QAAQ,EAAE,GAAI,CAAA,CAAC,OAAO,KACpC,qBAAA,GAAA;AAAA,EAAC,mBAAoB,CAAA,IAAA;AAAA,EAApB;AAAA,IAEG,mBAAmB,EAAA,IAAA;AAAA,IAElB,QAAA,EAAA,KAAA;AAAA,GAAA;AAAA,EAHK,cAAe,CAAA,KAAK,CAAK,IAAA,KAAA,CAAM,GAAQ,IAAA,KAAA;AAIjD,CACH,CACL,EAAA,CAAA,EACJ,CACJ,EAAA,CAAA,EACJ,CACJ,EAAA,EAAA;AAGJ,cAAA,CAAe,WAAc,GAAA,gBAAA;;;;"}
|
|
@@ -2,18 +2,23 @@ import './../index.css';
|
|
|
2
2
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
3
3
|
import { IconDefaultPropsContext } from '@hh.ru/magritte-ui-icon';
|
|
4
4
|
import { TextDefaultPropsContext } from '@hh.ru/magritte-ui-typography';
|
|
5
|
-
import { s as styles } from '../styles-
|
|
5
|
+
import { s as styles } from '../styles-hEUo5BXU.js';
|
|
6
6
|
|
|
7
7
|
const DEFAULT_ICON_PROPS = {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
initialColor: "secondary",
|
|
9
|
+
padding: 8
|
|
10
10
|
};
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
const TITLE_TEXT_PROPS = { typography: "subtitle-3-semibold", style: "primary" };
|
|
12
|
+
const SUBTITLE_TEXT_PROPS = { typography: "label-3-regular", style: "secondary", ellipsis: true };
|
|
13
|
+
const ChatInputPreview = ({ title, subtitle, leftActions, rightActions }) => /* @__PURE__ */ jsx(IconDefaultPropsContext, { props: DEFAULT_ICON_PROPS, children: /* @__PURE__ */ jsxs("div", { className: styles.previewBlock, children: [
|
|
14
|
+
!!leftActions && /* @__PURE__ */ jsx("div", { className: styles.previewSide, children: leftActions }),
|
|
15
|
+
/* @__PURE__ */ jsxs("div", { className: styles.previewContent, children: [
|
|
16
|
+
!!title && /* @__PURE__ */ jsx(TextDefaultPropsContext, { props: TITLE_TEXT_PROPS, children: /* @__PURE__ */ jsx("div", { className: styles.previewTitle, children: title }) }),
|
|
17
|
+
!!subtitle && /* @__PURE__ */ jsx(TextDefaultPropsContext, { props: SUBTITLE_TEXT_PROPS, children: /* @__PURE__ */ jsx("div", { className: styles.previewSubtitle, children: subtitle }) })
|
|
18
|
+
] }),
|
|
19
|
+
!!rightActions && /* @__PURE__ */ jsx("div", { className: styles.previewSide, children: rightActions })
|
|
20
|
+
] }) });
|
|
21
|
+
ChatInputPreview.displayName = "ChatInputPreview";
|
|
17
22
|
|
|
18
23
|
export { ChatInputPreview };
|
|
19
24
|
//# sourceMappingURL=ChatInputPreview.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatInputPreview.js","sources":["src/public/ChatInputPreview.tsx"],"sourcesContent":["import { type FC, type ReactNode } from 'react';\n\nimport { IconDefaultPropsContext } from '@hh.ru/magritte-ui-icon';\nimport { TextDefaultPropsContext } from '@hh.ru/magritte-ui-typography';\n\nimport styles from '../styles.less';\n\nexport interface ChatInputPreviewProps {\n /** Заголовок в центральной секции, типографика subtitle-3-semibold */\n title?: ReactNode;\n /** Подзаголовок под title, типографика label-3-regular; обрезается многоточием */\n subtitle?: ReactNode;\n /** Слот левой секции, обычно иконка */\n leftActions?: ReactNode;\n /** Слот правой секции, обычно иконка */\n rightActions?: ReactNode;\n}\n\nconst DEFAULT_ICON_PROPS = {\n initialColor: 'secondary',\n padding: 8,\n} as const;\n\n// строку оформляют стили слота, а Text, переданный пользователем в title/subtitle, получает те же дефолты.\n// ellipsis — для Text: блочный элемент не обрезается text-overflow родителя\nconst TITLE_TEXT_PROPS = { typography: 'subtitle-3-semibold', style: 'primary' } as const;\nconst SUBTITLE_TEXT_PROPS = { typography: 'label-3-regular', style: 'secondary', ellipsis: true } as const;\n\nexport const ChatInputPreview: FC<ChatInputPreviewProps> = ({ title, subtitle, leftActions, rightActions }) => (\n <IconDefaultPropsContext props={DEFAULT_ICON_PROPS}>\n <div className={styles.previewBlock}>\n {!!leftActions && <div className={styles.previewSide}>{leftActions}</div>}\n <div className={styles.previewContent}>\n {!!title && (\n <TextDefaultPropsContext props={TITLE_TEXT_PROPS}>\n <div className={styles.previewTitle}>{title}</div>\n </TextDefaultPropsContext>\n )}\n {!!subtitle && (\n <TextDefaultPropsContext props={SUBTITLE_TEXT_PROPS}>\n <div className={styles.previewSubtitle}>{subtitle}</div>\n </TextDefaultPropsContext>\n )}\n </div>\n {!!rightActions && <div className={styles.previewSide}>{rightActions}</div>}\n </div>\n </IconDefaultPropsContext>\n);\n\nChatInputPreview.displayName = 'ChatInputPreview';\n"],"names":[
|
|
1
|
+
{"version":3,"file":"ChatInputPreview.js","sources":["src/public/ChatInputPreview.tsx"],"sourcesContent":["import { type FC, type ReactNode } from 'react';\n\nimport { IconDefaultPropsContext } from '@hh.ru/magritte-ui-icon';\nimport { TextDefaultPropsContext } from '@hh.ru/magritte-ui-typography';\n\nimport styles from '../styles.less';\n\nexport interface ChatInputPreviewProps {\n /** Заголовок в центральной секции, типографика subtitle-3-semibold */\n title?: ReactNode;\n /** Подзаголовок под title, типографика label-3-regular; обрезается многоточием */\n subtitle?: ReactNode;\n /** Слот левой секции, обычно иконка */\n leftActions?: ReactNode;\n /** Слот правой секции, обычно иконка */\n rightActions?: ReactNode;\n}\n\nconst DEFAULT_ICON_PROPS = {\n initialColor: 'secondary',\n padding: 8,\n} as const;\n\n// строку оформляют стили слота, а Text, переданный пользователем в title/subtitle, получает те же дефолты.\n// ellipsis — для Text: блочный элемент не обрезается text-overflow родителя\nconst TITLE_TEXT_PROPS = { typography: 'subtitle-3-semibold', style: 'primary' } as const;\nconst SUBTITLE_TEXT_PROPS = { typography: 'label-3-regular', style: 'secondary', ellipsis: true } as const;\n\nexport const ChatInputPreview: FC<ChatInputPreviewProps> = ({ title, subtitle, leftActions, rightActions }) => (\n <IconDefaultPropsContext props={DEFAULT_ICON_PROPS}>\n <div className={styles.previewBlock}>\n {!!leftActions && <div className={styles.previewSide}>{leftActions}</div>}\n <div className={styles.previewContent}>\n {!!title && (\n <TextDefaultPropsContext props={TITLE_TEXT_PROPS}>\n <div className={styles.previewTitle}>{title}</div>\n </TextDefaultPropsContext>\n )}\n {!!subtitle && (\n <TextDefaultPropsContext props={SUBTITLE_TEXT_PROPS}>\n <div className={styles.previewSubtitle}>{subtitle}</div>\n </TextDefaultPropsContext>\n )}\n </div>\n {!!rightActions && <div className={styles.previewSide}>{rightActions}</div>}\n </div>\n </IconDefaultPropsContext>\n);\n\nChatInputPreview.displayName = 'ChatInputPreview';\n"],"names":[],"mappings":";;;;;AAkBA,MAAM,kBAAqB,GAAA;AAAA,EACvB,YAAc,EAAA,WAAA;AAAA,EACd,OAAS,EAAA,CAAA;AACb,CAAA,CAAA;AAIA,MAAM,gBAAmB,GAAA,EAAE,UAAY,EAAA,qBAAA,EAAuB,OAAO,SAAU,EAAA,CAAA;AAC/E,MAAM,sBAAsB,EAAE,UAAA,EAAY,mBAAmB,KAAO,EAAA,WAAA,EAAa,UAAU,IAAK,EAAA,CAAA;AAEzF,MAAM,mBAA8C,CAAC,EAAE,KAAO,EAAA,QAAA,EAAU,aAAa,YAAa,EAAA,qBACpG,GAAA,CAAA,uBAAA,EAAA,EAAwB,OAAO,kBAC5B,EAAA,QAAA,kBAAA,IAAA,CAAC,KAAI,EAAA,EAAA,SAAA,EAAW,OAAO,YAClB,EAAA,QAAA,EAAA;AAAA,EAAA,CAAC,CAAC,WAAe,oBAAA,GAAA,CAAC,SAAI,SAAW,EAAA,MAAA,CAAO,aAAc,QAAY,EAAA,WAAA,EAAA,CAAA;AAAA,kBAClE,IAAA,CAAA,KAAA,EAAA,EAAI,SAAW,EAAA,MAAA,CAAO,cAClB,EAAA,QAAA,EAAA;AAAA,IAAA,CAAC,CAAC,KAAA,oBACE,GAAA,CAAA,uBAAA,EAAA,EAAwB,KAAO,EAAA,gBAAA,EAC5B,QAAC,kBAAA,GAAA,CAAA,KAAA,EAAA,EAAI,SAAW,EAAA,MAAA,CAAO,YAAe,EAAA,QAAA,EAAA,KAAA,EAAM,CAChD,EAAA,CAAA;AAAA,IAEH,CAAC,CAAC,QACC,oBAAA,GAAA,CAAC,uBAAwB,EAAA,EAAA,KAAA,EAAO,mBAC5B,EAAA,QAAA,kBAAA,GAAA,CAAC,KAAI,EAAA,EAAA,SAAA,EAAW,MAAO,CAAA,eAAA,EAAkB,oBAAS,CACtD,EAAA,CAAA;AAAA,GAER,EAAA,CAAA;AAAA,EACC,CAAC,CAAC,YAAgB,oBAAA,GAAA,CAAC,SAAI,SAAW,EAAA,MAAA,CAAO,aAAc,QAAa,EAAA,YAAA,EAAA,CAAA;AAAA,CAAA,EACzE,CACJ,EAAA,EAAA;AAGJ,gBAAA,CAAiB,WAAc,GAAA,kBAAA;;;;"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type FC, type MouseEventHandler, type ReactNode } from 'react';
|
|
2
|
-
import { type ChatInputButtonAria } from '
|
|
2
|
+
import { type ChatInputButtonAria } from '../types';
|
|
3
3
|
export interface ChatInputSendButtonProps extends ChatInputButtonAria {
|
|
4
4
|
/** Доп. обработчик клика — вызывается вместе с `onSend`, а не вместо него */
|
|
5
5
|
onClick?: MouseEventHandler<HTMLButtonElement>;
|
|
@@ -10,30 +10,41 @@ import 'react';
|
|
|
10
10
|
import '@hh.ru/magritte-common-use-disabled';
|
|
11
11
|
import '@hh.ru/magritte-ui-icon';
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
13
|
+
const ChatInputSendButton = ({
|
|
14
|
+
onClick,
|
|
15
|
+
children,
|
|
16
|
+
disabled = false,
|
|
17
|
+
"aria-label": ariaLabel,
|
|
18
|
+
...ariaProps
|
|
19
|
+
}) => {
|
|
20
|
+
const { onSend } = useChatInputContext();
|
|
21
|
+
const sendLabel = useTranslation("ChatInput", "send");
|
|
22
|
+
const handleClick = (event) => {
|
|
23
|
+
onClick?.(event);
|
|
24
|
+
onSend?.();
|
|
25
|
+
};
|
|
26
|
+
const iconProps = {
|
|
27
|
+
// дефолтный data-qa до спреда — пользовательский data-qa из ariaProps его перекрывает
|
|
28
|
+
"data-qa": "send",
|
|
29
|
+
...ariaProps,
|
|
30
|
+
"aria-label": ariaLabel ?? sendLabel,
|
|
31
|
+
initialColor: "inverse",
|
|
32
|
+
backgroundStyle: "inverse",
|
|
33
|
+
disabled,
|
|
34
|
+
onClick: handleClick
|
|
35
|
+
};
|
|
36
|
+
return /* @__PURE__ */ jsx(FocusTarget, { priority: FOCUS_PRIORITY.send, children: /* @__PURE__ */ jsx(
|
|
37
|
+
ChatInputSlotButton,
|
|
38
|
+
{
|
|
39
|
+
icon: /* @__PURE__ */ jsx(ArrowUpOutlinedSize24, { ...iconProps }),
|
|
40
|
+
iconProps,
|
|
41
|
+
disabled,
|
|
42
|
+
onClick: handleClick,
|
|
43
|
+
children
|
|
44
|
+
}
|
|
45
|
+
) });
|
|
35
46
|
};
|
|
36
|
-
ChatInputSendButton.displayName =
|
|
47
|
+
ChatInputSendButton.displayName = "ChatInputSendButton";
|
|
37
48
|
|
|
38
49
|
export { ChatInputSendButton };
|
|
39
50
|
//# sourceMappingURL=ChatInputSendButton.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatInputSendButton.js","sources":["src/public/ChatInputSendButton.tsx"],"sourcesContent":["import { type FC, type MouseEvent, type MouseEventHandler, type ReactNode } from 'react';\n\nimport { useTranslation } from '@hh.ru/magritte-common-use-translation';\nimport { useChatInputContext } from '@hh.ru/magritte-ui-chat-input/internal/ChatInputContext';\nimport { ChatInputSlotButton } from '@hh.ru/magritte-ui-chat-input/internal/ChatInputSlotButton';\nimport { FocusTarget } from '@hh.ru/magritte-ui-chat-input/internal/FocusTarget';\nimport { FOCUS_PRIORITY } from '@hh.ru/magritte-ui-chat-input/internal/focusGroup';\nimport { type ChatInputButtonAria } from '@hh.ru/magritte-ui-chat-input/types';\nimport { ArrowUpOutlinedSize24 } from '@hh.ru/magritte-ui-icon/icon';\n\nexport interface ChatInputSendButtonProps extends ChatInputButtonAria {\n /** Доп. обработчик клика — вызывается вместе с `onSend`, а не вместо него */\n onClick?: MouseEventHandler<HTMLButtonElement>;\n /**\n * Блокирует кнопку отправки независимо от общего `disabled` компонента `ChatInput`\n * @default false\n */\n disabled?: boolean;\n /** Кастомный контент вместо иконки отправки. Иконку в children оформляем как встроенную\n * (через IconDefaultPropsContext); клик по любому content ловится в любом случае */\n children?: ReactNode;\n}\n\n/**\n * Кнопка отправки для слота ChatInput. Обёртка над иконкой с фиксированным оформлением;\n * по клику дёргает `onSend` из контекста ChatInput вместе с собственным `onClick`.\n */\nexport const ChatInputSendButton: FC<ChatInputSendButtonProps> = ({\n onClick,\n children,\n disabled = false,\n 'aria-label': ariaLabel,\n ...ariaProps\n}) => {\n const { onSend } = useChatInputContext();\n const sendLabel = useTranslation('ChatInput', 'send');\n\n const handleClick = (event: MouseEvent<HTMLButtonElement>) => {\n onClick?.(event);\n onSend?.();\n };\n\n const iconProps = {\n // дефолтный data-qa до спреда — пользовательский data-qa из ariaProps его перекрывает\n 'data-qa': 'send',\n ...ariaProps,\n 'aria-label': ariaLabel ?? sendLabel,\n initialColor: 'inverse',\n backgroundStyle: 'inverse',\n disabled,\n onClick: handleClick,\n } as const;\n\n return (\n <FocusTarget priority={FOCUS_PRIORITY.send}>\n <ChatInputSlotButton\n icon={<ArrowUpOutlinedSize24 {...iconProps} />}\n iconProps={iconProps}\n disabled={disabled}\n onClick={handleClick}\n >\n {children}\n </ChatInputSlotButton>\n </FocusTarget>\n );\n};\n\nChatInputSendButton.displayName = 'ChatInputSendButton';\n"],"names":[
|
|
1
|
+
{"version":3,"file":"ChatInputSendButton.js","sources":["src/public/ChatInputSendButton.tsx"],"sourcesContent":["import { type FC, type MouseEvent, type MouseEventHandler, type ReactNode } from 'react';\n\nimport { useTranslation } from '@hh.ru/magritte-common-use-translation';\nimport { useChatInputContext } from '@hh.ru/magritte-ui-chat-input/internal/ChatInputContext';\nimport { ChatInputSlotButton } from '@hh.ru/magritte-ui-chat-input/internal/ChatInputSlotButton';\nimport { FocusTarget } from '@hh.ru/magritte-ui-chat-input/internal/FocusTarget';\nimport { FOCUS_PRIORITY } from '@hh.ru/magritte-ui-chat-input/internal/focusGroup';\nimport { type ChatInputButtonAria } from '@hh.ru/magritte-ui-chat-input/types';\nimport { ArrowUpOutlinedSize24 } from '@hh.ru/magritte-ui-icon/icon';\n\nexport interface ChatInputSendButtonProps extends ChatInputButtonAria {\n /** Доп. обработчик клика — вызывается вместе с `onSend`, а не вместо него */\n onClick?: MouseEventHandler<HTMLButtonElement>;\n /**\n * Блокирует кнопку отправки независимо от общего `disabled` компонента `ChatInput`\n * @default false\n */\n disabled?: boolean;\n /** Кастомный контент вместо иконки отправки. Иконку в children оформляем как встроенную\n * (через IconDefaultPropsContext); клик по любому content ловится в любом случае */\n children?: ReactNode;\n}\n\n/**\n * Кнопка отправки для слота ChatInput. Обёртка над иконкой с фиксированным оформлением;\n * по клику дёргает `onSend` из контекста ChatInput вместе с собственным `onClick`.\n */\nexport const ChatInputSendButton: FC<ChatInputSendButtonProps> = ({\n onClick,\n children,\n disabled = false,\n 'aria-label': ariaLabel,\n ...ariaProps\n}) => {\n const { onSend } = useChatInputContext();\n const sendLabel = useTranslation('ChatInput', 'send');\n\n const handleClick = (event: MouseEvent<HTMLButtonElement>) => {\n onClick?.(event);\n onSend?.();\n };\n\n const iconProps = {\n // дефолтный data-qa до спреда — пользовательский data-qa из ariaProps его перекрывает\n 'data-qa': 'send',\n ...ariaProps,\n 'aria-label': ariaLabel ?? sendLabel,\n initialColor: 'inverse',\n backgroundStyle: 'inverse',\n disabled,\n onClick: handleClick,\n } as const;\n\n return (\n <FocusTarget priority={FOCUS_PRIORITY.send}>\n <ChatInputSlotButton\n icon={<ArrowUpOutlinedSize24 {...iconProps} />}\n iconProps={iconProps}\n disabled={disabled}\n onClick={handleClick}\n >\n {children}\n </ChatInputSlotButton>\n </FocusTarget>\n );\n};\n\nChatInputSendButton.displayName = 'ChatInputSendButton';\n"],"names":[],"mappings":";;;;;;;;;;;AA2BO,MAAM,sBAAoD,CAAC;AAAA,EAC9D,OAAA;AAAA,EACA,QAAA;AAAA,EACA,QAAW,GAAA,KAAA;AAAA,EACX,YAAc,EAAA,SAAA;AAAA,EACd,GAAG,SAAA;AACP,CAAM,KAAA;AACF,EAAM,MAAA,EAAE,MAAO,EAAA,GAAI,mBAAoB,EAAA,CAAA;AACvC,EAAM,MAAA,SAAA,GAAY,cAAe,CAAA,WAAA,EAAa,MAAM,CAAA,CAAA;AAEpD,EAAM,MAAA,WAAA,GAAc,CAAC,KAAyC,KAAA;AAC1D,IAAA,OAAA,GAAU,KAAK,CAAA,CAAA;AACf,IAAS,MAAA,IAAA,CAAA;AAAA,GACb,CAAA;AAEA,EAAA,MAAM,SAAY,GAAA;AAAA;AAAA,IAEd,SAAW,EAAA,MAAA;AAAA,IACX,GAAG,SAAA;AAAA,IACH,cAAc,SAAa,IAAA,SAAA;AAAA,IAC3B,YAAc,EAAA,SAAA;AAAA,IACd,eAAiB,EAAA,SAAA;AAAA,IACjB,QAAA;AAAA,IACA,OAAS,EAAA,WAAA;AAAA,GACb,CAAA;AAEA,EAAA,uBACK,GAAA,CAAA,WAAA,EAAA,EAAY,QAAU,EAAA,cAAA,CAAe,IAClC,EAAA,QAAA,kBAAA,GAAA;AAAA,IAAC,mBAAA;AAAA,IAAA;AAAA,MACG,IAAM,kBAAA,GAAA,CAAC,qBAAuB,EAAA,EAAA,GAAG,SAAW,EAAA,CAAA;AAAA,MAC5C,SAAA;AAAA,MACA,QAAA;AAAA,MACA,OAAS,EAAA,WAAA;AAAA,MAER,QAAA;AAAA,KAAA;AAAA,GAET,EAAA,CAAA,CAAA;AAER,EAAA;AAEA,mBAAA,CAAoB,WAAc,GAAA,qBAAA;;;;"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type FC, type MouseEventHandler, type ReactNode } from 'react';
|
|
2
|
-
import { type ChatInputButtonAria, type ChatInputVoiceRecordConfig } from '
|
|
2
|
+
import { type ChatInputButtonAria, type ChatInputVoiceRecordConfig } from '../types';
|
|
3
3
|
export interface ChatInputVoiceButtonProps extends ChatInputButtonAria, ChatInputVoiceRecordConfig {
|
|
4
4
|
/** Доп. обработчик клика — вызывается вместе со стартом записи, а не вместо него */
|
|
5
5
|
onClick?: MouseEventHandler<HTMLButtonElement>;
|
|
@@ -12,106 +12,102 @@ import '@hh.ru/magritte-common-use-disabled';
|
|
|
12
12
|
import '@hh.ru/magritte-ui-icon';
|
|
13
13
|
import '@hh.ru/magritte-internal-slot-registry';
|
|
14
14
|
|
|
15
|
-
// Сколько ждём микрофон, прежде чем показать лоадер. Если доступ уже выдан, `getUserMedia`
|
|
16
|
-
// резолвится почти мгновенно — показывать лоадер на такой срок значит мельтешить; порог оставляет
|
|
17
|
-
// индикацию только для реального ожидания (диалог разрешения браузера, медленный старт устройства)
|
|
18
15
|
const VOICE_START_LOADER_DELAY = 300;
|
|
19
|
-
/**
|
|
20
|
-
* Обёртка над колбэком из пропов со стабильной по ссылке идентичностью. Приложение обычно
|
|
21
|
-
* пересоздаёт хендлеры на каждый рендер, а в slot-registry нужны стабильные ссылки (иначе конфиг
|
|
22
|
-
* перерегистрируется на каждый рендер). Актуальную версию колбэка держим в рефе, наружу отдаём
|
|
23
|
-
* фиксированную обёртку, которая зовёт последний ref. Возвращает `undefined` из вызова, если
|
|
24
|
-
* исходный колбэк не передан.
|
|
25
|
-
*/
|
|
26
16
|
const useStableFn = (fn) => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
17
|
+
const fnRef = useRef(fn);
|
|
18
|
+
fnRef.current = fn;
|
|
19
|
+
return useCallback((...args) => fnRef.current?.(...args), []);
|
|
30
20
|
};
|
|
31
|
-
/**
|
|
32
|
-
* Флаг с задержкой на подъём: `true`, только если исходный продержался `VOICE_START_LOADER_DELAY`
|
|
33
|
-
* подряд; сброс — сразу. Повторное включение отсчитывает порог заново (таймер снимается в cleanup).
|
|
34
|
-
*/
|
|
35
21
|
const useDelayedFlag = (value) => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
22
|
+
const [delayed, setDelayed] = useState(false);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!value) {
|
|
25
|
+
setDelayed(false);
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
const timeoutId = setTimeout(() => setDelayed(true), VOICE_START_LOADER_DELAY);
|
|
29
|
+
return () => clearTimeout(timeoutId);
|
|
30
|
+
}, [value]);
|
|
31
|
+
return delayed;
|
|
46
32
|
};
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}),
|
|
33
|
+
const ChatInputVoiceButton = ({
|
|
34
|
+
maxDuration,
|
|
35
|
+
mimeType,
|
|
36
|
+
processingLabel,
|
|
37
|
+
onRecordStart,
|
|
38
|
+
onComplete,
|
|
39
|
+
onError,
|
|
40
|
+
onCancel,
|
|
41
|
+
completeButtonProps,
|
|
42
|
+
cancelButtonProps,
|
|
43
|
+
onClick,
|
|
44
|
+
children,
|
|
45
|
+
disabled = false,
|
|
46
|
+
"aria-label": ariaLabel,
|
|
47
|
+
...ariaProps
|
|
48
|
+
}) => {
|
|
49
|
+
const { startVoiceRecord, voiceRecordStatus } = useChatInputContext();
|
|
50
|
+
const recordVoiceLabel = useTranslation("ChatInput", "recordVoice");
|
|
51
|
+
const isButtonDisabled = disabled || !startVoiceRecord;
|
|
52
|
+
const isLoading = useDelayedFlag(voiceRecordStatus === "starting");
|
|
53
|
+
const mimeTypeKey = Array.isArray(mimeType) ? mimeType.join(",") : mimeType;
|
|
54
|
+
const stableRecordStart = useStableFn(onRecordStart);
|
|
55
|
+
const stableComplete = useStableFn(onComplete);
|
|
56
|
+
const stableError = useStableFn(onError);
|
|
57
|
+
const stableCancel = useStableFn(onCancel);
|
|
58
|
+
const config = useMemo(
|
|
59
|
+
() => ({
|
|
60
|
+
maxDuration,
|
|
61
|
+
mimeType,
|
|
62
|
+
processingLabel,
|
|
63
|
+
onRecordStart: stableRecordStart,
|
|
64
|
+
onComplete: stableComplete,
|
|
65
|
+
onError: stableError,
|
|
66
|
+
onCancel: stableCancel,
|
|
67
|
+
completeButtonProps,
|
|
68
|
+
cancelButtonProps
|
|
69
|
+
}),
|
|
84
70
|
// mimeType намеренно не в deps — его влияние учтено через стабильный mimeTypeKey
|
|
85
71
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
86
72
|
[
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
]
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
73
|
+
maxDuration,
|
|
74
|
+
mimeTypeKey,
|
|
75
|
+
processingLabel,
|
|
76
|
+
stableRecordStart,
|
|
77
|
+
stableComplete,
|
|
78
|
+
stableError,
|
|
79
|
+
stableCancel,
|
|
80
|
+
completeButtonProps,
|
|
81
|
+
cancelButtonProps
|
|
82
|
+
]
|
|
83
|
+
);
|
|
84
|
+
useRegisterVoiceConfig(config);
|
|
85
|
+
const handleClick = (event) => {
|
|
86
|
+
onClick?.(event);
|
|
87
|
+
startVoiceRecord?.();
|
|
88
|
+
};
|
|
89
|
+
const iconProps = {
|
|
90
|
+
// дефолтный data-qa до спреда — пользовательский data-qa из ariaProps его перекрывает
|
|
91
|
+
"data-qa": "record",
|
|
92
|
+
...ariaProps,
|
|
93
|
+
"aria-label": ariaLabel ?? recordVoiceLabel,
|
|
94
|
+
disabled: isButtonDisabled,
|
|
95
|
+
loading: isLoading,
|
|
96
|
+
onClick: handleClick
|
|
97
|
+
// цвета тут нет, потому что в ChatInput он уже пробрасывается на левый и правый слоты
|
|
98
|
+
};
|
|
99
|
+
return /* @__PURE__ */ jsx(FocusTarget, { priority: FOCUS_PRIORITY.voice, children: /* @__PURE__ */ jsx(
|
|
100
|
+
ChatInputSlotButton,
|
|
101
|
+
{
|
|
102
|
+
icon: /* @__PURE__ */ jsx(MicOutlinedSize24, { ...iconProps }),
|
|
103
|
+
iconProps,
|
|
104
|
+
disabled: isButtonDisabled,
|
|
105
|
+
onClick: handleClick,
|
|
106
|
+
children
|
|
107
|
+
}
|
|
108
|
+
) });
|
|
113
109
|
};
|
|
114
|
-
ChatInputVoiceButton.displayName =
|
|
110
|
+
ChatInputVoiceButton.displayName = "ChatInputVoiceButton";
|
|
115
111
|
|
|
116
112
|
export { ChatInputVoiceButton };
|
|
117
113
|
//# sourceMappingURL=ChatInputVoiceButton.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatInputVoiceButton.js","sources":["src/public/ChatInputVoiceButton.tsx"],"sourcesContent":["import {\n type FC,\n type MouseEvent,\n type MouseEventHandler,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\n\nimport { useTranslation } from '@hh.ru/magritte-common-use-translation';\nimport { useChatInputContext } from '@hh.ru/magritte-ui-chat-input/internal/ChatInputContext';\nimport { ChatInputSlotButton } from '@hh.ru/magritte-ui-chat-input/internal/ChatInputSlotButton';\nimport { FocusTarget } from '@hh.ru/magritte-ui-chat-input/internal/FocusTarget';\nimport { FOCUS_PRIORITY } from '@hh.ru/magritte-ui-chat-input/internal/focusGroup';\nimport { useRegisterVoiceConfig } from '@hh.ru/magritte-ui-chat-input/internal/voiceSlotRegistry';\nimport { type ChatInputButtonAria, type ChatInputVoiceRecordConfig } from '@hh.ru/magritte-ui-chat-input/types';\nimport { MicOutlinedSize24 } from '@hh.ru/magritte-ui-icon/icon';\n\n// Сколько ждём микрофон, прежде чем показать лоадер. Если доступ уже выдан, `getUserMedia`\n// резолвится почти мгновенно — показывать лоадер на такой срок значит мельтешить; порог оставляет\n// индикацию только для реального ожидания (диалог разрешения браузера, медленный старт устройства)\nconst VOICE_START_LOADER_DELAY = 300;\n\n/**\n * Обёртка над колбэком из пропов со стабильной по ссылке идентичностью. Приложение обычно\n * пересоздаёт хендлеры на каждый рендер, а в slot-registry нужны стабильные ссылки (иначе конфиг\n * перерегистрируется на каждый рендер). Актуальную версию колбэка держим в рефе, наружу отдаём\n * фиксированную обёртку, которая зовёт последний ref. Возвращает `undefined` из вызова, если\n * исходный колбэк не передан.\n */\nconst useStableFn = <Args extends unknown[], Return>(\n fn: ((...args: Args) => Return) | undefined\n): ((...args: Args) => Return | undefined) => {\n const fnRef = useRef(fn);\n fnRef.current = fn;\n return useCallback((...args: Args) => fnRef.current?.(...args), []);\n};\n\n/**\n * Флаг с задержкой на подъём: `true`, только если исходный продержался `VOICE_START_LOADER_DELAY`\n * подряд; сброс — сразу. Повторное включение отсчитывает порог заново (таймер снимается в cleanup).\n */\nconst useDelayedFlag = (value: boolean): boolean => {\n const [delayed, setDelayed] = useState(false);\n\n useEffect(() => {\n if (!value) {\n setDelayed(false);\n return undefined;\n }\n const timeoutId = setTimeout(() => setDelayed(true), VOICE_START_LOADER_DELAY);\n return () => clearTimeout(timeoutId);\n }, [value]);\n\n return delayed;\n};\n\nexport interface ChatInputVoiceButtonProps extends ChatInputButtonAria, ChatInputVoiceRecordConfig {\n /** Доп. обработчик клика — вызывается вместе со стартом записи, а не вместо него */\n onClick?: MouseEventHandler<HTMLButtonElement>;\n /**\n * Блокирует кнопку записи независимо от общего `disabled` компонента `ChatInput`.\n * Кнопка также неактивна сама по себе вне `ChatInput`\n * @default false\n */\n disabled?: boolean;\n /** Кастомный контент вместо иконки микрофона. Иконку в children оформляем как встроенную\n * (через IconDefaultPropsContext); клик по любому content ловится в любом случае */\n children?: ReactNode;\n}\n\n/**\n * Кнопка записи голоса для слота ChatInput. Несёт на себе конфигурацию записи (колбэки,\n * `maxDuration`, `processingLabel`) — она регистрируется в slot-registry и оттуда доезжает до\n * сессии в корне ChatInput. По клику запускает запись через контекст ChatInput. Если доступ к\n * микрофону не получен за `VOICE_START_LOADER_DELAY` — показывает лоадер (при уже выданном доступе\n * ожидание короче порога и лоадера нет); оформление (паддинги, цвет) берёт из дефолтов слота.\n * Вне ChatInput кнопка неактивна.\n */\nexport const ChatInputVoiceButton: FC<ChatInputVoiceButtonProps> = ({\n maxDuration,\n mimeType,\n processingLabel,\n onRecordStart,\n onComplete,\n onError,\n onCancel,\n completeButtonProps,\n cancelButtonProps,\n onClick,\n children,\n disabled = false,\n 'aria-label': ariaLabel,\n ...ariaProps\n}) => {\n const { startVoiceRecord, voiceRecordStatus } = useChatInputContext();\n const recordVoiceLabel = useTranslation('ChatInput', 'recordVoice');\n\n // кнопка неактивна и по собственному `disabled`, и вне ChatInput (нет startVoiceRecord)\n const isButtonDisabled = disabled || !startVoiceRecord;\n\n // лоадер только на затянувшемся ожидании микрофона; при уже выданном доступе статус `starting`\n // проскакивает быстрее порога, и лоадер не показывается вовсе\n const isLoading = useDelayedFlag(voiceRecordStatus === 'starting');\n\n // список mimeType приложение часто передаёт инлайн-массивом (новая ссылка на каждый рендер);\n // мемоизуем по стабильному строковому ключу, чтобы не перерегистрировать конфиг зря\n const mimeTypeKey = Array.isArray(mimeType) ? mimeType.join(',') : mimeType;\n\n // колбэки из пропов оборачиваем в стабильные ссылки, тогда конфиг для slot-registry\n // меняет идентичность только при смене примитивов (maxDuration/processingLabel), а не на\n // каждый рендер из-за инлайновых хендлеров приложения — регистрация не дёргается\n const stableRecordStart = useStableFn(onRecordStart);\n const stableComplete = useStableFn(onComplete);\n const stableError = useStableFn(onError);\n const stableCancel = useStableFn(onCancel);\n\n const config = useMemo<ChatInputVoiceRecordConfig>(\n () => ({\n maxDuration,\n mimeType,\n processingLabel,\n onRecordStart: stableRecordStart,\n onComplete: stableComplete,\n onError: stableError,\n onCancel: stableCancel,\n completeButtonProps,\n cancelButtonProps,\n }),\n // mimeType намеренно не в deps — его влияние учтено через стабильный mimeTypeKey\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [\n maxDuration,\n mimeTypeKey,\n processingLabel,\n stableRecordStart,\n stableComplete,\n stableError,\n stableCancel,\n completeButtonProps,\n cancelButtonProps,\n ]\n );\n useRegisterVoiceConfig(config);\n\n const handleClick = (event: MouseEvent<HTMLButtonElement>) => {\n onClick?.(event);\n startVoiceRecord?.();\n };\n\n const iconProps = {\n // дефолтный data-qa до спреда — пользовательский data-qa из ariaProps его перекрывает\n 'data-qa': 'record',\n ...ariaProps,\n 'aria-label': ariaLabel ?? recordVoiceLabel,\n disabled: isButtonDisabled,\n loading: isLoading,\n onClick: handleClick,\n // цвета тут нет, потому что в ChatInput он уже пробрасывается на левый и правый слоты\n };\n\n return (\n <FocusTarget priority={FOCUS_PRIORITY.voice}>\n <ChatInputSlotButton\n icon={<MicOutlinedSize24 {...iconProps} />}\n iconProps={iconProps}\n disabled={isButtonDisabled}\n onClick={handleClick}\n >\n {children}\n </ChatInputSlotButton>\n </FocusTarget>\n );\n};\n\nChatInputVoiceButton.displayName = 'ChatInputVoiceButton';\n"],"names":["_jsx"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ChatInputVoiceButton.js","sources":["src/public/ChatInputVoiceButton.tsx"],"sourcesContent":["import {\n type FC,\n type MouseEvent,\n type MouseEventHandler,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\n\nimport { useTranslation } from '@hh.ru/magritte-common-use-translation';\nimport { useChatInputContext } from '@hh.ru/magritte-ui-chat-input/internal/ChatInputContext';\nimport { ChatInputSlotButton } from '@hh.ru/magritte-ui-chat-input/internal/ChatInputSlotButton';\nimport { FocusTarget } from '@hh.ru/magritte-ui-chat-input/internal/FocusTarget';\nimport { FOCUS_PRIORITY } from '@hh.ru/magritte-ui-chat-input/internal/focusGroup';\nimport { useRegisterVoiceConfig } from '@hh.ru/magritte-ui-chat-input/internal/voiceSlotRegistry';\nimport { type ChatInputButtonAria, type ChatInputVoiceRecordConfig } from '@hh.ru/magritte-ui-chat-input/types';\nimport { MicOutlinedSize24 } from '@hh.ru/magritte-ui-icon/icon';\n\n// Сколько ждём микрофон, прежде чем показать лоадер. Если доступ уже выдан, `getUserMedia`\n// резолвится почти мгновенно — показывать лоадер на такой срок значит мельтешить; порог оставляет\n// индикацию только для реального ожидания (диалог разрешения браузера, медленный старт устройства)\nconst VOICE_START_LOADER_DELAY = 300;\n\n/**\n * Обёртка над колбэком из пропов со стабильной по ссылке идентичностью. Приложение обычно\n * пересоздаёт хендлеры на каждый рендер, а в slot-registry нужны стабильные ссылки (иначе конфиг\n * перерегистрируется на каждый рендер). Актуальную версию колбэка держим в рефе, наружу отдаём\n * фиксированную обёртку, которая зовёт последний ref. Возвращает `undefined` из вызова, если\n * исходный колбэк не передан.\n */\nconst useStableFn = <Args extends unknown[], Return>(\n fn: ((...args: Args) => Return) | undefined\n): ((...args: Args) => Return | undefined) => {\n const fnRef = useRef(fn);\n fnRef.current = fn;\n return useCallback((...args: Args) => fnRef.current?.(...args), []);\n};\n\n/**\n * Флаг с задержкой на подъём: `true`, только если исходный продержался `VOICE_START_LOADER_DELAY`\n * подряд; сброс — сразу. Повторное включение отсчитывает порог заново (таймер снимается в cleanup).\n */\nconst useDelayedFlag = (value: boolean): boolean => {\n const [delayed, setDelayed] = useState(false);\n\n useEffect(() => {\n if (!value) {\n setDelayed(false);\n return undefined;\n }\n const timeoutId = setTimeout(() => setDelayed(true), VOICE_START_LOADER_DELAY);\n return () => clearTimeout(timeoutId);\n }, [value]);\n\n return delayed;\n};\n\nexport interface ChatInputVoiceButtonProps extends ChatInputButtonAria, ChatInputVoiceRecordConfig {\n /** Доп. обработчик клика — вызывается вместе со стартом записи, а не вместо него */\n onClick?: MouseEventHandler<HTMLButtonElement>;\n /**\n * Блокирует кнопку записи независимо от общего `disabled` компонента `ChatInput`.\n * Кнопка также неактивна сама по себе вне `ChatInput`\n * @default false\n */\n disabled?: boolean;\n /** Кастомный контент вместо иконки микрофона. Иконку в children оформляем как встроенную\n * (через IconDefaultPropsContext); клик по любому content ловится в любом случае */\n children?: ReactNode;\n}\n\n/**\n * Кнопка записи голоса для слота ChatInput. Несёт на себе конфигурацию записи (колбэки,\n * `maxDuration`, `processingLabel`) — она регистрируется в slot-registry и оттуда доезжает до\n * сессии в корне ChatInput. По клику запускает запись через контекст ChatInput. Если доступ к\n * микрофону не получен за `VOICE_START_LOADER_DELAY` — показывает лоадер (при уже выданном доступе\n * ожидание короче порога и лоадера нет); оформление (паддинги, цвет) берёт из дефолтов слота.\n * Вне ChatInput кнопка неактивна.\n */\nexport const ChatInputVoiceButton: FC<ChatInputVoiceButtonProps> = ({\n maxDuration,\n mimeType,\n processingLabel,\n onRecordStart,\n onComplete,\n onError,\n onCancel,\n completeButtonProps,\n cancelButtonProps,\n onClick,\n children,\n disabled = false,\n 'aria-label': ariaLabel,\n ...ariaProps\n}) => {\n const { startVoiceRecord, voiceRecordStatus } = useChatInputContext();\n const recordVoiceLabel = useTranslation('ChatInput', 'recordVoice');\n\n // кнопка неактивна и по собственному `disabled`, и вне ChatInput (нет startVoiceRecord)\n const isButtonDisabled = disabled || !startVoiceRecord;\n\n // лоадер только на затянувшемся ожидании микрофона; при уже выданном доступе статус `starting`\n // проскакивает быстрее порога, и лоадер не показывается вовсе\n const isLoading = useDelayedFlag(voiceRecordStatus === 'starting');\n\n // список mimeType приложение часто передаёт инлайн-массивом (новая ссылка на каждый рендер);\n // мемоизуем по стабильному строковому ключу, чтобы не перерегистрировать конфиг зря\n const mimeTypeKey = Array.isArray(mimeType) ? mimeType.join(',') : mimeType;\n\n // колбэки из пропов оборачиваем в стабильные ссылки, тогда конфиг для slot-registry\n // меняет идентичность только при смене примитивов (maxDuration/processingLabel), а не на\n // каждый рендер из-за инлайновых хендлеров приложения — регистрация не дёргается\n const stableRecordStart = useStableFn(onRecordStart);\n const stableComplete = useStableFn(onComplete);\n const stableError = useStableFn(onError);\n const stableCancel = useStableFn(onCancel);\n\n const config = useMemo<ChatInputVoiceRecordConfig>(\n () => ({\n maxDuration,\n mimeType,\n processingLabel,\n onRecordStart: stableRecordStart,\n onComplete: stableComplete,\n onError: stableError,\n onCancel: stableCancel,\n completeButtonProps,\n cancelButtonProps,\n }),\n // mimeType намеренно не в deps — его влияние учтено через стабильный mimeTypeKey\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [\n maxDuration,\n mimeTypeKey,\n processingLabel,\n stableRecordStart,\n stableComplete,\n stableError,\n stableCancel,\n completeButtonProps,\n cancelButtonProps,\n ]\n );\n useRegisterVoiceConfig(config);\n\n const handleClick = (event: MouseEvent<HTMLButtonElement>) => {\n onClick?.(event);\n startVoiceRecord?.();\n };\n\n const iconProps = {\n // дефолтный data-qa до спреда — пользовательский data-qa из ariaProps его перекрывает\n 'data-qa': 'record',\n ...ariaProps,\n 'aria-label': ariaLabel ?? recordVoiceLabel,\n disabled: isButtonDisabled,\n loading: isLoading,\n onClick: handleClick,\n // цвета тут нет, потому что в ChatInput он уже пробрасывается на левый и правый слоты\n };\n\n return (\n <FocusTarget priority={FOCUS_PRIORITY.voice}>\n <ChatInputSlotButton\n icon={<MicOutlinedSize24 {...iconProps} />}\n iconProps={iconProps}\n disabled={isButtonDisabled}\n onClick={handleClick}\n >\n {children}\n </ChatInputSlotButton>\n </FocusTarget>\n );\n};\n\nChatInputVoiceButton.displayName = 'ChatInputVoiceButton';\n"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ChatInputFileType } from '
|
|
1
|
+
import { type ChatInputFileType } from '../public/ChatInputFile';
|
|
2
2
|
/**
|
|
3
3
|
* Тип `ChatInput.File` по MIME-типу файла: `image/*` — `image`, `video/*` — `video`, остальное — `doc`
|
|
4
4
|
*/
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import './../index.css';
|
|
2
|
-
/**
|
|
3
|
-
* Тип `ChatInput.File` по MIME-типу файла: `image/*` — `image`, `video/*` — `video`, остальное — `doc`
|
|
4
|
-
*/
|
|
5
2
|
const getChatInputFileType = (file) => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
if (file.type.startsWith("image/")) {
|
|
4
|
+
return "image";
|
|
5
|
+
}
|
|
6
|
+
if (file.type.startsWith("video/")) {
|
|
7
|
+
return "video";
|
|
8
|
+
}
|
|
9
|
+
return "doc";
|
|
13
10
|
};
|
|
14
11
|
|
|
15
12
|
export { getChatInputFileType };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"getChatInputFileType.js","sources":["src/public/getChatInputFileType.ts"],"sourcesContent":["import { type ChatInputFileType } from '@hh.ru/magritte-ui-chat-input/public/ChatInputFile';\n\n/**\n * Тип `ChatInput.File` по MIME-типу файла: `image/*` — `image`, `video/*` — `video`, остальное — `doc`\n */\nexport const getChatInputFileType = (file: Blob): ChatInputFileType => {\n if (file.type.startsWith('image/')) {\n return 'image';\n }\n if (file.type.startsWith('video/')) {\n return 'video';\n }\n return 'doc';\n};\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"getChatInputFileType.js","sources":["src/public/getChatInputFileType.ts"],"sourcesContent":["import { type ChatInputFileType } from '@hh.ru/magritte-ui-chat-input/public/ChatInputFile';\n\n/**\n * Тип `ChatInput.File` по MIME-типу файла: `image/*` — `image`, `video/*` — `video`, остальное — `doc`\n */\nexport const getChatInputFileType = (file: Blob): ChatInputFileType => {\n if (file.type.startsWith('image/')) {\n return 'image';\n }\n if (file.type.startsWith('video/')) {\n return 'video';\n }\n return 'doc';\n};\n"],"names":[],"mappings":"AAKa,MAAA,oBAAA,GAAuB,CAAC,IAAkC,KAAA;AACnE,EAAA,IAAI,IAAK,CAAA,IAAA,CAAK,UAAW,CAAA,QAAQ,CAAG,EAAA;AAChC,IAAO,OAAA,OAAA,CAAA;AAAA,GACX;AACA,EAAA,IAAI,IAAK,CAAA,IAAA,CAAK,UAAW,CAAA,QAAQ,CAAG,EAAA;AAChC,IAAO,OAAA,OAAA,CAAA;AAAA,GACX;AACA,EAAO,OAAA,KAAA,CAAA;AACX;;;;"}
|