@illinois-grad/grad-vue-rte 6.0.13 → 6.0.14

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.
@@ -335,7 +335,7 @@ var G = ["aria-pressed", "tabindex"], K = ["aria-pressed", "tabindex"], q = ["ar
335
335
  }
336
336
  });
337
337
  function L(e) {
338
- e && g("send", e);
338
+ e && (g("send", e), q(!1));
339
339
  }
340
340
  function R() {
341
341
  L(F.value?.getJSON());
@@ -562,4 +562,4 @@ var ue = { class: "g-rich-text-content-wrap" }, de = {
562
562
  //#endregion
563
563
  export { se as i, $ as n, Q as r, pe as t };
564
564
 
565
- //# sourceMappingURL=grad-vue-rte-Clrr14QX.js.map
565
+ //# sourceMappingURL=grad-vue-rte-BwR74lve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grad-vue-rte-BwR74lve.js","names":[],"sources":["../../../node_modules/@tiptap/extension-heading/dist/index.js","../src/composables/useRichTextEditor.ts","../src/composables/useToolbarNavigation.ts","../src/components/editor/GRichTextToolbar.vue","../src/components/editor/GRichTextToolbar.vue","../src/components/GChatInput.vue","../src/components/GChatInput.vue","../src/components/GNoteInput.vue","../src/components/GNoteInput.vue","../src/composables/useRichTextRenderer.ts","../src/components/GRichTextContent.vue","../src/components/GRichTextContent.vue"],"sourcesContent":["import { Node, mergeAttributes, textblockTypeInputRule } from \"@tiptap/core\";\n//#region src/heading.ts\n/**\n* This extension allows you to create headings.\n* @see https://www.tiptap.dev/api/nodes/heading\n*/\nconst Heading = Node.create({\n\tname: \"heading\",\n\taddOptions() {\n\t\treturn {\n\t\t\tlevels: [\n\t\t\t\t1,\n\t\t\t\t2,\n\t\t\t\t3,\n\t\t\t\t4,\n\t\t\t\t5,\n\t\t\t\t6\n\t\t\t],\n\t\t\tHTMLAttributes: {}\n\t\t};\n\t},\n\tcontent: \"inline*\",\n\tgroup: \"block\",\n\tdefining: true,\n\taddAttributes() {\n\t\treturn { level: {\n\t\t\tdefault: 1,\n\t\t\trendered: false\n\t\t} };\n\t},\n\tparseHTML() {\n\t\treturn this.options.levels.map((level) => ({\n\t\t\ttag: `h${level}`,\n\t\t\tattrs: { level }\n\t\t}));\n\t},\n\trenderHTML({ node, HTMLAttributes }) {\n\t\treturn [\n\t\t\t`h${this.options.levels.includes(node.attrs.level) ? node.attrs.level : this.options.levels[0]}`,\n\t\t\tmergeAttributes(this.options.HTMLAttributes, HTMLAttributes),\n\t\t\t0\n\t\t];\n\t},\n\tparseMarkdown: (token, helpers) => {\n\t\treturn helpers.createNode(\"heading\", { level: token.depth || 1 }, helpers.parseInline(token.tokens || []));\n\t},\n\trenderMarkdown: (node, h) => {\n\t\tvar _node$attrs;\n\t\tconst level = ((_node$attrs = node.attrs) === null || _node$attrs === void 0 ? void 0 : _node$attrs.level) ? parseInt(node.attrs.level, 10) : 1;\n\t\tconst headingChars = \"#\".repeat(level);\n\t\tif (!node.content) return \"\";\n\t\treturn `${headingChars} ${h.renderChildren(node.content)}`;\n\t},\n\taddCommands() {\n\t\treturn {\n\t\t\tsetHeading: (attributes) => ({ commands }) => {\n\t\t\t\tif (!this.options.levels.includes(attributes.level)) return false;\n\t\t\t\treturn commands.setNode(this.name, attributes);\n\t\t\t},\n\t\t\ttoggleHeading: (attributes) => ({ commands }) => {\n\t\t\t\tif (!this.options.levels.includes(attributes.level)) return false;\n\t\t\t\treturn commands.toggleNode(this.name, \"paragraph\", attributes);\n\t\t\t}\n\t\t};\n\t},\n\taddKeyboardShortcuts() {\n\t\treturn this.options.levels.reduce((items, level) => ({\n\t\t\t...items,\n\t\t\t[`Mod-Alt-${level}`]: () => this.editor.commands.toggleHeading({ level })\n\t\t}), {});\n\t},\n\taddInputRules() {\n\t\treturn this.options.levels.map((level) => {\n\t\t\treturn textblockTypeInputRule({\n\t\t\t\tfind: new RegExp(`^(#{${Math.min(...this.options.levels)},${level}})\\\\s$`),\n\t\t\t\ttype: this.type,\n\t\t\t\tgetAttributes: { level }\n\t\t\t});\n\t\t});\n\t}\n});\n//#endregion\n//#region src/index.ts\nvar src_default = Heading;\n//#endregion\nexport { Heading, src_default as default };\n\n//# sourceMappingURL=index.js.map","import { toRaw, watch, type Ref } from \"vue\";\nimport { useEditor } from \"@tiptap/vue-3\";\nimport Document from \"@tiptap/extension-document\";\nimport Paragraph from \"@tiptap/extension-paragraph\";\nimport Text from \"@tiptap/extension-text\";\nimport Bold from \"@tiptap/extension-bold\";\nimport Heading, { type Level } from \"@tiptap/extension-heading\";\nimport Italic from \"@tiptap/extension-italic\";\nimport { ListKit } from \"@tiptap/extension-list\";\nimport { UndoRedo, Placeholder } from \"@tiptap/extensions\";\n\ninterface UseRichTextEditorOptions {\n content: Ref<object | \"\" | undefined>;\n placeholder: Ref<string> | string;\n label: Ref<string> | string;\n onUpdate?: (editor: any) => void;\n editorProps?: Record<string, any>;\n multiline?: boolean;\n headingLevels?: Level[];\n}\n\nexport function useRichTextEditor(options: UseRichTextEditorOptions) {\n const {\n content,\n placeholder,\n label,\n onUpdate,\n editorProps = {},\n multiline = false,\n headingLevels,\n } = options;\n\n const placeholderValue = typeof placeholder === 'string' ? placeholder : placeholder.value;\n const labelValue = typeof label === 'string' ? label : label.value;\n\n const editor = useEditor({\n content: content.value || \"\",\n extensions: [\n Document,\n Paragraph,\n Text,\n Bold,\n Italic,\n ListKit,\n ...(headingLevels?.length\n ? [Heading.configure({ levels: headingLevels })]\n : []),\n UndoRedo,\n Placeholder.configure({ placeholder: placeholderValue }),\n ],\n editorProps: {\n ...editorProps,\n attributes: {\n \"aria-label\": labelValue,\n \"role\": \"textbox\",\n \"aria-multiline\": multiline ? \"true\" : undefined,\n ...editorProps.attributes,\n },\n },\n onUpdate({ editor }) {\n content.value = editor.getJSON();\n if (onUpdate) {\n onUpdate(editor);\n }\n },\n });\n\n // Watch for label changes and update editor\n watch(\n () => typeof label === 'string' ? label : label.value,\n (val) => {\n if (editor.value) {\n editor.value?.setOptions({\n editorProps: {\n attributes: {\n \"aria-label\": val,\n },\n },\n });\n }\n },\n );\n\n // Watch for content changes and update editor\n watch(\n () => content.value,\n (val) => {\n if (\n editor.value &&\n JSON.stringify(val) !== JSON.stringify(editor.value.getJSON())\n ) {\n editor.value.commands.setContent(toRaw(val) || \"\");\n }\n },\n );\n\n function focusEditor() {\n editor.value?.commands?.focus();\n }\n\n return {\n editor,\n focusEditor,\n };\n}\n","import { ref, type Ref } from \"vue\";\nimport type { Editor } from \"@tiptap/vue-3\";\n\nexport function useToolbarNavigation(editor: Ref<Editor | undefined>, toolbarRef: Ref<HTMLElement | null>) {\n const activeButtonIndex = ref(0);\n\n function handleToolbarKeyDown(event: KeyboardEvent) {\n const toolbar = toolbarRef.value;\n if (!toolbar) return;\n\n const buttons = Array.from(\n toolbar.querySelectorAll(\"button\"),\n ) as HTMLButtonElement[];\n const currentIndex = buttons.findIndex(\n (btn) => btn === document.activeElement,\n );\n\n // Handle Escape key - return focus to editor\n if (event.key === \"Escape\") {\n event.preventDefault();\n editor.value?.commands?.focus();\n return;\n }\n\n // Don't handle Tab - let it exit the toolbar naturally\n if (event.key === \"Tab\") {\n return;\n }\n\n let nextIndex = currentIndex;\n\n switch (event.key) {\n case \"ArrowRight\":\n case \"ArrowDown\":\n event.preventDefault();\n nextIndex =\n currentIndex < buttons.length - 1 ? currentIndex + 1 : 0;\n break;\n case \"ArrowLeft\":\n case \"ArrowUp\":\n event.preventDefault();\n nextIndex =\n currentIndex > 0 ? currentIndex - 1 : buttons.length - 1;\n break;\n case \"Home\":\n event.preventDefault();\n nextIndex = 0;\n break;\n case \"End\":\n event.preventDefault();\n nextIndex = buttons.length - 1;\n break;\n default:\n return;\n }\n\n // Update active button index and focus\n activeButtonIndex.value = nextIndex;\n buttons[nextIndex]?.focus();\n }\n\n function getButtonTabIndex(index: number): number {\n return index === activeButtonIndex.value ? 0 : -1;\n }\n\n return {\n activeButtonIndex,\n handleToolbarKeyDown,\n getButtonTabIndex,\n };\n}\n","<script lang=\"ts\" setup>\nimport { ref, computed } from \"vue\";\nimport type { Editor } from \"@tiptap/vue-3\";\nimport type { Level } from \"@tiptap/extension-heading\";\nimport { useToolbarNavigation } from \"../../composables/useToolbarNavigation.ts\";\n\ninterface Props {\n editor: Editor | undefined;\n headingLevels?: Level[];\n}\n\nconst props = defineProps<Props>();\n\nconst toolbarRef = ref<HTMLElement | null>(null);\nconst editorRef = computed(() => props.editor);\nconst { handleToolbarKeyDown, getButtonTabIndex } = useToolbarNavigation(\n editorRef,\n toolbarRef,\n);\n</script>\n\n<template>\n <div\n v-if=\"editor\"\n class=\"g-rich-text-toolbar\"\n role=\"toolbar\"\n aria-label=\"Text formatting\"\n ref=\"toolbarRef\"\n @keydown=\"handleToolbarKeyDown\"\n tabindex=\"-1\"\n >\n <button\n @click=\"editor.chain().focus().toggleBold().run()\"\n :class=\"{\n bold: true,\n 'is-active': editor.isActive('bold'),\n }\"\n :aria-pressed=\"editor.isActive('bold')\"\n title=\"Bold\"\n aria-label=\"Bold\"\n type=\"button\"\n :tabindex=\"getButtonTabIndex(0)\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 384 512\"\n width=\"16\"\n height=\"16\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M0 64C0 46.3 14.3 32 32 32H80 96 224c70.7 0 128 57.3 128 128c0 31.3-11.3 60.1-30 82.3c37.1 22.4 62 63.1 62 109.7c0 70.7-57.3 128-128 128H96 80 32c-17.7 0-32-14.3-32-32s14.3-32 32-32H48V256 96H32C14.3 96 0 81.7 0 64zM224 224c35.3 0 64-28.7 64-64s-28.7-64-64-64H112V224H224zM112 288V416H256c35.3 0 64-28.7 64-64s-28.7-64-64-64H224 112z\"\n />\n </svg>\n </button>\n <button\n @click=\"editor.chain().focus().toggleItalic().run()\"\n :class=\"{\n italic: true,\n 'is-active': editor.isActive('italic'),\n }\"\n :aria-pressed=\"editor.isActive('italic')\"\n title=\"Italic\"\n aria-label=\"Italic\"\n type=\"button\"\n :tabindex=\"getButtonTabIndex(1)\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 384 512\"\n width=\"16\"\n height=\"16\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M128 64c0-17.7 14.3-32 32-32H352c17.7 0 32 14.3 32 32s-14.3 32-32 32H293.3L160 416h64c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H90.7L224 96H160c-17.7 0-32-14.3-32-32z\"\n />\n </svg>\n </button>\n <button\n @click=\"editor.chain().focus().toggleOrderedList().run()\"\n :class=\"{ 'is-active': editor.isActive('orderedList') }\"\n :aria-pressed=\"editor.isActive('orderedList')\"\n title=\"Ordered List\"\n aria-label=\"Ordered List\"\n type=\"button\"\n :tabindex=\"getButtonTabIndex(2)\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 512 512\"\n width=\"16\"\n height=\"16\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M24 56c0-13.3 10.7-24 24-24H80c13.3 0 24 10.7 24 24V176h16c13.3 0 24 10.7 24 24s-10.7 24-24 24H48c-13.3 0-24-10.7-24-24s10.7-24 24-24H64V80H48C34.7 80 24 69.3 24 56zM86.7 341.2c-6.5-7.4-18.3-6.9-24 1.2L51.5 357.9c-7.7 10.8-22.7 13.3-33.5 5.6s-13.3-22.7-5.6-33.5l11.1-15.6c23.7-33.2 72.3-35.6 99.2-4.9c21.3 24.4 20.8 60.9-1.1 84.7L86.8 432H120c13.3 0 24 10.7 24 24s-10.7 24-24 24H48c-9.5 0-18.2-5.6-22-14.4s-2.1-18.9 4.3-25.9l72-78c5.3-5.8 5.4-14.6 .3-20.5zM224 64H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H224c-17.7 0-32-14.3-32-32s14.3-32 32-32zm0 160H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H224c-17.7 0-32-14.3-32-32s14.3-32 32-32zm0 160H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H224c-17.7 0-32-14.3-32-32s14.3-32 32-32z\"\n />\n </svg>\n </button>\n <button\n @click=\"editor.chain().focus().toggleBulletList().run()\"\n :class=\"{ 'is-active': editor.isActive('bulletList') }\"\n :aria-pressed=\"editor.isActive('bulletList')\"\n title=\"Unordered List\"\n aria-label=\"Unordered List\"\n type=\"button\"\n :tabindex=\"getButtonTabIndex(3)\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 512 512\"\n width=\"16\"\n height=\"16\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M40 48C26.7 48 16 58.7 16 72v48c0 13.3 10.7 24 24 24H88c13.3 0 24-10.7 24-24V72c0-13.3-10.7-24-24-24H40zM192 64c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zM16 232v48c0 13.3 10.7 24 24 24H88c13.3 0 24-10.7 24-24V232c0-13.3-10.7-24-24-24H40c-13.3 0-24 10.7-24 24zM40 368c-13.3 0-24 10.7-24 24v48c0 13.3 10.7 24 24 24H88c13.3 0 24-10.7 24-24V392c0-13.3-10.7-24-24-24H40z\"\n />\n </svg>\n </button>\n <button\n v-for=\"(level, index) in headingLevels\"\n :key=\"level\"\n @click=\"editor.chain().focus().toggleHeading({ level }).run()\"\n :class=\"{\n 'heading-button': true,\n 'is-active': editor.isActive('heading', { level }),\n }\"\n :aria-pressed=\"editor.isActive('heading', { level })\"\n :title=\"`Heading ${level}`\"\n :aria-label=\"`Heading ${level}`\"\n type=\"button\"\n :tabindex=\"getButtonTabIndex(4 + index)\"\n >\n <span aria-hidden=\"true\">H{{ level }}</span>\n </button>\n </div>\n</template>\n\n<style>\n.g-rich-text-toolbar {\n display: flex;\n\n button {\n background-color: unset;\n padding: 0.4rem 0.5rem;\n font-size: 0.875rem;\n border: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n\n &.bold {\n font-weight: 700;\n }\n &.italic {\n font-style: italic;\n }\n\n &.is-active {\n color: var(--g-accent-700);\n background-color: var(--g-surface-150);\n }\n\n &:hover {\n background-color: var(--g-primary-300);\n color: var(--g-primary-text);\n }\n }\n\n .heading-button {\n font-weight: 700;\n }\n}\n</style>\n","<script lang=\"ts\" setup>\nimport { ref, computed } from \"vue\";\nimport type { Editor } from \"@tiptap/vue-3\";\nimport type { Level } from \"@tiptap/extension-heading\";\nimport { useToolbarNavigation } from \"../../composables/useToolbarNavigation.ts\";\n\ninterface Props {\n editor: Editor | undefined;\n headingLevels?: Level[];\n}\n\nconst props = defineProps<Props>();\n\nconst toolbarRef = ref<HTMLElement | null>(null);\nconst editorRef = computed(() => props.editor);\nconst { handleToolbarKeyDown, getButtonTabIndex } = useToolbarNavigation(\n editorRef,\n toolbarRef,\n);\n</script>\n\n<template>\n <div\n v-if=\"editor\"\n class=\"g-rich-text-toolbar\"\n role=\"toolbar\"\n aria-label=\"Text formatting\"\n ref=\"toolbarRef\"\n @keydown=\"handleToolbarKeyDown\"\n tabindex=\"-1\"\n >\n <button\n @click=\"editor.chain().focus().toggleBold().run()\"\n :class=\"{\n bold: true,\n 'is-active': editor.isActive('bold'),\n }\"\n :aria-pressed=\"editor.isActive('bold')\"\n title=\"Bold\"\n aria-label=\"Bold\"\n type=\"button\"\n :tabindex=\"getButtonTabIndex(0)\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 384 512\"\n width=\"16\"\n height=\"16\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M0 64C0 46.3 14.3 32 32 32H80 96 224c70.7 0 128 57.3 128 128c0 31.3-11.3 60.1-30 82.3c37.1 22.4 62 63.1 62 109.7c0 70.7-57.3 128-128 128H96 80 32c-17.7 0-32-14.3-32-32s14.3-32 32-32H48V256 96H32C14.3 96 0 81.7 0 64zM224 224c35.3 0 64-28.7 64-64s-28.7-64-64-64H112V224H224zM112 288V416H256c35.3 0 64-28.7 64-64s-28.7-64-64-64H224 112z\"\n />\n </svg>\n </button>\n <button\n @click=\"editor.chain().focus().toggleItalic().run()\"\n :class=\"{\n italic: true,\n 'is-active': editor.isActive('italic'),\n }\"\n :aria-pressed=\"editor.isActive('italic')\"\n title=\"Italic\"\n aria-label=\"Italic\"\n type=\"button\"\n :tabindex=\"getButtonTabIndex(1)\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 384 512\"\n width=\"16\"\n height=\"16\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M128 64c0-17.7 14.3-32 32-32H352c17.7 0 32 14.3 32 32s-14.3 32-32 32H293.3L160 416h64c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H90.7L224 96H160c-17.7 0-32-14.3-32-32z\"\n />\n </svg>\n </button>\n <button\n @click=\"editor.chain().focus().toggleOrderedList().run()\"\n :class=\"{ 'is-active': editor.isActive('orderedList') }\"\n :aria-pressed=\"editor.isActive('orderedList')\"\n title=\"Ordered List\"\n aria-label=\"Ordered List\"\n type=\"button\"\n :tabindex=\"getButtonTabIndex(2)\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 512 512\"\n width=\"16\"\n height=\"16\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M24 56c0-13.3 10.7-24 24-24H80c13.3 0 24 10.7 24 24V176h16c13.3 0 24 10.7 24 24s-10.7 24-24 24H48c-13.3 0-24-10.7-24-24s10.7-24 24-24H64V80H48C34.7 80 24 69.3 24 56zM86.7 341.2c-6.5-7.4-18.3-6.9-24 1.2L51.5 357.9c-7.7 10.8-22.7 13.3-33.5 5.6s-13.3-22.7-5.6-33.5l11.1-15.6c23.7-33.2 72.3-35.6 99.2-4.9c21.3 24.4 20.8 60.9-1.1 84.7L86.8 432H120c13.3 0 24 10.7 24 24s-10.7 24-24 24H48c-9.5 0-18.2-5.6-22-14.4s-2.1-18.9 4.3-25.9l72-78c5.3-5.8 5.4-14.6 .3-20.5zM224 64H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H224c-17.7 0-32-14.3-32-32s14.3-32 32-32zm0 160H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H224c-17.7 0-32-14.3-32-32s14.3-32 32-32zm0 160H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H224c-17.7 0-32-14.3-32-32s14.3-32 32-32z\"\n />\n </svg>\n </button>\n <button\n @click=\"editor.chain().focus().toggleBulletList().run()\"\n :class=\"{ 'is-active': editor.isActive('bulletList') }\"\n :aria-pressed=\"editor.isActive('bulletList')\"\n title=\"Unordered List\"\n aria-label=\"Unordered List\"\n type=\"button\"\n :tabindex=\"getButtonTabIndex(3)\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 512 512\"\n width=\"16\"\n height=\"16\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M40 48C26.7 48 16 58.7 16 72v48c0 13.3 10.7 24 24 24H88c13.3 0 24-10.7 24-24V72c0-13.3-10.7-24-24-24H40zM192 64c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zM16 232v48c0 13.3 10.7 24 24 24H88c13.3 0 24-10.7 24-24V232c0-13.3-10.7-24-24-24H40c-13.3 0-24 10.7-24 24zM40 368c-13.3 0-24 10.7-24 24v48c0 13.3 10.7 24 24 24H88c13.3 0 24-10.7 24-24V392c0-13.3-10.7-24-24-24H40z\"\n />\n </svg>\n </button>\n <button\n v-for=\"(level, index) in headingLevels\"\n :key=\"level\"\n @click=\"editor.chain().focus().toggleHeading({ level }).run()\"\n :class=\"{\n 'heading-button': true,\n 'is-active': editor.isActive('heading', { level }),\n }\"\n :aria-pressed=\"editor.isActive('heading', { level })\"\n :title=\"`Heading ${level}`\"\n :aria-label=\"`Heading ${level}`\"\n type=\"button\"\n :tabindex=\"getButtonTabIndex(4 + index)\"\n >\n <span aria-hidden=\"true\">H{{ level }}</span>\n </button>\n </div>\n</template>\n\n<style>\n.g-rich-text-toolbar {\n display: flex;\n\n button {\n background-color: unset;\n padding: 0.4rem 0.5rem;\n font-size: 0.875rem;\n border: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n\n &.bold {\n font-weight: 700;\n }\n &.italic {\n font-style: italic;\n }\n\n &.is-active {\n color: var(--g-accent-700);\n background-color: var(--g-surface-150);\n }\n\n &:hover {\n background-color: var(--g-primary-300);\n color: var(--g-primary-text);\n }\n }\n\n .heading-button {\n font-weight: 700;\n }\n}\n</style>\n","<script lang=\"ts\">\n/**\n * The GChatInput component provides a rich text editing experience using Tiptap. It supports:\n *\n * - **Bold** and *italic* text formatting\n * - Bullet and numbered lists\n * - Bubble menu for formatting (appears when text is selected)\n * - Press <kbd>Enter</kbd> to send, <kbd>Shift+Enter</kbd> for new line\n * - Optional expanded composer toggled with <kbd>Ctrl+Shift+F</kbd>\n * - Undo/redo support\n *\n * **Note**: This component is part of the `@illinois-grad/grad-vue-rte` package, which includes Tiptap dependencies.\n */\nexport default {};\n</script>\n\n<script lang=\"ts\" setup>\nimport {\n computed,\n nextTick,\n onWatcherCleanup,\n ref,\n useId,\n useTemplateRef,\n watch,\n} from \"vue\";\nimport { useResizeObserver } from \"@vueuse/core\";\nimport { useFocusTrap } from \"@vueuse/integrations/useFocusTrap\";\nimport { EditorContent } from \"@tiptap/vue-3\";\nimport { BubbleMenu } from \"@tiptap/vue-3/menus\";\nimport { useRichTextEditor } from \"../composables/useRichTextEditor\";\nimport GRichTextToolbar from \"./editor/GRichTextToolbar.vue\";\n\ndefineOptions({ inheritAttrs: false });\n\ntype Props = {\n /**\n * Placeholder text\n * @demo\n */\n placeholder?: string;\n /**\n * Disabled\n * @demo\n */\n disabled?: boolean;\n /**\n * Maximum number of text rows shown before the editor scrolls\n * @demo\n */\n maxRows?: number;\n /**\n * Accessible label\n * @demo\n */\n label?: string;\n /**\n * Allow the editor to expand into a large viewport overlay\n * @demo\n */\n expandable?: boolean;\n};\n\nconst props = withDefaults(defineProps<Props>(), {\n placeholder: \"Type a comment\",\n label: \"Comment input\",\n disabled: false,\n maxRows: 5,\n expandable: false,\n});\nconst model = defineModel<object | \"\">();\nconst emit = defineEmits<{ send: [content: object | undefined | null] }>();\nconst expanded = ref(false);\nconst multiline = ref(false);\nconst composer = useTemplateRef<HTMLElement>(\"composer\");\nconst expandButton = useTemplateRef<HTMLButtonElement>(\"expandButton\");\nconst actions = useTemplateRef<HTMLElement>(\"actions\");\nconst composerId = `g-chat-input-${useId()}`;\nconst actionsBelow = computed(() => expanded.value || multiline.value);\nconst composerWidth = ref(0);\nconst maxRowsStyle = computed(() => ({\n \"--g-chat-input-max-rows\": String(\n Number.isFinite(props.maxRows) ? Math.max(1, props.maxRows) : 1,\n ),\n}));\n\nconst { editor, focusEditor } = useRichTextEditor({\n content: model,\n placeholder: computed(() => props.placeholder),\n label: computed(() => props.label),\n multiline: true,\n editorProps: {\n handleKeyDown(view: any, event: KeyboardEvent) {\n if (editor.value && event.key === \"Enter\") {\n if (\n editor.value.isActive(\"orderedList\") ||\n editor.value.isActive(\"bulletList\")\n ) {\n return false;\n }\n if (!event.shiftKey) {\n const content = editor.value.getJSON();\n event.preventDefault();\n onSend(content);\n return true;\n } else {\n editor.value.commands.splitBlock();\n }\n }\n return false;\n },\n attributes: {\n \"aria-keyshortcuts\": \"Shift+Enter\",\n },\n },\n});\n\nfunction onSend(content: object | undefined | null) {\n if (content) {\n emit(\"send\", content);\n collapse(false);\n }\n}\n\nfunction clickSend() {\n onSend(editor.value?.getJSON());\n}\n\nfunction focusInput() {\n focusEditor();\n}\n\nconst { activate: activateFocusTrap, deactivate: deactivateFocusTrap } =\n useFocusTrap(composer, {\n immediate: false,\n clickOutsideDeactivates: true,\n escapeDeactivates: false,\n fallbackFocus: () => composer.value!,\n initialFocus: () => editor.value?.view.dom ?? composer.value!,\n returnFocusOnDeactivate: false,\n onDeactivate() {\n expanded.value = false;\n },\n });\n\nfunction appendBubbleMenuTo() {\n return composer.value ?? document.body;\n}\n\nfunction updateLayout() {\n if (!editor.value || editor.value.isDestroyed) {\n return;\n }\n\n const editorElement = editor.value.view.dom;\n const composerElement = composer.value;\n const actionsElement = actions.value;\n\n if (\n expanded.value ||\n !editorElement ||\n !composerElement ||\n !actionsElement ||\n editor.value.isEmpty\n ) {\n multiline.value = false;\n return;\n }\n\n if (editor.value.state.doc.childCount > 1) {\n multiline.value = true;\n return;\n }\n\n const textNodes = document.createTreeWalker(\n editorElement,\n NodeFilter.SHOW_TEXT,\n );\n const lineRects: DOMRect[] = [];\n let textNode = textNodes.nextNode();\n\n while (textNode) {\n if (textNode.textContent) {\n const textRange = document.createRange();\n textRange.selectNodeContents(textNode);\n lineRects.push(\n ...Array.from(textRange.getClientRects()).filter(\n (rect) => rect.width || rect.height,\n ),\n );\n }\n textNode = textNodes.nextNode();\n }\n\n const lineTops = lineRects.reduce<number[]>((tops, rect) => {\n if (!tops.some((top) => Math.abs(top - rect.top) < 1)) {\n tops.push(rect.top);\n }\n return tops;\n }, []);\n\n if (lineTops.length > 1) {\n multiline.value = true;\n return;\n }\n\n const composerStyle = getComputedStyle(composerElement);\n const availableInlineWidth =\n composerElement.clientWidth -\n parseFloat(composerStyle.paddingLeft) -\n parseFloat(composerStyle.paddingRight) -\n actionsElement.offsetWidth -\n parseFloat(composerStyle.columnGap || \"0\");\n\n multiline.value =\n (lineRects.reduce((width, rect) => width + rect.width, 0) ||\n editorElement.scrollWidth) >\n availableInlineWidth;\n}\n\nfunction scheduleLayoutUpdate() {\n const frame = requestAnimationFrame(updateLayout);\n onWatcherCleanup(() => cancelAnimationFrame(frame));\n}\n\nfunction expand() {\n if (!props.expandable || props.disabled || expanded.value) {\n return;\n }\n\n expanded.value = true;\n}\n\nfunction collapse(restoreFocus = true) {\n if (!expanded.value) {\n return;\n }\n\n expanded.value = false;\n\n if (restoreFocus) {\n nextTick(() => expandButton.value?.focus()).catch((error) => {\n console.error(error);\n });\n }\n}\n\nfunction toggleExpanded() {\n if (expanded.value) {\n collapse();\n } else {\n expand();\n }\n}\n\nfunction onComposerKeydown(event: KeyboardEvent) {\n if (\n props.expandable &&\n !props.disabled &&\n event.ctrlKey &&\n event.shiftKey &&\n event.key.toLowerCase() === \"f\"\n ) {\n event.preventDefault();\n toggleExpanded();\n return;\n }\n\n if (expanded.value && event.key === \"Escape\") {\n event.preventDefault();\n event.stopPropagation();\n collapse();\n }\n}\n\nwatch(\n () => [props.expandable, props.disabled],\n ([expandable, disabled]) => {\n if (!expandable || disabled) {\n collapse(false);\n }\n },\n);\n\nwatch(\n expanded,\n (isExpanded) => {\n if (isExpanded) {\n activateFocusTrap();\n } else {\n deactivateFocusTrap();\n }\n },\n { flush: \"post\" },\n);\n\nwatch([model, expanded, composerWidth], scheduleLayoutUpdate, {\n flush: \"post\",\n});\n\nuseResizeObserver(composer, ([entry]) => {\n composerWidth.value = entry?.contentRect.width ?? 0;\n});\n\ndefineExpose({ focusInput });\n</script>\n\n<template>\n <div\n :id=\"composerId\"\n ref=\"composer\"\n class=\"g-chat-input-wrap\"\n :class=\"{\n 'g-chat-input-wrap--expanded': expanded,\n 'g-chat-input-wrap--actions-below': actionsBelow,\n }\"\n :style=\"maxRowsStyle\"\n :role=\"expanded ? 'dialog' : undefined\"\n :aria-label=\"expanded ? `${props.label} expanded` : undefined\"\n :tabindex=\"expanded ? -1 : undefined\"\n @keydown.capture=\"onComposerKeydown\"\n >\n <BubbleMenu\n :editor=\"editor\"\n :append-to=\"props.expandable ? appendBubbleMenuTo : undefined\"\n v-if=\"editor\"\n >\n <GRichTextToolbar :editor=\"editor\" class=\"bubble-menu\" />\n </BubbleMenu>\n <EditorContent :editor=\"editor\" class=\"editor-content\" />\n <div ref=\"actions\" class=\"g-chat-actions\">\n <button\n v-if=\"props.expandable\"\n ref=\"expandButton\"\n class=\"g-chat-expand-btn\"\n :disabled=\"props.disabled\"\n :title=\"expanded ? 'Collapse editor' : 'Expand editor'\"\n :aria-label=\"expanded ? 'Collapse editor' : 'Expand editor'\"\n :aria-expanded=\"expanded\"\n :aria-controls=\"composerId\"\n aria-haspopup=\"dialog\"\n aria-keyshortcuts=\"Control+Shift+F\"\n type=\"button\"\n @click=\"toggleExpanded\"\n >\n <svg\n v-if=\"expanded\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 640 640\"\n width=\"18\"\n height=\"18\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <!--!Font Awesome Pro v7.3.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2026 Fonticons, Inc.-->\n <path\n d=\"M105.4 105.4C117.9 92.9 138.2 92.9 150.7 105.4L224 178.7L224 144C224 126.3 238.3 112 256 112C273.7 112 288 126.3 288 144L288 256C288 273.7 273.7 288 256 288L144 288C126.3 288 112 273.7 112 256C112 238.3 126.3 224 144 224L178.7 224L105.3 150.6C92.9 138.1 92.9 117.9 105.4 105.4zM534.7 105.4C547.2 117.9 547.2 138.2 534.7 150.7L461.3 224L496 224C513.7 224 528 238.3 528 256C528 273.7 513.7 288 496 288L384 288C366.3 288 352 273.7 352 256L352 144C352 126.3 366.3 112 384 112C401.7 112 416 126.3 416 144L416 178.7L489.4 105.3C501.9 92.8 522.2 92.8 534.7 105.3zM144 416C126.3 416 112 401.7 112 384C112 366.3 126.3 352 144 352L256 352C273.7 352 288 366.3 288 384L288 496C288 513.7 273.7 528 256 528C238.3 528 224 513.7 224 496L224 461.3L150.6 534.7C138.1 547.2 117.8 547.2 105.3 534.7C92.8 522.2 92.8 501.9 105.3 489.4L178.7 416L144 416zM352 384C352 366.3 366.3 352 384 352L496 352C513.7 352 528 366.3 528 384C528 401.7 513.7 416 496 416L461.3 416L534.7 489.4C547.2 501.9 547.2 522.2 534.7 534.7C522.2 547.2 501.9 547.2 489.4 534.7L416 461.3L416 496C416 513.7 401.7 528 384 528C366.3 528 352 513.7 352 496L352 384z\"\n />\n </svg>\n <svg\n v-else\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 640 640\"\n width=\"18\"\n height=\"18\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <!--!Font Awesome Free v7.3.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.-->\n <path\n d=\"M128 96C110.3 96 96 110.3 96 128L96 224C96 241.7 110.3 256 128 256C145.7 256 160 241.7 160 224L160 160L224 160C241.7 160 256 145.7 256 128C256 110.3 241.7 96 224 96L128 96zM160 416C160 398.3 145.7 384 128 384C110.3 384 96 398.3 96 416L96 512C96 529.7 110.3 544 128 544L224 544C241.7 544 256 529.7 256 512C256 494.3 241.7 480 224 480L160 480L160 416zM416 96C398.3 96 384 110.3 384 128C384 145.7 398.3 160 416 160L480 160L480 224C480 241.7 494.3 256 512 256C529.7 256 544 241.7 544 224L544 128C544 110.3 529.7 96 512 96L416 96zM544 416C544 398.3 529.7 384 512 384C494.3 384 480 398.3 480 416L480 480L416 480C398.3 480 384 494.3 384 512C384 529.7 398.3 544 416 544L512 544C529.7 544 544 529.7 544 512L544 416z\"\n />\n </svg>\n </button>\n <button\n class=\"g-chat-send-btn\"\n :disabled=\"\n props.disabled ||\n !model ||\n (model && Object.keys(model).length === 0)\n \"\n @click=\"clickSend\"\n title=\"Send\"\n aria-label=\"Send\"\n type=\"button\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 512 512\"\n width=\"16\"\n height=\"16\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M498.1 5.6c10.1 7 15.4 19.1 13.5 31.2l-64 416c-1.5 9.7-7.4 18.2-16 23s-18.9 5.4-28 1.6L284 427.7l-68.5 74.1c-8.9 9.7-22.9 12.9-35.2 8.1S160 493.2 160 480V396.4c0-4 1.5-7.8 4.2-10.7L331.8 202.8c5.8-6.3 5.6-16-.4-22s-15.7-6.4-22-.7L106 360.8 17.7 316.6C7.1 311.3 .3 300.7 0 288.9s5.9-22.8 16.1-28.7l448-256c10.7-6.1 23.9-5.5 34 1.4z\"\n />\n </svg>\n </button>\n </div>\n </div>\n</template>\n\n<style>\n.g-chat-input-wrap {\n --g-chat-input-line-height: 1.5;\n --g-chat-input-row-spacing: 0.375em;\n --g-chat-input-max-rows: 5;\n\n .tiptap {\n background: transparent;\n border: none;\n padding: 0.15em 0;\n font-size: 15px;\n line-height: var(--g-chat-input-line-height);\n max-height: calc(\n var(--g-chat-input-max-rows) * var(--g-chat-input-line-height) *\n 1em + (var(--g-chat-input-max-rows) - 1) *\n var(--g-chat-input-row-spacing)\n );\n overflow-y: auto;\n flex: 1;\n outline: none;\n\n p {\n margin: var(--g-chat-input-row-spacing) 0 0;\n }\n\n > :first-child {\n margin-top: 0;\n }\n > :last-child {\n margin-bottom: 0;\n }\n\n ul,\n ol {\n padding: 0 1em;\n margin: var(--g-chat-input-row-spacing) 1em 0 0.4em;\n\n li p {\n margin-top: 0;\n margin-bottom: 0;\n }\n }\n p.is-editor-empty:first-child::before {\n color: var(--g-surface-600);\n content: attr(data-placeholder);\n float: left;\n height: 0;\n pointer-events: none;\n }\n }\n}\n.bubble-menu {\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);\n background-color: var(--g-surface-100);\n\n button {\n &:first-child {\n border-top-left-radius: 4px;\n border-bottom-left-radius: 4px;\n }\n &:last-child {\n border-top-right-radius: 4px;\n border-bottom-right-radius: 4px;\n }\n }\n}\n\n.g-chat-input-wrap {\n position: relative;\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto;\n column-gap: 0.25rem;\n align-items: center;\n background: var(--g-surface-0);\n border: 2px solid var(--g-primary-500);\n border-radius: 4px;\n padding: 0.5em;\n\n &:has(.ProseMirror-focused) {\n outline: 2px solid var(--g-primary-500);\n outline-offset: 2px;\n box-shadow: 0 0 0 2px var(--g-info-200);\n border-color: var(--g-info-200);\n }\n}\n\n.g-chat-input-wrap--expanded {\n position: fixed;\n right: 1rem;\n bottom: 1rem;\n z-index: 1000;\n width: min(48rem, calc(100vw - 2rem));\n height: min(32rem, calc(100dvh - 2rem));\n box-sizing: border-box;\n grid-template-rows: minmax(0, 1fr) auto;\n align-items: stretch;\n padding: 0.75rem;\n box-shadow: var(--il-shadow, 0 10px 30px rgba(0, 0, 0, 0.25));\n\n .tiptap {\n height: 100%;\n max-height: none;\n }\n}\n\n.editor-content {\n min-width: 0;\n min-height: 0;\n}\n\n.g-chat-actions {\n display: flex;\n align-self: end;\n}\n\n.g-chat-input-wrap--actions-below {\n row-gap: 0.25rem;\n\n .editor-content,\n .g-chat-actions {\n grid-column: 1 / -1;\n }\n\n .g-chat-actions {\n justify-self: end;\n }\n}\n\n.g-chat-input-wrap--expanded .editor-content {\n align-self: stretch;\n overflow: hidden;\n}\n\n.g-chat-expand-btn,\n.g-chat-send-btn {\n color: var(--g-primary-500);\n font-size: 1em;\n border: 2px solid transparent;\n border-radius: 4px;\n padding: 0.4em;\n margin: 0;\n align-self: flex-end;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n background: transparent;\n flex-shrink: 0;\n\n &:hover:not(:disabled) {\n color: var(--g-accent-700);\n background-color: var(--g-surface-100);\n }\n\n &:focus:not(:disabled) {\n background-color: var(--g-info-200);\n color: var(--g-primary-500);\n }\n\n &:active:not(:disabled) {\n background-color: var(--g-primary-500);\n color: var(--g-surface-0);\n }\n\n &:disabled {\n color: var(--g-surface-300);\n cursor: not-allowed;\n }\n}\n\n@media (max-width: 640px) {\n .g-chat-input-wrap--expanded {\n inset: 0;\n width: 100vw;\n height: 100vh;\n height: 100dvh;\n border-radius: 0;\n padding: max(0.75rem, env(safe-area-inset-top))\n max(0.75rem, env(safe-area-inset-right))\n max(0.75rem, env(safe-area-inset-bottom))\n max(0.75rem, env(safe-area-inset-left));\n }\n}\n</style>\n","<script lang=\"ts\">\n/**\n * The GChatInput component provides a rich text editing experience using Tiptap. It supports:\n *\n * - **Bold** and *italic* text formatting\n * - Bullet and numbered lists\n * - Bubble menu for formatting (appears when text is selected)\n * - Press <kbd>Enter</kbd> to send, <kbd>Shift+Enter</kbd> for new line\n * - Optional expanded composer toggled with <kbd>Ctrl+Shift+F</kbd>\n * - Undo/redo support\n *\n * **Note**: This component is part of the `@illinois-grad/grad-vue-rte` package, which includes Tiptap dependencies.\n */\nexport default {};\n</script>\n\n<script lang=\"ts\" setup>\nimport {\n computed,\n nextTick,\n onWatcherCleanup,\n ref,\n useId,\n useTemplateRef,\n watch,\n} from \"vue\";\nimport { useResizeObserver } from \"@vueuse/core\";\nimport { useFocusTrap } from \"@vueuse/integrations/useFocusTrap\";\nimport { EditorContent } from \"@tiptap/vue-3\";\nimport { BubbleMenu } from \"@tiptap/vue-3/menus\";\nimport { useRichTextEditor } from \"../composables/useRichTextEditor\";\nimport GRichTextToolbar from \"./editor/GRichTextToolbar.vue\";\n\ndefineOptions({ inheritAttrs: false });\n\ntype Props = {\n /**\n * Placeholder text\n * @demo\n */\n placeholder?: string;\n /**\n * Disabled\n * @demo\n */\n disabled?: boolean;\n /**\n * Maximum number of text rows shown before the editor scrolls\n * @demo\n */\n maxRows?: number;\n /**\n * Accessible label\n * @demo\n */\n label?: string;\n /**\n * Allow the editor to expand into a large viewport overlay\n * @demo\n */\n expandable?: boolean;\n};\n\nconst props = withDefaults(defineProps<Props>(), {\n placeholder: \"Type a comment\",\n label: \"Comment input\",\n disabled: false,\n maxRows: 5,\n expandable: false,\n});\nconst model = defineModel<object | \"\">();\nconst emit = defineEmits<{ send: [content: object | undefined | null] }>();\nconst expanded = ref(false);\nconst multiline = ref(false);\nconst composer = useTemplateRef<HTMLElement>(\"composer\");\nconst expandButton = useTemplateRef<HTMLButtonElement>(\"expandButton\");\nconst actions = useTemplateRef<HTMLElement>(\"actions\");\nconst composerId = `g-chat-input-${useId()}`;\nconst actionsBelow = computed(() => expanded.value || multiline.value);\nconst composerWidth = ref(0);\nconst maxRowsStyle = computed(() => ({\n \"--g-chat-input-max-rows\": String(\n Number.isFinite(props.maxRows) ? Math.max(1, props.maxRows) : 1,\n ),\n}));\n\nconst { editor, focusEditor } = useRichTextEditor({\n content: model,\n placeholder: computed(() => props.placeholder),\n label: computed(() => props.label),\n multiline: true,\n editorProps: {\n handleKeyDown(view: any, event: KeyboardEvent) {\n if (editor.value && event.key === \"Enter\") {\n if (\n editor.value.isActive(\"orderedList\") ||\n editor.value.isActive(\"bulletList\")\n ) {\n return false;\n }\n if (!event.shiftKey) {\n const content = editor.value.getJSON();\n event.preventDefault();\n onSend(content);\n return true;\n } else {\n editor.value.commands.splitBlock();\n }\n }\n return false;\n },\n attributes: {\n \"aria-keyshortcuts\": \"Shift+Enter\",\n },\n },\n});\n\nfunction onSend(content: object | undefined | null) {\n if (content) {\n emit(\"send\", content);\n collapse(false);\n }\n}\n\nfunction clickSend() {\n onSend(editor.value?.getJSON());\n}\n\nfunction focusInput() {\n focusEditor();\n}\n\nconst { activate: activateFocusTrap, deactivate: deactivateFocusTrap } =\n useFocusTrap(composer, {\n immediate: false,\n clickOutsideDeactivates: true,\n escapeDeactivates: false,\n fallbackFocus: () => composer.value!,\n initialFocus: () => editor.value?.view.dom ?? composer.value!,\n returnFocusOnDeactivate: false,\n onDeactivate() {\n expanded.value = false;\n },\n });\n\nfunction appendBubbleMenuTo() {\n return composer.value ?? document.body;\n}\n\nfunction updateLayout() {\n if (!editor.value || editor.value.isDestroyed) {\n return;\n }\n\n const editorElement = editor.value.view.dom;\n const composerElement = composer.value;\n const actionsElement = actions.value;\n\n if (\n expanded.value ||\n !editorElement ||\n !composerElement ||\n !actionsElement ||\n editor.value.isEmpty\n ) {\n multiline.value = false;\n return;\n }\n\n if (editor.value.state.doc.childCount > 1) {\n multiline.value = true;\n return;\n }\n\n const textNodes = document.createTreeWalker(\n editorElement,\n NodeFilter.SHOW_TEXT,\n );\n const lineRects: DOMRect[] = [];\n let textNode = textNodes.nextNode();\n\n while (textNode) {\n if (textNode.textContent) {\n const textRange = document.createRange();\n textRange.selectNodeContents(textNode);\n lineRects.push(\n ...Array.from(textRange.getClientRects()).filter(\n (rect) => rect.width || rect.height,\n ),\n );\n }\n textNode = textNodes.nextNode();\n }\n\n const lineTops = lineRects.reduce<number[]>((tops, rect) => {\n if (!tops.some((top) => Math.abs(top - rect.top) < 1)) {\n tops.push(rect.top);\n }\n return tops;\n }, []);\n\n if (lineTops.length > 1) {\n multiline.value = true;\n return;\n }\n\n const composerStyle = getComputedStyle(composerElement);\n const availableInlineWidth =\n composerElement.clientWidth -\n parseFloat(composerStyle.paddingLeft) -\n parseFloat(composerStyle.paddingRight) -\n actionsElement.offsetWidth -\n parseFloat(composerStyle.columnGap || \"0\");\n\n multiline.value =\n (lineRects.reduce((width, rect) => width + rect.width, 0) ||\n editorElement.scrollWidth) >\n availableInlineWidth;\n}\n\nfunction scheduleLayoutUpdate() {\n const frame = requestAnimationFrame(updateLayout);\n onWatcherCleanup(() => cancelAnimationFrame(frame));\n}\n\nfunction expand() {\n if (!props.expandable || props.disabled || expanded.value) {\n return;\n }\n\n expanded.value = true;\n}\n\nfunction collapse(restoreFocus = true) {\n if (!expanded.value) {\n return;\n }\n\n expanded.value = false;\n\n if (restoreFocus) {\n nextTick(() => expandButton.value?.focus()).catch((error) => {\n console.error(error);\n });\n }\n}\n\nfunction toggleExpanded() {\n if (expanded.value) {\n collapse();\n } else {\n expand();\n }\n}\n\nfunction onComposerKeydown(event: KeyboardEvent) {\n if (\n props.expandable &&\n !props.disabled &&\n event.ctrlKey &&\n event.shiftKey &&\n event.key.toLowerCase() === \"f\"\n ) {\n event.preventDefault();\n toggleExpanded();\n return;\n }\n\n if (expanded.value && event.key === \"Escape\") {\n event.preventDefault();\n event.stopPropagation();\n collapse();\n }\n}\n\nwatch(\n () => [props.expandable, props.disabled],\n ([expandable, disabled]) => {\n if (!expandable || disabled) {\n collapse(false);\n }\n },\n);\n\nwatch(\n expanded,\n (isExpanded) => {\n if (isExpanded) {\n activateFocusTrap();\n } else {\n deactivateFocusTrap();\n }\n },\n { flush: \"post\" },\n);\n\nwatch([model, expanded, composerWidth], scheduleLayoutUpdate, {\n flush: \"post\",\n});\n\nuseResizeObserver(composer, ([entry]) => {\n composerWidth.value = entry?.contentRect.width ?? 0;\n});\n\ndefineExpose({ focusInput });\n</script>\n\n<template>\n <div\n :id=\"composerId\"\n ref=\"composer\"\n class=\"g-chat-input-wrap\"\n :class=\"{\n 'g-chat-input-wrap--expanded': expanded,\n 'g-chat-input-wrap--actions-below': actionsBelow,\n }\"\n :style=\"maxRowsStyle\"\n :role=\"expanded ? 'dialog' : undefined\"\n :aria-label=\"expanded ? `${props.label} expanded` : undefined\"\n :tabindex=\"expanded ? -1 : undefined\"\n @keydown.capture=\"onComposerKeydown\"\n >\n <BubbleMenu\n :editor=\"editor\"\n :append-to=\"props.expandable ? appendBubbleMenuTo : undefined\"\n v-if=\"editor\"\n >\n <GRichTextToolbar :editor=\"editor\" class=\"bubble-menu\" />\n </BubbleMenu>\n <EditorContent :editor=\"editor\" class=\"editor-content\" />\n <div ref=\"actions\" class=\"g-chat-actions\">\n <button\n v-if=\"props.expandable\"\n ref=\"expandButton\"\n class=\"g-chat-expand-btn\"\n :disabled=\"props.disabled\"\n :title=\"expanded ? 'Collapse editor' : 'Expand editor'\"\n :aria-label=\"expanded ? 'Collapse editor' : 'Expand editor'\"\n :aria-expanded=\"expanded\"\n :aria-controls=\"composerId\"\n aria-haspopup=\"dialog\"\n aria-keyshortcuts=\"Control+Shift+F\"\n type=\"button\"\n @click=\"toggleExpanded\"\n >\n <svg\n v-if=\"expanded\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 640 640\"\n width=\"18\"\n height=\"18\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <!--!Font Awesome Pro v7.3.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2026 Fonticons, Inc.-->\n <path\n d=\"M105.4 105.4C117.9 92.9 138.2 92.9 150.7 105.4L224 178.7L224 144C224 126.3 238.3 112 256 112C273.7 112 288 126.3 288 144L288 256C288 273.7 273.7 288 256 288L144 288C126.3 288 112 273.7 112 256C112 238.3 126.3 224 144 224L178.7 224L105.3 150.6C92.9 138.1 92.9 117.9 105.4 105.4zM534.7 105.4C547.2 117.9 547.2 138.2 534.7 150.7L461.3 224L496 224C513.7 224 528 238.3 528 256C528 273.7 513.7 288 496 288L384 288C366.3 288 352 273.7 352 256L352 144C352 126.3 366.3 112 384 112C401.7 112 416 126.3 416 144L416 178.7L489.4 105.3C501.9 92.8 522.2 92.8 534.7 105.3zM144 416C126.3 416 112 401.7 112 384C112 366.3 126.3 352 144 352L256 352C273.7 352 288 366.3 288 384L288 496C288 513.7 273.7 528 256 528C238.3 528 224 513.7 224 496L224 461.3L150.6 534.7C138.1 547.2 117.8 547.2 105.3 534.7C92.8 522.2 92.8 501.9 105.3 489.4L178.7 416L144 416zM352 384C352 366.3 366.3 352 384 352L496 352C513.7 352 528 366.3 528 384C528 401.7 513.7 416 496 416L461.3 416L534.7 489.4C547.2 501.9 547.2 522.2 534.7 534.7C522.2 547.2 501.9 547.2 489.4 534.7L416 461.3L416 496C416 513.7 401.7 528 384 528C366.3 528 352 513.7 352 496L352 384z\"\n />\n </svg>\n <svg\n v-else\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 640 640\"\n width=\"18\"\n height=\"18\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <!--!Font Awesome Free v7.3.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.-->\n <path\n d=\"M128 96C110.3 96 96 110.3 96 128L96 224C96 241.7 110.3 256 128 256C145.7 256 160 241.7 160 224L160 160L224 160C241.7 160 256 145.7 256 128C256 110.3 241.7 96 224 96L128 96zM160 416C160 398.3 145.7 384 128 384C110.3 384 96 398.3 96 416L96 512C96 529.7 110.3 544 128 544L224 544C241.7 544 256 529.7 256 512C256 494.3 241.7 480 224 480L160 480L160 416zM416 96C398.3 96 384 110.3 384 128C384 145.7 398.3 160 416 160L480 160L480 224C480 241.7 494.3 256 512 256C529.7 256 544 241.7 544 224L544 128C544 110.3 529.7 96 512 96L416 96zM544 416C544 398.3 529.7 384 512 384C494.3 384 480 398.3 480 416L480 480L416 480C398.3 480 384 494.3 384 512C384 529.7 398.3 544 416 544L512 544C529.7 544 544 529.7 544 512L544 416z\"\n />\n </svg>\n </button>\n <button\n class=\"g-chat-send-btn\"\n :disabled=\"\n props.disabled ||\n !model ||\n (model && Object.keys(model).length === 0)\n \"\n @click=\"clickSend\"\n title=\"Send\"\n aria-label=\"Send\"\n type=\"button\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 512 512\"\n width=\"16\"\n height=\"16\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M498.1 5.6c10.1 7 15.4 19.1 13.5 31.2l-64 416c-1.5 9.7-7.4 18.2-16 23s-18.9 5.4-28 1.6L284 427.7l-68.5 74.1c-8.9 9.7-22.9 12.9-35.2 8.1S160 493.2 160 480V396.4c0-4 1.5-7.8 4.2-10.7L331.8 202.8c5.8-6.3 5.6-16-.4-22s-15.7-6.4-22-.7L106 360.8 17.7 316.6C7.1 311.3 .3 300.7 0 288.9s5.9-22.8 16.1-28.7l448-256c10.7-6.1 23.9-5.5 34 1.4z\"\n />\n </svg>\n </button>\n </div>\n </div>\n</template>\n\n<style>\n.g-chat-input-wrap {\n --g-chat-input-line-height: 1.5;\n --g-chat-input-row-spacing: 0.375em;\n --g-chat-input-max-rows: 5;\n\n .tiptap {\n background: transparent;\n border: none;\n padding: 0.15em 0;\n font-size: 15px;\n line-height: var(--g-chat-input-line-height);\n max-height: calc(\n var(--g-chat-input-max-rows) * var(--g-chat-input-line-height) *\n 1em + (var(--g-chat-input-max-rows) - 1) *\n var(--g-chat-input-row-spacing)\n );\n overflow-y: auto;\n flex: 1;\n outline: none;\n\n p {\n margin: var(--g-chat-input-row-spacing) 0 0;\n }\n\n > :first-child {\n margin-top: 0;\n }\n > :last-child {\n margin-bottom: 0;\n }\n\n ul,\n ol {\n padding: 0 1em;\n margin: var(--g-chat-input-row-spacing) 1em 0 0.4em;\n\n li p {\n margin-top: 0;\n margin-bottom: 0;\n }\n }\n p.is-editor-empty:first-child::before {\n color: var(--g-surface-600);\n content: attr(data-placeholder);\n float: left;\n height: 0;\n pointer-events: none;\n }\n }\n}\n.bubble-menu {\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);\n background-color: var(--g-surface-100);\n\n button {\n &:first-child {\n border-top-left-radius: 4px;\n border-bottom-left-radius: 4px;\n }\n &:last-child {\n border-top-right-radius: 4px;\n border-bottom-right-radius: 4px;\n }\n }\n}\n\n.g-chat-input-wrap {\n position: relative;\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto;\n column-gap: 0.25rem;\n align-items: center;\n background: var(--g-surface-0);\n border: 2px solid var(--g-primary-500);\n border-radius: 4px;\n padding: 0.5em;\n\n &:has(.ProseMirror-focused) {\n outline: 2px solid var(--g-primary-500);\n outline-offset: 2px;\n box-shadow: 0 0 0 2px var(--g-info-200);\n border-color: var(--g-info-200);\n }\n}\n\n.g-chat-input-wrap--expanded {\n position: fixed;\n right: 1rem;\n bottom: 1rem;\n z-index: 1000;\n width: min(48rem, calc(100vw - 2rem));\n height: min(32rem, calc(100dvh - 2rem));\n box-sizing: border-box;\n grid-template-rows: minmax(0, 1fr) auto;\n align-items: stretch;\n padding: 0.75rem;\n box-shadow: var(--il-shadow, 0 10px 30px rgba(0, 0, 0, 0.25));\n\n .tiptap {\n height: 100%;\n max-height: none;\n }\n}\n\n.editor-content {\n min-width: 0;\n min-height: 0;\n}\n\n.g-chat-actions {\n display: flex;\n align-self: end;\n}\n\n.g-chat-input-wrap--actions-below {\n row-gap: 0.25rem;\n\n .editor-content,\n .g-chat-actions {\n grid-column: 1 / -1;\n }\n\n .g-chat-actions {\n justify-self: end;\n }\n}\n\n.g-chat-input-wrap--expanded .editor-content {\n align-self: stretch;\n overflow: hidden;\n}\n\n.g-chat-expand-btn,\n.g-chat-send-btn {\n color: var(--g-primary-500);\n font-size: 1em;\n border: 2px solid transparent;\n border-radius: 4px;\n padding: 0.4em;\n margin: 0;\n align-self: flex-end;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n background: transparent;\n flex-shrink: 0;\n\n &:hover:not(:disabled) {\n color: var(--g-accent-700);\n background-color: var(--g-surface-100);\n }\n\n &:focus:not(:disabled) {\n background-color: var(--g-info-200);\n color: var(--g-primary-500);\n }\n\n &:active:not(:disabled) {\n background-color: var(--g-primary-500);\n color: var(--g-surface-0);\n }\n\n &:disabled {\n color: var(--g-surface-300);\n cursor: not-allowed;\n }\n}\n\n@media (max-width: 640px) {\n .g-chat-input-wrap--expanded {\n inset: 0;\n width: 100vw;\n height: 100vh;\n height: 100dvh;\n border-radius: 0;\n padding: max(0.75rem, env(safe-area-inset-top))\n max(0.75rem, env(safe-area-inset-right))\n max(0.75rem, env(safe-area-inset-bottom))\n max(0.75rem, env(safe-area-inset-left));\n }\n}\n</style>\n","<script lang=\"ts\">\n/**\n * The GNoteInput component provides a rich text editing experience using Tiptap for writing notes. It supports:\n *\n * - **Bold** and *italic* text formatting\n * - Bullet and numbered lists\n * - Configurable heading levels\n * - Always visible toolbar for formatting\n * - Undo/redo support\n *\n * **Note**: This component is part of the `@illinois-grad/grad-vue-rte` package, which includes Tiptap dependencies.\n */\nexport default {};\n</script>\n\n<script lang=\"ts\" setup>\nimport { computed } from \"vue\";\nimport { EditorContent } from \"@tiptap/vue-3\";\nimport type { Level } from \"@tiptap/extension-heading\";\nimport { useRichTextEditor } from \"../composables/useRichTextEditor\";\nimport GRichTextToolbar from \"./editor/GRichTextToolbar.vue\";\n\ndefineOptions({ inheritAttrs: false });\n\ntype Props = {\n /**\n * Placeholder text\n * @demo\n */\n placeholder?: string;\n /**\n * Accessible label\n * @demo\n */\n label?: string;\n /**\n * Heading levels available in the editor toolbar.\n * @demo\n */\n headingLevels?: Level[];\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n placeholder: \"Write a note...\",\n label: \"Note input\",\n});\nconst model = defineModel<object | \"\">();\n\nconst { editor, focusEditor } = useRichTextEditor({\n content: model as any,\n placeholder: computed(() => props.placeholder),\n label: computed(() => props.label),\n multiline: true,\n headingLevels: props.headingLevels,\n});\n\nfunction focusInput() {\n focusEditor();\n}\n\ndefineExpose({ focusInput });\n</script>\n\n<template>\n <div class=\"g-note-input-wrap\">\n <GRichTextToolbar\n :editor=\"editor\"\n :heading-levels=\"headingLevels\"\n class=\"toolbar\"\n />\n <EditorContent :editor=\"editor\" class=\"editor-content\" />\n </div>\n</template>\n\n<style>\n.g-note-input-wrap {\n .tiptap {\n background: transparent;\n border: none;\n padding: 0.5em;\n font-size: 15px;\n min-height: 12em;\n flex: 1;\n outline: none;\n\n p {\n margin: 0.375em 0 0;\n }\n\n > :first-child {\n margin-top: 0;\n }\n > :last-child {\n margin-bottom: 0;\n }\n\n ul,\n ol {\n padding: 0 1em;\n margin: 0.375em 1em 0 0.4em;\n\n li p {\n margin-top: 0;\n margin-bottom: 0;\n }\n }\n p.is-editor-empty:first-child::before {\n color: var(--g-surface-600);\n content: attr(data-placeholder);\n float: left;\n height: 0;\n pointer-events: none;\n }\n }\n}\n.g-note-input-wrap {\n display: flex;\n flex-direction: column;\n background: var(--g-surface-0);\n border: 2px solid var(--g-primary-500);\n border-radius: 4px;\n\n &:has(.ProseMirror-focused) {\n outline: 2px solid var(--g-primary-500);\n outline-offset: 2px;\n box-shadow: 0 0 0 2px var(--g-info-200);\n border-color: var(--g-info-200);\n }\n}\n\n.toolbar {\n border-bottom: 1px solid var(--g-surface-200);\n background-color: var(--g-surface-0);\n padding: 0.25rem;\n\n button {\n border-radius: 4px;\n\n &:focus {\n outline: 2px solid var(--g-primary-500);\n outline-offset: 2px;\n }\n }\n}\n\n.editor-content {\n flex: 1;\n min-width: 0;\n}\n</style>\n","<script lang=\"ts\">\n/**\n * The GNoteInput component provides a rich text editing experience using Tiptap for writing notes. It supports:\n *\n * - **Bold** and *italic* text formatting\n * - Bullet and numbered lists\n * - Configurable heading levels\n * - Always visible toolbar for formatting\n * - Undo/redo support\n *\n * **Note**: This component is part of the `@illinois-grad/grad-vue-rte` package, which includes Tiptap dependencies.\n */\nexport default {};\n</script>\n\n<script lang=\"ts\" setup>\nimport { computed } from \"vue\";\nimport { EditorContent } from \"@tiptap/vue-3\";\nimport type { Level } from \"@tiptap/extension-heading\";\nimport { useRichTextEditor } from \"../composables/useRichTextEditor\";\nimport GRichTextToolbar from \"./editor/GRichTextToolbar.vue\";\n\ndefineOptions({ inheritAttrs: false });\n\ntype Props = {\n /**\n * Placeholder text\n * @demo\n */\n placeholder?: string;\n /**\n * Accessible label\n * @demo\n */\n label?: string;\n /**\n * Heading levels available in the editor toolbar.\n * @demo\n */\n headingLevels?: Level[];\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n placeholder: \"Write a note...\",\n label: \"Note input\",\n});\nconst model = defineModel<object | \"\">();\n\nconst { editor, focusEditor } = useRichTextEditor({\n content: model as any,\n placeholder: computed(() => props.placeholder),\n label: computed(() => props.label),\n multiline: true,\n headingLevels: props.headingLevels,\n});\n\nfunction focusInput() {\n focusEditor();\n}\n\ndefineExpose({ focusInput });\n</script>\n\n<template>\n <div class=\"g-note-input-wrap\">\n <GRichTextToolbar\n :editor=\"editor\"\n :heading-levels=\"headingLevels\"\n class=\"toolbar\"\n />\n <EditorContent :editor=\"editor\" class=\"editor-content\" />\n </div>\n</template>\n\n<style>\n.g-note-input-wrap {\n .tiptap {\n background: transparent;\n border: none;\n padding: 0.5em;\n font-size: 15px;\n min-height: 12em;\n flex: 1;\n outline: none;\n\n p {\n margin: 0.375em 0 0;\n }\n\n > :first-child {\n margin-top: 0;\n }\n > :last-child {\n margin-bottom: 0;\n }\n\n ul,\n ol {\n padding: 0 1em;\n margin: 0.375em 1em 0 0.4em;\n\n li p {\n margin-top: 0;\n margin-bottom: 0;\n }\n }\n p.is-editor-empty:first-child::before {\n color: var(--g-surface-600);\n content: attr(data-placeholder);\n float: left;\n height: 0;\n pointer-events: none;\n }\n }\n}\n.g-note-input-wrap {\n display: flex;\n flex-direction: column;\n background: var(--g-surface-0);\n border: 2px solid var(--g-primary-500);\n border-radius: 4px;\n\n &:has(.ProseMirror-focused) {\n outline: 2px solid var(--g-primary-500);\n outline-offset: 2px;\n box-shadow: 0 0 0 2px var(--g-info-200);\n border-color: var(--g-info-200);\n }\n}\n\n.toolbar {\n border-bottom: 1px solid var(--g-surface-200);\n background-color: var(--g-surface-0);\n padding: 0.25rem;\n\n button {\n border-radius: 4px;\n\n &:focus {\n outline: 2px solid var(--g-primary-500);\n outline-offset: 2px;\n }\n }\n}\n\n.editor-content {\n flex: 1;\n min-width: 0;\n}\n</style>\n","import { computed, onMounted, ref, type Ref, toValue, watch } from \"vue\";\nimport Document from \"@tiptap/extension-document\";\nimport Paragraph from \"@tiptap/extension-paragraph\";\nimport Text from \"@tiptap/extension-text\";\nimport Bold from \"@tiptap/extension-bold\";\nimport Heading from \"@tiptap/extension-heading\";\nimport Italic from \"@tiptap/extension-italic\";\nimport { ListKit } from \"@tiptap/extension-list\";\nimport { generateHTML } from \"@tiptap/core\";\n\nconst extensions = [Document, Paragraph, Text, Bold, Italic, ListKit, Heading];\n\n/**\n * Composable for rendering a JSON string of tiptap content to HTML.\n * Supports all extensions used by GChatInput and GNoteInput.\n *\n * @param content - A reactive ref or plain string containing JSON-encoded tiptap content\n * @returns `rendered` — rendered HTML string, empty string for empty content, or `null` when rendering fails;\n * `hasError` — `true` when the content cannot be parsed or rendered\n */\nexport function useRichTextRenderer(content: Ref<string>) {\n const rendered = ref<string | null>(\"\");\n\n onMounted(() => {\n watch(content, () => {\n const value = toValue(content);\n if (!value || value.trim() === \"\") {\n return \"\";\n }\n\n try {\n const parsed = JSON.parse(value);\n let html = generateHTML(\n parsed,\n extensions\n );\n rendered.value = html;\n } catch (error) {\n console.error(\"Failed to parse content:\", value);\n console.error(error);\n rendered.value = null;\n }\n }, {immediate: true});\n });\n\n const hasError = computed(() => rendered.value === null);\n\n return { rendered, hasError };\n}\n","<script lang=\"ts\">\n/**\n * Renders a JSON string of tiptap content as HTML.\n * Supports all formatting produced by GChatInput and GNoteInput:\n * bold, italic, headings, ordered lists, and bullet lists.\n *\n * - Empty content is handled gracefully (renders nothing).\n * - Displays an error message when the content cannot be parsed or rendered.\n *\n * The rendering only happens in the client when used with Nuxt.js.\n *\n * **Security note**: rendered HTML is produced by tiptap's `generateHTML`, which only\n * serializes recognized document nodes - it does not inject raw HTML from the JSON.\n *\n * **Note**: This component is part of the `@illinois-grad/grad-vue-rte` package, which includes Tiptap dependencies.\n */\nexport default {};\n</script>\n\n<script setup lang=\"ts\">\nimport { toRef } from \"vue\";\nimport { useRichTextRenderer } from \"../composables/useRichTextRenderer\";\n\ntype Props = {\n /**\n * Error message when rendering fails\n * @demo\n */\n error?: string;\n\n /**\n * JSON-encoded tiptap content string to render.\n */\n content: string;\n}\n\nconst props = defineProps<Props>();\n\nconst contentRef = toRef(props, \"content\");\n\nconst { rendered, hasError } = useRichTextRenderer(contentRef);\n\n</script>\n\n<template>\n <div class=\"g-rich-text-content-wrap\">\n <div v-if=\"hasError\" role=\"alert\" class=\"g-rich-text-content-error\">\n {{ error || 'Failed to render content.' }}\n </div>\n <div v-else-if=\"rendered\" class=\"g-rich-text-content\" v-html=\"rendered\"></div>\n </div>\n</template>\n\n<style>\n.g-rich-text-content {\n p {\n margin: 0.375em 0;\n }\n\n > :first-child {\n margin-top: 0;\n }\n\n > :last-child {\n margin-bottom: 0;\n }\n\n ul,\n ol {\n padding: 0 1em;\n margin: 0.375em 1em 0 0.4em;\n\n li p {\n margin-top: 0;\n margin-bottom: 0;\n }\n }\n}\n\n.g-rich-text-content-error {\n color: var(--g-danger-700);\n font-size: 0.875em;\n}\n</style>\n","<script lang=\"ts\">\n/**\n * Renders a JSON string of tiptap content as HTML.\n * Supports all formatting produced by GChatInput and GNoteInput:\n * bold, italic, headings, ordered lists, and bullet lists.\n *\n * - Empty content is handled gracefully (renders nothing).\n * - Displays an error message when the content cannot be parsed or rendered.\n *\n * The rendering only happens in the client when used with Nuxt.js.\n *\n * **Security note**: rendered HTML is produced by tiptap's `generateHTML`, which only\n * serializes recognized document nodes - it does not inject raw HTML from the JSON.\n *\n * **Note**: This component is part of the `@illinois-grad/grad-vue-rte` package, which includes Tiptap dependencies.\n */\nexport default {};\n</script>\n\n<script setup lang=\"ts\">\nimport { toRef } from \"vue\";\nimport { useRichTextRenderer } from \"../composables/useRichTextRenderer\";\n\ntype Props = {\n /**\n * Error message when rendering fails\n * @demo\n */\n error?: string;\n\n /**\n * JSON-encoded tiptap content string to render.\n */\n content: string;\n}\n\nconst props = defineProps<Props>();\n\nconst contentRef = toRef(props, \"content\");\n\nconst { rendered, hasError } = useRichTextRenderer(contentRef);\n\n</script>\n\n<template>\n <div class=\"g-rich-text-content-wrap\">\n <div v-if=\"hasError\" role=\"alert\" class=\"g-rich-text-content-error\">\n {{ error || 'Failed to render content.' }}\n </div>\n <div v-else-if=\"rendered\" class=\"g-rich-text-content\" v-html=\"rendered\"></div>\n </div>\n</template>\n\n<style>\n.g-rich-text-content {\n p {\n margin: 0.375em 0;\n }\n\n > :first-child {\n margin-top: 0;\n }\n\n > :last-child {\n margin-bottom: 0;\n }\n\n ul,\n ol {\n padding: 0 1em;\n margin: 0.375em 1em 0 0.4em;\n\n li p {\n margin-top: 0;\n margin-bottom: 0;\n }\n }\n}\n\n.g-rich-text-content-error {\n color: var(--g-danger-700);\n font-size: 0.875em;\n}\n</style>\n"],"x_google_ignoreList":[0],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import { i as e, n as t, r as n, t as r } from "./grad-vue-rte-Clrr14QX.js";
1
+ import { i as e, n as t, r as n, t as r } from "./grad-vue-rte-BwR74lve.js";
2
2
  export { e as GChatInput, n as GNoteInput, r as GRichTextContent, t as useRichTextRenderer };
package/dist/plugin.js CHANGED
@@ -1,4 +1,4 @@
1
- import { i as e, r as t, t as n } from "./grad-vue-rte-Clrr14QX.js";
1
+ import { i as e, r as t, t as n } from "./grad-vue-rte-BwR74lve.js";
2
2
  //#region src/plugin.ts
3
3
  var r = { install(r) {
4
4
  r.component("GChatInput", e), r.component("GNoteInput", t), r.component("GRichTextContent", n);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@illinois-grad/grad-vue-rte",
3
- "version": "6.0.13",
3
+ "version": "6.0.14",
4
4
  "description": "Rich text editor components for Graduate College apps using Tiptap.",
5
5
  "keywords": [
6
6
  "vue",
@@ -1 +0,0 @@
1
- {"version":3,"file":"grad-vue-rte-Clrr14QX.js","names":[],"sources":["../../../node_modules/@tiptap/extension-heading/dist/index.js","../src/composables/useRichTextEditor.ts","../src/composables/useToolbarNavigation.ts","../src/components/editor/GRichTextToolbar.vue","../src/components/editor/GRichTextToolbar.vue","../src/components/GChatInput.vue","../src/components/GChatInput.vue","../src/components/GNoteInput.vue","../src/components/GNoteInput.vue","../src/composables/useRichTextRenderer.ts","../src/components/GRichTextContent.vue","../src/components/GRichTextContent.vue"],"sourcesContent":["import { Node, mergeAttributes, textblockTypeInputRule } from \"@tiptap/core\";\n//#region src/heading.ts\n/**\n* This extension allows you to create headings.\n* @see https://www.tiptap.dev/api/nodes/heading\n*/\nconst Heading = Node.create({\n\tname: \"heading\",\n\taddOptions() {\n\t\treturn {\n\t\t\tlevels: [\n\t\t\t\t1,\n\t\t\t\t2,\n\t\t\t\t3,\n\t\t\t\t4,\n\t\t\t\t5,\n\t\t\t\t6\n\t\t\t],\n\t\t\tHTMLAttributes: {}\n\t\t};\n\t},\n\tcontent: \"inline*\",\n\tgroup: \"block\",\n\tdefining: true,\n\taddAttributes() {\n\t\treturn { level: {\n\t\t\tdefault: 1,\n\t\t\trendered: false\n\t\t} };\n\t},\n\tparseHTML() {\n\t\treturn this.options.levels.map((level) => ({\n\t\t\ttag: `h${level}`,\n\t\t\tattrs: { level }\n\t\t}));\n\t},\n\trenderHTML({ node, HTMLAttributes }) {\n\t\treturn [\n\t\t\t`h${this.options.levels.includes(node.attrs.level) ? node.attrs.level : this.options.levels[0]}`,\n\t\t\tmergeAttributes(this.options.HTMLAttributes, HTMLAttributes),\n\t\t\t0\n\t\t];\n\t},\n\tparseMarkdown: (token, helpers) => {\n\t\treturn helpers.createNode(\"heading\", { level: token.depth || 1 }, helpers.parseInline(token.tokens || []));\n\t},\n\trenderMarkdown: (node, h) => {\n\t\tvar _node$attrs;\n\t\tconst level = ((_node$attrs = node.attrs) === null || _node$attrs === void 0 ? void 0 : _node$attrs.level) ? parseInt(node.attrs.level, 10) : 1;\n\t\tconst headingChars = \"#\".repeat(level);\n\t\tif (!node.content) return \"\";\n\t\treturn `${headingChars} ${h.renderChildren(node.content)}`;\n\t},\n\taddCommands() {\n\t\treturn {\n\t\t\tsetHeading: (attributes) => ({ commands }) => {\n\t\t\t\tif (!this.options.levels.includes(attributes.level)) return false;\n\t\t\t\treturn commands.setNode(this.name, attributes);\n\t\t\t},\n\t\t\ttoggleHeading: (attributes) => ({ commands }) => {\n\t\t\t\tif (!this.options.levels.includes(attributes.level)) return false;\n\t\t\t\treturn commands.toggleNode(this.name, \"paragraph\", attributes);\n\t\t\t}\n\t\t};\n\t},\n\taddKeyboardShortcuts() {\n\t\treturn this.options.levels.reduce((items, level) => ({\n\t\t\t...items,\n\t\t\t[`Mod-Alt-${level}`]: () => this.editor.commands.toggleHeading({ level })\n\t\t}), {});\n\t},\n\taddInputRules() {\n\t\treturn this.options.levels.map((level) => {\n\t\t\treturn textblockTypeInputRule({\n\t\t\t\tfind: new RegExp(`^(#{${Math.min(...this.options.levels)},${level}})\\\\s$`),\n\t\t\t\ttype: this.type,\n\t\t\t\tgetAttributes: { level }\n\t\t\t});\n\t\t});\n\t}\n});\n//#endregion\n//#region src/index.ts\nvar src_default = Heading;\n//#endregion\nexport { Heading, src_default as default };\n\n//# sourceMappingURL=index.js.map","import { toRaw, watch, type Ref } from \"vue\";\nimport { useEditor } from \"@tiptap/vue-3\";\nimport Document from \"@tiptap/extension-document\";\nimport Paragraph from \"@tiptap/extension-paragraph\";\nimport Text from \"@tiptap/extension-text\";\nimport Bold from \"@tiptap/extension-bold\";\nimport Heading, { type Level } from \"@tiptap/extension-heading\";\nimport Italic from \"@tiptap/extension-italic\";\nimport { ListKit } from \"@tiptap/extension-list\";\nimport { UndoRedo, Placeholder } from \"@tiptap/extensions\";\n\ninterface UseRichTextEditorOptions {\n content: Ref<object | \"\" | undefined>;\n placeholder: Ref<string> | string;\n label: Ref<string> | string;\n onUpdate?: (editor: any) => void;\n editorProps?: Record<string, any>;\n multiline?: boolean;\n headingLevels?: Level[];\n}\n\nexport function useRichTextEditor(options: UseRichTextEditorOptions) {\n const {\n content,\n placeholder,\n label,\n onUpdate,\n editorProps = {},\n multiline = false,\n headingLevels,\n } = options;\n\n const placeholderValue = typeof placeholder === 'string' ? placeholder : placeholder.value;\n const labelValue = typeof label === 'string' ? label : label.value;\n\n const editor = useEditor({\n content: content.value || \"\",\n extensions: [\n Document,\n Paragraph,\n Text,\n Bold,\n Italic,\n ListKit,\n ...(headingLevels?.length\n ? [Heading.configure({ levels: headingLevels })]\n : []),\n UndoRedo,\n Placeholder.configure({ placeholder: placeholderValue }),\n ],\n editorProps: {\n ...editorProps,\n attributes: {\n \"aria-label\": labelValue,\n \"role\": \"textbox\",\n \"aria-multiline\": multiline ? \"true\" : undefined,\n ...editorProps.attributes,\n },\n },\n onUpdate({ editor }) {\n content.value = editor.getJSON();\n if (onUpdate) {\n onUpdate(editor);\n }\n },\n });\n\n // Watch for label changes and update editor\n watch(\n () => typeof label === 'string' ? label : label.value,\n (val) => {\n if (editor.value) {\n editor.value?.setOptions({\n editorProps: {\n attributes: {\n \"aria-label\": val,\n },\n },\n });\n }\n },\n );\n\n // Watch for content changes and update editor\n watch(\n () => content.value,\n (val) => {\n if (\n editor.value &&\n JSON.stringify(val) !== JSON.stringify(editor.value.getJSON())\n ) {\n editor.value.commands.setContent(toRaw(val) || \"\");\n }\n },\n );\n\n function focusEditor() {\n editor.value?.commands?.focus();\n }\n\n return {\n editor,\n focusEditor,\n };\n}\n","import { ref, type Ref } from \"vue\";\nimport type { Editor } from \"@tiptap/vue-3\";\n\nexport function useToolbarNavigation(editor: Ref<Editor | undefined>, toolbarRef: Ref<HTMLElement | null>) {\n const activeButtonIndex = ref(0);\n\n function handleToolbarKeyDown(event: KeyboardEvent) {\n const toolbar = toolbarRef.value;\n if (!toolbar) return;\n\n const buttons = Array.from(\n toolbar.querySelectorAll(\"button\"),\n ) as HTMLButtonElement[];\n const currentIndex = buttons.findIndex(\n (btn) => btn === document.activeElement,\n );\n\n // Handle Escape key - return focus to editor\n if (event.key === \"Escape\") {\n event.preventDefault();\n editor.value?.commands?.focus();\n return;\n }\n\n // Don't handle Tab - let it exit the toolbar naturally\n if (event.key === \"Tab\") {\n return;\n }\n\n let nextIndex = currentIndex;\n\n switch (event.key) {\n case \"ArrowRight\":\n case \"ArrowDown\":\n event.preventDefault();\n nextIndex =\n currentIndex < buttons.length - 1 ? currentIndex + 1 : 0;\n break;\n case \"ArrowLeft\":\n case \"ArrowUp\":\n event.preventDefault();\n nextIndex =\n currentIndex > 0 ? currentIndex - 1 : buttons.length - 1;\n break;\n case \"Home\":\n event.preventDefault();\n nextIndex = 0;\n break;\n case \"End\":\n event.preventDefault();\n nextIndex = buttons.length - 1;\n break;\n default:\n return;\n }\n\n // Update active button index and focus\n activeButtonIndex.value = nextIndex;\n buttons[nextIndex]?.focus();\n }\n\n function getButtonTabIndex(index: number): number {\n return index === activeButtonIndex.value ? 0 : -1;\n }\n\n return {\n activeButtonIndex,\n handleToolbarKeyDown,\n getButtonTabIndex,\n };\n}\n","<script lang=\"ts\" setup>\nimport { ref, computed } from \"vue\";\nimport type { Editor } from \"@tiptap/vue-3\";\nimport type { Level } from \"@tiptap/extension-heading\";\nimport { useToolbarNavigation } from \"../../composables/useToolbarNavigation.ts\";\n\ninterface Props {\n editor: Editor | undefined;\n headingLevels?: Level[];\n}\n\nconst props = defineProps<Props>();\n\nconst toolbarRef = ref<HTMLElement | null>(null);\nconst editorRef = computed(() => props.editor);\nconst { handleToolbarKeyDown, getButtonTabIndex } = useToolbarNavigation(\n editorRef,\n toolbarRef,\n);\n</script>\n\n<template>\n <div\n v-if=\"editor\"\n class=\"g-rich-text-toolbar\"\n role=\"toolbar\"\n aria-label=\"Text formatting\"\n ref=\"toolbarRef\"\n @keydown=\"handleToolbarKeyDown\"\n tabindex=\"-1\"\n >\n <button\n @click=\"editor.chain().focus().toggleBold().run()\"\n :class=\"{\n bold: true,\n 'is-active': editor.isActive('bold'),\n }\"\n :aria-pressed=\"editor.isActive('bold')\"\n title=\"Bold\"\n aria-label=\"Bold\"\n type=\"button\"\n :tabindex=\"getButtonTabIndex(0)\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 384 512\"\n width=\"16\"\n height=\"16\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M0 64C0 46.3 14.3 32 32 32H80 96 224c70.7 0 128 57.3 128 128c0 31.3-11.3 60.1-30 82.3c37.1 22.4 62 63.1 62 109.7c0 70.7-57.3 128-128 128H96 80 32c-17.7 0-32-14.3-32-32s14.3-32 32-32H48V256 96H32C14.3 96 0 81.7 0 64zM224 224c35.3 0 64-28.7 64-64s-28.7-64-64-64H112V224H224zM112 288V416H256c35.3 0 64-28.7 64-64s-28.7-64-64-64H224 112z\"\n />\n </svg>\n </button>\n <button\n @click=\"editor.chain().focus().toggleItalic().run()\"\n :class=\"{\n italic: true,\n 'is-active': editor.isActive('italic'),\n }\"\n :aria-pressed=\"editor.isActive('italic')\"\n title=\"Italic\"\n aria-label=\"Italic\"\n type=\"button\"\n :tabindex=\"getButtonTabIndex(1)\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 384 512\"\n width=\"16\"\n height=\"16\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M128 64c0-17.7 14.3-32 32-32H352c17.7 0 32 14.3 32 32s-14.3 32-32 32H293.3L160 416h64c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H90.7L224 96H160c-17.7 0-32-14.3-32-32z\"\n />\n </svg>\n </button>\n <button\n @click=\"editor.chain().focus().toggleOrderedList().run()\"\n :class=\"{ 'is-active': editor.isActive('orderedList') }\"\n :aria-pressed=\"editor.isActive('orderedList')\"\n title=\"Ordered List\"\n aria-label=\"Ordered List\"\n type=\"button\"\n :tabindex=\"getButtonTabIndex(2)\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 512 512\"\n width=\"16\"\n height=\"16\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M24 56c0-13.3 10.7-24 24-24H80c13.3 0 24 10.7 24 24V176h16c13.3 0 24 10.7 24 24s-10.7 24-24 24H48c-13.3 0-24-10.7-24-24s10.7-24 24-24H64V80H48C34.7 80 24 69.3 24 56zM86.7 341.2c-6.5-7.4-18.3-6.9-24 1.2L51.5 357.9c-7.7 10.8-22.7 13.3-33.5 5.6s-13.3-22.7-5.6-33.5l11.1-15.6c23.7-33.2 72.3-35.6 99.2-4.9c21.3 24.4 20.8 60.9-1.1 84.7L86.8 432H120c13.3 0 24 10.7 24 24s-10.7 24-24 24H48c-9.5 0-18.2-5.6-22-14.4s-2.1-18.9 4.3-25.9l72-78c5.3-5.8 5.4-14.6 .3-20.5zM224 64H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H224c-17.7 0-32-14.3-32-32s14.3-32 32-32zm0 160H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H224c-17.7 0-32-14.3-32-32s14.3-32 32-32zm0 160H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H224c-17.7 0-32-14.3-32-32s14.3-32 32-32z\"\n />\n </svg>\n </button>\n <button\n @click=\"editor.chain().focus().toggleBulletList().run()\"\n :class=\"{ 'is-active': editor.isActive('bulletList') }\"\n :aria-pressed=\"editor.isActive('bulletList')\"\n title=\"Unordered List\"\n aria-label=\"Unordered List\"\n type=\"button\"\n :tabindex=\"getButtonTabIndex(3)\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 512 512\"\n width=\"16\"\n height=\"16\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M40 48C26.7 48 16 58.7 16 72v48c0 13.3 10.7 24 24 24H88c13.3 0 24-10.7 24-24V72c0-13.3-10.7-24-24-24H40zM192 64c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zM16 232v48c0 13.3 10.7 24 24 24H88c13.3 0 24-10.7 24-24V232c0-13.3-10.7-24-24-24H40c-13.3 0-24 10.7-24 24zM40 368c-13.3 0-24 10.7-24 24v48c0 13.3 10.7 24 24 24H88c13.3 0 24-10.7 24-24V392c0-13.3-10.7-24-24-24H40z\"\n />\n </svg>\n </button>\n <button\n v-for=\"(level, index) in headingLevels\"\n :key=\"level\"\n @click=\"editor.chain().focus().toggleHeading({ level }).run()\"\n :class=\"{\n 'heading-button': true,\n 'is-active': editor.isActive('heading', { level }),\n }\"\n :aria-pressed=\"editor.isActive('heading', { level })\"\n :title=\"`Heading ${level}`\"\n :aria-label=\"`Heading ${level}`\"\n type=\"button\"\n :tabindex=\"getButtonTabIndex(4 + index)\"\n >\n <span aria-hidden=\"true\">H{{ level }}</span>\n </button>\n </div>\n</template>\n\n<style>\n.g-rich-text-toolbar {\n display: flex;\n\n button {\n background-color: unset;\n padding: 0.4rem 0.5rem;\n font-size: 0.875rem;\n border: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n\n &.bold {\n font-weight: 700;\n }\n &.italic {\n font-style: italic;\n }\n\n &.is-active {\n color: var(--g-accent-700);\n background-color: var(--g-surface-150);\n }\n\n &:hover {\n background-color: var(--g-primary-300);\n color: var(--g-primary-text);\n }\n }\n\n .heading-button {\n font-weight: 700;\n }\n}\n</style>\n","<script lang=\"ts\" setup>\nimport { ref, computed } from \"vue\";\nimport type { Editor } from \"@tiptap/vue-3\";\nimport type { Level } from \"@tiptap/extension-heading\";\nimport { useToolbarNavigation } from \"../../composables/useToolbarNavigation.ts\";\n\ninterface Props {\n editor: Editor | undefined;\n headingLevels?: Level[];\n}\n\nconst props = defineProps<Props>();\n\nconst toolbarRef = ref<HTMLElement | null>(null);\nconst editorRef = computed(() => props.editor);\nconst { handleToolbarKeyDown, getButtonTabIndex } = useToolbarNavigation(\n editorRef,\n toolbarRef,\n);\n</script>\n\n<template>\n <div\n v-if=\"editor\"\n class=\"g-rich-text-toolbar\"\n role=\"toolbar\"\n aria-label=\"Text formatting\"\n ref=\"toolbarRef\"\n @keydown=\"handleToolbarKeyDown\"\n tabindex=\"-1\"\n >\n <button\n @click=\"editor.chain().focus().toggleBold().run()\"\n :class=\"{\n bold: true,\n 'is-active': editor.isActive('bold'),\n }\"\n :aria-pressed=\"editor.isActive('bold')\"\n title=\"Bold\"\n aria-label=\"Bold\"\n type=\"button\"\n :tabindex=\"getButtonTabIndex(0)\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 384 512\"\n width=\"16\"\n height=\"16\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M0 64C0 46.3 14.3 32 32 32H80 96 224c70.7 0 128 57.3 128 128c0 31.3-11.3 60.1-30 82.3c37.1 22.4 62 63.1 62 109.7c0 70.7-57.3 128-128 128H96 80 32c-17.7 0-32-14.3-32-32s14.3-32 32-32H48V256 96H32C14.3 96 0 81.7 0 64zM224 224c35.3 0 64-28.7 64-64s-28.7-64-64-64H112V224H224zM112 288V416H256c35.3 0 64-28.7 64-64s-28.7-64-64-64H224 112z\"\n />\n </svg>\n </button>\n <button\n @click=\"editor.chain().focus().toggleItalic().run()\"\n :class=\"{\n italic: true,\n 'is-active': editor.isActive('italic'),\n }\"\n :aria-pressed=\"editor.isActive('italic')\"\n title=\"Italic\"\n aria-label=\"Italic\"\n type=\"button\"\n :tabindex=\"getButtonTabIndex(1)\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 384 512\"\n width=\"16\"\n height=\"16\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M128 64c0-17.7 14.3-32 32-32H352c17.7 0 32 14.3 32 32s-14.3 32-32 32H293.3L160 416h64c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H90.7L224 96H160c-17.7 0-32-14.3-32-32z\"\n />\n </svg>\n </button>\n <button\n @click=\"editor.chain().focus().toggleOrderedList().run()\"\n :class=\"{ 'is-active': editor.isActive('orderedList') }\"\n :aria-pressed=\"editor.isActive('orderedList')\"\n title=\"Ordered List\"\n aria-label=\"Ordered List\"\n type=\"button\"\n :tabindex=\"getButtonTabIndex(2)\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 512 512\"\n width=\"16\"\n height=\"16\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M24 56c0-13.3 10.7-24 24-24H80c13.3 0 24 10.7 24 24V176h16c13.3 0 24 10.7 24 24s-10.7 24-24 24H48c-13.3 0-24-10.7-24-24s10.7-24 24-24H64V80H48C34.7 80 24 69.3 24 56zM86.7 341.2c-6.5-7.4-18.3-6.9-24 1.2L51.5 357.9c-7.7 10.8-22.7 13.3-33.5 5.6s-13.3-22.7-5.6-33.5l11.1-15.6c23.7-33.2 72.3-35.6 99.2-4.9c21.3 24.4 20.8 60.9-1.1 84.7L86.8 432H120c13.3 0 24 10.7 24 24s-10.7 24-24 24H48c-9.5 0-18.2-5.6-22-14.4s-2.1-18.9 4.3-25.9l72-78c5.3-5.8 5.4-14.6 .3-20.5zM224 64H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H224c-17.7 0-32-14.3-32-32s14.3-32 32-32zm0 160H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H224c-17.7 0-32-14.3-32-32s14.3-32 32-32zm0 160H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H224c-17.7 0-32-14.3-32-32s14.3-32 32-32z\"\n />\n </svg>\n </button>\n <button\n @click=\"editor.chain().focus().toggleBulletList().run()\"\n :class=\"{ 'is-active': editor.isActive('bulletList') }\"\n :aria-pressed=\"editor.isActive('bulletList')\"\n title=\"Unordered List\"\n aria-label=\"Unordered List\"\n type=\"button\"\n :tabindex=\"getButtonTabIndex(3)\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 512 512\"\n width=\"16\"\n height=\"16\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M40 48C26.7 48 16 58.7 16 72v48c0 13.3 10.7 24 24 24H88c13.3 0 24-10.7 24-24V72c0-13.3-10.7-24-24-24H40zM192 64c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zM16 232v48c0 13.3 10.7 24 24 24H88c13.3 0 24-10.7 24-24V232c0-13.3-10.7-24-24-24H40c-13.3 0-24 10.7-24 24zM40 368c-13.3 0-24 10.7-24 24v48c0 13.3 10.7 24 24 24H88c13.3 0 24-10.7 24-24V392c0-13.3-10.7-24-24-24H40z\"\n />\n </svg>\n </button>\n <button\n v-for=\"(level, index) in headingLevels\"\n :key=\"level\"\n @click=\"editor.chain().focus().toggleHeading({ level }).run()\"\n :class=\"{\n 'heading-button': true,\n 'is-active': editor.isActive('heading', { level }),\n }\"\n :aria-pressed=\"editor.isActive('heading', { level })\"\n :title=\"`Heading ${level}`\"\n :aria-label=\"`Heading ${level}`\"\n type=\"button\"\n :tabindex=\"getButtonTabIndex(4 + index)\"\n >\n <span aria-hidden=\"true\">H{{ level }}</span>\n </button>\n </div>\n</template>\n\n<style>\n.g-rich-text-toolbar {\n display: flex;\n\n button {\n background-color: unset;\n padding: 0.4rem 0.5rem;\n font-size: 0.875rem;\n border: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n\n &.bold {\n font-weight: 700;\n }\n &.italic {\n font-style: italic;\n }\n\n &.is-active {\n color: var(--g-accent-700);\n background-color: var(--g-surface-150);\n }\n\n &:hover {\n background-color: var(--g-primary-300);\n color: var(--g-primary-text);\n }\n }\n\n .heading-button {\n font-weight: 700;\n }\n}\n</style>\n","<script lang=\"ts\">\n/**\n * The GChatInput component provides a rich text editing experience using Tiptap. It supports:\n *\n * - **Bold** and *italic* text formatting\n * - Bullet and numbered lists\n * - Bubble menu for formatting (appears when text is selected)\n * - Press <kbd>Enter</kbd> to send, <kbd>Shift+Enter</kbd> for new line\n * - Optional expanded composer toggled with <kbd>Ctrl+Shift+F</kbd>\n * - Undo/redo support\n *\n * **Note**: This component is part of the `@illinois-grad/grad-vue-rte` package, which includes Tiptap dependencies.\n */\nexport default {};\n</script>\n\n<script lang=\"ts\" setup>\nimport {\n computed,\n nextTick,\n onWatcherCleanup,\n ref,\n useId,\n useTemplateRef,\n watch,\n} from \"vue\";\nimport { useResizeObserver } from \"@vueuse/core\";\nimport { useFocusTrap } from \"@vueuse/integrations/useFocusTrap\";\nimport { EditorContent } from \"@tiptap/vue-3\";\nimport { BubbleMenu } from \"@tiptap/vue-3/menus\";\nimport { useRichTextEditor } from \"../composables/useRichTextEditor\";\nimport GRichTextToolbar from \"./editor/GRichTextToolbar.vue\";\n\ndefineOptions({ inheritAttrs: false });\n\ntype Props = {\n /**\n * Placeholder text\n * @demo\n */\n placeholder?: string;\n /**\n * Disabled\n * @demo\n */\n disabled?: boolean;\n /**\n * Maximum number of text rows shown before the editor scrolls\n * @demo\n */\n maxRows?: number;\n /**\n * Accessible label\n * @demo\n */\n label?: string;\n /**\n * Allow the editor to expand into a large viewport overlay\n * @demo\n */\n expandable?: boolean;\n};\n\nconst props = withDefaults(defineProps<Props>(), {\n placeholder: \"Type a comment\",\n label: \"Comment input\",\n disabled: false,\n maxRows: 5,\n expandable: false,\n});\nconst model = defineModel<object | \"\">();\nconst emit = defineEmits<{ send: [content: object | undefined | null] }>();\nconst expanded = ref(false);\nconst multiline = ref(false);\nconst composer = useTemplateRef<HTMLElement>(\"composer\");\nconst expandButton = useTemplateRef<HTMLButtonElement>(\"expandButton\");\nconst actions = useTemplateRef<HTMLElement>(\"actions\");\nconst composerId = `g-chat-input-${useId()}`;\nconst actionsBelow = computed(() => expanded.value || multiline.value);\nconst composerWidth = ref(0);\nconst maxRowsStyle = computed(() => ({\n \"--g-chat-input-max-rows\": String(\n Number.isFinite(props.maxRows) ? Math.max(1, props.maxRows) : 1,\n ),\n}));\n\nconst { editor, focusEditor } = useRichTextEditor({\n content: model,\n placeholder: computed(() => props.placeholder),\n label: computed(() => props.label),\n multiline: true,\n editorProps: {\n handleKeyDown(view: any, event: KeyboardEvent) {\n if (editor.value && event.key === \"Enter\") {\n if (\n editor.value.isActive(\"orderedList\") ||\n editor.value.isActive(\"bulletList\")\n ) {\n return false;\n }\n if (!event.shiftKey) {\n const content = editor.value.getJSON();\n event.preventDefault();\n onSend(content);\n return true;\n } else {\n editor.value.commands.splitBlock();\n }\n }\n return false;\n },\n attributes: {\n \"aria-keyshortcuts\": \"Shift+Enter\",\n },\n },\n});\n\nfunction onSend(content: object | undefined | null) {\n if (content) {\n emit(\"send\", content);\n }\n}\n\nfunction clickSend() {\n onSend(editor.value?.getJSON());\n}\n\nfunction focusInput() {\n focusEditor();\n}\n\nconst { activate: activateFocusTrap, deactivate: deactivateFocusTrap } =\n useFocusTrap(composer, {\n immediate: false,\n clickOutsideDeactivates: true,\n escapeDeactivates: false,\n fallbackFocus: () => composer.value!,\n initialFocus: () => editor.value?.view.dom ?? composer.value!,\n returnFocusOnDeactivate: false,\n onDeactivate() {\n expanded.value = false;\n },\n });\n\nfunction appendBubbleMenuTo() {\n return composer.value ?? document.body;\n}\n\nfunction updateLayout() {\n if (!editor.value || editor.value.isDestroyed) {\n return;\n }\n\n const editorElement = editor.value.view.dom;\n const composerElement = composer.value;\n const actionsElement = actions.value;\n\n if (\n expanded.value ||\n !editorElement ||\n !composerElement ||\n !actionsElement ||\n editor.value.isEmpty\n ) {\n multiline.value = false;\n return;\n }\n\n if (editor.value.state.doc.childCount > 1) {\n multiline.value = true;\n return;\n }\n\n const textNodes = document.createTreeWalker(\n editorElement,\n NodeFilter.SHOW_TEXT,\n );\n const lineRects: DOMRect[] = [];\n let textNode = textNodes.nextNode();\n\n while (textNode) {\n if (textNode.textContent) {\n const textRange = document.createRange();\n textRange.selectNodeContents(textNode);\n lineRects.push(\n ...Array.from(textRange.getClientRects()).filter(\n (rect) => rect.width || rect.height,\n ),\n );\n }\n textNode = textNodes.nextNode();\n }\n\n const lineTops = lineRects.reduce<number[]>((tops, rect) => {\n if (!tops.some((top) => Math.abs(top - rect.top) < 1)) {\n tops.push(rect.top);\n }\n return tops;\n }, []);\n\n if (lineTops.length > 1) {\n multiline.value = true;\n return;\n }\n\n const composerStyle = getComputedStyle(composerElement);\n const availableInlineWidth =\n composerElement.clientWidth -\n parseFloat(composerStyle.paddingLeft) -\n parseFloat(composerStyle.paddingRight) -\n actionsElement.offsetWidth -\n parseFloat(composerStyle.columnGap || \"0\");\n\n multiline.value =\n (lineRects.reduce((width, rect) => width + rect.width, 0) ||\n editorElement.scrollWidth) >\n availableInlineWidth;\n}\n\nfunction scheduleLayoutUpdate() {\n const frame = requestAnimationFrame(updateLayout);\n onWatcherCleanup(() => cancelAnimationFrame(frame));\n}\n\nfunction expand() {\n if (!props.expandable || props.disabled || expanded.value) {\n return;\n }\n\n expanded.value = true;\n}\n\nfunction collapse(restoreFocus = true) {\n if (!expanded.value) {\n return;\n }\n\n expanded.value = false;\n\n if (restoreFocus) {\n nextTick(() => expandButton.value?.focus()).catch((error) => {\n console.error(error);\n });\n }\n}\n\nfunction toggleExpanded() {\n if (expanded.value) {\n collapse();\n } else {\n expand();\n }\n}\n\nfunction onComposerKeydown(event: KeyboardEvent) {\n if (\n props.expandable &&\n !props.disabled &&\n event.ctrlKey &&\n event.shiftKey &&\n event.key.toLowerCase() === \"f\"\n ) {\n event.preventDefault();\n toggleExpanded();\n return;\n }\n\n if (expanded.value && event.key === \"Escape\") {\n event.preventDefault();\n event.stopPropagation();\n collapse();\n }\n}\n\nwatch(\n () => [props.expandable, props.disabled],\n ([expandable, disabled]) => {\n if (!expandable || disabled) {\n collapse(false);\n }\n },\n);\n\nwatch(\n expanded,\n (isExpanded) => {\n if (isExpanded) {\n activateFocusTrap();\n } else {\n deactivateFocusTrap();\n }\n },\n { flush: \"post\" },\n);\n\nwatch([model, expanded, composerWidth], scheduleLayoutUpdate, {\n flush: \"post\",\n});\n\nuseResizeObserver(composer, ([entry]) => {\n composerWidth.value = entry?.contentRect.width ?? 0;\n});\n\ndefineExpose({ focusInput });\n</script>\n\n<template>\n <div\n :id=\"composerId\"\n ref=\"composer\"\n class=\"g-chat-input-wrap\"\n :class=\"{\n 'g-chat-input-wrap--expanded': expanded,\n 'g-chat-input-wrap--actions-below': actionsBelow,\n }\"\n :style=\"maxRowsStyle\"\n :role=\"expanded ? 'dialog' : undefined\"\n :aria-label=\"expanded ? `${props.label} expanded` : undefined\"\n :tabindex=\"expanded ? -1 : undefined\"\n @keydown.capture=\"onComposerKeydown\"\n >\n <BubbleMenu\n :editor=\"editor\"\n :append-to=\"props.expandable ? appendBubbleMenuTo : undefined\"\n v-if=\"editor\"\n >\n <GRichTextToolbar :editor=\"editor\" class=\"bubble-menu\" />\n </BubbleMenu>\n <EditorContent :editor=\"editor\" class=\"editor-content\" />\n <div ref=\"actions\" class=\"g-chat-actions\">\n <button\n v-if=\"props.expandable\"\n ref=\"expandButton\"\n class=\"g-chat-expand-btn\"\n :disabled=\"props.disabled\"\n :title=\"expanded ? 'Collapse editor' : 'Expand editor'\"\n :aria-label=\"expanded ? 'Collapse editor' : 'Expand editor'\"\n :aria-expanded=\"expanded\"\n :aria-controls=\"composerId\"\n aria-haspopup=\"dialog\"\n aria-keyshortcuts=\"Control+Shift+F\"\n type=\"button\"\n @click=\"toggleExpanded\"\n >\n <svg\n v-if=\"expanded\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 640 640\"\n width=\"18\"\n height=\"18\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <!--!Font Awesome Pro v7.3.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2026 Fonticons, Inc.-->\n <path\n d=\"M105.4 105.4C117.9 92.9 138.2 92.9 150.7 105.4L224 178.7L224 144C224 126.3 238.3 112 256 112C273.7 112 288 126.3 288 144L288 256C288 273.7 273.7 288 256 288L144 288C126.3 288 112 273.7 112 256C112 238.3 126.3 224 144 224L178.7 224L105.3 150.6C92.9 138.1 92.9 117.9 105.4 105.4zM534.7 105.4C547.2 117.9 547.2 138.2 534.7 150.7L461.3 224L496 224C513.7 224 528 238.3 528 256C528 273.7 513.7 288 496 288L384 288C366.3 288 352 273.7 352 256L352 144C352 126.3 366.3 112 384 112C401.7 112 416 126.3 416 144L416 178.7L489.4 105.3C501.9 92.8 522.2 92.8 534.7 105.3zM144 416C126.3 416 112 401.7 112 384C112 366.3 126.3 352 144 352L256 352C273.7 352 288 366.3 288 384L288 496C288 513.7 273.7 528 256 528C238.3 528 224 513.7 224 496L224 461.3L150.6 534.7C138.1 547.2 117.8 547.2 105.3 534.7C92.8 522.2 92.8 501.9 105.3 489.4L178.7 416L144 416zM352 384C352 366.3 366.3 352 384 352L496 352C513.7 352 528 366.3 528 384C528 401.7 513.7 416 496 416L461.3 416L534.7 489.4C547.2 501.9 547.2 522.2 534.7 534.7C522.2 547.2 501.9 547.2 489.4 534.7L416 461.3L416 496C416 513.7 401.7 528 384 528C366.3 528 352 513.7 352 496L352 384z\"\n />\n </svg>\n <svg\n v-else\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 640 640\"\n width=\"18\"\n height=\"18\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <!--!Font Awesome Free v7.3.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.-->\n <path\n d=\"M128 96C110.3 96 96 110.3 96 128L96 224C96 241.7 110.3 256 128 256C145.7 256 160 241.7 160 224L160 160L224 160C241.7 160 256 145.7 256 128C256 110.3 241.7 96 224 96L128 96zM160 416C160 398.3 145.7 384 128 384C110.3 384 96 398.3 96 416L96 512C96 529.7 110.3 544 128 544L224 544C241.7 544 256 529.7 256 512C256 494.3 241.7 480 224 480L160 480L160 416zM416 96C398.3 96 384 110.3 384 128C384 145.7 398.3 160 416 160L480 160L480 224C480 241.7 494.3 256 512 256C529.7 256 544 241.7 544 224L544 128C544 110.3 529.7 96 512 96L416 96zM544 416C544 398.3 529.7 384 512 384C494.3 384 480 398.3 480 416L480 480L416 480C398.3 480 384 494.3 384 512C384 529.7 398.3 544 416 544L512 544C529.7 544 544 529.7 544 512L544 416z\"\n />\n </svg>\n </button>\n <button\n class=\"g-chat-send-btn\"\n :disabled=\"\n props.disabled ||\n !model ||\n (model && Object.keys(model).length === 0)\n \"\n @click=\"clickSend\"\n title=\"Send\"\n aria-label=\"Send\"\n type=\"button\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 512 512\"\n width=\"16\"\n height=\"16\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M498.1 5.6c10.1 7 15.4 19.1 13.5 31.2l-64 416c-1.5 9.7-7.4 18.2-16 23s-18.9 5.4-28 1.6L284 427.7l-68.5 74.1c-8.9 9.7-22.9 12.9-35.2 8.1S160 493.2 160 480V396.4c0-4 1.5-7.8 4.2-10.7L331.8 202.8c5.8-6.3 5.6-16-.4-22s-15.7-6.4-22-.7L106 360.8 17.7 316.6C7.1 311.3 .3 300.7 0 288.9s5.9-22.8 16.1-28.7l448-256c10.7-6.1 23.9-5.5 34 1.4z\"\n />\n </svg>\n </button>\n </div>\n </div>\n</template>\n\n<style>\n.g-chat-input-wrap {\n --g-chat-input-line-height: 1.5;\n --g-chat-input-row-spacing: 0.375em;\n --g-chat-input-max-rows: 5;\n\n .tiptap {\n background: transparent;\n border: none;\n padding: 0.15em 0;\n font-size: 15px;\n line-height: var(--g-chat-input-line-height);\n max-height: calc(\n var(--g-chat-input-max-rows) * var(--g-chat-input-line-height) *\n 1em + (var(--g-chat-input-max-rows) - 1) *\n var(--g-chat-input-row-spacing)\n );\n overflow-y: auto;\n flex: 1;\n outline: none;\n\n p {\n margin: var(--g-chat-input-row-spacing) 0 0;\n }\n\n > :first-child {\n margin-top: 0;\n }\n > :last-child {\n margin-bottom: 0;\n }\n\n ul,\n ol {\n padding: 0 1em;\n margin: var(--g-chat-input-row-spacing) 1em 0 0.4em;\n\n li p {\n margin-top: 0;\n margin-bottom: 0;\n }\n }\n p.is-editor-empty:first-child::before {\n color: var(--g-surface-600);\n content: attr(data-placeholder);\n float: left;\n height: 0;\n pointer-events: none;\n }\n }\n}\n.bubble-menu {\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);\n background-color: var(--g-surface-100);\n\n button {\n &:first-child {\n border-top-left-radius: 4px;\n border-bottom-left-radius: 4px;\n }\n &:last-child {\n border-top-right-radius: 4px;\n border-bottom-right-radius: 4px;\n }\n }\n}\n\n.g-chat-input-wrap {\n position: relative;\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto;\n column-gap: 0.25rem;\n align-items: center;\n background: var(--g-surface-0);\n border: 2px solid var(--g-primary-500);\n border-radius: 4px;\n padding: 0.5em;\n\n &:has(.ProseMirror-focused) {\n outline: 2px solid var(--g-primary-500);\n outline-offset: 2px;\n box-shadow: 0 0 0 2px var(--g-info-200);\n border-color: var(--g-info-200);\n }\n}\n\n.g-chat-input-wrap--expanded {\n position: fixed;\n right: 1rem;\n bottom: 1rem;\n z-index: 1000;\n width: min(48rem, calc(100vw - 2rem));\n height: min(32rem, calc(100dvh - 2rem));\n box-sizing: border-box;\n grid-template-rows: minmax(0, 1fr) auto;\n align-items: stretch;\n padding: 0.75rem;\n box-shadow: var(--il-shadow, 0 10px 30px rgba(0, 0, 0, 0.25));\n\n .tiptap {\n height: 100%;\n max-height: none;\n }\n}\n\n.editor-content {\n min-width: 0;\n min-height: 0;\n}\n\n.g-chat-actions {\n display: flex;\n align-self: end;\n}\n\n.g-chat-input-wrap--actions-below {\n row-gap: 0.25rem;\n\n .editor-content,\n .g-chat-actions {\n grid-column: 1 / -1;\n }\n\n .g-chat-actions {\n justify-self: end;\n }\n}\n\n.g-chat-input-wrap--expanded .editor-content {\n align-self: stretch;\n overflow: hidden;\n}\n\n.g-chat-expand-btn,\n.g-chat-send-btn {\n color: var(--g-primary-500);\n font-size: 1em;\n border: 2px solid transparent;\n border-radius: 4px;\n padding: 0.4em;\n margin: 0;\n align-self: flex-end;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n background: transparent;\n flex-shrink: 0;\n\n &:hover:not(:disabled) {\n color: var(--g-accent-700);\n background-color: var(--g-surface-100);\n }\n\n &:focus:not(:disabled) {\n background-color: var(--g-info-200);\n color: var(--g-primary-500);\n }\n\n &:active:not(:disabled) {\n background-color: var(--g-primary-500);\n color: var(--g-surface-0);\n }\n\n &:disabled {\n color: var(--g-surface-300);\n cursor: not-allowed;\n }\n}\n\n@media (max-width: 640px) {\n .g-chat-input-wrap--expanded {\n inset: 0;\n width: 100vw;\n height: 100vh;\n height: 100dvh;\n border-radius: 0;\n padding: max(0.75rem, env(safe-area-inset-top))\n max(0.75rem, env(safe-area-inset-right))\n max(0.75rem, env(safe-area-inset-bottom))\n max(0.75rem, env(safe-area-inset-left));\n }\n}\n</style>\n","<script lang=\"ts\">\n/**\n * The GChatInput component provides a rich text editing experience using Tiptap. It supports:\n *\n * - **Bold** and *italic* text formatting\n * - Bullet and numbered lists\n * - Bubble menu for formatting (appears when text is selected)\n * - Press <kbd>Enter</kbd> to send, <kbd>Shift+Enter</kbd> for new line\n * - Optional expanded composer toggled with <kbd>Ctrl+Shift+F</kbd>\n * - Undo/redo support\n *\n * **Note**: This component is part of the `@illinois-grad/grad-vue-rte` package, which includes Tiptap dependencies.\n */\nexport default {};\n</script>\n\n<script lang=\"ts\" setup>\nimport {\n computed,\n nextTick,\n onWatcherCleanup,\n ref,\n useId,\n useTemplateRef,\n watch,\n} from \"vue\";\nimport { useResizeObserver } from \"@vueuse/core\";\nimport { useFocusTrap } from \"@vueuse/integrations/useFocusTrap\";\nimport { EditorContent } from \"@tiptap/vue-3\";\nimport { BubbleMenu } from \"@tiptap/vue-3/menus\";\nimport { useRichTextEditor } from \"../composables/useRichTextEditor\";\nimport GRichTextToolbar from \"./editor/GRichTextToolbar.vue\";\n\ndefineOptions({ inheritAttrs: false });\n\ntype Props = {\n /**\n * Placeholder text\n * @demo\n */\n placeholder?: string;\n /**\n * Disabled\n * @demo\n */\n disabled?: boolean;\n /**\n * Maximum number of text rows shown before the editor scrolls\n * @demo\n */\n maxRows?: number;\n /**\n * Accessible label\n * @demo\n */\n label?: string;\n /**\n * Allow the editor to expand into a large viewport overlay\n * @demo\n */\n expandable?: boolean;\n};\n\nconst props = withDefaults(defineProps<Props>(), {\n placeholder: \"Type a comment\",\n label: \"Comment input\",\n disabled: false,\n maxRows: 5,\n expandable: false,\n});\nconst model = defineModel<object | \"\">();\nconst emit = defineEmits<{ send: [content: object | undefined | null] }>();\nconst expanded = ref(false);\nconst multiline = ref(false);\nconst composer = useTemplateRef<HTMLElement>(\"composer\");\nconst expandButton = useTemplateRef<HTMLButtonElement>(\"expandButton\");\nconst actions = useTemplateRef<HTMLElement>(\"actions\");\nconst composerId = `g-chat-input-${useId()}`;\nconst actionsBelow = computed(() => expanded.value || multiline.value);\nconst composerWidth = ref(0);\nconst maxRowsStyle = computed(() => ({\n \"--g-chat-input-max-rows\": String(\n Number.isFinite(props.maxRows) ? Math.max(1, props.maxRows) : 1,\n ),\n}));\n\nconst { editor, focusEditor } = useRichTextEditor({\n content: model,\n placeholder: computed(() => props.placeholder),\n label: computed(() => props.label),\n multiline: true,\n editorProps: {\n handleKeyDown(view: any, event: KeyboardEvent) {\n if (editor.value && event.key === \"Enter\") {\n if (\n editor.value.isActive(\"orderedList\") ||\n editor.value.isActive(\"bulletList\")\n ) {\n return false;\n }\n if (!event.shiftKey) {\n const content = editor.value.getJSON();\n event.preventDefault();\n onSend(content);\n return true;\n } else {\n editor.value.commands.splitBlock();\n }\n }\n return false;\n },\n attributes: {\n \"aria-keyshortcuts\": \"Shift+Enter\",\n },\n },\n});\n\nfunction onSend(content: object | undefined | null) {\n if (content) {\n emit(\"send\", content);\n }\n}\n\nfunction clickSend() {\n onSend(editor.value?.getJSON());\n}\n\nfunction focusInput() {\n focusEditor();\n}\n\nconst { activate: activateFocusTrap, deactivate: deactivateFocusTrap } =\n useFocusTrap(composer, {\n immediate: false,\n clickOutsideDeactivates: true,\n escapeDeactivates: false,\n fallbackFocus: () => composer.value!,\n initialFocus: () => editor.value?.view.dom ?? composer.value!,\n returnFocusOnDeactivate: false,\n onDeactivate() {\n expanded.value = false;\n },\n });\n\nfunction appendBubbleMenuTo() {\n return composer.value ?? document.body;\n}\n\nfunction updateLayout() {\n if (!editor.value || editor.value.isDestroyed) {\n return;\n }\n\n const editorElement = editor.value.view.dom;\n const composerElement = composer.value;\n const actionsElement = actions.value;\n\n if (\n expanded.value ||\n !editorElement ||\n !composerElement ||\n !actionsElement ||\n editor.value.isEmpty\n ) {\n multiline.value = false;\n return;\n }\n\n if (editor.value.state.doc.childCount > 1) {\n multiline.value = true;\n return;\n }\n\n const textNodes = document.createTreeWalker(\n editorElement,\n NodeFilter.SHOW_TEXT,\n );\n const lineRects: DOMRect[] = [];\n let textNode = textNodes.nextNode();\n\n while (textNode) {\n if (textNode.textContent) {\n const textRange = document.createRange();\n textRange.selectNodeContents(textNode);\n lineRects.push(\n ...Array.from(textRange.getClientRects()).filter(\n (rect) => rect.width || rect.height,\n ),\n );\n }\n textNode = textNodes.nextNode();\n }\n\n const lineTops = lineRects.reduce<number[]>((tops, rect) => {\n if (!tops.some((top) => Math.abs(top - rect.top) < 1)) {\n tops.push(rect.top);\n }\n return tops;\n }, []);\n\n if (lineTops.length > 1) {\n multiline.value = true;\n return;\n }\n\n const composerStyle = getComputedStyle(composerElement);\n const availableInlineWidth =\n composerElement.clientWidth -\n parseFloat(composerStyle.paddingLeft) -\n parseFloat(composerStyle.paddingRight) -\n actionsElement.offsetWidth -\n parseFloat(composerStyle.columnGap || \"0\");\n\n multiline.value =\n (lineRects.reduce((width, rect) => width + rect.width, 0) ||\n editorElement.scrollWidth) >\n availableInlineWidth;\n}\n\nfunction scheduleLayoutUpdate() {\n const frame = requestAnimationFrame(updateLayout);\n onWatcherCleanup(() => cancelAnimationFrame(frame));\n}\n\nfunction expand() {\n if (!props.expandable || props.disabled || expanded.value) {\n return;\n }\n\n expanded.value = true;\n}\n\nfunction collapse(restoreFocus = true) {\n if (!expanded.value) {\n return;\n }\n\n expanded.value = false;\n\n if (restoreFocus) {\n nextTick(() => expandButton.value?.focus()).catch((error) => {\n console.error(error);\n });\n }\n}\n\nfunction toggleExpanded() {\n if (expanded.value) {\n collapse();\n } else {\n expand();\n }\n}\n\nfunction onComposerKeydown(event: KeyboardEvent) {\n if (\n props.expandable &&\n !props.disabled &&\n event.ctrlKey &&\n event.shiftKey &&\n event.key.toLowerCase() === \"f\"\n ) {\n event.preventDefault();\n toggleExpanded();\n return;\n }\n\n if (expanded.value && event.key === \"Escape\") {\n event.preventDefault();\n event.stopPropagation();\n collapse();\n }\n}\n\nwatch(\n () => [props.expandable, props.disabled],\n ([expandable, disabled]) => {\n if (!expandable || disabled) {\n collapse(false);\n }\n },\n);\n\nwatch(\n expanded,\n (isExpanded) => {\n if (isExpanded) {\n activateFocusTrap();\n } else {\n deactivateFocusTrap();\n }\n },\n { flush: \"post\" },\n);\n\nwatch([model, expanded, composerWidth], scheduleLayoutUpdate, {\n flush: \"post\",\n});\n\nuseResizeObserver(composer, ([entry]) => {\n composerWidth.value = entry?.contentRect.width ?? 0;\n});\n\ndefineExpose({ focusInput });\n</script>\n\n<template>\n <div\n :id=\"composerId\"\n ref=\"composer\"\n class=\"g-chat-input-wrap\"\n :class=\"{\n 'g-chat-input-wrap--expanded': expanded,\n 'g-chat-input-wrap--actions-below': actionsBelow,\n }\"\n :style=\"maxRowsStyle\"\n :role=\"expanded ? 'dialog' : undefined\"\n :aria-label=\"expanded ? `${props.label} expanded` : undefined\"\n :tabindex=\"expanded ? -1 : undefined\"\n @keydown.capture=\"onComposerKeydown\"\n >\n <BubbleMenu\n :editor=\"editor\"\n :append-to=\"props.expandable ? appendBubbleMenuTo : undefined\"\n v-if=\"editor\"\n >\n <GRichTextToolbar :editor=\"editor\" class=\"bubble-menu\" />\n </BubbleMenu>\n <EditorContent :editor=\"editor\" class=\"editor-content\" />\n <div ref=\"actions\" class=\"g-chat-actions\">\n <button\n v-if=\"props.expandable\"\n ref=\"expandButton\"\n class=\"g-chat-expand-btn\"\n :disabled=\"props.disabled\"\n :title=\"expanded ? 'Collapse editor' : 'Expand editor'\"\n :aria-label=\"expanded ? 'Collapse editor' : 'Expand editor'\"\n :aria-expanded=\"expanded\"\n :aria-controls=\"composerId\"\n aria-haspopup=\"dialog\"\n aria-keyshortcuts=\"Control+Shift+F\"\n type=\"button\"\n @click=\"toggleExpanded\"\n >\n <svg\n v-if=\"expanded\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 640 640\"\n width=\"18\"\n height=\"18\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <!--!Font Awesome Pro v7.3.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2026 Fonticons, Inc.-->\n <path\n d=\"M105.4 105.4C117.9 92.9 138.2 92.9 150.7 105.4L224 178.7L224 144C224 126.3 238.3 112 256 112C273.7 112 288 126.3 288 144L288 256C288 273.7 273.7 288 256 288L144 288C126.3 288 112 273.7 112 256C112 238.3 126.3 224 144 224L178.7 224L105.3 150.6C92.9 138.1 92.9 117.9 105.4 105.4zM534.7 105.4C547.2 117.9 547.2 138.2 534.7 150.7L461.3 224L496 224C513.7 224 528 238.3 528 256C528 273.7 513.7 288 496 288L384 288C366.3 288 352 273.7 352 256L352 144C352 126.3 366.3 112 384 112C401.7 112 416 126.3 416 144L416 178.7L489.4 105.3C501.9 92.8 522.2 92.8 534.7 105.3zM144 416C126.3 416 112 401.7 112 384C112 366.3 126.3 352 144 352L256 352C273.7 352 288 366.3 288 384L288 496C288 513.7 273.7 528 256 528C238.3 528 224 513.7 224 496L224 461.3L150.6 534.7C138.1 547.2 117.8 547.2 105.3 534.7C92.8 522.2 92.8 501.9 105.3 489.4L178.7 416L144 416zM352 384C352 366.3 366.3 352 384 352L496 352C513.7 352 528 366.3 528 384C528 401.7 513.7 416 496 416L461.3 416L534.7 489.4C547.2 501.9 547.2 522.2 534.7 534.7C522.2 547.2 501.9 547.2 489.4 534.7L416 461.3L416 496C416 513.7 401.7 528 384 528C366.3 528 352 513.7 352 496L352 384z\"\n />\n </svg>\n <svg\n v-else\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 640 640\"\n width=\"18\"\n height=\"18\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <!--!Font Awesome Free v7.3.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.-->\n <path\n d=\"M128 96C110.3 96 96 110.3 96 128L96 224C96 241.7 110.3 256 128 256C145.7 256 160 241.7 160 224L160 160L224 160C241.7 160 256 145.7 256 128C256 110.3 241.7 96 224 96L128 96zM160 416C160 398.3 145.7 384 128 384C110.3 384 96 398.3 96 416L96 512C96 529.7 110.3 544 128 544L224 544C241.7 544 256 529.7 256 512C256 494.3 241.7 480 224 480L160 480L160 416zM416 96C398.3 96 384 110.3 384 128C384 145.7 398.3 160 416 160L480 160L480 224C480 241.7 494.3 256 512 256C529.7 256 544 241.7 544 224L544 128C544 110.3 529.7 96 512 96L416 96zM544 416C544 398.3 529.7 384 512 384C494.3 384 480 398.3 480 416L480 480L416 480C398.3 480 384 494.3 384 512C384 529.7 398.3 544 416 544L512 544C529.7 544 544 529.7 544 512L544 416z\"\n />\n </svg>\n </button>\n <button\n class=\"g-chat-send-btn\"\n :disabled=\"\n props.disabled ||\n !model ||\n (model && Object.keys(model).length === 0)\n \"\n @click=\"clickSend\"\n title=\"Send\"\n aria-label=\"Send\"\n type=\"button\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 512 512\"\n width=\"16\"\n height=\"16\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M498.1 5.6c10.1 7 15.4 19.1 13.5 31.2l-64 416c-1.5 9.7-7.4 18.2-16 23s-18.9 5.4-28 1.6L284 427.7l-68.5 74.1c-8.9 9.7-22.9 12.9-35.2 8.1S160 493.2 160 480V396.4c0-4 1.5-7.8 4.2-10.7L331.8 202.8c5.8-6.3 5.6-16-.4-22s-15.7-6.4-22-.7L106 360.8 17.7 316.6C7.1 311.3 .3 300.7 0 288.9s5.9-22.8 16.1-28.7l448-256c10.7-6.1 23.9-5.5 34 1.4z\"\n />\n </svg>\n </button>\n </div>\n </div>\n</template>\n\n<style>\n.g-chat-input-wrap {\n --g-chat-input-line-height: 1.5;\n --g-chat-input-row-spacing: 0.375em;\n --g-chat-input-max-rows: 5;\n\n .tiptap {\n background: transparent;\n border: none;\n padding: 0.15em 0;\n font-size: 15px;\n line-height: var(--g-chat-input-line-height);\n max-height: calc(\n var(--g-chat-input-max-rows) * var(--g-chat-input-line-height) *\n 1em + (var(--g-chat-input-max-rows) - 1) *\n var(--g-chat-input-row-spacing)\n );\n overflow-y: auto;\n flex: 1;\n outline: none;\n\n p {\n margin: var(--g-chat-input-row-spacing) 0 0;\n }\n\n > :first-child {\n margin-top: 0;\n }\n > :last-child {\n margin-bottom: 0;\n }\n\n ul,\n ol {\n padding: 0 1em;\n margin: var(--g-chat-input-row-spacing) 1em 0 0.4em;\n\n li p {\n margin-top: 0;\n margin-bottom: 0;\n }\n }\n p.is-editor-empty:first-child::before {\n color: var(--g-surface-600);\n content: attr(data-placeholder);\n float: left;\n height: 0;\n pointer-events: none;\n }\n }\n}\n.bubble-menu {\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);\n background-color: var(--g-surface-100);\n\n button {\n &:first-child {\n border-top-left-radius: 4px;\n border-bottom-left-radius: 4px;\n }\n &:last-child {\n border-top-right-radius: 4px;\n border-bottom-right-radius: 4px;\n }\n }\n}\n\n.g-chat-input-wrap {\n position: relative;\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto;\n column-gap: 0.25rem;\n align-items: center;\n background: var(--g-surface-0);\n border: 2px solid var(--g-primary-500);\n border-radius: 4px;\n padding: 0.5em;\n\n &:has(.ProseMirror-focused) {\n outline: 2px solid var(--g-primary-500);\n outline-offset: 2px;\n box-shadow: 0 0 0 2px var(--g-info-200);\n border-color: var(--g-info-200);\n }\n}\n\n.g-chat-input-wrap--expanded {\n position: fixed;\n right: 1rem;\n bottom: 1rem;\n z-index: 1000;\n width: min(48rem, calc(100vw - 2rem));\n height: min(32rem, calc(100dvh - 2rem));\n box-sizing: border-box;\n grid-template-rows: minmax(0, 1fr) auto;\n align-items: stretch;\n padding: 0.75rem;\n box-shadow: var(--il-shadow, 0 10px 30px rgba(0, 0, 0, 0.25));\n\n .tiptap {\n height: 100%;\n max-height: none;\n }\n}\n\n.editor-content {\n min-width: 0;\n min-height: 0;\n}\n\n.g-chat-actions {\n display: flex;\n align-self: end;\n}\n\n.g-chat-input-wrap--actions-below {\n row-gap: 0.25rem;\n\n .editor-content,\n .g-chat-actions {\n grid-column: 1 / -1;\n }\n\n .g-chat-actions {\n justify-self: end;\n }\n}\n\n.g-chat-input-wrap--expanded .editor-content {\n align-self: stretch;\n overflow: hidden;\n}\n\n.g-chat-expand-btn,\n.g-chat-send-btn {\n color: var(--g-primary-500);\n font-size: 1em;\n border: 2px solid transparent;\n border-radius: 4px;\n padding: 0.4em;\n margin: 0;\n align-self: flex-end;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n background: transparent;\n flex-shrink: 0;\n\n &:hover:not(:disabled) {\n color: var(--g-accent-700);\n background-color: var(--g-surface-100);\n }\n\n &:focus:not(:disabled) {\n background-color: var(--g-info-200);\n color: var(--g-primary-500);\n }\n\n &:active:not(:disabled) {\n background-color: var(--g-primary-500);\n color: var(--g-surface-0);\n }\n\n &:disabled {\n color: var(--g-surface-300);\n cursor: not-allowed;\n }\n}\n\n@media (max-width: 640px) {\n .g-chat-input-wrap--expanded {\n inset: 0;\n width: 100vw;\n height: 100vh;\n height: 100dvh;\n border-radius: 0;\n padding: max(0.75rem, env(safe-area-inset-top))\n max(0.75rem, env(safe-area-inset-right))\n max(0.75rem, env(safe-area-inset-bottom))\n max(0.75rem, env(safe-area-inset-left));\n }\n}\n</style>\n","<script lang=\"ts\">\n/**\n * The GNoteInput component provides a rich text editing experience using Tiptap for writing notes. It supports:\n *\n * - **Bold** and *italic* text formatting\n * - Bullet and numbered lists\n * - Configurable heading levels\n * - Always visible toolbar for formatting\n * - Undo/redo support\n *\n * **Note**: This component is part of the `@illinois-grad/grad-vue-rte` package, which includes Tiptap dependencies.\n */\nexport default {};\n</script>\n\n<script lang=\"ts\" setup>\nimport { computed } from \"vue\";\nimport { EditorContent } from \"@tiptap/vue-3\";\nimport type { Level } from \"@tiptap/extension-heading\";\nimport { useRichTextEditor } from \"../composables/useRichTextEditor\";\nimport GRichTextToolbar from \"./editor/GRichTextToolbar.vue\";\n\ndefineOptions({ inheritAttrs: false });\n\ntype Props = {\n /**\n * Placeholder text\n * @demo\n */\n placeholder?: string;\n /**\n * Accessible label\n * @demo\n */\n label?: string;\n /**\n * Heading levels available in the editor toolbar.\n * @demo\n */\n headingLevels?: Level[];\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n placeholder: \"Write a note...\",\n label: \"Note input\",\n});\nconst model = defineModel<object | \"\">();\n\nconst { editor, focusEditor } = useRichTextEditor({\n content: model as any,\n placeholder: computed(() => props.placeholder),\n label: computed(() => props.label),\n multiline: true,\n headingLevels: props.headingLevels,\n});\n\nfunction focusInput() {\n focusEditor();\n}\n\ndefineExpose({ focusInput });\n</script>\n\n<template>\n <div class=\"g-note-input-wrap\">\n <GRichTextToolbar\n :editor=\"editor\"\n :heading-levels=\"headingLevels\"\n class=\"toolbar\"\n />\n <EditorContent :editor=\"editor\" class=\"editor-content\" />\n </div>\n</template>\n\n<style>\n.g-note-input-wrap {\n .tiptap {\n background: transparent;\n border: none;\n padding: 0.5em;\n font-size: 15px;\n min-height: 12em;\n flex: 1;\n outline: none;\n\n p {\n margin: 0.375em 0 0;\n }\n\n > :first-child {\n margin-top: 0;\n }\n > :last-child {\n margin-bottom: 0;\n }\n\n ul,\n ol {\n padding: 0 1em;\n margin: 0.375em 1em 0 0.4em;\n\n li p {\n margin-top: 0;\n margin-bottom: 0;\n }\n }\n p.is-editor-empty:first-child::before {\n color: var(--g-surface-600);\n content: attr(data-placeholder);\n float: left;\n height: 0;\n pointer-events: none;\n }\n }\n}\n.g-note-input-wrap {\n display: flex;\n flex-direction: column;\n background: var(--g-surface-0);\n border: 2px solid var(--g-primary-500);\n border-radius: 4px;\n\n &:has(.ProseMirror-focused) {\n outline: 2px solid var(--g-primary-500);\n outline-offset: 2px;\n box-shadow: 0 0 0 2px var(--g-info-200);\n border-color: var(--g-info-200);\n }\n}\n\n.toolbar {\n border-bottom: 1px solid var(--g-surface-200);\n background-color: var(--g-surface-0);\n padding: 0.25rem;\n\n button {\n border-radius: 4px;\n\n &:focus {\n outline: 2px solid var(--g-primary-500);\n outline-offset: 2px;\n }\n }\n}\n\n.editor-content {\n flex: 1;\n min-width: 0;\n}\n</style>\n","<script lang=\"ts\">\n/**\n * The GNoteInput component provides a rich text editing experience using Tiptap for writing notes. It supports:\n *\n * - **Bold** and *italic* text formatting\n * - Bullet and numbered lists\n * - Configurable heading levels\n * - Always visible toolbar for formatting\n * - Undo/redo support\n *\n * **Note**: This component is part of the `@illinois-grad/grad-vue-rte` package, which includes Tiptap dependencies.\n */\nexport default {};\n</script>\n\n<script lang=\"ts\" setup>\nimport { computed } from \"vue\";\nimport { EditorContent } from \"@tiptap/vue-3\";\nimport type { Level } from \"@tiptap/extension-heading\";\nimport { useRichTextEditor } from \"../composables/useRichTextEditor\";\nimport GRichTextToolbar from \"./editor/GRichTextToolbar.vue\";\n\ndefineOptions({ inheritAttrs: false });\n\ntype Props = {\n /**\n * Placeholder text\n * @demo\n */\n placeholder?: string;\n /**\n * Accessible label\n * @demo\n */\n label?: string;\n /**\n * Heading levels available in the editor toolbar.\n * @demo\n */\n headingLevels?: Level[];\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n placeholder: \"Write a note...\",\n label: \"Note input\",\n});\nconst model = defineModel<object | \"\">();\n\nconst { editor, focusEditor } = useRichTextEditor({\n content: model as any,\n placeholder: computed(() => props.placeholder),\n label: computed(() => props.label),\n multiline: true,\n headingLevels: props.headingLevels,\n});\n\nfunction focusInput() {\n focusEditor();\n}\n\ndefineExpose({ focusInput });\n</script>\n\n<template>\n <div class=\"g-note-input-wrap\">\n <GRichTextToolbar\n :editor=\"editor\"\n :heading-levels=\"headingLevels\"\n class=\"toolbar\"\n />\n <EditorContent :editor=\"editor\" class=\"editor-content\" />\n </div>\n</template>\n\n<style>\n.g-note-input-wrap {\n .tiptap {\n background: transparent;\n border: none;\n padding: 0.5em;\n font-size: 15px;\n min-height: 12em;\n flex: 1;\n outline: none;\n\n p {\n margin: 0.375em 0 0;\n }\n\n > :first-child {\n margin-top: 0;\n }\n > :last-child {\n margin-bottom: 0;\n }\n\n ul,\n ol {\n padding: 0 1em;\n margin: 0.375em 1em 0 0.4em;\n\n li p {\n margin-top: 0;\n margin-bottom: 0;\n }\n }\n p.is-editor-empty:first-child::before {\n color: var(--g-surface-600);\n content: attr(data-placeholder);\n float: left;\n height: 0;\n pointer-events: none;\n }\n }\n}\n.g-note-input-wrap {\n display: flex;\n flex-direction: column;\n background: var(--g-surface-0);\n border: 2px solid var(--g-primary-500);\n border-radius: 4px;\n\n &:has(.ProseMirror-focused) {\n outline: 2px solid var(--g-primary-500);\n outline-offset: 2px;\n box-shadow: 0 0 0 2px var(--g-info-200);\n border-color: var(--g-info-200);\n }\n}\n\n.toolbar {\n border-bottom: 1px solid var(--g-surface-200);\n background-color: var(--g-surface-0);\n padding: 0.25rem;\n\n button {\n border-radius: 4px;\n\n &:focus {\n outline: 2px solid var(--g-primary-500);\n outline-offset: 2px;\n }\n }\n}\n\n.editor-content {\n flex: 1;\n min-width: 0;\n}\n</style>\n","import { computed, onMounted, ref, type Ref, toValue, watch } from \"vue\";\nimport Document from \"@tiptap/extension-document\";\nimport Paragraph from \"@tiptap/extension-paragraph\";\nimport Text from \"@tiptap/extension-text\";\nimport Bold from \"@tiptap/extension-bold\";\nimport Heading from \"@tiptap/extension-heading\";\nimport Italic from \"@tiptap/extension-italic\";\nimport { ListKit } from \"@tiptap/extension-list\";\nimport { generateHTML } from \"@tiptap/core\";\n\nconst extensions = [Document, Paragraph, Text, Bold, Italic, ListKit, Heading];\n\n/**\n * Composable for rendering a JSON string of tiptap content to HTML.\n * Supports all extensions used by GChatInput and GNoteInput.\n *\n * @param content - A reactive ref or plain string containing JSON-encoded tiptap content\n * @returns `rendered` — rendered HTML string, empty string for empty content, or `null` when rendering fails;\n * `hasError` — `true` when the content cannot be parsed or rendered\n */\nexport function useRichTextRenderer(content: Ref<string>) {\n const rendered = ref<string | null>(\"\");\n\n onMounted(() => {\n watch(content, () => {\n const value = toValue(content);\n if (!value || value.trim() === \"\") {\n return \"\";\n }\n\n try {\n const parsed = JSON.parse(value);\n let html = generateHTML(\n parsed,\n extensions\n );\n rendered.value = html;\n } catch (error) {\n console.error(\"Failed to parse content:\", value);\n console.error(error);\n rendered.value = null;\n }\n }, {immediate: true});\n });\n\n const hasError = computed(() => rendered.value === null);\n\n return { rendered, hasError };\n}\n","<script lang=\"ts\">\n/**\n * Renders a JSON string of tiptap content as HTML.\n * Supports all formatting produced by GChatInput and GNoteInput:\n * bold, italic, headings, ordered lists, and bullet lists.\n *\n * - Empty content is handled gracefully (renders nothing).\n * - Displays an error message when the content cannot be parsed or rendered.\n *\n * The rendering only happens in the client when used with Nuxt.js.\n *\n * **Security note**: rendered HTML is produced by tiptap's `generateHTML`, which only\n * serializes recognized document nodes - it does not inject raw HTML from the JSON.\n *\n * **Note**: This component is part of the `@illinois-grad/grad-vue-rte` package, which includes Tiptap dependencies.\n */\nexport default {};\n</script>\n\n<script setup lang=\"ts\">\nimport { toRef } from \"vue\";\nimport { useRichTextRenderer } from \"../composables/useRichTextRenderer\";\n\ntype Props = {\n /**\n * Error message when rendering fails\n * @demo\n */\n error?: string;\n\n /**\n * JSON-encoded tiptap content string to render.\n */\n content: string;\n}\n\nconst props = defineProps<Props>();\n\nconst contentRef = toRef(props, \"content\");\n\nconst { rendered, hasError } = useRichTextRenderer(contentRef);\n\n</script>\n\n<template>\n <div class=\"g-rich-text-content-wrap\">\n <div v-if=\"hasError\" role=\"alert\" class=\"g-rich-text-content-error\">\n {{ error || 'Failed to render content.' }}\n </div>\n <div v-else-if=\"rendered\" class=\"g-rich-text-content\" v-html=\"rendered\"></div>\n </div>\n</template>\n\n<style>\n.g-rich-text-content {\n p {\n margin: 0.375em 0;\n }\n\n > :first-child {\n margin-top: 0;\n }\n\n > :last-child {\n margin-bottom: 0;\n }\n\n ul,\n ol {\n padding: 0 1em;\n margin: 0.375em 1em 0 0.4em;\n\n li p {\n margin-top: 0;\n margin-bottom: 0;\n }\n }\n}\n\n.g-rich-text-content-error {\n color: var(--g-danger-700);\n font-size: 0.875em;\n}\n</style>\n","<script lang=\"ts\">\n/**\n * Renders a JSON string of tiptap content as HTML.\n * Supports all formatting produced by GChatInput and GNoteInput:\n * bold, italic, headings, ordered lists, and bullet lists.\n *\n * - Empty content is handled gracefully (renders nothing).\n * - Displays an error message when the content cannot be parsed or rendered.\n *\n * The rendering only happens in the client when used with Nuxt.js.\n *\n * **Security note**: rendered HTML is produced by tiptap's `generateHTML`, which only\n * serializes recognized document nodes - it does not inject raw HTML from the JSON.\n *\n * **Note**: This component is part of the `@illinois-grad/grad-vue-rte` package, which includes Tiptap dependencies.\n */\nexport default {};\n</script>\n\n<script setup lang=\"ts\">\nimport { toRef } from \"vue\";\nimport { useRichTextRenderer } from \"../composables/useRichTextRenderer\";\n\ntype Props = {\n /**\n * Error message when rendering fails\n * @demo\n */\n error?: string;\n\n /**\n * JSON-encoded tiptap content string to render.\n */\n content: string;\n}\n\nconst props = defineProps<Props>();\n\nconst contentRef = toRef(props, \"content\");\n\nconst { rendered, hasError } = useRichTextRenderer(contentRef);\n\n</script>\n\n<template>\n <div class=\"g-rich-text-content-wrap\">\n <div v-if=\"hasError\" role=\"alert\" class=\"g-rich-text-content-error\">\n {{ error || 'Failed to render content.' }}\n </div>\n <div v-else-if=\"rendered\" class=\"g-rich-text-content\" v-html=\"rendered\"></div>\n </div>\n</template>\n\n<style>\n.g-rich-text-content {\n p {\n margin: 0.375em 0;\n }\n\n > :first-child {\n margin-top: 0;\n }\n\n > :last-child {\n margin-bottom: 0;\n }\n\n ul,\n ol {\n padding: 0 1em;\n margin: 0.375em 1em 0 0.4em;\n\n li p {\n margin-top: 0;\n margin-bottom: 0;\n }\n }\n}\n\n.g-rich-text-content-error {\n color: var(--g-danger-700);\n font-size: 0.875em;\n}\n</style>\n"],"x_google_ignoreList":[0],"mappings":"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"}