@umodeapporg/ui 0.7.0 → 0.8.0

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.
@@ -1,5 +1,6 @@
1
1
  import { IUseFileAttachmentsResult, TFileUploader } from '../../hooks/useFileAttachments';
2
2
  import { SymbolCodepoints } from '../UI/MaterialSymbol';
3
+ import { IStagedImageAction } from './attachments/AttachmentPreviewList';
3
4
  /** An extra entry in the attach menu, for a source only the host knows about (a camera dialog). */
4
5
  export interface IRichTextAttachAction {
5
6
  key: string;
@@ -14,6 +15,11 @@ export interface IRichTextAttachAction {
14
15
  export interface IRichTextAttachmentsConfig {
15
16
  controller: IUseFileAttachmentsResult;
16
17
  extraActions?: Array<IRichTextAttachAction>;
18
+ /**
19
+ * Actions offered on a staged IMAGE before it is sent, so a picture is corrected on its way out
20
+ * instead of being sent and then corrected as a second file.
21
+ */
22
+ imageActions?: Array<IStagedImageAction>;
17
23
  /** The host already renders the staged previews somewhere else. */
18
24
  hidePreviews?: boolean;
19
25
  /** The host already wraps a wider drop target around this editor. */
@@ -487,7 +487,8 @@ const RichTextEditor = forwardRef(function RichTextEditor2({
487
487
  {
488
488
  stagedAttachments: attachmentsController.stagedAttachments,
489
489
  onRemove: attachmentsController.removeAttachment,
490
- onRetry: attachmentsController.retryAttachment
490
+ onRetry: attachmentsController.retryAttachment,
491
+ imageActions: attachments?.imageActions
491
492
  }
492
493
  ) }),
493
494
  showActionsBar && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-0.5 px-2 pb-2 pt-0.5", children: [
@@ -1 +1 @@
1
- {"version":3,"file":"RichTextEditor.js","sources":["../../../src/components/Form/RichTextEditor.tsx"],"sourcesContent":["import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport {\n ControlledMenuHandler,\n ControlledMenuItem,\n ControlledMenuList,\n ControlledMenuRoot,\n} from '@/components/UI/ControlledMenu';\nimport {\n ControlledPopoverContent,\n ControlledPopoverHandler,\n ControlledPopoverRoot,\n} from '@/components/UI/ControlledPopover';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport {\n activeRichTextColor,\n applyRichTextColor,\n buildFileChipMarkup,\n normalizeRichTextColorNodes,\n RICH_TEXT_COLOR_SWATCH_CLASS,\n RICH_TEXT_COLORS,\n TRichTextColor,\n} from '@/helpers/richText';\nimport { IUseFileAttachmentsResult, TFileUploader, useFileAttachments } from '@/hooks/useFileAttachments';\n\nimport { MaterialSymbol, SymbolCodepoints } from '../UI/MaterialSymbol';\n\nimport AttachmentPreviewList from './attachments/AttachmentPreviewList';\nimport FileDropzone from './attachments/FileDropzone';\nimport ControlledButton from './ControlledButton';\nimport ControlledInput from './ControlledInput';\n\n/** An extra entry in the attach menu, for a source only the host knows about (a camera dialog). */\nexport interface IRichTextAttachAction {\n key: string;\n icon: SymbolCodepoints;\n label: string;\n onClick: () => void;\n}\n\n/**\n * Files travel BESIDE the text: the host owns a list of attachments of its own (a chat message\n * has one) and reads `controller.readyAttachments` when it saves.\n */\nexport interface IRichTextAttachmentsConfig {\n controller: IUseFileAttachmentsResult;\n extraActions?: Array<IRichTextAttachAction>;\n /** The host already renders the staged previews somewhere else. */\n hidePreviews?: boolean;\n /** The host already wraps a wider drop target around this editor. */\n hideDropzone?: boolean;\n}\n\n/**\n * Files travel INSIDE the text: the upload result is written into the body as a chip, so the\n * reference persists anywhere the HTML itself is stored and no host needs a new field.\n */\nexport interface IRichTextInlineUploadConfig {\n uploadFile?: TFileUploader;\n maxFiles?: number;\n maxFileSizeBytes?: number;\n}\n\ninterface IRichTextEditorProps {\n value?: string;\n onChange?: (html: string) => void;\n placeholder?: string;\n disabled?: boolean;\n error?: boolean;\n className?: string;\n minHeight?: number;\n /** Rendered inside the toolbar row, right-aligned: host actions that belong to the field. */\n toolbarSlot?: React.ReactNode;\n /** Rendered in the editor's own bottom bar, after the controls the editor owns. */\n actionsSlot?: React.ReactNode;\n onKeyDown?: (event: React.KeyboardEvent<HTMLDivElement>) => void;\n /** Fires when the editable area loses focus, for hosts that persist on blur. */\n onBlur?: () => void;\n /**\n * Fires with the full HTML when the editor changes the content on its own, which today means\n * an inline upload landing after the field already lost focus. A host that persists on blur\n * would otherwise never hear about the file it just attached.\n */\n onCommit?: (html: string) => void;\n /** 'bare' drops the border and focus ring: the host owns the chrome around the field. */\n variant?: 'default' | 'bare';\n /** Hosts with their own action bar (the chat composer) toggle the formatting row on demand. */\n showToolbar?: boolean;\n /** Renders the formatting toggle in the bottom bar and lets the editor own the row's state. */\n formattingToggle?: boolean;\n attachments?: IRichTextAttachmentsConfig;\n inlineUpload?: IRichTextInlineUploadConfig;\n}\n\nexport interface IRichTextEditorHandle {\n focus: () => void;\n /** Viewport rect of the caret, for anchoring a popover to what is being typed. */\n getCaretRect: () => DOMRect | null;\n /**\n * Text typed after the trigger and before the caret, or null when the caret is not inside a\n * trigger word. Drives both \"is the mention menu relevant\" and \"what is being searched\".\n * Spaces are allowed inside the query (people have compound names); the host decides when a\n * spaced query stopped matching anyone and closes the menu.\n */\n getMentionQuery: (trigger: string) => string | null;\n /**\n * Deletes the trigger AND the query the user typed after it, then writes the markup in their\n * place, so picking someone from a mention menu replaces everything typed since the `@`\n * instead of stacking the mention after it.\n */\n replaceTriggerWith: (trigger: string, query: string, html: string) => void;\n}\n\ninterface IToolbarGroup {\n buttons: Array<IToolbarButton>;\n}\n\ninterface IToolbarButton {\n command: string;\n arg?: string;\n icon: SymbolCodepoints;\n tooltipKey: string;\n isBlock?: boolean;\n}\n\nconst TOOLBAR_GROUPS: Array<IToolbarGroup> = [\n {\n buttons: [\n { command: 'bold', icon: 'format_bold', tooltipKey: 'createTask.rich.bold' },\n { command: 'italic', icon: 'format_italic', tooltipKey: 'createTask.rich.italic' },\n { command: 'strikeThrough', icon: 'strikethrough_s', tooltipKey: 'createTask.rich.strikethrough' },\n ],\n },\n {\n buttons: [\n {\n command: 'insertUnorderedList',\n icon: 'format_list_bulleted',\n tooltipKey: 'createTask.rich.bulletList',\n isBlock: true,\n },\n {\n command: 'insertOrderedList',\n icon: 'format_list_numbered',\n tooltipKey: 'createTask.rich.numberedList',\n isBlock: true,\n },\n {\n command: 'formatBlock',\n arg: 'BLOCKQUOTE',\n icon: 'format_quote',\n tooltipKey: 'createTask.rich.quote',\n isBlock: true,\n },\n ],\n },\n {\n buttons: [\n { command: 'indent', icon: 'format_indent_increase', tooltipKey: 'createTask.rich.indent' },\n { command: 'outdent', icon: 'format_indent_decrease', tooltipKey: 'createTask.rich.outdent' },\n ],\n },\n];\n\nconst MENTION_QUERY_MAX_LENGTH = 40;\nconst IMAGE_INPUT_ACCEPT = 'image/*';\nconst DROPZONE_OVERLAY_CLASS = 'rounded-xl';\n\nconst TOOLBAR_BUTTON_CLASS = 'flex size-7 items-center justify-center rounded-lg transition-colors';\nconst TOOLBAR_BUTTON_IDLE_CLASS = 'text-foreground-subtle hover:bg-muted-hover hover:text-foreground-muted';\nconst TOOLBAR_BUTTON_ACTIVE_CLASS = 'bg-brand-soft text-brand-emphasis';\nconst ACTION_BUTTON_CLASS =\n 'flex size-8 items-center justify-center rounded-full text-foreground-faint transition-colors hover:bg-muted-hover hover:text-foreground-muted disabled:text-foreground-faint';\nconst ACTION_BUTTON_ACTIVE_CLASS =\n 'flex size-8 items-center justify-center rounded-full bg-brand-soft text-brand-emphasis transition-colors hover:bg-brand-soft';\n\nconst isCommandActive = (command: string, arg?: string): boolean => {\n try {\n if (arg) return document.queryCommandValue(command) === arg.toLowerCase();\n return document.queryCommandState(command);\n } catch {\n return false;\n }\n};\n\nconst RichTextEditor = forwardRef<IRichTextEditorHandle, IRichTextEditorProps>(function RichTextEditor(\n {\n value = '',\n onChange,\n placeholder,\n disabled = false,\n error = false,\n className,\n minHeight = 120,\n toolbarSlot,\n actionsSlot,\n onKeyDown,\n onBlur,\n onCommit,\n variant = 'default',\n showToolbar = true,\n formattingToggle = false,\n attachments,\n inlineUpload,\n },\n ref,\n) {\n const { t } = useTranslation('ui');\n const editorRef = useRef<HTMLDivElement>(null);\n const [activeCommands, setActiveCommands] = useState<Set<string>>(new Set());\n const [activeColor, setActiveColor] = useState<TRichTextColor>('default');\n const [colorOpen, setColorOpen] = useState(false);\n const [showLinkInput, setShowLinkInput] = useState(false);\n const [linkUrl, setLinkUrl] = useState('');\n const [isFocused, setIsFocused] = useState(false);\n const [formattingOpen, setFormattingOpen] = useState(false);\n const savedRangeRef = useRef<Range | null>(null);\n const isInternalUpdateRef = useRef(false);\n const fileInputRef = useRef<HTMLInputElement>(null);\n const imageInputRef = useRef<HTMLInputElement>(null);\n const insertedStagingIdsRef = useRef<Set<string>>(new Set());\n const pendingColorRef = useRef<TRichTextColor>('default');\n\n const inlineController = useFileAttachments({\n uploadFile: inlineUpload?.uploadFile,\n maxFiles: inlineUpload?.maxFiles,\n maxFileSizeBytes: inlineUpload?.maxFileSizeBytes,\n });\n\n const attachmentsController = attachments?.controller ?? (inlineUpload ? inlineController : null);\n const inlineStagedAttachments = inlineController.stagedAttachments;\n const toolbarVisible = formattingToggle ? formattingOpen : showToolbar;\n\n useEffect(() => {\n const editor = editorRef.current;\n if (!editor) return;\n if (isInternalUpdateRef.current) return;\n if (editor.innerHTML !== value) {\n editor.innerHTML = value;\n }\n }, [value]);\n\n const emitChange = useCallback(() => {\n const editor = editorRef.current;\n if (!editor) return;\n isInternalUpdateRef.current = true;\n onChange?.(editor.innerHTML);\n isInternalUpdateRef.current = false;\n }, [onChange]);\n\n const syncActiveCommands = useCallback(() => {\n const active = new Set<string>();\n TOOLBAR_GROUPS.flatMap((group) => group.buttons).forEach(({ command, arg }) => {\n if (isCommandActive(command, arg)) active.add(arg ?? command);\n });\n setActiveCommands(active);\n\n const editor = editorRef.current;\n if (editor) setActiveColor(activeRichTextColor(editor));\n }, []);\n\n const execFormat = useCallback(\n (command: string, arg?: string) => {\n const editor = editorRef.current;\n if (!editor || disabled) return;\n editor.focus();\n document.execCommand(command, false, arg);\n emitChange();\n syncActiveCommands();\n },\n [disabled, emitChange, syncActiveCommands],\n );\n\n const saveRange = useCallback(() => {\n const selection = window.getSelection();\n if (selection && selection.rangeCount > 0) {\n savedRangeRef.current = selection.getRangeAt(0).cloneRange();\n }\n }, []);\n\n const restoreRange = useCallback(() => {\n const range = savedRangeRef.current;\n if (!range) return;\n const selection = window.getSelection();\n if (!selection) return;\n selection.removeAllRanges();\n selection.addRange(range);\n }, []);\n\n const applyLink = useCallback(() => {\n const url = linkUrl.trim();\n restoreRange();\n if (url) execFormat('createLink', url.startsWith('http') ? url : `https://${url}`);\n setShowLinkInput(false);\n setLinkUrl('');\n }, [linkUrl, restoreRange, execFormat]);\n\n const handleApplyColor = useCallback(\n (color: TRichTextColor) => {\n const editor = editorRef.current;\n if (!editor || disabled) return;\n\n editor.focus();\n restoreRange();\n pendingColorRef.current = color;\n applyRichTextColor(editor, color);\n emitChange();\n setActiveColor(color);\n setColorOpen(false);\n },\n [disabled, emitChange, restoreRange],\n );\n\n const insertHtmlAtCaret = useCallback(\n (html: string, commit = false) => {\n const editor = editorRef.current;\n if (!editor || disabled) return;\n\n const previouslyFocused = document.activeElement as HTMLElement | null;\n const editorHadFocus = editor.contains(previouslyFocused);\n\n editor.focus();\n restoreRange();\n\n const selection = window.getSelection();\n const caretIsInside = selection?.anchorNode ? editor.contains(selection.anchorNode) : false;\n\n if (!caretIsInside) {\n const range = document.createRange();\n range.selectNodeContents(editor);\n range.collapse(false);\n selection?.removeAllRanges();\n selection?.addRange(range);\n }\n\n document.execCommand('insertHTML', false, html);\n saveRange();\n emitChange();\n\n if (!editorHadFocus) previouslyFocused?.focus();\n if (commit) onCommit?.(editor.innerHTML);\n },\n [disabled, emitChange, onCommit, restoreRange, saveRange],\n );\n\n const inlineControllerRef = useRef(inlineController);\n inlineControllerRef.current = inlineController;\n\n // An inline upload lands in the body the moment it finishes, and leaves the staging list so\n // the progress card disappears with it.\n useEffect(() => {\n if (!inlineUpload) return;\n\n inlineStagedAttachments.forEach((staged) => {\n const uploaded = staged.uploaded;\n if (staged.status !== 'done' || !uploaded) return;\n\n if (!insertedStagingIdsRef.current.has(staged.stagingId)) {\n insertedStagingIdsRef.current.add(staged.stagingId);\n insertHtmlAtCaret(`${buildFileChipMarkup(uploaded)}&nbsp;`, true);\n }\n inlineControllerRef.current.removeAttachment(staged.stagingId);\n });\n }, [inlineUpload, inlineStagedAttachments, insertHtmlAtCaret]);\n\n const handleInput = useCallback(() => {\n const editor = editorRef.current;\n if (editor) normalizeRichTextColorNodes(editor, pendingColorRef.current);\n emitChange();\n syncActiveCommands();\n }, [emitChange, syncActiveCommands]);\n\n const handleFilesPicked = (event: React.ChangeEvent<HTMLInputElement>) => {\n const files = Array.from(event.target.files ?? []);\n if (files.length) attachmentsController?.addFiles(files);\n event.target.value = '';\n };\n\n const handlePaste = (event: React.ClipboardEvent) => {\n if (!attachmentsController) return;\n\n const files = Array.from(event.clipboardData?.files ?? []);\n if (!files.length) return;\n\n event.preventDefault();\n event.stopPropagation();\n attachmentsController.addFiles(files);\n };\n\n useImperativeHandle(ref, () => ({\n focus: () => editorRef.current?.focus(),\n\n getCaretRect: () => {\n const selection = window.getSelection();\n if (!selection?.rangeCount) return null;\n\n const range = selection.getRangeAt(0).cloneRange();\n range.collapse(false);\n\n const [rect] = Array.from(range.getClientRects());\n if (rect) return rect;\n\n // A collapsed range inside an empty node measures nothing, so a throwaway marker gives it\n // something to measure. Removed in the same tick, before anything can observe it.\n const marker = document.createElement('span');\n marker.textContent = '\\u200b';\n range.insertNode(marker);\n const markerRect = marker.getBoundingClientRect();\n marker.remove();\n\n return markerRect;\n },\n\n getMentionQuery: (trigger) => {\n const selection = window.getSelection();\n if (!selection?.rangeCount) return null;\n\n const range = selection.getRangeAt(0);\n if (!range.collapsed || range.startContainer.nodeType !== Node.TEXT_NODE) return null;\n\n const textBeforeCaret = range.startContainer.textContent?.slice(0, range.startOffset) ?? '';\n const escapedTrigger = trigger.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const match = new RegExp(`(?:^|\\\\s)${escapedTrigger}([^${escapedTrigger}]*)$`).exec(textBeforeCaret);\n if (!match) return null;\n\n const query = match[1];\n if (query.startsWith(' ') || query.length > MENTION_QUERY_MAX_LENGTH) return null;\n\n return query;\n },\n\n replaceTriggerWith: (trigger, query, html) => {\n const editor = editorRef.current;\n if (!editor || disabled) return;\n\n editor.focus();\n restoreRange();\n\n const selection = window.getSelection();\n const range = selection?.rangeCount ? selection.getRangeAt(0) : null;\n\n if (range?.collapsed && range.startContainer.nodeType === Node.TEXT_NODE) {\n const textBeforeCaret = range.startContainer.textContent?.slice(0, range.startOffset) ?? '';\n const typed = `${trigger}${query}`;\n if (textBeforeCaret.endsWith(typed)) {\n range.setStart(range.startContainer, range.startOffset - typed.length);\n range.deleteContents();\n } else if (textBeforeCaret.endsWith(trigger)) {\n range.setStart(range.startContainer, range.startOffset - trigger.length);\n range.deleteContents();\n }\n }\n\n document.execCommand('insertHTML', false, `${html}&nbsp;`);\n emitChange();\n },\n }));\n\n const isEmpty = !editorRef.current?.textContent?.trim() && !editorRef.current?.querySelector('img, li, blockquote');\n\n const attachMenuActions = useMemo<Array<IRichTextAttachAction>>(\n () => [\n {\n key: 'file',\n icon: 'description',\n label: t('followUp.attachments.pickFile'),\n onClick: () => fileInputRef.current?.click(),\n },\n {\n key: 'image',\n icon: 'image',\n label: t('followUp.attachments.pickImage'),\n onClick: () => imageInputRef.current?.click(),\n },\n ...(attachments?.extraActions ?? []),\n ],\n [attachments?.extraActions, t],\n );\n\n const showPreviews = Boolean(attachmentsController) && !attachments?.hidePreviews;\n const showActionsBar = Boolean(attachmentsController) || Boolean(actionsSlot) || formattingToggle;\n\n const editorShell = (\n <div\n className={classNames(\n 'flex w-full flex-col transition-colors',\n variant === 'default' && 'rounded-xl border bg-surface',\n variant === 'default' &&\n (error\n ? 'border-danger-border'\n : isFocused\n ? 'border-brand-border ring-2 ring-brand-border'\n : 'border-border'),\n className,\n )}\n >\n {toolbarVisible && (\n <div\n className={classNames(\n 'flex flex-wrap items-center gap-px border-b px-2 py-1.5',\n error ? 'border-danger-border' : 'border-border-subtle',\n )}\n >\n {TOOLBAR_GROUPS.map((group, groupIndex) => (\n <div\n key={groupIndex}\n className={classNames(\n 'flex items-center gap-px',\n groupIndex < TOOLBAR_GROUPS.length - 1 && 'mr-1 border-r border-border pr-1',\n )}\n >\n {group.buttons.map(({ command, arg, icon, tooltipKey }) => {\n const key = arg ?? command;\n const isActive = activeCommands.has(key);\n\n return (\n <ControlledTooltip key={key} content={t(tooltipKey)}>\n <ControlledButton\n defaultHtmlButton\n disabled={disabled}\n onMouseDown={(event: React.MouseEvent) => {\n event.preventDefault();\n execFormat(command, arg);\n }}\n className={classNames(\n TOOLBAR_BUTTON_CLASS,\n isActive ? TOOLBAR_BUTTON_ACTIVE_CLASS : TOOLBAR_BUTTON_IDLE_CLASS,\n disabled && 'cursor-not-allowed opacity-40',\n )}\n >\n <MaterialSymbol icon={icon} size={18} />\n </ControlledButton>\n </ControlledTooltip>\n );\n })}\n </div>\n ))}\n\n <div className=\"flex items-center gap-px border-l border-border pl-1\">\n <ControlledPopoverRoot open={colorOpen} handler={setColorOpen} placement=\"bottom-start\">\n <ControlledTooltip content={t('createTask.rich.textColor')}>\n <ControlledPopoverHandler>\n <ControlledButton\n defaultHtmlButton\n disabled={disabled}\n onMouseDown={(event: React.MouseEvent) => {\n event.preventDefault();\n saveRange();\n }}\n className={classNames(\n TOOLBAR_BUTTON_CLASS,\n colorOpen ? TOOLBAR_BUTTON_ACTIVE_CLASS : TOOLBAR_BUTTON_IDLE_CLASS,\n disabled && 'cursor-not-allowed opacity-40',\n )}\n >\n <MaterialSymbol icon=\"format_color_text\" size={18} />\n </ControlledButton>\n </ControlledPopoverHandler>\n </ControlledTooltip>\n\n <ControlledPopoverContent className=\"zIndexMax w-44 p-2\">\n <div className=\"flex flex-wrap gap-1.5\">\n {RICH_TEXT_COLORS.map((color) => (\n <ControlledTooltip key={color} content={t(`createTask.rich.colors.${color}`)}>\n <ControlledButton\n defaultHtmlButton\n aria-pressed={activeColor === color}\n onMouseDown={(event: React.MouseEvent) => {\n event.preventDefault();\n handleApplyColor(color);\n }}\n className={classNames(\n 'flex size-7 items-center justify-center rounded-lg border transition-colors',\n activeColor === color ? 'border-brand-border ring-2 ring-brand-border' : 'border-border',\n )}\n >\n <span\n className={classNames(\n 'size-4 rounded-full',\n RICH_TEXT_COLOR_SWATCH_CLASS[color],\n color === 'default' && 'ring-1 ring-inset ring-border-strong',\n )}\n />\n </ControlledButton>\n </ControlledTooltip>\n ))}\n </div>\n </ControlledPopoverContent>\n </ControlledPopoverRoot>\n\n <ControlledTooltip content={t('createTask.rich.link')}>\n <ControlledButton\n defaultHtmlButton\n disabled={disabled}\n onMouseDown={(event: React.MouseEvent) => {\n event.preventDefault();\n saveRange();\n setShowLinkInput((previous) => !previous);\n }}\n className={classNames(\n TOOLBAR_BUTTON_CLASS,\n showLinkInput ? TOOLBAR_BUTTON_ACTIVE_CLASS : TOOLBAR_BUTTON_IDLE_CLASS,\n disabled && 'cursor-not-allowed opacity-40',\n )}\n >\n <MaterialSymbol icon=\"link\" size={18} />\n </ControlledButton>\n </ControlledTooltip>\n </div>\n\n {toolbarSlot && <div className=\"ml-auto flex items-center gap-x-1\">{toolbarSlot}</div>}\n </div>\n )}\n\n {showLinkInput && (\n <div className=\"flex items-center gap-2 border-b border-border-subtle bg-muted px-3 py-2\">\n <ControlledInput\n autoFocus\n value={linkUrl}\n onChange={(event) => setLinkUrl(event.target.value)}\n placeholder=\"https://\"\n aria-label={t('createTask.rich.link')}\n onKeyDown={(event: React.KeyboardEvent) => {\n if (event.key === 'Enter') applyLink();\n if (event.key === 'Escape') setShowLinkInput(false);\n }}\n />\n <ControlledButton size=\"sm\" onClick={applyLink}>\n {t('actions.apply')}\n </ControlledButton>\n <ControlledButton size=\"sm\" variant=\"outlined\" tone=\"neutral\" onClick={() => setShowLinkInput(false)}>\n {t('actions.cancel')}\n </ControlledButton>\n </div>\n )}\n\n <div className=\"relative\">\n <div\n ref={editorRef}\n contentEditable={!disabled}\n suppressContentEditableWarning\n onInput={handleInput}\n onKeyDown={onKeyDown}\n onPaste={handlePaste}\n onKeyUp={() => {\n saveRange();\n syncActiveCommands();\n }}\n onMouseUp={() => {\n saveRange();\n syncActiveCommands();\n }}\n onFocus={() => setIsFocused(true)}\n onBlur={() => {\n saveRange();\n setIsFocused(false);\n onBlur?.();\n }}\n className={classNames(\n 'w-full px-3 py-2.5 text-sm text-foreground outline-none',\n '[&_ol]:ml-4 [&_ol]:list-decimal [&_ul]:ml-4 [&_ul]:list-disc',\n '[&_blockquote]:border-l-2 [&_blockquote]:border-brand-border [&_blockquote]:pl-3 [&_blockquote]:italic [&_blockquote]:text-foreground-subtle',\n '[&_a]:text-brand-emphasis [&_a]:underline',\n '[&_a.rt-file]:text-foreground-muted [&_a.rt-file]:no-underline',\n '[&_a.rt-image-link]:no-underline',\n '[&_user]:whitespace-nowrap [&_user]:rounded [&_user]:bg-brand-soft [&_user]:px-1 [&_user]:font-medium [&_user]:text-brand-emphasis',\n disabled && 'cursor-not-allowed opacity-60',\n )}\n style={{ minHeight }}\n />\n {isEmpty && !isFocused && placeholder && (\n <span className=\"pointer-events-none absolute left-3 top-2.5 text-sm text-foreground-faint\">\n {placeholder}\n </span>\n )}\n </div>\n\n {showPreviews && attachmentsController && attachmentsController.stagedAttachments.length > 0 && (\n <div className=\"px-3 pb-2\">\n <AttachmentPreviewList\n stagedAttachments={attachmentsController.stagedAttachments}\n onRemove={attachmentsController.removeAttachment}\n onRetry={attachmentsController.retryAttachment}\n />\n </div>\n )}\n\n {showActionsBar && (\n <div className=\"flex items-center gap-x-0.5 px-2 pb-2 pt-0.5\">\n {formattingToggle && (\n <ControlledTooltip content={t('followUp.chat.formatting')} className=\"z-[9999]\">\n <ControlledButton\n defaultHtmlButton\n disabled={disabled}\n onClick={() => setFormattingOpen((previous) => !previous)}\n aria-pressed={formattingOpen}\n className={formattingOpen ? ACTION_BUTTON_ACTIVE_CLASS : ACTION_BUTTON_CLASS}\n >\n <MaterialSymbol icon=\"format_size\" size={18} />\n </ControlledButton>\n </ControlledTooltip>\n )}\n\n {attachmentsController && (\n <ControlledMenuRoot placement=\"top-start\">\n <ControlledTooltip content={t('followUp.attachments.attach')} className=\"z-[9999]\">\n <ControlledMenuHandler>\n <ControlledButton defaultHtmlButton disabled={disabled} className={ACTION_BUTTON_CLASS}>\n <MaterialSymbol icon=\"attach_file\" size={18} />\n </ControlledButton>\n </ControlledMenuHandler>\n </ControlledTooltip>\n <ControlledMenuList className=\"zIndexMax p-1\">\n {attachMenuActions.map((action) => (\n <ControlledMenuItem key={action.key} className=\"flex items-center gap-x-2\" onClick={action.onClick}>\n <MaterialSymbol icon={action.icon} size={18} />\n {action.label}\n </ControlledMenuItem>\n ))}\n </ControlledMenuList>\n </ControlledMenuRoot>\n )}\n\n {actionsSlot}\n </div>\n )}\n\n {attachmentsController && (\n <>\n <input ref={fileInputRef} type=\"file\" multiple hidden onChange={handleFilesPicked} />\n <input\n ref={imageInputRef}\n type=\"file\"\n multiple\n hidden\n accept={IMAGE_INPUT_ACCEPT}\n onChange={handleFilesPicked}\n />\n </>\n )}\n </div>\n );\n\n if (!attachmentsController || attachments?.hideDropzone) return editorShell;\n\n return (\n <FileDropzone\n stopPropagation\n disabled={disabled}\n className=\"flex-none\"\n overlayClassName={DROPZONE_OVERLAY_CLASS}\n onFilesDropped={(files) => attachmentsController.addFiles(files)}\n >\n {editorShell}\n </FileDropzone>\n );\n});\n\nexport default RichTextEditor;\n"],"names":["RichTextEditor"],"mappings":"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"}
1
+ {"version":3,"file":"RichTextEditor.js","sources":["../../../src/components/Form/RichTextEditor.tsx"],"sourcesContent":["import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport {\n ControlledMenuHandler,\n ControlledMenuItem,\n ControlledMenuList,\n ControlledMenuRoot,\n} from '@/components/UI/ControlledMenu';\nimport {\n ControlledPopoverContent,\n ControlledPopoverHandler,\n ControlledPopoverRoot,\n} from '@/components/UI/ControlledPopover';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport {\n activeRichTextColor,\n applyRichTextColor,\n buildFileChipMarkup,\n normalizeRichTextColorNodes,\n RICH_TEXT_COLOR_SWATCH_CLASS,\n RICH_TEXT_COLORS,\n TRichTextColor,\n} from '@/helpers/richText';\nimport { IUseFileAttachmentsResult, TFileUploader, useFileAttachments } from '@/hooks/useFileAttachments';\n\nimport { MaterialSymbol, SymbolCodepoints } from '../UI/MaterialSymbol';\n\nimport AttachmentPreviewList, { IStagedImageAction } from './attachments/AttachmentPreviewList';\nimport FileDropzone from './attachments/FileDropzone';\nimport ControlledButton from './ControlledButton';\nimport ControlledInput from './ControlledInput';\n\n/** An extra entry in the attach menu, for a source only the host knows about (a camera dialog). */\nexport interface IRichTextAttachAction {\n key: string;\n icon: SymbolCodepoints;\n label: string;\n onClick: () => void;\n}\n\n/**\n * Files travel BESIDE the text: the host owns a list of attachments of its own (a chat message\n * has one) and reads `controller.readyAttachments` when it saves.\n */\nexport interface IRichTextAttachmentsConfig {\n controller: IUseFileAttachmentsResult;\n extraActions?: Array<IRichTextAttachAction>;\n /**\n * Actions offered on a staged IMAGE before it is sent, so a picture is corrected on its way out\n * instead of being sent and then corrected as a second file.\n */\n imageActions?: Array<IStagedImageAction>;\n /** The host already renders the staged previews somewhere else. */\n hidePreviews?: boolean;\n /** The host already wraps a wider drop target around this editor. */\n hideDropzone?: boolean;\n}\n\n/**\n * Files travel INSIDE the text: the upload result is written into the body as a chip, so the\n * reference persists anywhere the HTML itself is stored and no host needs a new field.\n */\nexport interface IRichTextInlineUploadConfig {\n uploadFile?: TFileUploader;\n maxFiles?: number;\n maxFileSizeBytes?: number;\n}\n\ninterface IRichTextEditorProps {\n value?: string;\n onChange?: (html: string) => void;\n placeholder?: string;\n disabled?: boolean;\n error?: boolean;\n className?: string;\n minHeight?: number;\n /** Rendered inside the toolbar row, right-aligned: host actions that belong to the field. */\n toolbarSlot?: React.ReactNode;\n /** Rendered in the editor's own bottom bar, after the controls the editor owns. */\n actionsSlot?: React.ReactNode;\n onKeyDown?: (event: React.KeyboardEvent<HTMLDivElement>) => void;\n /** Fires when the editable area loses focus, for hosts that persist on blur. */\n onBlur?: () => void;\n /**\n * Fires with the full HTML when the editor changes the content on its own, which today means\n * an inline upload landing after the field already lost focus. A host that persists on blur\n * would otherwise never hear about the file it just attached.\n */\n onCommit?: (html: string) => void;\n /** 'bare' drops the border and focus ring: the host owns the chrome around the field. */\n variant?: 'default' | 'bare';\n /** Hosts with their own action bar (the chat composer) toggle the formatting row on demand. */\n showToolbar?: boolean;\n /** Renders the formatting toggle in the bottom bar and lets the editor own the row's state. */\n formattingToggle?: boolean;\n attachments?: IRichTextAttachmentsConfig;\n inlineUpload?: IRichTextInlineUploadConfig;\n}\n\nexport interface IRichTextEditorHandle {\n focus: () => void;\n /** Viewport rect of the caret, for anchoring a popover to what is being typed. */\n getCaretRect: () => DOMRect | null;\n /**\n * Text typed after the trigger and before the caret, or null when the caret is not inside a\n * trigger word. Drives both \"is the mention menu relevant\" and \"what is being searched\".\n * Spaces are allowed inside the query (people have compound names); the host decides when a\n * spaced query stopped matching anyone and closes the menu.\n */\n getMentionQuery: (trigger: string) => string | null;\n /**\n * Deletes the trigger AND the query the user typed after it, then writes the markup in their\n * place, so picking someone from a mention menu replaces everything typed since the `@`\n * instead of stacking the mention after it.\n */\n replaceTriggerWith: (trigger: string, query: string, html: string) => void;\n}\n\ninterface IToolbarGroup {\n buttons: Array<IToolbarButton>;\n}\n\ninterface IToolbarButton {\n command: string;\n arg?: string;\n icon: SymbolCodepoints;\n tooltipKey: string;\n isBlock?: boolean;\n}\n\nconst TOOLBAR_GROUPS: Array<IToolbarGroup> = [\n {\n buttons: [\n { command: 'bold', icon: 'format_bold', tooltipKey: 'createTask.rich.bold' },\n { command: 'italic', icon: 'format_italic', tooltipKey: 'createTask.rich.italic' },\n { command: 'strikeThrough', icon: 'strikethrough_s', tooltipKey: 'createTask.rich.strikethrough' },\n ],\n },\n {\n buttons: [\n {\n command: 'insertUnorderedList',\n icon: 'format_list_bulleted',\n tooltipKey: 'createTask.rich.bulletList',\n isBlock: true,\n },\n {\n command: 'insertOrderedList',\n icon: 'format_list_numbered',\n tooltipKey: 'createTask.rich.numberedList',\n isBlock: true,\n },\n {\n command: 'formatBlock',\n arg: 'BLOCKQUOTE',\n icon: 'format_quote',\n tooltipKey: 'createTask.rich.quote',\n isBlock: true,\n },\n ],\n },\n {\n buttons: [\n { command: 'indent', icon: 'format_indent_increase', tooltipKey: 'createTask.rich.indent' },\n { command: 'outdent', icon: 'format_indent_decrease', tooltipKey: 'createTask.rich.outdent' },\n ],\n },\n];\n\nconst MENTION_QUERY_MAX_LENGTH = 40;\nconst IMAGE_INPUT_ACCEPT = 'image/*';\nconst DROPZONE_OVERLAY_CLASS = 'rounded-xl';\n\nconst TOOLBAR_BUTTON_CLASS = 'flex size-7 items-center justify-center rounded-lg transition-colors';\nconst TOOLBAR_BUTTON_IDLE_CLASS = 'text-foreground-subtle hover:bg-muted-hover hover:text-foreground-muted';\nconst TOOLBAR_BUTTON_ACTIVE_CLASS = 'bg-brand-soft text-brand-emphasis';\nconst ACTION_BUTTON_CLASS =\n 'flex size-8 items-center justify-center rounded-full text-foreground-faint transition-colors hover:bg-muted-hover hover:text-foreground-muted disabled:text-foreground-faint';\nconst ACTION_BUTTON_ACTIVE_CLASS =\n 'flex size-8 items-center justify-center rounded-full bg-brand-soft text-brand-emphasis transition-colors hover:bg-brand-soft';\n\nconst isCommandActive = (command: string, arg?: string): boolean => {\n try {\n if (arg) return document.queryCommandValue(command) === arg.toLowerCase();\n return document.queryCommandState(command);\n } catch {\n return false;\n }\n};\n\nconst RichTextEditor = forwardRef<IRichTextEditorHandle, IRichTextEditorProps>(function RichTextEditor(\n {\n value = '',\n onChange,\n placeholder,\n disabled = false,\n error = false,\n className,\n minHeight = 120,\n toolbarSlot,\n actionsSlot,\n onKeyDown,\n onBlur,\n onCommit,\n variant = 'default',\n showToolbar = true,\n formattingToggle = false,\n attachments,\n inlineUpload,\n },\n ref,\n) {\n const { t } = useTranslation('ui');\n const editorRef = useRef<HTMLDivElement>(null);\n const [activeCommands, setActiveCommands] = useState<Set<string>>(new Set());\n const [activeColor, setActiveColor] = useState<TRichTextColor>('default');\n const [colorOpen, setColorOpen] = useState(false);\n const [showLinkInput, setShowLinkInput] = useState(false);\n const [linkUrl, setLinkUrl] = useState('');\n const [isFocused, setIsFocused] = useState(false);\n const [formattingOpen, setFormattingOpen] = useState(false);\n const savedRangeRef = useRef<Range | null>(null);\n const isInternalUpdateRef = useRef(false);\n const fileInputRef = useRef<HTMLInputElement>(null);\n const imageInputRef = useRef<HTMLInputElement>(null);\n const insertedStagingIdsRef = useRef<Set<string>>(new Set());\n const pendingColorRef = useRef<TRichTextColor>('default');\n\n const inlineController = useFileAttachments({\n uploadFile: inlineUpload?.uploadFile,\n maxFiles: inlineUpload?.maxFiles,\n maxFileSizeBytes: inlineUpload?.maxFileSizeBytes,\n });\n\n const attachmentsController = attachments?.controller ?? (inlineUpload ? inlineController : null);\n const inlineStagedAttachments = inlineController.stagedAttachments;\n const toolbarVisible = formattingToggle ? formattingOpen : showToolbar;\n\n useEffect(() => {\n const editor = editorRef.current;\n if (!editor) return;\n if (isInternalUpdateRef.current) return;\n if (editor.innerHTML !== value) {\n editor.innerHTML = value;\n }\n }, [value]);\n\n const emitChange = useCallback(() => {\n const editor = editorRef.current;\n if (!editor) return;\n isInternalUpdateRef.current = true;\n onChange?.(editor.innerHTML);\n isInternalUpdateRef.current = false;\n }, [onChange]);\n\n const syncActiveCommands = useCallback(() => {\n const active = new Set<string>();\n TOOLBAR_GROUPS.flatMap((group) => group.buttons).forEach(({ command, arg }) => {\n if (isCommandActive(command, arg)) active.add(arg ?? command);\n });\n setActiveCommands(active);\n\n const editor = editorRef.current;\n if (editor) setActiveColor(activeRichTextColor(editor));\n }, []);\n\n const execFormat = useCallback(\n (command: string, arg?: string) => {\n const editor = editorRef.current;\n if (!editor || disabled) return;\n editor.focus();\n document.execCommand(command, false, arg);\n emitChange();\n syncActiveCommands();\n },\n [disabled, emitChange, syncActiveCommands],\n );\n\n const saveRange = useCallback(() => {\n const selection = window.getSelection();\n if (selection && selection.rangeCount > 0) {\n savedRangeRef.current = selection.getRangeAt(0).cloneRange();\n }\n }, []);\n\n const restoreRange = useCallback(() => {\n const range = savedRangeRef.current;\n if (!range) return;\n const selection = window.getSelection();\n if (!selection) return;\n selection.removeAllRanges();\n selection.addRange(range);\n }, []);\n\n const applyLink = useCallback(() => {\n const url = linkUrl.trim();\n restoreRange();\n if (url) execFormat('createLink', url.startsWith('http') ? url : `https://${url}`);\n setShowLinkInput(false);\n setLinkUrl('');\n }, [linkUrl, restoreRange, execFormat]);\n\n const handleApplyColor = useCallback(\n (color: TRichTextColor) => {\n const editor = editorRef.current;\n if (!editor || disabled) return;\n\n editor.focus();\n restoreRange();\n pendingColorRef.current = color;\n applyRichTextColor(editor, color);\n emitChange();\n setActiveColor(color);\n setColorOpen(false);\n },\n [disabled, emitChange, restoreRange],\n );\n\n const insertHtmlAtCaret = useCallback(\n (html: string, commit = false) => {\n const editor = editorRef.current;\n if (!editor || disabled) return;\n\n const previouslyFocused = document.activeElement as HTMLElement | null;\n const editorHadFocus = editor.contains(previouslyFocused);\n\n editor.focus();\n restoreRange();\n\n const selection = window.getSelection();\n const caretIsInside = selection?.anchorNode ? editor.contains(selection.anchorNode) : false;\n\n if (!caretIsInside) {\n const range = document.createRange();\n range.selectNodeContents(editor);\n range.collapse(false);\n selection?.removeAllRanges();\n selection?.addRange(range);\n }\n\n document.execCommand('insertHTML', false, html);\n saveRange();\n emitChange();\n\n if (!editorHadFocus) previouslyFocused?.focus();\n if (commit) onCommit?.(editor.innerHTML);\n },\n [disabled, emitChange, onCommit, restoreRange, saveRange],\n );\n\n const inlineControllerRef = useRef(inlineController);\n inlineControllerRef.current = inlineController;\n\n // An inline upload lands in the body the moment it finishes, and leaves the staging list so\n // the progress card disappears with it.\n useEffect(() => {\n if (!inlineUpload) return;\n\n inlineStagedAttachments.forEach((staged) => {\n const uploaded = staged.uploaded;\n if (staged.status !== 'done' || !uploaded) return;\n\n if (!insertedStagingIdsRef.current.has(staged.stagingId)) {\n insertedStagingIdsRef.current.add(staged.stagingId);\n insertHtmlAtCaret(`${buildFileChipMarkup(uploaded)}&nbsp;`, true);\n }\n inlineControllerRef.current.removeAttachment(staged.stagingId);\n });\n }, [inlineUpload, inlineStagedAttachments, insertHtmlAtCaret]);\n\n const handleInput = useCallback(() => {\n const editor = editorRef.current;\n if (editor) normalizeRichTextColorNodes(editor, pendingColorRef.current);\n emitChange();\n syncActiveCommands();\n }, [emitChange, syncActiveCommands]);\n\n const handleFilesPicked = (event: React.ChangeEvent<HTMLInputElement>) => {\n const files = Array.from(event.target.files ?? []);\n if (files.length) attachmentsController?.addFiles(files);\n event.target.value = '';\n };\n\n const handlePaste = (event: React.ClipboardEvent) => {\n if (!attachmentsController) return;\n\n const files = Array.from(event.clipboardData?.files ?? []);\n if (!files.length) return;\n\n event.preventDefault();\n event.stopPropagation();\n attachmentsController.addFiles(files);\n };\n\n useImperativeHandle(ref, () => ({\n focus: () => editorRef.current?.focus(),\n\n getCaretRect: () => {\n const selection = window.getSelection();\n if (!selection?.rangeCount) return null;\n\n const range = selection.getRangeAt(0).cloneRange();\n range.collapse(false);\n\n const [rect] = Array.from(range.getClientRects());\n if (rect) return rect;\n\n // A collapsed range inside an empty node measures nothing, so a throwaway marker gives it\n // something to measure. Removed in the same tick, before anything can observe it.\n const marker = document.createElement('span');\n marker.textContent = '\\u200b';\n range.insertNode(marker);\n const markerRect = marker.getBoundingClientRect();\n marker.remove();\n\n return markerRect;\n },\n\n getMentionQuery: (trigger) => {\n const selection = window.getSelection();\n if (!selection?.rangeCount) return null;\n\n const range = selection.getRangeAt(0);\n if (!range.collapsed || range.startContainer.nodeType !== Node.TEXT_NODE) return null;\n\n const textBeforeCaret = range.startContainer.textContent?.slice(0, range.startOffset) ?? '';\n const escapedTrigger = trigger.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const match = new RegExp(`(?:^|\\\\s)${escapedTrigger}([^${escapedTrigger}]*)$`).exec(textBeforeCaret);\n if (!match) return null;\n\n const query = match[1];\n if (query.startsWith(' ') || query.length > MENTION_QUERY_MAX_LENGTH) return null;\n\n return query;\n },\n\n replaceTriggerWith: (trigger, query, html) => {\n const editor = editorRef.current;\n if (!editor || disabled) return;\n\n editor.focus();\n restoreRange();\n\n const selection = window.getSelection();\n const range = selection?.rangeCount ? selection.getRangeAt(0) : null;\n\n if (range?.collapsed && range.startContainer.nodeType === Node.TEXT_NODE) {\n const textBeforeCaret = range.startContainer.textContent?.slice(0, range.startOffset) ?? '';\n const typed = `${trigger}${query}`;\n if (textBeforeCaret.endsWith(typed)) {\n range.setStart(range.startContainer, range.startOffset - typed.length);\n range.deleteContents();\n } else if (textBeforeCaret.endsWith(trigger)) {\n range.setStart(range.startContainer, range.startOffset - trigger.length);\n range.deleteContents();\n }\n }\n\n document.execCommand('insertHTML', false, `${html}&nbsp;`);\n emitChange();\n },\n }));\n\n const isEmpty = !editorRef.current?.textContent?.trim() && !editorRef.current?.querySelector('img, li, blockquote');\n\n const attachMenuActions = useMemo<Array<IRichTextAttachAction>>(\n () => [\n {\n key: 'file',\n icon: 'description',\n label: t('followUp.attachments.pickFile'),\n onClick: () => fileInputRef.current?.click(),\n },\n {\n key: 'image',\n icon: 'image',\n label: t('followUp.attachments.pickImage'),\n onClick: () => imageInputRef.current?.click(),\n },\n ...(attachments?.extraActions ?? []),\n ],\n [attachments?.extraActions, t],\n );\n\n const showPreviews = Boolean(attachmentsController) && !attachments?.hidePreviews;\n const showActionsBar = Boolean(attachmentsController) || Boolean(actionsSlot) || formattingToggle;\n\n const editorShell = (\n <div\n className={classNames(\n 'flex w-full flex-col transition-colors',\n variant === 'default' && 'rounded-xl border bg-surface',\n variant === 'default' &&\n (error\n ? 'border-danger-border'\n : isFocused\n ? 'border-brand-border ring-2 ring-brand-border'\n : 'border-border'),\n className,\n )}\n >\n {toolbarVisible && (\n <div\n className={classNames(\n 'flex flex-wrap items-center gap-px border-b px-2 py-1.5',\n error ? 'border-danger-border' : 'border-border-subtle',\n )}\n >\n {TOOLBAR_GROUPS.map((group, groupIndex) => (\n <div\n key={groupIndex}\n className={classNames(\n 'flex items-center gap-px',\n groupIndex < TOOLBAR_GROUPS.length - 1 && 'mr-1 border-r border-border pr-1',\n )}\n >\n {group.buttons.map(({ command, arg, icon, tooltipKey }) => {\n const key = arg ?? command;\n const isActive = activeCommands.has(key);\n\n return (\n <ControlledTooltip key={key} content={t(tooltipKey)}>\n <ControlledButton\n defaultHtmlButton\n disabled={disabled}\n onMouseDown={(event: React.MouseEvent) => {\n event.preventDefault();\n execFormat(command, arg);\n }}\n className={classNames(\n TOOLBAR_BUTTON_CLASS,\n isActive ? TOOLBAR_BUTTON_ACTIVE_CLASS : TOOLBAR_BUTTON_IDLE_CLASS,\n disabled && 'cursor-not-allowed opacity-40',\n )}\n >\n <MaterialSymbol icon={icon} size={18} />\n </ControlledButton>\n </ControlledTooltip>\n );\n })}\n </div>\n ))}\n\n <div className=\"flex items-center gap-px border-l border-border pl-1\">\n <ControlledPopoverRoot open={colorOpen} handler={setColorOpen} placement=\"bottom-start\">\n <ControlledTooltip content={t('createTask.rich.textColor')}>\n <ControlledPopoverHandler>\n <ControlledButton\n defaultHtmlButton\n disabled={disabled}\n onMouseDown={(event: React.MouseEvent) => {\n event.preventDefault();\n saveRange();\n }}\n className={classNames(\n TOOLBAR_BUTTON_CLASS,\n colorOpen ? TOOLBAR_BUTTON_ACTIVE_CLASS : TOOLBAR_BUTTON_IDLE_CLASS,\n disabled && 'cursor-not-allowed opacity-40',\n )}\n >\n <MaterialSymbol icon=\"format_color_text\" size={18} />\n </ControlledButton>\n </ControlledPopoverHandler>\n </ControlledTooltip>\n\n <ControlledPopoverContent className=\"zIndexMax w-44 p-2\">\n <div className=\"flex flex-wrap gap-1.5\">\n {RICH_TEXT_COLORS.map((color) => (\n <ControlledTooltip key={color} content={t(`createTask.rich.colors.${color}`)}>\n <ControlledButton\n defaultHtmlButton\n aria-pressed={activeColor === color}\n onMouseDown={(event: React.MouseEvent) => {\n event.preventDefault();\n handleApplyColor(color);\n }}\n className={classNames(\n 'flex size-7 items-center justify-center rounded-lg border transition-colors',\n activeColor === color ? 'border-brand-border ring-2 ring-brand-border' : 'border-border',\n )}\n >\n <span\n className={classNames(\n 'size-4 rounded-full',\n RICH_TEXT_COLOR_SWATCH_CLASS[color],\n color === 'default' && 'ring-1 ring-inset ring-border-strong',\n )}\n />\n </ControlledButton>\n </ControlledTooltip>\n ))}\n </div>\n </ControlledPopoverContent>\n </ControlledPopoverRoot>\n\n <ControlledTooltip content={t('createTask.rich.link')}>\n <ControlledButton\n defaultHtmlButton\n disabled={disabled}\n onMouseDown={(event: React.MouseEvent) => {\n event.preventDefault();\n saveRange();\n setShowLinkInput((previous) => !previous);\n }}\n className={classNames(\n TOOLBAR_BUTTON_CLASS,\n showLinkInput ? TOOLBAR_BUTTON_ACTIVE_CLASS : TOOLBAR_BUTTON_IDLE_CLASS,\n disabled && 'cursor-not-allowed opacity-40',\n )}\n >\n <MaterialSymbol icon=\"link\" size={18} />\n </ControlledButton>\n </ControlledTooltip>\n </div>\n\n {toolbarSlot && <div className=\"ml-auto flex items-center gap-x-1\">{toolbarSlot}</div>}\n </div>\n )}\n\n {showLinkInput && (\n <div className=\"flex items-center gap-2 border-b border-border-subtle bg-muted px-3 py-2\">\n <ControlledInput\n autoFocus\n value={linkUrl}\n onChange={(event) => setLinkUrl(event.target.value)}\n placeholder=\"https://\"\n aria-label={t('createTask.rich.link')}\n onKeyDown={(event: React.KeyboardEvent) => {\n if (event.key === 'Enter') applyLink();\n if (event.key === 'Escape') setShowLinkInput(false);\n }}\n />\n <ControlledButton size=\"sm\" onClick={applyLink}>\n {t('actions.apply')}\n </ControlledButton>\n <ControlledButton size=\"sm\" variant=\"outlined\" tone=\"neutral\" onClick={() => setShowLinkInput(false)}>\n {t('actions.cancel')}\n </ControlledButton>\n </div>\n )}\n\n <div className=\"relative\">\n <div\n ref={editorRef}\n contentEditable={!disabled}\n suppressContentEditableWarning\n onInput={handleInput}\n onKeyDown={onKeyDown}\n onPaste={handlePaste}\n onKeyUp={() => {\n saveRange();\n syncActiveCommands();\n }}\n onMouseUp={() => {\n saveRange();\n syncActiveCommands();\n }}\n onFocus={() => setIsFocused(true)}\n onBlur={() => {\n saveRange();\n setIsFocused(false);\n onBlur?.();\n }}\n className={classNames(\n 'w-full px-3 py-2.5 text-sm text-foreground outline-none',\n '[&_ol]:ml-4 [&_ol]:list-decimal [&_ul]:ml-4 [&_ul]:list-disc',\n '[&_blockquote]:border-l-2 [&_blockquote]:border-brand-border [&_blockquote]:pl-3 [&_blockquote]:italic [&_blockquote]:text-foreground-subtle',\n '[&_a]:text-brand-emphasis [&_a]:underline',\n '[&_a.rt-file]:text-foreground-muted [&_a.rt-file]:no-underline',\n '[&_a.rt-image-link]:no-underline',\n '[&_user]:whitespace-nowrap [&_user]:rounded [&_user]:bg-brand-soft [&_user]:px-1 [&_user]:font-medium [&_user]:text-brand-emphasis',\n disabled && 'cursor-not-allowed opacity-60',\n )}\n style={{ minHeight }}\n />\n {isEmpty && !isFocused && placeholder && (\n <span className=\"pointer-events-none absolute left-3 top-2.5 text-sm text-foreground-faint\">\n {placeholder}\n </span>\n )}\n </div>\n\n {showPreviews && attachmentsController && attachmentsController.stagedAttachments.length > 0 && (\n <div className=\"px-3 pb-2\">\n <AttachmentPreviewList\n stagedAttachments={attachmentsController.stagedAttachments}\n onRemove={attachmentsController.removeAttachment}\n onRetry={attachmentsController.retryAttachment}\n imageActions={attachments?.imageActions}\n />\n </div>\n )}\n\n {showActionsBar && (\n <div className=\"flex items-center gap-x-0.5 px-2 pb-2 pt-0.5\">\n {formattingToggle && (\n <ControlledTooltip content={t('followUp.chat.formatting')} className=\"z-[9999]\">\n <ControlledButton\n defaultHtmlButton\n disabled={disabled}\n onClick={() => setFormattingOpen((previous) => !previous)}\n aria-pressed={formattingOpen}\n className={formattingOpen ? ACTION_BUTTON_ACTIVE_CLASS : ACTION_BUTTON_CLASS}\n >\n <MaterialSymbol icon=\"format_size\" size={18} />\n </ControlledButton>\n </ControlledTooltip>\n )}\n\n {attachmentsController && (\n <ControlledMenuRoot placement=\"top-start\">\n <ControlledTooltip content={t('followUp.attachments.attach')} className=\"z-[9999]\">\n <ControlledMenuHandler>\n <ControlledButton defaultHtmlButton disabled={disabled} className={ACTION_BUTTON_CLASS}>\n <MaterialSymbol icon=\"attach_file\" size={18} />\n </ControlledButton>\n </ControlledMenuHandler>\n </ControlledTooltip>\n <ControlledMenuList className=\"zIndexMax p-1\">\n {attachMenuActions.map((action) => (\n <ControlledMenuItem key={action.key} className=\"flex items-center gap-x-2\" onClick={action.onClick}>\n <MaterialSymbol icon={action.icon} size={18} />\n {action.label}\n </ControlledMenuItem>\n ))}\n </ControlledMenuList>\n </ControlledMenuRoot>\n )}\n\n {actionsSlot}\n </div>\n )}\n\n {attachmentsController && (\n <>\n <input ref={fileInputRef} type=\"file\" multiple hidden onChange={handleFilesPicked} />\n <input\n ref={imageInputRef}\n type=\"file\"\n multiple\n hidden\n accept={IMAGE_INPUT_ACCEPT}\n onChange={handleFilesPicked}\n />\n </>\n )}\n </div>\n );\n\n if (!attachmentsController || attachments?.hideDropzone) return editorShell;\n\n return (\n <FileDropzone\n stopPropagation\n disabled={disabled}\n className=\"flex-none\"\n overlayClassName={DROPZONE_OVERLAY_CLASS}\n onFilesDropped={(files) => attachmentsController.addFiles(files)}\n >\n {editorShell}\n </FileDropzone>\n );\n});\n\nexport default RichTextEditor;\n"],"names":["RichTextEditor"],"mappings":"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"}
@@ -1,8 +1,22 @@
1
1
  import { IStagedAttachment } from '../../../hooks/useFileAttachments';
2
+ import { SymbolCodepoints } from '../../UI/MaterialSymbol';
3
+ /**
4
+ * An action offered on a staged IMAGE before it is sent (annotate it, run an AI adjustment). The
5
+ * library does not know what any of them do: it draws the button and hands the staged file back.
6
+ */
7
+ export interface IStagedImageAction {
8
+ key: string;
9
+ icon: SymbolCodepoints;
10
+ label: string;
11
+ onClick: (attachment: IStagedAttachment) => void;
12
+ hidden?: (attachment: IStagedAttachment) => boolean;
13
+ }
2
14
  interface IAttachmentPreviewListProps {
3
15
  stagedAttachments: Array<IStagedAttachment>;
4
16
  onRemove: (stagingId: string) => void;
5
17
  onRetry: (stagingId: string) => void;
18
+ /** Offered only on an image whose upload finished: editing one mid-flight would race the upload. */
19
+ imageActions?: Array<IStagedImageAction>;
6
20
  }
7
21
  declare const AttachmentPreviewList: React.FC<IAttachmentPreviewListProps>;
8
22
  export default AttachmentPreviewList;
@@ -6,7 +6,13 @@ import ControlledImage from "../ControlledImage/index.js";
6
6
  import ControlledTooltip from "../../UI/ControlledTooltip.js";
7
7
  import { MaterialSymbol } from "../../UI/MaterialSymbol/index.js";
8
8
  import { iconForMimeType, formatFileSize } from "../../../helpers/fileAttachments.js";
9
- const AttachmentPreviewList = ({ stagedAttachments, onRemove, onRetry }) => {
9
+ const isEditableImage = (attachment) => attachment.status === "done" && Boolean(attachment.previewUrl) && attachment.file.type.startsWith("image/");
10
+ const AttachmentPreviewList = ({
11
+ stagedAttachments,
12
+ onRemove,
13
+ onRetry,
14
+ imageActions
15
+ }) => {
10
16
  const { t } = useTranslation("ui");
11
17
  if (!stagedAttachments.length) return null;
12
18
  return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: stagedAttachments.map((attachment) => /* @__PURE__ */ jsxs(
@@ -42,6 +48,15 @@ const AttachmentPreviewList = ({ stagedAttachments, onRemove, onRetry }) => {
42
48
  }
43
49
  ) })
44
50
  ] }),
51
+ isEditableImage(attachment) && (imageActions?.length ?? 0) > 0 && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-x-1 border-t border-border-subtle px-2 py-1", children: (imageActions ?? []).filter((action) => !action.hidden?.(attachment)).map((action) => /* @__PURE__ */ jsx(ControlledTooltip, { content: action.label, className: "z-[9999]", children: /* @__PURE__ */ jsx(
52
+ ControlledButton,
53
+ {
54
+ defaultHtmlButton: true,
55
+ onClick: () => action.onClick(attachment),
56
+ className: "flex size-6 shrink-0 items-center justify-center rounded-full text-foreground-faint transition-colors hover:bg-muted-hover hover:text-brand-emphasis",
57
+ children: /* @__PURE__ */ jsx(MaterialSymbol, { icon: action.icon, size: 14 })
58
+ }
59
+ ) }, action.key)) }),
45
60
  attachment.status === "uploading" && /* @__PURE__ */ jsx(
46
61
  "div",
47
62
  {
@@ -1 +1 @@
1
- {"version":3,"file":"AttachmentPreviewList.js","sources":["../../../../src/components/Form/attachments/AttachmentPreviewList.tsx"],"sourcesContent":["import classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledImage from '@/components/Form/ControlledImage';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport { formatFileSize, iconForMimeType } from '@/helpers/fileAttachments';\nimport { IStagedAttachment } from '@/hooks/useFileAttachments';\n\ninterface IAttachmentPreviewListProps {\n stagedAttachments: Array<IStagedAttachment>;\n onRemove: (stagingId: string) => void;\n onRetry: (stagingId: string) => void;\n}\n\nconst AttachmentPreviewList: React.FC<IAttachmentPreviewListProps> = ({ stagedAttachments, onRemove, onRetry }) => {\n const { t } = useTranslation('ui');\n\n if (!stagedAttachments.length) return null;\n\n return (\n <div className=\"flex flex-wrap gap-2\">\n {stagedAttachments.map((attachment) => (\n <div\n key={attachment.stagingId}\n className={classNames(\n 'relative flex w-44 animate-pop-in flex-col overflow-hidden rounded-xl border bg-surface',\n attachment.status === 'error' ? 'border-danger-border' : 'border-border',\n )}\n >\n <div className=\"flex items-center gap-x-2 p-2\">\n {attachment.previewUrl && attachment.file.type.startsWith('image/') ? (\n <ControlledImage src={attachment.previewUrl} className=\"size-10 shrink-0 rounded-lg object-cover\" />\n ) : (\n <span className=\"flex size-10 shrink-0 items-center justify-center rounded-lg bg-brand-soft text-brand-emphasis\">\n <MaterialSymbol icon={iconForMimeType(attachment.file.type)} size={20} />\n </span>\n )}\n\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"truncate text-xs font-medium text-foreground\">{attachment.file.name}</span>\n <span className=\"text-xxs text-foreground-subtle\">\n {attachment.status === 'error'\n ? t('followUp.attachments.uploadFailed')\n : formatFileSize(attachment.file.size)}\n </span>\n </span>\n\n {attachment.status === 'error' && (\n <ControlledTooltip content={t('actions.tryAgain')} className=\"z-[9999]\">\n <ControlledButton\n defaultHtmlButton\n onClick={() => onRetry(attachment.stagingId)}\n className=\"flex size-6 shrink-0 items-center justify-center rounded-full text-danger-emphasis hover:bg-danger-soft\"\n >\n <MaterialSymbol icon=\"refresh\" size={14} />\n </ControlledButton>\n </ControlledTooltip>\n )}\n\n <ControlledTooltip content={t('actions.remove')} className=\"z-[9999]\">\n <ControlledButton\n defaultHtmlButton\n onClick={() => onRemove(attachment.stagingId)}\n className=\"flex size-6 shrink-0 items-center justify-center rounded-full text-foreground-faint hover:bg-muted-hover hover:text-foreground-muted\"\n >\n <MaterialSymbol icon=\"close\" size={14} />\n </ControlledButton>\n </ControlledTooltip>\n </div>\n\n {attachment.status === 'uploading' && (\n <div\n className=\"h-1 w-full bg-muted-hover\"\n role=\"progressbar\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={attachment.progress}\n aria-label={t('followUp.attachments.uploading')}\n >\n <div\n className=\"h-1 rounded-r-full bg-brand transition-interactive duration-base\"\n style={{ width: `${attachment.progress}%` }}\n />\n </div>\n )}\n </div>\n ))}\n </div>\n );\n};\n\nexport default AttachmentPreviewList;\n"],"names":[],"mappings":";;;;;;;;AAgBA,MAAM,wBAA+D,CAAC,EAAE,mBAAmB,UAAU,cAAc;AACjH,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AAEjC,MAAI,CAAC,kBAAkB,OAAQ,QAAO;AAEtC,6BACG,OAAA,EAAI,WAAU,wBACZ,UAAA,kBAAkB,IAAI,CAAC,eACtB;AAAA,IAAC;AAAA,IAAA;AAAA,MAEC,WAAW;AAAA,QACT;AAAA,QACA,WAAW,WAAW,UAAU,yBAAyB;AAAA,MAAA;AAAA,MAG3D,UAAA;AAAA,QAAA,qBAAC,OAAA,EAAI,WAAU,iCACZ,UAAA;AAAA,UAAA,WAAW,cAAc,WAAW,KAAK,KAAK,WAAW,QAAQ,IAChE,oBAAC,iBAAA,EAAgB,KAAK,WAAW,YAAY,WAAU,4CAA2C,IAElG,oBAAC,QAAA,EAAK,WAAU,kGACd,UAAA,oBAAC,gBAAA,EAAe,MAAM,gBAAgB,WAAW,KAAK,IAAI,GAAG,MAAM,IAAI,GACzE;AAAA,UAGF,qBAAC,QAAA,EAAK,WAAU,gCACd,UAAA;AAAA,YAAA,oBAAC,QAAA,EAAK,WAAU,gDAAgD,UAAA,WAAW,KAAK,MAAK;AAAA,YACrF,oBAAC,QAAA,EAAK,WAAU,mCACb,qBAAW,WAAW,UACnB,EAAE,mCAAmC,IACrC,eAAe,WAAW,KAAK,IAAI,EAAA,CACzC;AAAA,UAAA,GACF;AAAA,UAEC,WAAW,WAAW,WACrB,oBAAC,mBAAA,EAAkB,SAAS,EAAE,kBAAkB,GAAG,WAAU,YAC3D,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,mBAAiB;AAAA,cACjB,SAAS,MAAM,QAAQ,WAAW,SAAS;AAAA,cAC3C,WAAU;AAAA,cAEV,UAAA,oBAAC,gBAAA,EAAe,MAAK,WAAU,MAAM,GAAA,CAAI;AAAA,YAAA;AAAA,UAAA,GAE7C;AAAA,8BAGD,mBAAA,EAAkB,SAAS,EAAE,gBAAgB,GAAG,WAAU,YACzD,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,mBAAiB;AAAA,cACjB,SAAS,MAAM,SAAS,WAAW,SAAS;AAAA,cAC5C,WAAU;AAAA,cAEV,UAAA,oBAAC,gBAAA,EAAe,MAAK,SAAQ,MAAM,GAAA,CAAI;AAAA,YAAA;AAAA,UAAA,EACzC,CACF;AAAA,QAAA,GACF;AAAA,QAEC,WAAW,WAAW,eACrB;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,WAAU;AAAA,YACV,MAAK;AAAA,YACL,iBAAe;AAAA,YACf,iBAAe;AAAA,YACf,iBAAe,WAAW;AAAA,YAC1B,cAAY,EAAE,gCAAgC;AAAA,YAE9C,UAAA;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,WAAU;AAAA,gBACV,OAAO,EAAE,OAAO,GAAG,WAAW,QAAQ,IAAA;AAAA,cAAI;AAAA,YAAA;AAAA,UAC5C;AAAA,QAAA;AAAA,MACF;AAAA,IAAA;AAAA,IA5DG,WAAW;AAAA,EAAA,CA+DnB,GACH;AAEJ;"}
1
+ {"version":3,"file":"AttachmentPreviewList.js","sources":["../../../../src/components/Form/attachments/AttachmentPreviewList.tsx"],"sourcesContent":["import classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledImage from '@/components/Form/ControlledImage';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport { formatFileSize, iconForMimeType } from '@/helpers/fileAttachments';\nimport { IStagedAttachment } from '@/hooks/useFileAttachments';\n\nimport { SymbolCodepoints } from '../../UI/MaterialSymbol';\n\n/**\n * An action offered on a staged IMAGE before it is sent (annotate it, run an AI adjustment). The\n * library does not know what any of them do: it draws the button and hands the staged file back.\n */\nexport interface IStagedImageAction {\n key: string;\n icon: SymbolCodepoints;\n label: string;\n onClick: (attachment: IStagedAttachment) => void;\n hidden?: (attachment: IStagedAttachment) => boolean;\n}\n\ninterface IAttachmentPreviewListProps {\n stagedAttachments: Array<IStagedAttachment>;\n onRemove: (stagingId: string) => void;\n onRetry: (stagingId: string) => void;\n /** Offered only on an image whose upload finished: editing one mid-flight would race the upload. */\n imageActions?: Array<IStagedImageAction>;\n}\n\nconst isEditableImage = (attachment: IStagedAttachment): boolean =>\n attachment.status === 'done' && Boolean(attachment.previewUrl) && attachment.file.type.startsWith('image/');\n\nconst AttachmentPreviewList: React.FC<IAttachmentPreviewListProps> = ({\n stagedAttachments,\n onRemove,\n onRetry,\n imageActions,\n}) => {\n const { t } = useTranslation('ui');\n\n if (!stagedAttachments.length) return null;\n\n return (\n <div className=\"flex flex-wrap gap-2\">\n {stagedAttachments.map((attachment) => (\n <div\n key={attachment.stagingId}\n className={classNames(\n 'relative flex w-44 animate-pop-in flex-col overflow-hidden rounded-xl border bg-surface',\n attachment.status === 'error' ? 'border-danger-border' : 'border-border',\n )}\n >\n <div className=\"flex items-center gap-x-2 p-2\">\n {attachment.previewUrl && attachment.file.type.startsWith('image/') ? (\n <ControlledImage src={attachment.previewUrl} className=\"size-10 shrink-0 rounded-lg object-cover\" />\n ) : (\n <span className=\"flex size-10 shrink-0 items-center justify-center rounded-lg bg-brand-soft text-brand-emphasis\">\n <MaterialSymbol icon={iconForMimeType(attachment.file.type)} size={20} />\n </span>\n )}\n\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"truncate text-xs font-medium text-foreground\">{attachment.file.name}</span>\n <span className=\"text-xxs text-foreground-subtle\">\n {attachment.status === 'error'\n ? t('followUp.attachments.uploadFailed')\n : formatFileSize(attachment.file.size)}\n </span>\n </span>\n\n {attachment.status === 'error' && (\n <ControlledTooltip content={t('actions.tryAgain')} className=\"z-[9999]\">\n <ControlledButton\n defaultHtmlButton\n onClick={() => onRetry(attachment.stagingId)}\n className=\"flex size-6 shrink-0 items-center justify-center rounded-full text-danger-emphasis hover:bg-danger-soft\"\n >\n <MaterialSymbol icon=\"refresh\" size={14} />\n </ControlledButton>\n </ControlledTooltip>\n )}\n\n <ControlledTooltip content={t('actions.remove')} className=\"z-[9999]\">\n <ControlledButton\n defaultHtmlButton\n onClick={() => onRemove(attachment.stagingId)}\n className=\"flex size-6 shrink-0 items-center justify-center rounded-full text-foreground-faint hover:bg-muted-hover hover:text-foreground-muted\"\n >\n <MaterialSymbol icon=\"close\" size={14} />\n </ControlledButton>\n </ControlledTooltip>\n </div>\n\n {isEditableImage(attachment) && (imageActions?.length ?? 0) > 0 && (\n <div className=\"flex items-center gap-x-1 border-t border-border-subtle px-2 py-1\">\n {(imageActions ?? [])\n .filter((action) => !action.hidden?.(attachment))\n .map((action) => (\n <ControlledTooltip key={action.key} content={action.label} className=\"z-[9999]\">\n <ControlledButton\n defaultHtmlButton\n onClick={() => action.onClick(attachment)}\n className=\"flex size-6 shrink-0 items-center justify-center rounded-full text-foreground-faint transition-colors hover:bg-muted-hover hover:text-brand-emphasis\"\n >\n <MaterialSymbol icon={action.icon} size={14} />\n </ControlledButton>\n </ControlledTooltip>\n ))}\n </div>\n )}\n\n {attachment.status === 'uploading' && (\n <div\n className=\"h-1 w-full bg-muted-hover\"\n role=\"progressbar\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={attachment.progress}\n aria-label={t('followUp.attachments.uploading')}\n >\n <div\n className=\"h-1 rounded-r-full bg-brand transition-interactive duration-base\"\n style={{ width: `${attachment.progress}%` }}\n />\n </div>\n )}\n </div>\n ))}\n </div>\n );\n};\n\nexport default AttachmentPreviewList;\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umodeapporg/ui",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "sideEffects": [