@illinois-grad/grad-vue-rte 3.0.1 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -178,11 +178,7 @@ var P = ["aria-pressed", "tabindex"], F = ["aria-pressed", "tabindex"], I = ["ar
178
178
  }, [i("path", { 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" })], -1)]], 10, L)
179
179
  ], 544)) : n("", !0);
180
180
  }
181
- }), z = (e, t) => {
182
- let n = e.__vccOpts || e;
183
- for (let [e, r] of t) n[e] = r;
184
- return n;
185
- }, B = /* @__PURE__ */ z(R, [["__scopeId", "data-v-8c490655"]]), V = { class: "g-chat-input-wrap" }, H = ["disabled"], U = /* @__PURE__ */ z(/* @__PURE__ */ o({
181
+ }), z = { class: "g-chat-input-wrap" }, B = ["disabled"], V = /* @__PURE__ */ o({
186
182
  inheritAttrs: !1,
187
183
  __name: "GChatInput",
188
184
  props: /* @__PURE__ */ s({
@@ -224,12 +220,12 @@ var P = ["aria-pressed", "tabindex"], F = ["aria-pressed", "tabindex"], I = ["ar
224
220
  function x() {
225
221
  m();
226
222
  }
227
- return s({ focusInput: x }), (e, o) => (u(), r("div", V, [
223
+ return s({ focusInput: x }), (e, o) => (u(), r("div", z, [
228
224
  g(p) ? (u(), t(g(S), {
229
225
  key: 0,
230
226
  editor: g(p)
231
227
  }, {
232
- default: y(() => [a(B, {
228
+ default: y(() => [a(R, {
233
229
  editor: g(p),
234
230
  class: "bubble-menu"
235
231
  }, null, 8, ["editor"])]),
@@ -253,10 +249,10 @@ var P = ["aria-pressed", "tabindex"], F = ["aria-pressed", "tabindex"], I = ["ar
253
249
  height: "16",
254
250
  fill: "currentColor",
255
251
  "aria-hidden": "true"
256
- }, [i("path", { 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" })], -1)]], 8, H)
252
+ }, [i("path", { 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" })], -1)]], 8, B)
257
253
  ]));
258
254
  }
259
- }), [["__scopeId", "data-v-09bab62a"]]), W = { class: "g-note-input-wrap" }, G = /* @__PURE__ */ z(/* @__PURE__ */ o({
255
+ }), H = { class: "g-note-input-wrap" }, U = /* @__PURE__ */ o({
260
256
  inheritAttrs: !1,
261
257
  __name: "GNoteInput",
262
258
  props: /* @__PURE__ */ s({
@@ -276,7 +272,7 @@ var P = ["aria-pressed", "tabindex"], F = ["aria-pressed", "tabindex"], I = ["ar
276
272
  function c() {
277
273
  s();
278
274
  }
279
- return n({ focusInput: c }), (e, t) => (u(), r("div", W, [a(B, {
275
+ return n({ focusInput: c }), (e, t) => (u(), r("div", H, [a(R, {
280
276
  editor: g(o),
281
277
  class: "toolbar"
282
278
  }, null, 8, ["editor"]), a(g(b), {
@@ -284,7 +280,7 @@ var P = ["aria-pressed", "tabindex"], F = ["aria-pressed", "tabindex"], I = ["ar
284
280
  class: "editor-content"
285
281
  }, null, 8, ["editor"])]));
286
282
  }
287
- }), [["__scopeId", "data-v-ebd8cfef"]]), K = [
283
+ }), W = [
288
284
  C,
289
285
  w,
290
286
  T,
@@ -292,14 +288,14 @@ var P = ["aria-pressed", "tabindex"], F = ["aria-pressed", "tabindex"], I = ["ar
292
288
  D,
293
289
  O
294
290
  ];
295
- function q(t) {
291
+ function G(t) {
296
292
  let n = d("");
297
293
  return l(() => {
298
294
  v(t, () => {
299
295
  let e = h(t);
300
296
  if (!e || e.trim() === "") return "";
301
297
  try {
302
- n.value = j(JSON.parse(e), K);
298
+ n.value = j(JSON.parse(e), W);
303
299
  } catch (t) {
304
300
  console.error("Failed to parse content:", e), console.error(t), n.value = null;
305
301
  }
@@ -311,26 +307,26 @@ function q(t) {
311
307
  }
312
308
  //#endregion
313
309
  //#region src/components/GRichTextContent.vue?vue&type=script&setup=true&lang.ts
314
- var J = { class: "g-rich-text-content-wrap" }, Y = {
310
+ var K = { class: "g-rich-text-content-wrap" }, q = {
315
311
  key: 0,
316
312
  role: "alert",
317
313
  class: "g-rich-text-content-error"
318
- }, X = ["innerHTML"], Z = /* @__PURE__ */ z(/* @__PURE__ */ o({
314
+ }, J = ["innerHTML"], Y = /* @__PURE__ */ o({
319
315
  __name: "GRichTextContent",
320
316
  props: {
321
317
  error: {},
322
318
  content: {}
323
319
  },
324
320
  setup(e) {
325
- let { rendered: t, hasError: i } = q(m(e, "content"));
326
- return (a, o) => (u(), r("div", J, [g(i) ? (u(), r("div", Y, f(e.error || "Failed to render content."), 1)) : g(t) ? (u(), r("div", {
321
+ let { rendered: t, hasError: i } = G(m(e, "content"));
322
+ return (a, o) => (u(), r("div", K, [g(i) ? (u(), r("div", q, f(e.error || "Failed to render content."), 1)) : g(t) ? (u(), r("div", {
327
323
  key: 1,
328
324
  class: "g-rich-text-content",
329
325
  innerHTML: g(t)
330
- }, null, 8, X)) : n("", !0)]));
326
+ }, null, 8, J)) : n("", !0)]));
331
327
  }
332
- }), [["__scopeId", "data-v-68026e47"]]);
328
+ });
333
329
  //#endregion
334
- export { U as i, q as n, G as r, Z as t };
330
+ export { V as i, G as n, U as r, Y as t };
335
331
 
336
- //# sourceMappingURL=grad-vue-rte-DDZoWaEh.js.map
332
+ //# sourceMappingURL=grad-vue-rte-BfsFrmlo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grad-vue-rte-BfsFrmlo.js","names":[],"sources":["../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 { 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 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}\n\nexport function useRichTextEditor(options: UseRichTextEditorOptions) {\n const { content, placeholder, label, onUpdate, editorProps = {} } = 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 UndoRedo,\n Placeholder.configure({ placeholder: placeholderValue }),\n ],\n editorProps: {\n ...editorProps,\n attributes: {\n \"aria-label\": labelValue,\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 { useToolbarNavigation } from \"../../composables/useToolbarNavigation.ts\";\n\ninterface Props {\n editor: Editor | undefined;\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 </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</style>\n","<script lang=\"ts\" setup>\nimport { ref, computed } from \"vue\";\nimport type { Editor } from \"@tiptap/vue-3\";\nimport { useToolbarNavigation } from \"../../composables/useToolbarNavigation.ts\";\n\ninterface Props {\n editor: Editor | undefined;\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 </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</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 * - 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 { 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 rows\n * @demo\n */\n maxRows?: number;\n /**\n * Accessible label\n * @demo\n */\n label?: string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n placeholder: \"Type a comment\",\n label: \"Comment input\",\n disabled: false,\n maxRows: 5,\n});\nconst model = defineModel<object | \"\">();\nconst emit = defineEmits<{ send: [content: object] }>();\n\nconst { editor, focusEditor } = useRichTextEditor({\n content: model as any,\n placeholder: computed(() => props.placeholder),\n label: computed(() => props.label),\n editorProps: {\n handleKeyDown(view: any, event: any) {\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 model.value = editor.value?.getJSON();\n event.preventDefault();\n onSend(editor.value?.getJSON());\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: any) {\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\ndefineExpose({ focusInput });\n</script>\n\n<template>\n <div class=\"g-chat-input-wrap\">\n <BubbleMenu :editor=\"editor\" v-if=\"editor\">\n <GRichTextToolbar :editor=\"editor\" class=\"bubble-menu\" />\n </BubbleMenu>\n <EditorContent :editor=\"editor\" class=\"editor-content\" />\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</template>\n\n<style>\n.g-chat-input-wrap {\n .tiptap {\n background: transparent;\n border: none;\n padding: 0.15em 0;\n font-size: 15px;\n max-height: 10em;\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.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: flex;\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.editor-content {\n flex: 1;\n min-width: 0;\n}\n\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</style>\n\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 * - 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 { 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 rows\n * @demo\n */\n maxRows?: number;\n /**\n * Accessible label\n * @demo\n */\n label?: string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n placeholder: \"Type a comment\",\n label: \"Comment input\",\n disabled: false,\n maxRows: 5,\n});\nconst model = defineModel<object | \"\">();\nconst emit = defineEmits<{ send: [content: object] }>();\n\nconst { editor, focusEditor } = useRichTextEditor({\n content: model as any,\n placeholder: computed(() => props.placeholder),\n label: computed(() => props.label),\n editorProps: {\n handleKeyDown(view: any, event: any) {\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 model.value = editor.value?.getJSON();\n event.preventDefault();\n onSend(editor.value?.getJSON());\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: any) {\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\ndefineExpose({ focusInput });\n</script>\n\n<template>\n <div class=\"g-chat-input-wrap\">\n <BubbleMenu :editor=\"editor\" v-if=\"editor\">\n <GRichTextToolbar :editor=\"editor\" class=\"bubble-menu\" />\n </BubbleMenu>\n <EditorContent :editor=\"editor\" class=\"editor-content\" />\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</template>\n\n<style>\n.g-chat-input-wrap {\n .tiptap {\n background: transparent;\n border: none;\n padding: 0.15em 0;\n font-size: 15px;\n max-height: 10em;\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.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: flex;\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.editor-content {\n flex: 1;\n min-width: 0;\n}\n\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</style>\n\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 * - 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 { 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\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});\n\nfunction focusInput() {\n focusEditor();\n}\n\ndefineExpose({ focusInput });\n</script>\n\n<template>\n <div class=\"g-note-input-wrap\">\n <GRichTextToolbar :editor=\"editor\" class=\"toolbar\" />\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\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 * - 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 { 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\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});\n\nfunction focusInput() {\n focusEditor();\n}\n\ndefineExpose({ focusInput });\n</script>\n\n<template>\n <div class=\"g-note-input-wrap\">\n <GRichTextToolbar :editor=\"editor\" class=\"toolbar\" />\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\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 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];\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, 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, 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"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- .g-rich-text-toolbar[data-v-8c490655]{display:flex}.g-rich-text-toolbar button[data-v-8c490655]{background-color:unset;cursor:pointer;border:none;justify-content:center;align-items:center;padding:.4rem .5rem;font-size:.875rem;display:flex}.g-rich-text-toolbar button.bold[data-v-8c490655]{font-weight:700}.g-rich-text-toolbar button.italic[data-v-8c490655]{font-style:italic}.g-rich-text-toolbar button.is-active[data-v-8c490655]{color:var(--g-accent-700);background-color:var(--g-surface-150)}.g-rich-text-toolbar button[data-v-8c490655]:hover{background-color:var(--g-primary-300);color:var(--g-primary-text)}.g-chat-input-wrap .tiptap{background:0 0;border:none;outline:none;flex:1;max-height:10em;padding:.15em 0;font-size:15px}.g-chat-input-wrap .tiptap p{margin:.375em 0 0}.g-chat-input-wrap .tiptap>:first-child{margin-top:0}.g-chat-input-wrap .tiptap>:last-child{margin-bottom:0}.g-chat-input-wrap .tiptap ul,.g-chat-input-wrap .tiptap ol{margin:.375em 1em 0 .4em;padding:0 1em}:is(.g-chat-input-wrap .tiptap ul,.g-chat-input-wrap .tiptap ol) li p{margin-top:0;margin-bottom:0}.g-chat-input-wrap .tiptap p.is-editor-empty:first-child:before{color:var(--g-surface-600);content:attr(data-placeholder);float:left;pointer-events:none;height:0}.bubble-menu[data-v-09bab62a]{background-color:var(--g-surface-100);box-shadow:0 1px 4px #00000080}.bubble-menu [data-v-09bab62a] button:first-child{border-top-left-radius:4px;border-bottom-left-radius:4px}.bubble-menu [data-v-09bab62a] button:last-child{border-top-right-radius:4px;border-bottom-right-radius:4px}.g-chat-input-wrap[data-v-09bab62a]{background:var(--g-surface-0);border:2px solid var(--g-primary-500);border-radius:4px;align-items:center;padding:.5em;display:flex;position:relative}.g-chat-input-wrap[data-v-09bab62a]:has(.ProseMirror-focused){outline:2px solid var(--g-primary-500);outline-offset:2px;box-shadow:0 0 0 2px var(--g-info-200);border-color:var(--g-info-200)}.editor-content[data-v-09bab62a]{flex:1;min-width:0}.g-chat-send-btn[data-v-09bab62a]{color:var(--g-primary-500);cursor:pointer;background:0 0;border:2px solid #0000;border-radius:4px;flex-shrink:0;justify-content:center;align-self:flex-end;align-items:center;margin:0;padding:.4em;font-size:1em;display:flex}.g-chat-send-btn[data-v-09bab62a]:hover:not(:disabled){color:var(--g-accent-700);background-color:var(--g-surface-100)}.g-chat-send-btn[data-v-09bab62a]:focus:not(:disabled){background-color:var(--g-info-200);color:var(--g-primary-500)}.g-chat-send-btn[data-v-09bab62a]:active:not(:disabled){background-color:var(--g-primary-500);color:var(--g-surface-0)}.g-chat-send-btn[data-v-09bab62a]:disabled{color:var(--g-surface-300);cursor:not-allowed}.g-note-input-wrap .tiptap{background:0 0;border:none;outline:none;flex:1;min-height:12em;padding:.5em;font-size:15px}.g-note-input-wrap .tiptap p{margin:.375em 0 0}.g-note-input-wrap .tiptap>:first-child{margin-top:0}.g-note-input-wrap .tiptap>:last-child{margin-bottom:0}.g-note-input-wrap .tiptap ul,.g-note-input-wrap .tiptap ol{margin:.375em 1em 0 .4em;padding:0 1em}:is(.g-note-input-wrap .tiptap ul,.g-note-input-wrap .tiptap ol) li p{margin-top:0;margin-bottom:0}.g-note-input-wrap .tiptap p.is-editor-empty:first-child:before{color:var(--g-surface-600);content:attr(data-placeholder);float:left;pointer-events:none;height:0}.g-note-input-wrap[data-v-ebd8cfef]{background:var(--g-surface-0);border:2px solid var(--g-primary-500);border-radius:4px;flex-direction:column;display:flex}.g-note-input-wrap[data-v-ebd8cfef]:has(.ProseMirror-focused){outline:2px solid var(--g-primary-500);outline-offset:2px;box-shadow:0 0 0 2px var(--g-info-200);border-color:var(--g-info-200)}.toolbar[data-v-ebd8cfef]{border-bottom:1px solid var(--g-surface-200);background-color:var(--g-surface-0);padding:.25rem}.toolbar [data-v-ebd8cfef] button{border-radius:4px}.toolbar [data-v-ebd8cfef] button:focus{outline:2px solid var(--g-primary-500);outline-offset:2px}.editor-content[data-v-ebd8cfef]{flex:1;min-width:0}.g-rich-text-content p[data-v-68026e47]{margin:.375em 0}.g-rich-text-content [data-v-68026e47]>:first-child{margin-top:0}.g-rich-text-content [data-v-68026e47]>:last-child{margin-bottom:0}:is(.g-rich-text-content ul,.g-rich-text-content ol)[data-v-68026e47]{margin:.375em 1em 0 .4em;padding:0 1em}:is(.g-rich-text-content ul,.g-rich-text-content ol) li p[data-v-68026e47]{margin-top:0;margin-bottom:0}.g-rich-text-content-error[data-v-68026e47]{color:var(--g-danger-700);font-size:.875em}
1
+ .g-rich-text-toolbar{display:flex}.g-rich-text-toolbar button{background-color:unset;cursor:pointer;border:none;justify-content:center;align-items:center;padding:.4rem .5rem;font-size:.875rem;display:flex}.g-rich-text-toolbar button.bold{font-weight:700}.g-rich-text-toolbar button.italic{font-style:italic}.g-rich-text-toolbar button.is-active{color:var(--g-accent-700);background-color:var(--g-surface-150)}.g-rich-text-toolbar button:hover{background-color:var(--g-primary-300);color:var(--g-primary-text)}.g-chat-input-wrap .tiptap{background:0 0;border:none;outline:none;flex:1;max-height:10em;padding:.15em 0;font-size:15px}.g-chat-input-wrap .tiptap p{margin:.375em 0 0}.g-chat-input-wrap .tiptap>:first-child{margin-top:0}.g-chat-input-wrap .tiptap>:last-child{margin-bottom:0}.g-chat-input-wrap .tiptap ul,.g-chat-input-wrap .tiptap ol{margin:.375em 1em 0 .4em;padding:0 1em}:is(.g-chat-input-wrap .tiptap ul,.g-chat-input-wrap .tiptap ol) li p{margin-top:0;margin-bottom:0}.g-chat-input-wrap .tiptap p.is-editor-empty:first-child:before{color:var(--g-surface-600);content:attr(data-placeholder);float:left;pointer-events:none;height:0}.bubble-menu{background-color:var(--g-surface-100);box-shadow:0 1px 4px #00000080}.bubble-menu button:first-child{border-top-left-radius:4px;border-bottom-left-radius:4px}.bubble-menu button:last-child{border-top-right-radius:4px;border-bottom-right-radius:4px}.g-chat-input-wrap{background:var(--g-surface-0);border:2px solid var(--g-primary-500);border-radius:4px;align-items:center;padding:.5em;display:flex;position:relative}.g-chat-input-wrap:has(.ProseMirror-focused){outline:2px solid var(--g-primary-500);outline-offset:2px;box-shadow:0 0 0 2px var(--g-info-200);border-color:var(--g-info-200)}.g-chat-send-btn{color:var(--g-primary-500);cursor:pointer;background:0 0;border:2px solid #0000;border-radius:4px;flex-shrink:0;justify-content:center;align-self:flex-end;align-items:center;margin:0;padding:.4em;font-size:1em;display:flex}.g-chat-send-btn:hover:not(:disabled){color:var(--g-accent-700);background-color:var(--g-surface-100)}.g-chat-send-btn:focus:not(:disabled){background-color:var(--g-info-200);color:var(--g-primary-500)}.g-chat-send-btn:active:not(:disabled){background-color:var(--g-primary-500);color:var(--g-surface-0)}.g-chat-send-btn:disabled{color:var(--g-surface-300);cursor:not-allowed}.g-note-input-wrap .tiptap{background:0 0;border:none;outline:none;flex:1;min-height:12em;padding:.5em;font-size:15px}.g-note-input-wrap .tiptap p{margin:.375em 0 0}.g-note-input-wrap .tiptap>:first-child{margin-top:0}.g-note-input-wrap .tiptap>:last-child{margin-bottom:0}.g-note-input-wrap .tiptap ul,.g-note-input-wrap .tiptap ol{margin:.375em 1em 0 .4em;padding:0 1em}:is(.g-note-input-wrap .tiptap ul,.g-note-input-wrap .tiptap ol) li p{margin-top:0;margin-bottom:0}.g-note-input-wrap .tiptap p.is-editor-empty:first-child:before{color:var(--g-surface-600);content:attr(data-placeholder);float:left;pointer-events:none;height:0}.g-note-input-wrap{background:var(--g-surface-0);border:2px solid var(--g-primary-500);border-radius:4px;flex-direction:column;display:flex}.g-note-input-wrap:has(.ProseMirror-focused){outline:2px solid var(--g-primary-500);outline-offset:2px;box-shadow:0 0 0 2px var(--g-info-200);border-color:var(--g-info-200)}.toolbar{border-bottom:1px solid var(--g-surface-200);background-color:var(--g-surface-0);padding:.25rem}.toolbar button{border-radius:4px}.toolbar button:focus{outline:2px solid var(--g-primary-500);outline-offset:2px}.editor-content{flex:1;min-width:0}.g-rich-text-content p{margin:.375em 0}.g-rich-text-content>:first-child{margin-top:0}.g-rich-text-content>:last-child{margin-bottom:0}.g-rich-text-content ul,.g-rich-text-content ol{margin:.375em 1em 0 .4em;padding:0 1em}:is(.g-rich-text-content ul,.g-rich-text-content ol) li p{margin-top:0;margin-bottom:0}.g-rich-text-content-error{color:var(--g-danger-700);font-size:.875em}
2
2
  /*$vite$:1*/
@@ -1,2 +1,2 @@
1
- import { i as e, n as t, r as n, t as r } from "./grad-vue-rte-DDZoWaEh.js";
1
+ import { i as e, n as t, r as n, t as r } from "./grad-vue-rte-BfsFrmlo.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-DDZoWaEh.js";
1
+ import { i as e, r as t, t as n } from "./grad-vue-rte-BfsFrmlo.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": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "description": "Rich text editor components for Graduate College apps using Tiptap.",
5
5
  "keywords": [
6
6
  "vue",
@@ -38,22 +38,22 @@
38
38
  "watch": "vite build --watch"
39
39
  },
40
40
  "dependencies": {
41
- "@tiptap/extension-bold": "^3.20.1",
42
- "@tiptap/extension-document": "^3.20.1",
43
- "@tiptap/extension-italic": "^3.20.1",
44
- "@tiptap/extension-list": "^3.20.1",
45
- "@tiptap/extension-paragraph": "^3.20.1",
46
- "@tiptap/extension-placeholder": "^3.20.1",
47
- "@tiptap/extension-text": "^3.20.1",
48
- "@tiptap/extensions": "^3.20.1",
49
- "@tiptap/vue-3": "^3.20.1",
41
+ "@tiptap/extension-bold": "^3.22.5",
42
+ "@tiptap/extension-document": "^3.22.5",
43
+ "@tiptap/extension-italic": "^3.22.5",
44
+ "@tiptap/extension-list": "^3.22.5",
45
+ "@tiptap/extension-paragraph": "^3.22.5",
46
+ "@tiptap/extension-placeholder": "^3.22.5",
47
+ "@tiptap/extension-text": "^3.22.5",
48
+ "@tiptap/extensions": "^3.22.5",
49
+ "@tiptap/vue-3": "^3.22.5",
50
50
  "vue": "^3.5"
51
51
  },
52
52
  "devDependencies": {
53
- "@vitejs/plugin-vue": "^6.0.4",
54
- "typescript": "^5.9.3",
55
- "vite": "^7.3.1",
53
+ "@vitejs/plugin-vue": "^6.0.6",
54
+ "typescript": "^6.0.3",
55
+ "vite": "^8.0.10",
56
56
  "vite-plugin-dts": "^4.5.4",
57
- "vue-tsc": "^3.2.5"
57
+ "vue-tsc": "^3.2.7"
58
58
  }
59
59
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"grad-vue-rte-DDZoWaEh.js","names":[],"sources":["../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 { 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 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}\n\nexport function useRichTextEditor(options: UseRichTextEditorOptions) {\n const { content, placeholder, label, onUpdate, editorProps = {} } = 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 UndoRedo,\n Placeholder.configure({ placeholder: placeholderValue }),\n ],\n editorProps: {\n ...editorProps,\n attributes: {\n \"aria-label\": labelValue,\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 { useToolbarNavigation } from \"../../composables/useToolbarNavigation.ts\";\n\ninterface Props {\n editor: Editor | undefined;\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 </div>\n</template>\n\n<style scoped>\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</style>\n","<script lang=\"ts\" setup>\nimport { ref, computed } from \"vue\";\nimport type { Editor } from \"@tiptap/vue-3\";\nimport { useToolbarNavigation } from \"../../composables/useToolbarNavigation.ts\";\n\ninterface Props {\n editor: Editor | undefined;\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 </div>\n</template>\n\n<style scoped>\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</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 * - 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 { 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 rows\n * @demo\n */\n maxRows?: number;\n /**\n * Accessible label\n * @demo\n */\n label?: string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n placeholder: \"Type a comment\",\n label: \"Comment input\",\n disabled: false,\n maxRows: 5,\n});\nconst model = defineModel<object | \"\">();\nconst emit = defineEmits<{ send: [content: object] }>();\n\nconst { editor, focusEditor } = useRichTextEditor({\n content: model as any,\n placeholder: computed(() => props.placeholder),\n label: computed(() => props.label),\n editorProps: {\n handleKeyDown(view: any, event: any) {\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 model.value = editor.value?.getJSON();\n event.preventDefault();\n onSend(editor.value?.getJSON());\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: any) {\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\ndefineExpose({ focusInput });\n</script>\n\n<template>\n <div class=\"g-chat-input-wrap\">\n <BubbleMenu :editor=\"editor\" v-if=\"editor\">\n <GRichTextToolbar :editor=\"editor\" class=\"bubble-menu\" />\n </BubbleMenu>\n <EditorContent :editor=\"editor\" class=\"editor-content\" />\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</template>\n\n<style>\n.g-chat-input-wrap {\n .tiptap {\n background: transparent;\n border: none;\n padding: 0.15em 0;\n font-size: 15px;\n max-height: 10em;\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</style>\n\n<style scoped>\n.bubble-menu {\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);\n background-color: var(--g-surface-100);\n\n :deep(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: flex;\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.editor-content {\n flex: 1;\n min-width: 0;\n}\n\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</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 * - 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 { 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 rows\n * @demo\n */\n maxRows?: number;\n /**\n * Accessible label\n * @demo\n */\n label?: string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n placeholder: \"Type a comment\",\n label: \"Comment input\",\n disabled: false,\n maxRows: 5,\n});\nconst model = defineModel<object | \"\">();\nconst emit = defineEmits<{ send: [content: object] }>();\n\nconst { editor, focusEditor } = useRichTextEditor({\n content: model as any,\n placeholder: computed(() => props.placeholder),\n label: computed(() => props.label),\n editorProps: {\n handleKeyDown(view: any, event: any) {\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 model.value = editor.value?.getJSON();\n event.preventDefault();\n onSend(editor.value?.getJSON());\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: any) {\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\ndefineExpose({ focusInput });\n</script>\n\n<template>\n <div class=\"g-chat-input-wrap\">\n <BubbleMenu :editor=\"editor\" v-if=\"editor\">\n <GRichTextToolbar :editor=\"editor\" class=\"bubble-menu\" />\n </BubbleMenu>\n <EditorContent :editor=\"editor\" class=\"editor-content\" />\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</template>\n\n<style>\n.g-chat-input-wrap {\n .tiptap {\n background: transparent;\n border: none;\n padding: 0.15em 0;\n font-size: 15px;\n max-height: 10em;\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</style>\n\n<style scoped>\n.bubble-menu {\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);\n background-color: var(--g-surface-100);\n\n :deep(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: flex;\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.editor-content {\n flex: 1;\n min-width: 0;\n}\n\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</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 * - 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 { 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\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});\n\nfunction focusInput() {\n focusEditor();\n}\n\ndefineExpose({ focusInput });\n</script>\n\n<template>\n <div class=\"g-note-input-wrap\">\n <GRichTextToolbar :editor=\"editor\" class=\"toolbar\" />\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</style>\n\n<style scoped>\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 :deep(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 * - 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 { 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\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});\n\nfunction focusInput() {\n focusEditor();\n}\n\ndefineExpose({ focusInput });\n</script>\n\n<template>\n <div class=\"g-note-input-wrap\">\n <GRichTextToolbar :editor=\"editor\" class=\"toolbar\" />\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</style>\n\n<style scoped>\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 :deep(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 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];\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, 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 scoped>\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, 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 scoped>\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"],"mappings":"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"}