@exsto/ds 0.0.1-rc.6 → 0.0.1-rc.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/README.md +33 -0
  2. package/dist/chunks/toggle-C2c5oDaE.js +629 -0
  3. package/dist/chunks/toggle-C2c5oDaE.js.map +1 -0
  4. package/dist/ds/src/rich-text-editor.d.ts +8 -0
  5. package/dist/index.js +3 -470
  6. package/dist/index.js.map +1 -1
  7. package/dist/rich-text-editor.js +904 -0
  8. package/dist/rich-text-editor.js.map +1 -0
  9. package/dist/styles.css +159 -0
  10. package/dist/ui/src/components/edyn/rich-text-content/allow-list.d.ts +11 -0
  11. package/dist/ui/src/components/edyn/rich-text-content/index.d.ts +2 -0
  12. package/dist/ui/src/components/edyn/rich-text-content/rich-text-content.d.ts +22 -0
  13. package/dist/ui/src/components/edyn/rich-text-content/rich-text-content.test.d.ts +8 -0
  14. package/dist/ui/src/components/edyn/rich-text-editor/extensions.d.ts +18 -0
  15. package/dist/ui/src/components/edyn/rich-text-editor/index.d.ts +3 -0
  16. package/dist/ui/src/components/edyn/rich-text-editor/mention-list.d.ts +17 -0
  17. package/dist/ui/src/components/edyn/rich-text-editor/rich-text-editor.d.ts +53 -0
  18. package/dist/ui/src/components/edyn/rich-text-editor/rich-text-editor.test.d.ts +6 -0
  19. package/dist/ui/src/components/edyn/rich-text-editor/serialize.d.ts +13 -0
  20. package/dist/ui/src/components/edyn/rich-text-editor/sync.d.ts +19 -0
  21. package/dist/ui/src/components/edyn/rich-text-editor/sync.test.d.ts +11 -0
  22. package/dist/ui/src/components/edyn/rich-text-editor/toolbar.d.ts +20 -0
  23. package/dist/ui/src/components/edyn/rich-text-editor/types.d.ts +68 -0
  24. package/dist/ui/src/components/ui/popover/popover.d.ts +1 -1
  25. package/dist/ui/src/index.d.ts +1 -0
  26. package/package.json +21 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rich-text-editor.js","names":[],"sources":["../../ui/src/components/edyn/rich-text-editor/serialize.ts","../../ui/src/components/edyn/rich-text-editor/extensions.ts","../../ui/src/components/edyn/rich-text-editor/mention-list.tsx","../../ui/src/components/edyn/rich-text-editor/sync.ts","../../ui/src/components/edyn/rich-text-editor/toolbar.tsx","../../ui/src/components/edyn/rich-text-editor/types.ts","../../ui/src/components/edyn/rich-text-editor/rich-text-editor.tsx"],"sourcesContent":["import { type DOMOutputSpec, DOMSerializer, type Node as TProseNode } from '@tiptap/pm/model';\nimport type { IRichTextMention } from './types';\n\nexport type TMentionToText = NonNullable<IRichTextMention['toText']>;\n\n// A serializer can only return elements, so a mention leaves as one and is unwrapped to its text below.\nconst MENTION_MARK = 'data-rich-text-mention';\n\nexport const defaultMentionToText: TMentionToText = (item) => `@${item.id}`;\n\n/**\n * The document as HTML. Not `editor.getHTML()`: that would write a mention as its chip element, and\n * the HTML must carry the plain text the app chose (`@person@example.test`) and nothing else.\n */\nexport const serializeDocument = (doc: TProseNode, toText: TMentionToText): string => {\n const schema = doc.type.schema;\n const nodes = {\n ...DOMSerializer.nodesFromSchema(schema),\n mention: (node: TProseNode): DOMOutputSpec => [\n 'span',\n { [MENTION_MARK]: '' },\n toText({ id: String(node.attrs.id ?? ''), label: String(node.attrs.label ?? '') }),\n ],\n };\n const serializer = new DOMSerializer(nodes, DOMSerializer.marksFromSchema(schema));\n const container = document.createElement('div');\n container.appendChild(serializer.serializeFragment(doc.content));\n for (const element of Array.from(container.querySelectorAll(`[${MENTION_MARK}]`))) {\n element.replaceWith(document.createTextNode(element.textContent ?? ''));\n }\n return container.innerHTML;\n};\n\n/** No text and no mention. A document of blank paragraphs, spaces and line breaks is empty too. */\nexport const isDocumentEmpty = (doc: TProseNode): boolean => {\n if (doc.textContent.trim() !== '') {\n return false;\n }\n let hasMention = false;\n doc.descendants((node) => {\n if (node.type.name === 'mention') {\n hasMention = true;\n }\n return !hasMention;\n });\n return !hasMention;\n};\n\n/** Characters the HTML text will have: a mention counts as the text it is written as. */\nexport const countCharacters = (doc: TProseNode, toText: TMentionToText): number => {\n let total = 0;\n doc.descendants((node) => {\n if (node.isText) {\n total += node.text?.length ?? 0;\n } else if (node.type.name === 'mention') {\n total += toText({ id: String(node.attrs.id ?? ''), label: String(node.attrs.label ?? '') }).length;\n } else if (node.type.name === 'hardBreak') {\n total += 1;\n }\n return true;\n });\n return total;\n};\n","import { isAllowedRichTextHref } from '@edyn/ui/components/edyn/rich-text-content/allow-list';\nimport { Extension, mergeAttributes } from '@tiptap/core';\nimport { Bold } from '@tiptap/extension-bold';\nimport { Document } from '@tiptap/extension-document';\nimport { HardBreak } from '@tiptap/extension-hard-break';\nimport { HorizontalRule } from '@tiptap/extension-horizontal-rule';\nimport { Italic } from '@tiptap/extension-italic';\nimport { Link } from '@tiptap/extension-link';\nimport { BulletList, ListItem, OrderedList } from '@tiptap/extension-list';\nimport { Mention } from '@tiptap/extension-mention';\nimport { Paragraph } from '@tiptap/extension-paragraph';\nimport { Text } from '@tiptap/extension-text';\nimport { Underline } from '@tiptap/extension-underline';\nimport { UndoRedo } from '@tiptap/extensions';\nimport { Plugin, PluginKey } from '@tiptap/pm/state';\nimport type { SuggestionKeyDownProps, SuggestionProps } from '@tiptap/suggestion';\nimport { countCharacters, type TMentionToText } from './serialize';\nimport type { IRichTextMentionItem } from './types';\n\n/** What the editor needs from the component while it runs. Held in a ref, so the extensions are built once. */\nexport interface IEditorHost {\n mentionEnabled: () => boolean;\n maxLength: () => number | undefined;\n toText: () => TMentionToText;\n onSuggestion: (props: SuggestionProps<IRichTextMentionItem, IRichTextMentionItem> | null) => void;\n onSuggestionKey: (props: SuggestionKeyDownProps) => boolean;\n}\n\nconst MENTION_CHIP_CLASS = 'rounded-sm bg-primary/10 px-1 py-0.5 font-medium text-primary';\n\nconst limitKey = new PluginKey('richTextMaxLength');\n\n/** Refuses an edit that would take the text over the limit (a paste included) and lets one that shortens it through. */\nconst maxLengthExtension = (host: IEditorHost) =>\n Extension.create({\n name: 'maxLength',\n addProseMirrorPlugins() {\n return [\n new Plugin({\n key: limitKey,\n filterTransaction: (tr, state) => {\n const limit = host.maxLength();\n if (!tr.docChanged || limit === undefined) {\n return true;\n }\n const next = countCharacters(tr.doc, host.toText());\n return next <= limit || next <= countCharacters(state.doc, host.toText());\n },\n }),\n ];\n },\n });\n\n/**\n * The editor's whole schema: paragraph, line break, bold, italic, underline, bullet and numbered lists, link, and the\n * mention chip. Whatever else arrives (a paste, `value`) is not in the schema, so ProseMirror drops the markup and keeps\n * the text.\n */\nexport const buildExtensions = (host: IEditorHost) => [\n Document,\n Paragraph,\n Text,\n HardBreak,\n Bold,\n Italic,\n Underline,\n BulletList,\n // A list is numbered from 1; the `start` it would otherwise keep is an attribute outside the allow-list.\n OrderedList.extend({\n addAttributes() {\n return {};\n },\n }),\n ListItem,\n HorizontalRule,\n Link.extend({\n addAttributes() {\n return {\n href: {\n default: null,\n parseHTML: (element) => element.getAttribute('href'),\n renderHTML: (attributes) => (attributes.href ? { href: attributes.href } : {}),\n },\n };\n },\n }).configure({\n openOnClick: false,\n autolink: true,\n linkOnPaste: true,\n defaultProtocol: 'https',\n protocols: ['mailto'],\n isAllowedUri: (url) => isAllowedRichTextHref(url),\n HTMLAttributes: { rel: null, target: null, class: null },\n }),\n UndoRedo,\n maxLengthExtension(host),\n Mention.extend({\n addAttributes() {\n return {\n id: {\n default: null,\n parseHTML: (element) => element.getAttribute('data-id'),\n renderHTML: (attributes) => (attributes.id ? { 'data-id': attributes.id } : {}),\n },\n label: {\n default: null,\n parseHTML: (element) => element.getAttribute('data-label'),\n renderHTML: (attributes) => (attributes.label ? { 'data-label': attributes.label } : {}),\n },\n };\n },\n }).configure({\n HTMLAttributes: { class: MENTION_CHIP_CLASS },\n renderText: ({ node }) => `@${node.attrs.label ?? node.attrs.id}`,\n renderHTML: ({ options, node }) => ['span', mergeAttributes(options.HTMLAttributes), `@${node.attrs.label ?? node.attrs.id}`],\n suggestion: {\n char: '@',\n allow: () => host.mentionEnabled(),\n items: () => [],\n command: ({ editor, range, props }) => {\n editor\n .chain()\n .focus()\n .insertContentAt(range, [\n { type: 'mention', attrs: { id: props.id, label: props.label } },\n { type: 'text', text: ' ' },\n ])\n .run();\n },\n render: () => ({\n onStart: (props) => host.onSuggestion(props),\n onUpdate: (props) => host.onSuggestion(props),\n onKeyDown: (props) => host.onSuggestionKey(props),\n onExit: () => host.onSuggestion(null),\n }),\n },\n }),\n];\n","import { Popover, PopoverContent } from '@edyn/ui/components/ui/popover';\nimport { ScrollArea } from '@edyn/ui/components/ui/scroll-area';\nimport { popupItemClass } from '@edyn/ui/lib/popup-list';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { IconLoader2 } from '@tabler/icons-react';\nimport { type RefObject, useCallback, useEffect, useRef, useState } from 'react';\nimport type { IRichTextLabels, IRichTextMention, IRichTextMentionItem, IRichTextMentionPage } from './types';\n\ninterface IListState {\n status: 'loading' | 'ready' | 'error';\n items: IRichTextMentionItem[];\n page: number;\n hasMore: boolean;\n more: boolean;\n active: number;\n}\n\nconst FIRST: IListState = { status: 'loading', items: [], page: 1, hasMore: false, more: false, active: 0 };\n\nconst toPage = (result: IRichTextMentionItem[] | IRichTextMentionPage): IRichTextMentionPage =>\n Array.isArray(result) ? { items: result, hasMore: false } : result;\n\nexport interface IMentionListProps {\n query: string;\n /** Where the @ was typed. */\n rect: (() => DOMRect | null) | null;\n config: IRichTextMention;\n labels: IRichTextLabels;\n listId: string;\n onPick: (item: IRichTextMentionItem) => void;\n /** The editor keeps focus, so the list is driven by its keys: the editor calls this one. */\n keyHandler: RefObject<((event: KeyboardEvent) => boolean) | null>;\n /** The id of the option the editor should name as `aria-activedescendant`. */\n onActiveChange: (optionId: string | undefined) => void;\n}\n\nconst optionId = (listId: string, index: number): string => `${listId}-${index}`;\n\n/** The list that opens under the caret when the writer types @. */\nexport const MentionList = ({ query, rect, config, labels, listId, onPick, keyHandler, onActiveChange }: IMentionListProps) => {\n const [state, setState] = useState<IListState>(FIRST);\n const refSearch = useRef(config.search);\n refSearch.current = config.search;\n const refFirst = useRef(true);\n const refController = useRef<AbortController | null>(null);\n const refViewport = useRef<HTMLDivElement | null>(null);\n const debounce = config.debounce ?? 250;\n\n useEffect(() => {\n const controller = new AbortController();\n refController.current = controller;\n setState((previous) => ({ ...previous, status: 'loading', page: 1, more: false }));\n const timer = setTimeout(\n async () => {\n try {\n const page = toPage(await refSearch.current(query, { page: 1, signal: controller.signal }));\n if (controller.signal.aborted) {\n return;\n }\n setState({ status: 'ready', items: page.items, page: 1, hasMore: page.hasMore === true, more: false, active: 0 });\n } catch {\n if (!controller.signal.aborted) {\n setState((previous) => ({ ...previous, status: 'error', items: [], hasMore: false }));\n }\n }\n },\n refFirst.current ? 0 : debounce,\n );\n refFirst.current = false;\n return () => {\n clearTimeout(timer);\n controller.abort();\n };\n }, [query, debounce]);\n\n const loadMore = () => {\n const controller = refController.current;\n if (!controller || state.status !== 'ready' || !state.hasMore || state.more) {\n return;\n }\n const next = state.page + 1;\n setState((previous) => ({ ...previous, more: true }));\n refSearch\n .current(query, { page: next, signal: controller.signal })\n .then((result) => {\n if (controller.signal.aborted) {\n return;\n }\n const page = toPage(result);\n setState((previous) => ({\n ...previous,\n items: [...previous.items, ...page.items.filter((item) => !previous.items.some((known) => known.id === item.id))],\n page: next,\n hasMore: page.hasMore === true,\n more: false,\n }));\n })\n .catch(() => {\n if (!controller.signal.aborted) {\n setState((previous) => ({ ...previous, more: false, hasMore: false }));\n }\n });\n };\n const refLoadMore = useRef(loadMore);\n refLoadMore.current = loadMore;\n\n const activeItem = state.items[state.active];\n\n useEffect(() => {\n onActiveChange(activeItem ? optionId(listId, state.active) : undefined);\n }, [activeItem, listId, onActiveChange, state.active]);\n\n useEffect(() => {\n document.getElementById(optionId(listId, state.active))?.scrollIntoView({ block: 'nearest' });\n }, [listId, state.active]);\n\n const watchSentinel = useCallback((node: HTMLDivElement | null) => {\n if (!node) {\n return;\n }\n const observer = new IntersectionObserver(\n (entries) => {\n if (entries.some((entry) => entry.isIntersecting)) {\n refLoadMore.current();\n }\n },\n { root: refViewport.current },\n );\n observer.observe(node);\n return () => observer.disconnect();\n }, []);\n\n useEffect(() => {\n keyHandler.current = (event) => {\n const count = state.items.length;\n if (event.key === 'Escape') {\n event.stopPropagation();\n return false;\n }\n if (count === 0) {\n return false;\n }\n if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n const step = event.key === 'ArrowDown' ? 1 : -1;\n const next = (state.active + step + count) % count;\n if (step === 1 && state.active === count - 1) {\n refLoadMore.current();\n }\n setState((previous) => ({ ...previous, active: next }));\n return true;\n }\n if ((event.key === 'Enter' || event.key === 'Tab') && activeItem) {\n onPick(activeItem);\n return true;\n }\n return false;\n };\n return () => {\n keyHandler.current = null;\n };\n }, [activeItem, keyHandler, onPick, state.active, state.items.length]);\n\n const anchor = { getBoundingClientRect: () => rect?.() ?? new DOMRect(0, 0, 0, 0) };\n const announcement =\n state.status === 'loading'\n ? labels.mentionSearching\n : state.status === 'error'\n ? labels.mentionError\n : state.items.length === 0\n ? labels.mentionEmpty\n : labels.mentionResults.replace('{count}', String(state.items.length));\n\n return (\n <Popover open onOpenChange={() => undefined}>\n <PopoverContent\n anchor={anchor}\n align=\"start\"\n side=\"bottom\"\n sideOffset={6}\n initialFocus={false}\n finalFocus={false}\n data-slot=\"rich-text-mention-popup\"\n className=\"w-72 gap-0 p-1\"\n >\n <ScrollArea className=\"max-h-60\" viewportRef={refViewport}>\n <div\n role=\"listbox\"\n id={listId}\n aria-label={labels.mentionList}\n className={cn('flex flex-col gap-px', state.items.length === 0 && 'hidden')}\n >\n {state.items.map((item, index) => (\n // biome-ignore lint/a11y/useKeyWithClickEvents: the editor keeps focus and drives the list with its own keys\n // biome-ignore lint/a11y/useFocusableInteractive: options are named by aria-activedescendant on the editor, never focused\n <div\n key={item.id}\n id={optionId(listId, index)}\n role=\"option\"\n aria-selected={index === state.active}\n data-slot=\"rich-text-mention-option\"\n data-highlighted={index === state.active ? '' : undefined}\n className={cn(popupItemClass('base-ui'), 'h-auto min-h-8 flex-col items-start gap-0')}\n onMouseDown={(event) => event.preventDefault()}\n onMouseMove={() => setState((previous) => (previous.active === index ? previous : { ...previous, active: index }))}\n onClick={() => onPick(item)}\n >\n <span className=\"truncate\">{item.label}</span>\n {item.secondary ? <span className=\"truncate text-xs text-foreground/70\">{item.secondary}</span> : null}\n </div>\n ))}\n </div>\n {state.status === 'loading' && state.items.length === 0 ? (\n <div className=\"flex items-center gap-2 px-2 py-1.5 text-sm text-muted-foreground\">\n <IconLoader2 className=\"size-4 animate-spin motion-reduce:animate-none\" aria-hidden />\n {labels.mentionSearching}\n </div>\n ) : null}\n {state.status === 'ready' && state.items.length === 0 ? (\n <div className=\"px-2 py-1.5 text-sm text-muted-foreground\">{labels.mentionEmpty}</div>\n ) : null}\n {state.status === 'error' ? <div className=\"px-2 py-1.5 text-sm text-destructive\">{labels.mentionError}</div> : null}\n {state.hasMore ? (\n <div key={state.items.length} ref={watchSentinel} className=\"flex items-center gap-2 px-2 py-1.5 text-xs text-muted-foreground\">\n {state.more ? labels.mentionLoadingMore : null}\n </div>\n ) : null}\n </ScrollArea>\n <div role=\"status\" className=\"sr-only\">\n {announcement}\n </div>\n </PopoverContent>\n </Popover>\n );\n};\n","import type { Editor } from '@tiptap/core';\nimport { TextSelection } from '@tiptap/pm/state';\n\n/**\n * Whether commands may be called on the editor. TipTap builds an editor during the render, mounts its view later, and\n * destroys one that has not been mounted within a moment (useEditor then makes a new one), so an effect can be handed an\n * editor that is not there yet or no longer is. `isDestroyed` is true for both: it reads true until the view exists. A\n * destroyed editor has no command manager, so `editor.commands` throws on it.\n */\nexport const isLive = (editor: Editor): boolean => editor.isInitialized && !editor.isDestroyed;\n\n/**\n * Brings the editor to a value the parent passed in, if it is one the editor did not just report itself.\n * `known` is the last HTML the editor and the parent agree on (the starting value, then whatever the editor emitted or was\n * given). The caret stays where it was, kept inside the new content, so a value changed from outside while the writer is\n * typing does not throw the caret to the start.\n */\nexport const syncValue = (editor: Editor, value: string | undefined, known: { current: string | undefined }): void => {\n if (!isLive(editor) || value === undefined || value === known.current) {\n return;\n }\n known.current = value;\n const { from, to } = editor.state.selection;\n editor.commands.setContent(value, { emitUpdate: false });\n editor.commands.command(({ tr }) => {\n const size = tr.doc.content.size;\n tr.setSelection(TextSelection.between(tr.doc.resolve(Math.min(from, size)), tr.doc.resolve(Math.min(to, size))));\n return true;\n });\n};\n\n/** Sets whether the editor can be edited without telling the app something changed (TipTap emits an update by default). */\nexport const syncEditable = (editor: Editor, editable: boolean): void => {\n if (isLive(editor) && editor.isEditable !== editable) {\n editor.setEditable(editable, false);\n }\n};\n","import { Toolbar as ToolbarPrimitive } from '@base-ui/react/toolbar';\nimport { isAllowedRichTextHref } from '@edyn/ui/components/edyn/rich-text-content/allow-list';\nimport { Button } from '@edyn/ui/components/ui/button';\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@edyn/ui/components/ui/dropdown-menu';\nimport { Input } from '@edyn/ui/components/ui/input';\nimport { Popover, PopoverContent, PopoverTrigger } from '@edyn/ui/components/ui/popover';\nimport { toggleVariants } from '@edyn/ui/components/ui/toggle';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { IconBold, IconItalic, IconLink, IconList, IconListNumbers, IconSeparatorHorizontal, IconUnderline, IconVariable } from '@tabler/icons-react';\nimport type { Editor } from '@tiptap/core';\nimport { useEditorState } from '@tiptap/react';\nimport { type FormEvent, type ReactNode, type RefObject, useEffect, useState } from 'react';\nimport type { IRichTextField, IRichTextLabels } from './types';\n\nconst BUTTON_CLASS = cn(toggleVariants({ variant: 'default', size: 'sm' }), 'size-7 px-0');\n\n/** A typed address becomes a link: \"name@example.test\" a mailto, \"example.test\" an https address. */\nexport const normalizeHref = (raw: string): string => {\n const value = raw.trim();\n if (value === '' || /^[a-z][a-z0-9+.-]*:/i.test(value)) {\n return value;\n }\n return /^[^\\s@/]+@[^\\s@/]+\\.[^\\s@/]+$/.test(value) ? `mailto:${value}` : `https://${value}`;\n};\n\ninterface IToolbarButtonProps {\n label: string;\n pressed?: boolean;\n disabled?: boolean;\n onClick?: () => void;\n children: ReactNode;\n}\n\nconst ToolbarButton = ({ label, pressed, disabled, onClick, children }: IToolbarButtonProps) => (\n <ToolbarPrimitive.Button\n type=\"button\"\n aria-label={label}\n title={label}\n aria-pressed={pressed}\n data-pressed={pressed ? '' : undefined}\n disabled={disabled}\n className={BUTTON_CLASS}\n onMouseDown={(event) => event.preventDefault()}\n onClick={onClick}\n >\n {children}\n </ToolbarPrimitive.Button>\n);\n\n/** Where the writer's selection was when the link box opened: moving focus into the box must not lose it. */\nexport interface ILinkRange {\n from: number;\n to: number;\n}\n\nconst hrefAt = (editor: Editor, range: ILinkRange): string => {\n const { doc } = editor.state;\n const marks = range.from < range.to ? (doc.nodeAt(range.from)?.marks ?? []) : doc.resolve(range.from).marks();\n return String(marks.find((mark) => mark.type.name === 'link')?.attrs.href ?? '');\n};\n\ninterface ILinkControlProps {\n range: RefObject<ILinkRange | null>;\n editor: Editor;\n labels: IRichTextLabels;\n active: boolean;\n disabled: boolean;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n}\n\nconst LinkControl = ({ range, editor, labels, active, disabled, open, onOpenChange }: ILinkControlProps) => {\n const [address, setAddress] = useState('');\n const [invalid, setInvalid] = useState(false);\n\n useEffect(() => {\n if (open) {\n setAddress(range.current ? hrefAt(editor, range.current) : '');\n setInvalid(false);\n }\n }, [editor, open, range]);\n\n const apply = (event: FormEvent) => {\n event.preventDefault();\n event.stopPropagation();\n const href = normalizeHref(address);\n if (!isAllowedRichTextHref(href)) {\n setInvalid(true);\n return;\n }\n const saved = range.current ?? { from: editor.state.selection.from, to: editor.state.selection.to };\n const chain = editor.chain().focus().setTextSelection(saved);\n if (saved.from === saved.to && !hrefAt(editor, saved)) {\n chain.insertContent({ type: 'text', text: href, marks: [{ type: 'link', attrs: { href } }] }).run();\n } else {\n chain.extendMarkRange('link').setLink({ href }).run();\n }\n onOpenChange(false);\n };\n\n const remove = () => {\n const saved = range.current ?? { from: editor.state.selection.from, to: editor.state.selection.to };\n editor.chain().focus().setTextSelection(saved).extendMarkRange('link').unsetLink().run();\n onOpenChange(false);\n };\n\n return (\n <Popover open={open} onOpenChange={onOpenChange}>\n <PopoverTrigger\n render={\n <ToolbarPrimitive.Button\n type=\"button\"\n aria-label={labels.link}\n title={labels.link}\n aria-pressed={active}\n data-pressed={active ? '' : undefined}\n disabled={disabled}\n className={BUTTON_CLASS}\n />\n }\n >\n <IconLink aria-hidden />\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-80\">\n <form className=\"flex flex-col gap-2\" onSubmit={apply}>\n <Input\n type=\"text\"\n inputMode=\"url\"\n aria-label={labels.linkAddress}\n aria-invalid={invalid || undefined}\n value={address}\n onChange={(event) => {\n setAddress(event.target.value);\n setInvalid(false);\n }}\n />\n {invalid ? <p className=\"text-xs text-destructive\">{labels.linkInvalid}</p> : null}\n <div className=\"flex justify-end gap-2\">\n {active ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={remove}>\n {labels.linkRemove}\n </Button>\n ) : null}\n <Button type=\"submit\" size=\"sm\">\n {labels.linkApply}\n </Button>\n </div>\n </form>\n </PopoverContent>\n </Popover>\n );\n};\n\nexport interface IRichTextToolbarProps {\n linkRange: RefObject<ILinkRange | null>;\n editor: Editor;\n labels: IRichTextLabels;\n fields: IRichTextField[] | undefined;\n disabled: boolean;\n linkOpen: boolean;\n onLinkOpenChange: (open: boolean) => void;\n}\n\nexport const RichTextToolbar = ({ linkRange, editor, labels, fields, disabled, linkOpen, onLinkOpenChange }: IRichTextToolbarProps) => {\n const state = useEditorState({\n editor,\n selector: ({ editor: current }) => ({\n bold: current.isActive('bold'),\n italic: current.isActive('italic'),\n underline: current.isActive('underline'),\n bullets: current.isActive('bulletList'),\n numbers: current.isActive('orderedList'),\n link: current.isActive('link'),\n }),\n });\n\n return (\n <ToolbarPrimitive.Root\n data-slot=\"rich-text-toolbar\"\n aria-label={labels.toolbar}\n className=\"flex flex-wrap items-center gap-0.5 border-b border-input p-1\"\n >\n <ToolbarButton label={labels.bold} pressed={state.bold} disabled={disabled} onClick={() => editor.chain().focus().toggleBold().run()}>\n <IconBold aria-hidden />\n </ToolbarButton>\n <ToolbarButton label={labels.italic} pressed={state.italic} disabled={disabled} onClick={() => editor.chain().focus().toggleItalic().run()}>\n <IconItalic aria-hidden />\n </ToolbarButton>\n <ToolbarButton\n label={labels.underline}\n pressed={state.underline}\n disabled={disabled}\n onClick={() => editor.chain().focus().toggleUnderline().run()}\n >\n <IconUnderline aria-hidden />\n </ToolbarButton>\n <ToolbarPrimitive.Separator className=\"mx-1 h-4 w-px bg-border\" />\n <ToolbarButton\n label={labels.bulletList}\n pressed={state.bullets}\n disabled={disabled}\n onClick={() => editor.chain().focus().toggleBulletList().run()}\n >\n <IconList aria-hidden />\n </ToolbarButton>\n <ToolbarButton\n label={labels.numberedList}\n pressed={state.numbers}\n disabled={disabled}\n onClick={() => editor.chain().focus().toggleOrderedList().run()}\n >\n <IconListNumbers aria-hidden />\n </ToolbarButton>\n <LinkControl\n range={linkRange}\n editor={editor}\n labels={labels}\n active={state.link}\n disabled={disabled}\n open={linkOpen}\n onOpenChange={onLinkOpenChange}\n />\n <ToolbarButton label={labels.horizontalRule} disabled={disabled} onClick={() => editor.chain().focus().setHorizontalRule().run()}>\n <IconSeparatorHorizontal aria-hidden />\n </ToolbarButton>\n {fields && fields.length > 0 ? (\n <>\n <ToolbarPrimitive.Separator className=\"mx-1 h-4 w-px bg-border\" />\n <DropdownMenu>\n <DropdownMenuTrigger\n render={\n <ToolbarPrimitive.Button\n type=\"button\"\n aria-label={labels.insertField}\n title={labels.insertField}\n disabled={disabled}\n className={BUTTON_CLASS}\n />\n }\n >\n <IconVariable aria-hidden />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"w-56\">\n {fields.map((field) => (\n <DropdownMenuItem\n key={field.id}\n onClick={() =>\n editor\n .chain()\n .focus()\n .insertContent({ type: 'text', text: `{{${field.id}}}` })\n .run()\n }\n >\n {field.label}\n </DropdownMenuItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n </>\n ) : null}\n </ToolbarPrimitive.Root>\n );\n};\n","/** One person (or anything else) the app offers when the writer types @. */\nexport interface IRichTextMentionItem {\n /** What the HTML carries. By default the value is written as `@${id}`, so an email address works as an id. */\n id: string;\n /** What the chip shows while the text is being edited. */\n label: string;\n /** A second, muted line in the list (a role, an email address). */\n secondary?: string;\n}\n\n/** A page of results. Return a plain array instead when there is only ever one page. */\nexport interface IRichTextMentionPage {\n items: IRichTextMentionItem[];\n /** True when asking for `page + 1` would return more. The list loads it when it is scrolled to the end. */\n hasMore?: boolean;\n}\n\nexport interface IRichTextMentionSearchOptions {\n /** 1-based. */\n page: number;\n /** Aborted when the writer types on or closes the list: pass it to `fetch` so a stale search stops. */\n signal: AbortSignal;\n}\n\nexport type TRichTextMentionSearch = (\n query: string,\n options: IRichTextMentionSearchOptions,\n) => Promise<IRichTextMentionItem[] | IRichTextMentionPage>;\n\nexport interface IRichTextMention {\n /** Asked for the first page when the writer types @ and again for every change of the text after it, debounced. */\n search: TRichTextMentionSearch;\n /**\n * How a mention is written into the HTML. The chip exists only while editing; the HTML carries this text\n * and no element. Default: `@${item.id}`.\n */\n toText?: (item: Pick<IRichTextMentionItem, 'id' | 'label'>) => string;\n /** Milliseconds to wait after the last keystroke before searching. Default 250. */\n debounce?: number;\n}\n\n/** A field of a template (a mail merge token). Inserted as the plain text `{{id}}`. */\nexport interface IRichTextField {\n id: string;\n label: string;\n}\n\n/** What a ref to the editor can do, for a button outside it (a merge tag, say). */\nexport interface IRichTextEditorHandle {\n /** Writes plain text at the caret (over the selection), and puts focus back in the editor. */\n insertText: (text: string) => void;\n focus: () => void;\n}\n\n/** Every string the editor shows, so an app can translate them. */\nexport interface IRichTextLabels {\n toolbar: string;\n bold: string;\n italic: string;\n underline: string;\n bulletList: string;\n numberedList: string;\n link: string;\n horizontalRule: string;\n insertField: string;\n linkAddress: string;\n linkApply: string;\n linkRemove: string;\n linkInvalid: string;\n mentionList: string;\n mentionSearching: string;\n mentionEmpty: string;\n mentionError: string;\n mentionLoadingMore: string;\n /** Announced as the list changes. `{count}` is replaced. */\n mentionResults: string;\n}\n\nexport const DEFAULT_RICH_TEXT_LABELS: IRichTextLabels = {\n toolbar: 'Formatting',\n bold: 'Bold',\n italic: 'Italic',\n underline: 'Underline',\n bulletList: 'Bulleted list',\n numberedList: 'Numbered list',\n link: 'Link',\n horizontalRule: 'Horizontal line',\n insertField: 'Insert a field',\n linkAddress: 'Link address',\n linkApply: 'Apply',\n linkRemove: 'Remove link',\n linkInvalid: 'Enter an http, https or mailto address.',\n mentionList: 'Mentions',\n mentionSearching: 'Searching...',\n mentionEmpty: 'No matches',\n mentionError: 'The search failed',\n mentionLoadingMore: 'Loading more...',\n mentionResults: '{count} results',\n};\n","import { RICH_TEXT_BODY_CLASS } from '@edyn/ui/components/edyn/rich-text-content/rich-text-content';\nimport { ScrollArea } from '@edyn/ui/components/ui/scroll-area';\nimport { cn } from '@edyn/ui/lib/utils';\nimport type { Editor } from '@tiptap/core';\nimport { EditorContent, useEditor, useEditorState } from '@tiptap/react';\nimport type { SuggestionProps } from '@tiptap/suggestion';\nimport {\n type ComponentPropsWithoutRef,\n type Ref,\n useCallback,\n useEffect,\n useId,\n useImperativeHandle,\n useMemo,\n useReducer,\n useRef,\n useState,\n} from 'react';\nimport { buildExtensions, type IEditorHost } from './extensions';\nimport { MentionList } from './mention-list';\nimport { defaultMentionToText, isDocumentEmpty, serializeDocument } from './serialize';\nimport { isLive, syncEditable, syncValue } from './sync';\nimport { type ILinkRange, RichTextToolbar } from './toolbar';\nimport {\n DEFAULT_RICH_TEXT_LABELS,\n type IRichTextEditorHandle,\n type IRichTextField,\n type IRichTextLabels,\n type IRichTextMention,\n type IRichTextMentionItem,\n} from './types';\n\nconst LINE = 1.5;\n\ninterface ISuggestionState {\n query: string;\n rect: (() => DOMRect | null) | null;\n command: (item: IRichTextMentionItem) => void;\n}\n\nexport interface IRichTextEditorProps extends Omit<ComponentPropsWithoutRef<'div'>, 'children' | 'defaultValue' | 'onChange' | 'id'> {\n /** The HTML, when the parent owns it. Pass back what `onChange` gave. An empty editor is `\"\"`. */\n value?: string;\n /** The HTML to start from when the editor owns its own value. */\n defaultValue?: string;\n /** Called with the HTML after every edit. Only the elements in `RICH_TEXT_ELEMENTS` can appear, and `\"\"` when there is no text. */\n onChange?: (html: string) => void;\n placeholder?: string;\n disabled?: boolean;\n /** Shows the content and keeps it selectable and copyable, with no toolbar. */\n readOnly?: boolean;\n /** Draws the error border and sets `aria-invalid`. The message is the form's to draw: point `aria-describedby` at it. */\n invalid?: boolean;\n /**\n * Lines the box is tall when empty.\n * @default 6\n */\n minRows?: number;\n /**\n * Lines it grows to before it scrolls inside.\n * @default 20\n */\n maxRows?: number;\n /** The most characters of text (a mention counts as the text it is written as). An edit past it is refused. Off by default. */\n maxLength?: number;\n /** Turns @ into a search the app answers. Without it, @ is an ordinary character. */\n mention?: IRichTextMention;\n /** Adds an \"Insert a field\" menu that writes `{{id}}` at the caret. */\n fields?: IRichTextField[];\n /**\n * Whether the formatting toolbar shows.\n * @default true\n */\n toolbar?: boolean;\n /** Replaces any of the strings the editor shows (the defaults are English). */\n labels?: Partial<IRichTextLabels>;\n /** On the editable element, so a form can name it. */\n id?: string;\n autoFocus?: boolean;\n /** Called with the files when the writer pastes only files (a screenshot). Without it, such a paste does nothing. */\n onPasteFiles?: (files: File[]) => void;\n /** Gives `insertText` and `focus`, for buttons outside the editor. */\n ref?: Ref<IRichTextEditorHandle>;\n}\n\n/**\n * A small rich-text field: paragraphs, bold, italic, underline, bulleted and numbered lists and links, with optional\n * @mentions the app answers asynchronously. The value is HTML in and out, and only the elements in `RICH_TEXT_ELEMENTS`\n * can come out (no class, no style), whatever is pasted. A mention is a chip while editing and plain text in the HTML.\n * The server should still sanitise: this is a convenience for the writer, not a security boundary.\n */\nexport const RichTextEditor = ({\n value,\n defaultValue = '',\n onChange,\n placeholder,\n disabled = false,\n readOnly = false,\n invalid = false,\n minRows = 6,\n maxRows = 20,\n maxLength,\n mention,\n fields,\n toolbar = true,\n labels,\n id,\n autoFocus = false,\n onPasteFiles,\n ref,\n className,\n 'aria-invalid': ariaInvalid,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n 'aria-describedby': ariaDescribedBy,\n ...props\n}: IRichTextEditorProps) => {\n const isInvalid = invalid || ariaInvalid === true || ariaInvalid === 'true';\n const text = useMemo(() => ({ ...DEFAULT_RICH_TEXT_LABELS, ...labels }), [labels]);\n const listId = useId();\n const [suggestion, setSuggestion] = useState<ISuggestionState | null>(null);\n const [activeOption, setActiveOption] = useState<string | undefined>(undefined);\n const [linkOpen, setLinkOpen] = useState(false);\n const refLinkRange = useRef<ILinkRange | null>(null);\n const refKeys = useRef<((event: KeyboardEvent) => boolean) | null>(null);\n // What the editor and the parent agree on. It starts as the value the editor is built from, so a controlled value\n // that has not changed asks for nothing at mount.\n const refKnown = useRef<string | undefined>(value ?? defaultValue);\n const refPending = useRef<Array<(live: Editor) => void>>([]);\n const [, wake] = useReducer((count: number) => count + 1, 0);\n const editable = !disabled && !readOnly;\n const showToolbar = toolbar && !readOnly;\n\n const latest = useRef({ mention, maxLength, onChange, onPasteFiles, toolbar: showToolbar });\n latest.current = { mention, maxLength, onChange, onPasteFiles, toolbar: showToolbar };\n\n const host = useMemo<IEditorHost>(\n () => ({\n mentionEnabled: () => latest.current.mention !== undefined,\n maxLength: () => latest.current.maxLength,\n toText: () => latest.current.mention?.toText ?? defaultMentionToText,\n onSuggestion: (props: SuggestionProps<IRichTextMentionItem, IRichTextMentionItem> | null) => {\n if (props === null) {\n setActiveOption(undefined);\n }\n setSuggestion(props === null ? null : { query: props.query, rect: props.clientRect ?? null, command: props.command });\n },\n onSuggestionKey: ({ event }) => refKeys.current?.(event) ?? false,\n }),\n [],\n );\n const extensions = useMemo(() => buildExtensions(host), [host]);\n\n const emit = useCallback(\n (current: Editor): string => {\n const html = isDocumentEmpty(current.state.doc) ? '' : serializeDocument(current.state.doc, host.toText());\n refKnown.current = html;\n return html;\n },\n [host],\n );\n\n const attributes = useMemo(() => {\n const entries: Record<string, string | undefined> = {\n role: 'textbox',\n 'aria-multiline': 'true',\n 'aria-autocomplete': mention ? 'list' : undefined,\n 'aria-controls': suggestion ? listId : undefined,\n 'aria-activedescendant': suggestion ? activeOption : undefined,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n 'aria-describedby': ariaDescribedBy,\n 'aria-invalid': isInvalid ? 'true' : undefined,\n 'aria-readonly': readOnly ? 'true' : undefined,\n 'aria-disabled': disabled ? 'true' : undefined,\n 'aria-placeholder': placeholder,\n tabindex: readOnly && !disabled ? '0' : undefined,\n id,\n 'data-slot': 'rich-text-editor-content',\n class: cn(RICH_TEXT_BODY_CLASS, 'min-h-(--rich-text-min) px-2.5 py-2 outline-none'),\n };\n return Object.fromEntries(Object.entries(entries).filter((entry): entry is [string, string] => entry[1] !== undefined));\n }, [activeOption, ariaDescribedBy, ariaLabel, ariaLabelledBy, disabled, id, isInvalid, listId, mention, placeholder, readOnly, suggestion]);\n\n const editor = useEditor({\n extensions,\n content: value ?? defaultValue,\n editable,\n autofocus: autoFocus ? 'end' : false,\n editorProps: {\n attributes,\n handlePaste: (_view, event) => {\n const data = event.clipboardData;\n const onlyFiles = data !== null && data.files.length > 0 && data.getData('text/plain') === '' && data.getData('text/html') === '';\n if (!onlyFiles || !latest.current.onPasteFiles) {\n return false;\n }\n latest.current.onPasteFiles(Array.from(data.files));\n return true;\n },\n handleKeyDown: (view, event) => {\n if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === 'k' && latest.current.toolbar) {\n event.preventDefault();\n refLinkRange.current = { from: view.state.selection.from, to: view.state.selection.to };\n setLinkOpen(true);\n return true;\n }\n return false;\n },\n },\n onBlur: () => {\n setSuggestion(null);\n setActiveOption(undefined);\n },\n onUpdate: ({ editor: current }) => {\n latest.current.onChange?.(emit(current));\n },\n });\n\n // useEditor can replace the editor (it destroys one that was not mounted in time, and in development a second render\n // makes a second), so readiness is read off the editor in hand: wake up when this one finishes initialising.\n const ready = isLive(editor);\n useEffect(() => {\n editor.on('create', wake);\n wake();\n return () => {\n editor.off('create', wake);\n };\n }, [editor]);\n\n const whenLive = useCallback(\n (action: (live: Editor) => void) => {\n if (isLive(editor)) {\n action(editor);\n } else {\n refPending.current.push(action);\n }\n },\n [editor],\n );\n\n useImperativeHandle(\n ref,\n () => ({\n insertText: (text) => {\n whenLive((live) => {\n live.chain().focus().insertContent({ type: 'text', text }).run();\n });\n },\n focus: () => {\n whenLive((live) => {\n live.commands.focus();\n });\n },\n }),\n [whenLive],\n );\n\n useEffect(() => {\n if (!ready || !isLive(editor)) {\n return;\n }\n for (const action of refPending.current.splice(0)) {\n action(editor);\n }\n }, [editor, ready]);\n\n useEffect(() => {\n if (ready) {\n syncEditable(editor, editable);\n }\n }, [editable, editor, ready]);\n\n useEffect(() => {\n if (ready) {\n syncValue(editor, value, refKnown);\n }\n }, [editor, ready, value]);\n\n const empty = useEditorState({ editor, selector: ({ editor: current }) => isDocumentEmpty(current.state.doc) });\n\n const pick = useCallback(\n (item: IRichTextMentionItem) => {\n suggestion?.command(item);\n },\n [suggestion],\n );\n\n return (\n <div\n data-slot=\"rich-text-editor\"\n data-invalid={isInvalid ? '' : undefined}\n data-disabled={disabled ? '' : undefined}\n data-readonly={readOnly ? '' : undefined}\n className={cn(\n 'flex w-full min-w-0 flex-col rounded-lg border border-input bg-transparent text-base transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:bg-input/50 data-disabled:opacity-50 data-invalid:border-destructive data-invalid:ring-3 data-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:data-disabled:bg-input/80 dark:data-invalid:ring-destructive/40',\n className,\n )}\n {...props}\n >\n {showToolbar ? (\n <RichTextToolbar\n editor={editor}\n labels={text}\n fields={fields}\n disabled={disabled}\n linkOpen={linkOpen}\n linkRange={refLinkRange}\n onLinkOpenChange={(next) => {\n if (next && !linkOpen) {\n refLinkRange.current = { from: editor.state.selection.from, to: editor.state.selection.to };\n }\n setLinkOpen(next);\n }}\n />\n ) : null}\n <ScrollArea\n className=\"relative\"\n style={{ maxHeight: `${maxRows * LINE + 1}rem`, ['--rich-text-min' as string]: `${minRows * LINE + 1}rem` }}\n focusable={false}\n >\n {empty && placeholder ? (\n <p aria-hidden className=\"pointer-events-none absolute start-2.5 top-2 text-muted-foreground\">\n {placeholder}\n </p>\n ) : null}\n <EditorContent editor={editor} />\n </ScrollArea>\n {suggestion && mention ? (\n <MentionList\n query={suggestion.query}\n rect={suggestion.rect}\n config={mention}\n labels={text}\n listId={listId}\n onPick={pick}\n keyHandler={refKeys}\n onActiveChange={setActiveOption}\n />\n ) : null}\n </div>\n );\n};\n"],"mappings":"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"}
package/dist/styles.css CHANGED
@@ -523,6 +523,10 @@
523
523
  inset-inline-start: 50%;
524
524
  }
525
525
 
526
+ .start-2\.5 {
527
+ inset-inline-start: calc(var(--spacing) * 2.5);
528
+ }
529
+
526
530
  .-top-12 {
527
531
  top: calc(var(--spacing) * -12);
528
532
  }
@@ -733,6 +737,10 @@
733
737
  margin-inline: calc(var(--spacing) * -4);
734
738
  }
735
739
 
740
+ .mx-1 {
741
+ margin-inline: var(--spacing);
742
+ }
743
+
736
744
  .mx-2 {
737
745
  margin-inline: calc(var(--spacing) * 2);
738
746
  }
@@ -1106,6 +1114,10 @@
1106
1114
  max-height: inherit;
1107
1115
  }
1108
1116
 
1117
+ .min-h-\(--rich-text-min\) {
1118
+ min-height: var(--rich-text-min);
1119
+ }
1120
+
1109
1121
  .min-h-0 {
1110
1122
  min-height: 0;
1111
1123
  }
@@ -1198,6 +1210,10 @@
1198
1210
  width: calc(var(--spacing) * 24);
1199
1211
  }
1200
1212
 
1213
+ .w-56 {
1214
+ width: calc(var(--spacing) * 56);
1215
+ }
1216
+
1201
1217
  .w-64 {
1202
1218
  width: calc(var(--spacing) * 64);
1203
1219
  }
@@ -1206,6 +1222,10 @@
1206
1222
  width: calc(var(--spacing) * 72);
1207
1223
  }
1208
1224
 
1225
+ .w-80 {
1226
+ width: calc(var(--spacing) * 80);
1227
+ }
1228
+
1209
1229
  .w-auto {
1210
1230
  width: auto;
1211
1231
  }
@@ -1645,6 +1665,10 @@
1645
1665
  justify-content: center;
1646
1666
  }
1647
1667
 
1668
+ .justify-end {
1669
+ justify-content: flex-end;
1670
+ }
1671
+
1648
1672
  .justify-start {
1649
1673
  justify-content: flex-start;
1650
1674
  }
@@ -1685,6 +1709,10 @@
1685
1709
  gap: calc(var(--spacing) * 6);
1686
1710
  }
1687
1711
 
1712
+ .gap-px {
1713
+ gap: 1px;
1714
+ }
1715
+
1688
1716
  :where(.-space-x-2 > :not(:last-child)) {
1689
1717
  --tw-space-x-reverse: 0;
1690
1718
  margin-inline-start: calc(calc(var(--spacing) * -2) * var(--tw-space-x-reverse));
@@ -2049,6 +2077,16 @@
2049
2077
  background-color: var(--primary-foreground);
2050
2078
  }
2051
2079
 
2080
+ .bg-primary\/10 {
2081
+ background-color: var(--primary);
2082
+ }
2083
+
2084
+ @supports (color: color-mix(in lab, red, red)) {
2085
+ .bg-primary\/10 {
2086
+ background-color: color-mix(in oklab, var(--primary) 10%, transparent);
2087
+ }
2088
+ }
2089
+
2052
2090
  .bg-secondary {
2053
2091
  background-color: var(--secondary);
2054
2092
  }
@@ -2172,6 +2210,10 @@
2172
2210
  padding-inline: var(--cell-size);
2173
2211
  }
2174
2212
 
2213
+ .px-0 {
2214
+ padding-inline: 0;
2215
+ }
2216
+
2175
2217
  .px-1 {
2176
2218
  padding-inline: var(--spacing);
2177
2219
  }
@@ -2361,6 +2403,11 @@
2361
2403
  font-size: .625rem;
2362
2404
  }
2363
2405
 
2406
+ .leading-6 {
2407
+ --tw-leading: calc(var(--spacing) * 6);
2408
+ line-height: calc(var(--spacing) * 6);
2409
+ }
2410
+
2364
2411
  .leading-none {
2365
2412
  --tw-leading: 1;
2366
2413
  line-height: 1;
@@ -2513,11 +2560,19 @@
2513
2560
  color: var(--warning);
2514
2561
  }
2515
2562
 
2563
+ .italic {
2564
+ font-style: italic;
2565
+ }
2566
+
2516
2567
  .tabular-nums {
2517
2568
  --tw-numeric-spacing: tabular-nums;
2518
2569
  font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, );
2519
2570
  }
2520
2571
 
2572
+ .underline {
2573
+ text-decoration-line: underline;
2574
+ }
2575
+
2521
2576
  .underline-offset-4 {
2522
2577
  text-underline-offset: 4px;
2523
2578
  }
@@ -3734,11 +3789,20 @@
3734
3789
  padding-block: 0;
3735
3790
  }
3736
3791
 
3792
+ .focus-within\:border-ring:focus-within {
3793
+ border-color: var(--ring);
3794
+ }
3795
+
3737
3796
  .focus-within\:ring-1:focus-within {
3738
3797
  --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
3739
3798
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
3740
3799
  }
3741
3800
 
3801
+ .focus-within\:ring-3:focus-within {
3802
+ --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
3803
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
3804
+ }
3805
+
3742
3806
  .focus-within\:ring-ring\/50:focus-within {
3743
3807
  --tw-ring-color: var(--ring);
3744
3808
  }
@@ -4516,6 +4580,21 @@
4516
4580
  border-color: var(--destructive);
4517
4581
  }
4518
4582
 
4583
+ .data-invalid\:ring-3[data-invalid] {
4584
+ --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
4585
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
4586
+ }
4587
+
4588
+ .data-invalid\:ring-destructive\/20[data-invalid] {
4589
+ --tw-ring-color: var(--destructive);
4590
+ }
4591
+
4592
+ @supports (color: color-mix(in lab, red, red)) {
4593
+ .data-invalid\:ring-destructive\/20[data-invalid] {
4594
+ --tw-ring-color: color-mix(in oklab, var(--destructive) 20%, transparent);
4595
+ }
4596
+ }
4597
+
4519
4598
  .data-limited\:opacity-0[data-limited] {
4520
4599
  opacity: 0;
4521
4600
  }
@@ -5819,6 +5898,16 @@
5819
5898
  }
5820
5899
  }
5821
5900
 
5901
+ .dark\:data-invalid\:ring-destructive\/40:is(.dark *)[data-invalid] {
5902
+ --tw-ring-color: var(--destructive);
5903
+ }
5904
+
5905
+ @supports (color: color-mix(in lab, red, red)) {
5906
+ .dark\:data-invalid\:ring-destructive\/40:is(.dark *)[data-invalid] {
5907
+ --tw-ring-color: color-mix(in oklab, var(--destructive) 40%, transparent);
5908
+ }
5909
+ }
5910
+
5822
5911
  .dark\:data-\[active\=true\]\:aria-invalid\:ring-destructive\/40:is(.dark *)[data-active="true"][aria-invalid="true"] {
5823
5912
  --tw-ring-color: var(--destructive);
5824
5913
  }
@@ -6074,10 +6163,30 @@
6074
6163
  cursor: not-allowed;
6075
6164
  }
6076
6165
 
6166
+ .data-disabled\:bg-input\/50:where([data-disabled="true"]), .data-disabled\:bg-input\/50:where([data-disabled]:not([data-disabled="false"])) {
6167
+ background-color: var(--input);
6168
+ }
6169
+
6170
+ @supports (color: color-mix(in lab, red, red)) {
6171
+ .data-disabled\:bg-input\/50:where([data-disabled="true"]), .data-disabled\:bg-input\/50:where([data-disabled]:not([data-disabled="false"])) {
6172
+ background-color: color-mix(in oklab, var(--input) 50%, transparent);
6173
+ }
6174
+ }
6175
+
6077
6176
  .data-disabled\:opacity-50:where([data-disabled="true"]), .data-disabled\:opacity-50:where([data-disabled]:not([data-disabled="false"])) {
6078
6177
  opacity: .5;
6079
6178
  }
6080
6179
 
6180
+ .dark\:data-disabled\:bg-input\/80:is(.dark *):where([data-disabled="true"]), .dark\:data-disabled\:bg-input\/80:is(.dark *):where([data-disabled]:not([data-disabled="false"])) {
6181
+ background-color: var(--input);
6182
+ }
6183
+
6184
+ @supports (color: color-mix(in lab, red, red)) {
6185
+ .dark\:data-disabled\:bg-input\/80:is(.dark *):where([data-disabled="true"]), .dark\:data-disabled\:bg-input\/80:is(.dark *):where([data-disabled]:not([data-disabled="false"])) {
6186
+ background-color: color-mix(in oklab, var(--input) 80%, transparent);
6187
+ }
6188
+ }
6189
+
6081
6190
  .data-active\:bg-background:where([data-state="active"]), .data-active\:bg-background:where([data-active]:not([data-active="false"])) {
6082
6191
  background-color: var(--background);
6083
6192
  }
@@ -6261,10 +6370,18 @@
6261
6370
  }
6262
6371
  }
6263
6372
 
6373
+ .\[\&_a\]\:text-primary a {
6374
+ color: var(--primary);
6375
+ }
6376
+
6264
6377
  .\[\&_a\]\:underline a {
6265
6378
  text-decoration-line: underline;
6266
6379
  }
6267
6380
 
6381
+ .\[\&_a\]\:underline-offset-2 a {
6382
+ text-underline-offset: 2px;
6383
+ }
6384
+
6268
6385
  .\[\&_a\]\:underline-offset-3 a {
6269
6386
  text-underline-offset: 3px;
6270
6387
  }
@@ -6275,6 +6392,24 @@
6275
6392
  }
6276
6393
  }
6277
6394
 
6395
+ .\[\&_hr\]\:my-3 hr {
6396
+ margin-block: calc(var(--spacing) * 3);
6397
+ }
6398
+
6399
+ .\[\&_hr\]\:border-0 hr {
6400
+ border-style: var(--tw-border-style);
6401
+ border-width: 0;
6402
+ }
6403
+
6404
+ .\[\&_hr\]\:border-t hr {
6405
+ border-top-style: var(--tw-border-style);
6406
+ border-top-width: 1px;
6407
+ }
6408
+
6409
+ .\[\&_hr\]\:border-border hr {
6410
+ border-color: var(--border);
6411
+ }
6412
+
6278
6413
  .\[\&_img\]\:size-full img {
6279
6414
  width: 100%;
6280
6415
  height: 100%;
@@ -6284,6 +6419,18 @@
6284
6419
  object-fit: cover;
6285
6420
  }
6286
6421
 
6422
+ .\[\&_ol\]\:list-decimal ol {
6423
+ list-style-type: decimal;
6424
+ }
6425
+
6426
+ .\[\&_ol\]\:ps-5 ol {
6427
+ padding-inline-start: calc(var(--spacing) * 5);
6428
+ }
6429
+
6430
+ .\[\&_ol\+p\]\:mt-2 ol + p, .\[\&_p\+ol\]\:mt-2 p + ol, .\[\&_p\+p\]\:mt-2 p + p, .\[\&_p\+ul\]\:mt-2 p + ul {
6431
+ margin-top: calc(var(--spacing) * 2);
6432
+ }
6433
+
6287
6434
  .\[\&_p\:not\(\:last-child\)\]\:mb-4 p:not(:last-child) {
6288
6435
  margin-bottom: calc(var(--spacing) * 4);
6289
6436
  }
@@ -6345,6 +6492,18 @@
6345
6492
  border-width: 0;
6346
6493
  }
6347
6494
 
6495
+ .\[\&_ul\]\:list-disc ul {
6496
+ list-style-type: disc;
6497
+ }
6498
+
6499
+ .\[\&_ul\]\:ps-5 ul {
6500
+ padding-inline-start: calc(var(--spacing) * 5);
6501
+ }
6502
+
6503
+ .\[\&_ul\+p\]\:mt-2 ul + p {
6504
+ margin-top: calc(var(--spacing) * 2);
6505
+ }
6506
+
6348
6507
  .\[\&\+\[data-slot\=item-content\]\]\:flex-none + [data-slot="item-content"] {
6349
6508
  flex: none;
6350
6509
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The HTML the rich-text editor emits, and the only HTML RichTextContent renders. An app that
3
+ * sanitises on a server takes its allow-list from here: nothing outside these elements and this
4
+ * attribute ever comes out of the editor, whatever was pasted into it.
5
+ */
6
+ export declare const RICH_TEXT_ELEMENTS: readonly ["p", "br", "hr", "strong", "em", "u", "ul", "ol", "li", "a"];
7
+ /** The attributes each element may carry. Every other element carries none (no class, no style). */
8
+ export declare const RICH_TEXT_ATTRIBUTES: Readonly<Record<string, readonly string[]>>;
9
+ /** A link must be absolute and use one of these. `javascript:`, `data:` and relative links are refused. */
10
+ export declare const RICH_TEXT_LINK_PROTOCOLS: readonly ["http", "https", "mailto"];
11
+ export declare const isAllowedRichTextHref: (href: string) => boolean;
@@ -0,0 +1,2 @@
1
+ export * from './allow-list';
2
+ export * from './rich-text-content';
@@ -0,0 +1,22 @@
1
+ import { HTMLAttributes, ReactNode } from 'react';
2
+ /**
3
+ * How rich text looks, shared by the editor and by RichTextContent so a comment reads the same while it is typed and
4
+ * after it is saved. Descendant selectors rather than classes on the elements, because the HTML that is stored carries
5
+ * no classes.
6
+ */
7
+ export declare const RICH_TEXT_BODY_CLASS = "text-sm leading-6 wrap-break-word [&_a]:text-primary [&_a]:underline [&_a]:underline-offset-2 [&_hr]:my-3 [&_hr]:border-0 [&_hr]:border-t [&_hr]:border-border [&_ol]:list-decimal [&_ol]:ps-5 [&_p+p]:mt-2 [&_ul]:list-disc [&_ul]:ps-5 [&_ul+p]:mt-2 [&_ol+p]:mt-2 [&_p+ul]:mt-2 [&_p+ol]:mt-2";
8
+ export declare const RICH_TEXT_MENTION_CLASS = "rounded-sm bg-primary/10 px-1 py-0.5 font-medium text-primary";
9
+ /** The HTML as React elements, keeping only what the editor can produce. Browser only (it needs a DOM parser). */
10
+ export declare const richTextToNodes: (html: string) => ReactNode[];
11
+ export interface IRichTextContentProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'dangerouslySetInnerHTML'> {
12
+ /** The HTML to show. Only the editor's elements are rendered, whatever else it holds. */
13
+ html: string;
14
+ }
15
+ /**
16
+ * Shows rich text the editor produced (or any HTML), as the same elements the editor emits and nothing else:
17
+ * `p`, `br`, `hr`, `strong`, `em`, `u`, `ul`, `ol`, `li` and `a` with an `href` (http, https or mailto; it opens in a new tab
18
+ * with `rel="noopener noreferrer"`). Everything else is dropped and its text kept. Built as React elements, not injected
19
+ * as markup, so it is safe on HTML that has not been cleaned. A run like `@name@example.test` or `@assignees` is shown
20
+ * as a chip.
21
+ */
22
+ export declare const RichTextContent: ({ html, className, ...props }: IRichTextContentProps) => import('react').JSX.Element;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * RichTextContent shows saved HTML as the editor's own elements and nothing else. It is built as React elements,
3
+ * so these assertions read the markup it renders: a script or a javascript: link that survived would show here.
4
+ *
5
+ * PATH TAKEN: happy-dom parses the input (DOMParser) and renderToStaticMarkup prints the result, which is enough
6
+ * for what this component decides (what stays, what goes, what is added). How it looks is the browser spec's.
7
+ */
8
+ export {};
@@ -0,0 +1,18 @@
1
+ import { Extension } from '@tiptap/core';
2
+ import { SuggestionKeyDownProps, SuggestionProps } from '@tiptap/suggestion';
3
+ import { TMentionToText } from './serialize';
4
+ import { IRichTextMentionItem } from './types';
5
+ /** What the editor needs from the component while it runs. Held in a ref, so the extensions are built once. */
6
+ export interface IEditorHost {
7
+ mentionEnabled: () => boolean;
8
+ maxLength: () => number | undefined;
9
+ toText: () => TMentionToText;
10
+ onSuggestion: (props: SuggestionProps<IRichTextMentionItem, IRichTextMentionItem> | null) => void;
11
+ onSuggestionKey: (props: SuggestionKeyDownProps) => boolean;
12
+ }
13
+ /**
14
+ * The editor's whole schema: paragraph, line break, bold, italic, underline, bullet and numbered lists, link, and the
15
+ * mention chip. Whatever else arrives (a paste, `value`) is not in the schema, so ProseMirror drops the markup and keeps
16
+ * the text.
17
+ */
18
+ export declare const buildExtensions: (host: IEditorHost) => (Extension<any, any> | import('@tiptap/core').Node<any, any> | import('@tiptap/core').Node<import('@tiptap/extension-paragraph').ParagraphOptions, any> | import('@tiptap/core').Node<import('@tiptap/extension-hard-break').HardBreakOptions, any> | import('@tiptap/core').Mark<import('@tiptap/extension-bold').BoldOptions, any> | import('@tiptap/core').Node<import('@tiptap/extension-list').BulletListOptions, any> | import('@tiptap/core').Node<import('@tiptap/extension-list').ListItemOptions, any> | import('@tiptap/core').Node<import('@tiptap/extension-horizontal-rule').HorizontalRuleOptions, any> | import('@tiptap/core').Mark<import('@tiptap/extension-link').LinkOptions, any> | Extension<import('@tiptap/extensions').UndoRedoOptions, any> | import('@tiptap/core').Node<import('@tiptap/extension-mention').MentionOptions<any, import('@tiptap/extension-mention').MentionNodeAttrs>, any>)[];
@@ -0,0 +1,3 @@
1
+ export { RICH_TEXT_ATTRIBUTES, RICH_TEXT_ELEMENTS, RICH_TEXT_LINK_PROTOCOLS } from '../rich-text-content/allow-list';
2
+ export * from './rich-text-editor';
3
+ export * from './types';
@@ -0,0 +1,17 @@
1
+ import { RefObject } from 'react';
2
+ import { IRichTextLabels, IRichTextMention, IRichTextMentionItem } from './types';
3
+ export interface IMentionListProps {
4
+ query: string;
5
+ /** Where the @ was typed. */
6
+ rect: (() => DOMRect | null) | null;
7
+ config: IRichTextMention;
8
+ labels: IRichTextLabels;
9
+ listId: string;
10
+ onPick: (item: IRichTextMentionItem) => void;
11
+ /** The editor keeps focus, so the list is driven by its keys: the editor calls this one. */
12
+ keyHandler: RefObject<((event: KeyboardEvent) => boolean) | null>;
13
+ /** The id of the option the editor should name as `aria-activedescendant`. */
14
+ onActiveChange: (optionId: string | undefined) => void;
15
+ }
16
+ /** The list that opens under the caret when the writer types @. */
17
+ export declare const MentionList: ({ query, rect, config, labels, listId, onPick, keyHandler, onActiveChange }: IMentionListProps) => import('react').JSX.Element;
@@ -0,0 +1,53 @@
1
+ import { ComponentPropsWithoutRef, Ref } from 'react';
2
+ import { IRichTextEditorHandle, IRichTextField, IRichTextLabels, IRichTextMention } from './types';
3
+ export interface IRichTextEditorProps extends Omit<ComponentPropsWithoutRef<'div'>, 'children' | 'defaultValue' | 'onChange' | 'id'> {
4
+ /** The HTML, when the parent owns it. Pass back what `onChange` gave. An empty editor is `""`. */
5
+ value?: string;
6
+ /** The HTML to start from when the editor owns its own value. */
7
+ defaultValue?: string;
8
+ /** Called with the HTML after every edit. Only the elements in `RICH_TEXT_ELEMENTS` can appear, and `""` when there is no text. */
9
+ onChange?: (html: string) => void;
10
+ placeholder?: string;
11
+ disabled?: boolean;
12
+ /** Shows the content and keeps it selectable and copyable, with no toolbar. */
13
+ readOnly?: boolean;
14
+ /** Draws the error border and sets `aria-invalid`. The message is the form's to draw: point `aria-describedby` at it. */
15
+ invalid?: boolean;
16
+ /**
17
+ * Lines the box is tall when empty.
18
+ * @default 6
19
+ */
20
+ minRows?: number;
21
+ /**
22
+ * Lines it grows to before it scrolls inside.
23
+ * @default 20
24
+ */
25
+ maxRows?: number;
26
+ /** The most characters of text (a mention counts as the text it is written as). An edit past it is refused. Off by default. */
27
+ maxLength?: number;
28
+ /** Turns @ into a search the app answers. Without it, @ is an ordinary character. */
29
+ mention?: IRichTextMention;
30
+ /** Adds an "Insert a field" menu that writes `{{id}}` at the caret. */
31
+ fields?: IRichTextField[];
32
+ /**
33
+ * Whether the formatting toolbar shows.
34
+ * @default true
35
+ */
36
+ toolbar?: boolean;
37
+ /** Replaces any of the strings the editor shows (the defaults are English). */
38
+ labels?: Partial<IRichTextLabels>;
39
+ /** On the editable element, so a form can name it. */
40
+ id?: string;
41
+ autoFocus?: boolean;
42
+ /** Called with the files when the writer pastes only files (a screenshot). Without it, such a paste does nothing. */
43
+ onPasteFiles?: (files: File[]) => void;
44
+ /** Gives `insertText` and `focus`, for buttons outside the editor. */
45
+ ref?: Ref<IRichTextEditorHandle>;
46
+ }
47
+ /**
48
+ * A small rich-text field: paragraphs, bold, italic, underline, bulleted and numbered lists and links, with optional
49
+ * @mentions the app answers asynchronously. The value is HTML in and out, and only the elements in `RICH_TEXT_ELEMENTS`
50
+ * can come out (no class, no style), whatever is pasted. A mention is a chip while editing and plain text in the HTML.
51
+ * The server should still sanitise: this is a convenience for the writer, not a security boundary.
52
+ */
53
+ export declare const RichTextEditor: ({ value, defaultValue, onChange, placeholder, disabled, readOnly, invalid, minRows, maxRows, maxLength, mention, fields, toolbar, labels, id, autoFocus, onPasteFiles, ref, className, "aria-invalid": ariaInvalid, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, ...props }: IRichTextEditorProps) => import('react').JSX.Element;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The editor's schema decides what HTML can come out, whatever goes in. These tests drive the real extension set
3
+ * through a headless ProseMirror editor and read what it serialises, so the allow-list the docs promise is the one
4
+ * that is checked. Typing, focus and the mention list need a real browser: e2e/rich-text-editor.spec.ts.
5
+ */
6
+ export {};
@@ -0,0 +1,13 @@
1
+ import { Node as TProseNode } from '@tiptap/pm/model';
2
+ import { IRichTextMention } from './types';
3
+ export type TMentionToText = NonNullable<IRichTextMention['toText']>;
4
+ export declare const defaultMentionToText: TMentionToText;
5
+ /**
6
+ * The document as HTML. Not `editor.getHTML()`: that would write a mention as its chip element, and
7
+ * the HTML must carry the plain text the app chose (`@person@example.test`) and nothing else.
8
+ */
9
+ export declare const serializeDocument: (doc: TProseNode, toText: TMentionToText) => string;
10
+ /** No text and no mention. A document of blank paragraphs, spaces and line breaks is empty too. */
11
+ export declare const isDocumentEmpty: (doc: TProseNode) => boolean;
12
+ /** Characters the HTML text will have: a mention counts as the text it is written as. */
13
+ export declare const countCharacters: (doc: TProseNode, toText: TMentionToText) => number;
@@ -0,0 +1,19 @@
1
+ import { Editor } from '@tiptap/core';
2
+ /**
3
+ * Whether commands may be called on the editor. TipTap builds an editor during the render, mounts its view later, and
4
+ * destroys one that has not been mounted within a moment (useEditor then makes a new one), so an effect can be handed an
5
+ * editor that is not there yet or no longer is. `isDestroyed` is true for both: it reads true until the view exists. A
6
+ * destroyed editor has no command manager, so `editor.commands` throws on it.
7
+ */
8
+ export declare const isLive: (editor: Editor) => boolean;
9
+ /**
10
+ * Brings the editor to a value the parent passed in, if it is one the editor did not just report itself.
11
+ * `known` is the last HTML the editor and the parent agree on (the starting value, then whatever the editor emitted or was
12
+ * given). The caret stays where it was, kept inside the new content, so a value changed from outside while the writer is
13
+ * typing does not throw the caret to the start.
14
+ */
15
+ export declare const syncValue: (editor: Editor, value: string | undefined, known: {
16
+ current: string | undefined;
17
+ }) => void;
18
+ /** Sets whether the editor can be edited without telling the app something changed (TipTap emits an update by default). */
19
+ export declare const syncEditable: (editor: Editor, editable: boolean) => void;