@estiva-app/ui 0.69.2 → 0.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/editor.js CHANGED
@@ -339,6 +339,47 @@ var PastedImages = Extension2.create({
339
339
  }
340
340
  });
341
341
 
342
+ // src/PastedMarkdown.ts
343
+ import { Extension as Extension3 } from "@tiptap/core";
344
+ import { Slice } from "@tiptap/pm/model";
345
+ import { Plugin as Plugin3, PluginKey as PluginKey3 } from "@tiptap/pm/state";
346
+ function inTableCell($context) {
347
+ for (let depth = $context.depth; depth > 0; depth--) {
348
+ const role = $context.node(depth).type.spec.tableRole;
349
+ if (role === "cell" || role === "header_cell") return true;
350
+ }
351
+ return false;
352
+ }
353
+ var hasTable = (blocks) => blocks.some((block) => block.type === "table" || hasTable(block.content ?? []));
354
+ var PastedMarkdown = Extension3.create({
355
+ name: "pastedMarkdown",
356
+ addOptions() {
357
+ return { read: () => void 0 };
358
+ },
359
+ addProseMirrorPlugins() {
360
+ const options = this.options;
361
+ return [
362
+ new Plugin3({
363
+ key: new PluginKey3("pastedMarkdown"),
364
+ props: {
365
+ // Reached only for a text paste outside code; `plain` is the Shift paste.
366
+ clipboardTextParser: (text, $context, plain, view) => {
367
+ if (plain) return void 0;
368
+ try {
369
+ const blocks = options.read(text.replace(/\r\n?/g, "\n"))?.content ?? [];
370
+ if (blocks.every((block) => block.type === "paragraph")) return void 0;
371
+ if (inTableCell($context) && hasTable(blocks)) return void 0;
372
+ return Slice.maxOpen(view.state.schema.nodeFromJSON({ type: "doc", content: blocks }).content);
373
+ } catch {
374
+ return void 0;
375
+ }
376
+ }
377
+ }
378
+ })
379
+ ];
380
+ }
381
+ });
382
+
342
383
  // src/CodeBlockColours.ts
343
384
  import { CodeBlockLowlight } from "@tiptap/extension-code-block-lowlight";
344
385
 
@@ -475,271 +516,19 @@ function suggestionPopup(options) {
475
516
  import { useMemo } from "react";
476
517
  import { IconCopy, IconTrash } from "@tabler/icons-react";
477
518
  import { BlockHandle, blockAtY } from "@estiva-app/ui";
519
+ import { Selection } from "@tiptap/pm/state";
478
520
  import { MenuItem, MenuSub } from "@estiva-app/ui";
479
- import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
480
- function editorBlocks(view) {
481
- const out = [];
482
- view.state.doc.forEach((node, pos) => {
483
- const element = view.nodeDOM(pos);
484
- if (element instanceof HTMLElement) out.push({ id: node.attrs.blockId || void 0, element, pos });
485
- });
486
- return out;
487
- }
488
- function duplicateBlock(editor, pos) {
489
- const node = editor.state.doc.nodeAt(pos);
490
- if (!node) return false;
491
- editor.view.dispatch(editor.state.tr.insert(pos + node.nodeSize, node).scrollIntoView());
492
- editor.commands.focus(pos + node.nodeSize + 1);
493
- return true;
494
- }
495
- function deleteBlock(editor, pos) {
496
- const { state } = editor;
497
- const node = state.doc.nodeAt(pos);
498
- if (!node) return false;
499
- const tr = state.doc.childCount === 1 ? state.tr.replaceWith(pos, pos + node.nodeSize, state.schema.nodes.paragraph.create()) : state.tr.delete(pos, pos + node.nodeSize);
500
- editor.view.dispatch(tr);
501
- editor.commands.focus(Math.min(pos + 1, editor.state.doc.content.size));
502
- return true;
503
- }
504
- function moveBlock(editor, from, to) {
505
- const node = editor.state.doc.nodeAt(from);
506
- if (!node || to >= from && to <= from + node.nodeSize) return false;
507
- const tr = editor.state.tr.delete(from, from + node.nodeSize);
508
- const at = tr.mapping.map(to);
509
- editor.view.dispatch(tr.insert(at, node).scrollIntoView());
510
- editor.commands.focus(at + 1);
511
- return true;
512
- }
513
- function startBlockDrag(editor, pos, event, image) {
514
- editor.commands.setNodeSelection(pos);
515
- const { view } = editor;
516
- const slice = view.state.selection.content();
517
- const { dom, text } = view.serializeForClipboard(slice);
518
- const transfer = event.dataTransfer;
519
- if (transfer) {
520
- transfer.clearData();
521
- transfer.setData("text/html", dom.innerHTML);
522
- transfer.setData("text/plain", text);
523
- transfer.effectAllowed = "copyMove";
524
- if (image) transfer.setDragImage(image, 0, 0);
525
- }
526
- view.dragging = { slice, move: true };
527
- }
528
- var blockDropCursor = { color: false, class: "bg-accent-primary" };
529
- var NOTHING_YET = { find: () => null, rows: () => null, startDrag: () => {
530
- }, drop: () => {
531
- }, endDrag: () => {
532
- }, onChange: () => () => {
533
- } };
534
- function EditorBlockHandle({ editor, turnInto = [], actions, children, className }) {
535
- const editing = useMemo(() => {
536
- if (!editor) return NOTHING_YET;
537
- let dragged;
538
- return {
539
- find: (y) => blockAtY(editorBlocks(editor.view), y),
540
- rows: (block) => {
541
- const pos = block.pos;
542
- if (!editor.isEditable) return null;
543
- const node = pos === void 0 ? null : editor.state.doc.nodeAt(pos);
544
- if (pos === void 0 || !node) return null;
545
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
546
- node.isTextblock && turnInto.length > 0 && /* @__PURE__ */ jsx3(MenuSub, { label: "Turn into", children: turnInto.map((row) => /* @__PURE__ */ jsx3(
547
- MenuItem,
548
- {
549
- label: row.label,
550
- selected: row.active?.(node) ?? false,
551
- onClick: () => {
552
- editor.chain().focus().setTextSelection(pos + 1).run();
553
- row.run(editor);
554
- }
555
- },
556
- row.label
557
- )) }),
558
- /* @__PURE__ */ jsx3(MenuItem, { label: "Duplicate", leading: /* @__PURE__ */ jsx3(IconCopy, { size: 16, stroke: 1.5, className: "text-text-secondary" }), onClick: () => duplicateBlock(editor, pos) }),
559
- /* @__PURE__ */ jsx3(MenuItem, { label: "Delete", destructive: true, leading: /* @__PURE__ */ jsx3(IconTrash, { size: 16, stroke: 1.5, className: "text-error-default" }), onClick: () => deleteBlock(editor, pos) })
560
- ] });
561
- },
562
- startDrag: (block, event) => {
563
- dragged = block.pos;
564
- if (block.pos !== void 0) startBlockDrag(editor, block.pos, event, block.element);
565
- },
566
- drop: (target, after) => {
567
- const node = target.pos === void 0 ? null : editor.state.doc.nodeAt(target.pos);
568
- if (dragged === void 0 || target.pos === void 0 || !node) return;
569
- moveBlock(editor, dragged, after ? target.pos + node.nodeSize : target.pos);
570
- },
571
- // ProseMirror clears `dragging` on its own dragend, which a drag started outside its element never reaches.
572
- endDrag: () => setTimeout(() => editor.view.dragging = null, 50),
573
- onChange: (changed) => {
574
- editor.on("update", changed);
575
- return () => {
576
- editor.off("update", changed);
577
- };
578
- }
579
- };
580
- }, [editor, turnInto]);
581
- return /* @__PURE__ */ jsx3(BlockHandle, { editing, actions, className, children });
582
- }
583
-
584
- // src/EditorTableControls.tsx
585
- import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef3, useState as useState2 } from "react";
586
- import { TextSelection } from "@tiptap/pm/state";
587
- import { addColumnAfter, addColumnBefore, addRowAfter, addRowBefore, deleteColumn, deleteRow, deleteTable, toggleHeader } from "@tiptap/pm/tables";
588
- import { IconColumnInsertLeft, IconColumnInsertRight, IconGripHorizontal, IconGripVertical, IconPlus, IconRowInsertBottom, IconRowInsertTop, IconTrash as IconTrash2 } from "@tabler/icons-react";
589
- import { cn } from "@estiva-app/ui";
590
- import { IconButton } from "@estiva-app/ui";
591
-
592
- // src/looks.ts
593
- var FLOATING_SURFACE_CLASSES = "rounded-lg border border-border-default bg-bg-elevated shadow-lg";
594
-
595
- // src/EditorTableControls.tsx
596
- import { Menu, MenuItem as MenuItem2, MenuSeparator } from "@estiva-app/ui";
597
- import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
598
- function inCell(editor, cell, command) {
599
- const { view } = editor;
600
- if (!view.dom.contains(cell)) return false;
601
- const at = view.posAtDOM(cell, 0);
602
- view.dispatch(view.state.tr.setSelection(TextSelection.near(view.state.doc.resolve(at))));
603
- const done = command(view.state, view.dispatch, view);
604
- editor.commands.focus();
605
- return done;
606
- }
607
- function isHeaderRow(row) {
608
- return row.cells.length > 0 && Array.from(row.cells).every((c) => c.tagName === "TH");
609
- }
610
- function deleteTableRow(editor, cell) {
611
- const row = cell.closest("tr");
612
- const table = cell.closest("table");
613
- if (!row || !table || table.rows.length <= 1) return inCell(editor, cell, deleteTable);
614
- const header = isHeaderRow(row);
615
- return inCell(editor, cell, header ? (state, dispatch, view) => deleteRow(state, dispatch) && (!view || toggleHeader("row")(view.state, view.dispatch)) : deleteRow);
616
- }
617
- function deleteTableColumn(editor, cell) {
618
- const row = cell.closest("tr");
619
- return inCell(editor, cell, row && row.cells.length > 1 ? deleteColumn : deleteTable);
620
- }
621
- function appendTableRow(editor, table) {
622
- const last = table.rows[table.rows.length - 1]?.cells[0];
623
- return !!last && inCell(editor, last, addRowAfter);
624
- }
625
- function appendTableColumn(editor, table) {
626
- const first = table.rows[0];
627
- const last = first?.cells[first.cells.length - 1];
628
- return !!last && inCell(editor, last, addColumnAfter);
629
- }
630
- function place(frame, { table, row, cell }) {
631
- const at = frame.getBoundingClientRect();
632
- const t = table.getBoundingClientRect();
633
- const r = row.getBoundingClientRect();
634
- const c = cell.getBoundingClientRect();
635
- return {
636
- tableBox: { top: t.top - at.top, left: t.left - at.left, width: t.width, height: t.height },
637
- rowBox: { top: r.top - at.top, height: r.height },
638
- columnBox: { left: c.left - at.left, width: c.width }
639
- };
640
- }
641
- var ICON = { size: 16, stroke: 1.5, className: "text-text-secondary" };
642
- var keepFocus = (e) => e.preventDefault();
643
- var HANDLE_BOX = cn(FLOATING_SURFACE_CLASSES, "shadow-none");
644
- function EditorTableControls({ editor, children, className }) {
645
- const frame = useRef3(null);
646
- const [hovered, setHovered] = useState2(null);
647
- const [menu, setMenu] = useState2(null);
648
- const editable = !!editor && editor.isEditable;
649
- const follow = useCallback2(
650
- (target) => {
651
- if (!editor || !frame.current || !(target instanceof Element)) return;
652
- if (target.closest("[data-table-control]")) return;
653
- const cell = target.closest("td, th");
654
- const row = cell?.closest("tr");
655
- const table = row?.closest("table");
656
- if (!cell || !row || !table || !editor.view.dom.contains(table)) {
657
- setHovered(null);
658
- return;
659
- }
660
- const next = { table, row, cell, ...place(frame.current, { table, row, cell }) };
661
- setHovered((was) => was && was.cell === cell && JSON.stringify([was.tableBox, was.rowBox, was.columnBox]) === JSON.stringify([next.tableBox, next.rowBox, next.columnBox]) ? was : next);
662
- },
663
- [editor]
664
- );
665
- useEffect2(() => {
666
- if (!editor) return;
667
- const changed = ({ transaction }) => {
668
- if (transaction.docChanged && !menu) setHovered(null);
669
- };
670
- editor.on("transaction", changed);
671
- return () => {
672
- editor.off("transaction", changed);
673
- };
674
- }, [editor, menu]);
675
- const shown = editable ? hovered : null;
676
- const run = (act) => () => {
677
- if (editor) act();
678
- setMenu(null);
679
- setHovered(null);
680
- };
681
- return /* @__PURE__ */ jsxs3(
682
- "div",
683
- {
684
- ref: frame,
685
- className: cn("relative", className),
686
- onMouseMove: (e) => {
687
- if (!menu) follow(e.target);
688
- },
689
- onMouseLeave: () => {
690
- if (!menu) setHovered(null);
691
- },
692
- children: [
693
- children,
694
- shown && editor && /* @__PURE__ */ jsxs3(Fragment2, { children: [
695
- /* @__PURE__ */ jsx4("div", { "data-table-control": true, className: cn(HANDLE_BOX, "absolute -translate-x-1/2 -translate-y-1/2"), style: { left: shown.tableBox.left, top: shown.rowBox.top + shown.rowBox.height / 2 }, children: /* @__PURE__ */ jsxs3(
696
- Menu,
697
- {
698
- open: menu === "row",
699
- onOpenChange: (open) => setMenu(open ? "row" : null),
700
- trigger: /* @__PURE__ */ jsx4(IconButton, { "aria-label": "Row menu", tooltip: "Row", className: "h-6 w-4 p-0", children: /* @__PURE__ */ jsx4(IconGripVertical, { size: 16, stroke: 1.5 }) }),
701
- children: [
702
- !isHeaderRow(shown.row) && /* @__PURE__ */ jsx4(MenuItem2, { label: "Insert above", leading: /* @__PURE__ */ jsx4(IconRowInsertTop, { ...ICON }), onClick: run(() => inCell(editor, shown.cell, addRowBefore)) }),
703
- /* @__PURE__ */ jsx4(MenuItem2, { label: "Insert below", leading: /* @__PURE__ */ jsx4(IconRowInsertBottom, { ...ICON }), onClick: run(() => inCell(editor, shown.cell, addRowAfter)) }),
704
- /* @__PURE__ */ jsx4(MenuSeparator, {}),
705
- /* @__PURE__ */ jsx4(MenuItem2, { label: "Delete", destructive: true, leading: /* @__PURE__ */ jsx4(IconTrash2, { size: 16, stroke: 1.5, className: "text-error-default" }), onClick: run(() => deleteTableRow(editor, shown.cell)) }),
706
- /* @__PURE__ */ jsx4(MenuItem2, { label: "Delete table", destructive: true, leading: /* @__PURE__ */ jsx4(IconTrash2, { size: 16, stroke: 1.5, className: "text-error-default" }), onClick: run(() => inCell(editor, shown.cell, deleteTable)) })
707
- ]
708
- }
709
- ) }),
710
- /* @__PURE__ */ jsx4("div", { "data-table-control": true, className: cn(HANDLE_BOX, "absolute -translate-x-1/2 -translate-y-1/2"), style: { left: shown.columnBox.left + shown.columnBox.width / 2, top: shown.tableBox.top }, children: /* @__PURE__ */ jsxs3(
711
- Menu,
712
- {
713
- open: menu === "column",
714
- onOpenChange: (open) => setMenu(open ? "column" : null),
715
- trigger: /* @__PURE__ */ jsx4(IconButton, { "aria-label": "Column menu", tooltip: "Column", className: "h-4 w-6 p-0", children: /* @__PURE__ */ jsx4(IconGripHorizontal, { size: 16, stroke: 1.5 }) }),
716
- children: [
717
- /* @__PURE__ */ jsx4(MenuItem2, { label: "Insert left", leading: /* @__PURE__ */ jsx4(IconColumnInsertLeft, { ...ICON }), onClick: run(() => inCell(editor, shown.cell, addColumnBefore)) }),
718
- /* @__PURE__ */ jsx4(MenuItem2, { label: "Insert right", leading: /* @__PURE__ */ jsx4(IconColumnInsertRight, { ...ICON }), onClick: run(() => inCell(editor, shown.cell, addColumnAfter)) }),
719
- /* @__PURE__ */ jsx4(MenuSeparator, {}),
720
- /* @__PURE__ */ jsx4(MenuItem2, { label: "Delete", destructive: true, leading: /* @__PURE__ */ jsx4(IconTrash2, { size: 16, stroke: 1.5, className: "text-error-default" }), onClick: run(() => deleteTableColumn(editor, shown.cell)) }),
721
- /* @__PURE__ */ jsx4(MenuItem2, { label: "Delete table", destructive: true, leading: /* @__PURE__ */ jsx4(IconTrash2, { size: 16, stroke: 1.5, className: "text-error-default" }), onClick: run(() => inCell(editor, shown.cell, deleteTable)) })
722
- ]
723
- }
724
- ) }),
725
- shown.row === shown.table.rows[shown.table.rows.length - 1] && /* @__PURE__ */ jsx4("div", { "data-table-control": true, className: cn(HANDLE_BOX, "absolute flex h-4"), style: { left: shown.tableBox.left, top: shown.tableBox.top + shown.tableBox.height, width: shown.tableBox.width }, children: /* @__PURE__ */ jsx4(IconButton, { "aria-label": "Add row", tooltip: "Add row", className: "h-full w-full p-0", onMouseDown: keepFocus, onClick: run(() => appendTableRow(editor, shown.table)), children: /* @__PURE__ */ jsx4(IconPlus, { size: 12, stroke: 1.5 }) }) }),
726
- shown.cell === shown.row.cells[shown.row.cells.length - 1] && /* @__PURE__ */ jsx4("div", { "data-table-control": true, className: cn(HANDLE_BOX, "absolute flex w-4"), style: { left: shown.tableBox.left + shown.tableBox.width, top: shown.tableBox.top, height: shown.tableBox.height }, children: /* @__PURE__ */ jsx4(IconButton, { "aria-label": "Add column", tooltip: "Add column", className: "h-full w-full p-0", onMouseDown: keepFocus, onClick: run(() => appendTableColumn(editor, shown.table)), children: /* @__PURE__ */ jsx4(IconPlus, { size: 12, stroke: 1.5 }) }) })
727
- ] })
728
- ]
729
- }
730
- );
731
- }
732
521
 
733
522
  // src/ComposerTriggers.tsx
734
- import { Extension as Extension3 } from "@tiptap/core";
735
- import { PluginKey as PluginKey3 } from "@tiptap/pm/state";
523
+ import { Extension as Extension4 } from "@tiptap/core";
524
+ import { PluginKey as PluginKey4 } from "@tiptap/pm/state";
736
525
  import Mention from "@tiptap/extension-mention";
737
526
  import Suggestion from "@tiptap/suggestion";
738
527
  import { NodeViewWrapper, ReactNodeViewRenderer } from "@tiptap/react";
739
528
  import { Avatar } from "@estiva-app/ui";
740
529
  import { EnterHint } from "@estiva-app/ui";
741
530
  import { inlineChipClassName } from "@estiva-app/ui";
742
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
531
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
743
532
  var PEOPLE_LIMIT = 6;
744
533
  function filterPeople(people, query) {
745
534
  const q = query.toLowerCase();
@@ -757,8 +546,8 @@ function peopleMenu(urgent) {
757
546
  row: (person) => ({
758
547
  label: person.name,
759
548
  description: person.description,
760
- leading: person.avatar ?? /* @__PURE__ */ jsx5(Avatar, { size: 32, alt: person.name }),
761
- hint: /* @__PURE__ */ jsx5(EnterHint, {}),
549
+ leading: person.avatar ?? /* @__PURE__ */ jsx3(Avatar, { size: 32, alt: person.name }),
550
+ hint: /* @__PURE__ */ jsx3(EnterHint, {}),
762
551
  size: "tall"
763
552
  })
764
553
  };
@@ -885,9 +674,9 @@ var CaptionedReference = Mention.extend({
885
674
  addNodeView() {
886
675
  const { icon, iconFor } = this.options;
887
676
  return ReactNodeViewRenderer(
888
- ({ node }) => /* @__PURE__ */ jsxs4(NodeViewWrapper, { as: "span", className: inlineChipClassName("neutral", "max-w-[24ch] cursor-default"), children: [
889
- /* @__PURE__ */ jsx5("span", { className: "flex items-center justify-center w-4 h-4 shrink-0 text-text-secondary", children: iconFor?.(node.attrs.uri ?? null) ?? icon }),
890
- /* @__PURE__ */ jsx5("span", { className: "truncate", children: captionOf(node.attrs.label, node.attrs.snippet) })
677
+ ({ node }) => /* @__PURE__ */ jsxs2(NodeViewWrapper, { as: "span", className: inlineChipClassName("neutral", "max-w-[24ch] cursor-default"), children: [
678
+ /* @__PURE__ */ jsx3("span", { className: "flex items-center justify-center w-4 h-4 shrink-0 text-text-secondary", children: iconFor?.(node.attrs.uri ?? null) ?? icon }),
679
+ /* @__PURE__ */ jsx3("span", { className: "truncate", children: captionOf(node.attrs.label, node.attrs.snippet) })
891
680
  ] }),
892
681
  { as: "span" }
893
682
  );
@@ -909,11 +698,11 @@ function referenceMenu({ ariaLabel, sectionLabel, icon, iconFor }) {
909
698
  return groups;
910
699
  },
911
700
  itemKey: (item) => `reference-${item.id}`,
912
- row: (item) => ({ leading: item.icon ?? iconFor?.(item.uri) ?? icon, label: captionOf(item.label, item.snippet), description: item.description, hint: /* @__PURE__ */ jsx5(EnterHint, {}) })
701
+ row: (item) => ({ leading: item.icon ?? iconFor?.(item.uri) ?? icon, label: captionOf(item.label, item.snippet), description: item.description, hint: /* @__PURE__ */ jsx3(EnterHint, {}) })
913
702
  };
914
703
  }
915
704
  var listed = (options, query) => options.sections ? options.sections(query).flatMap((section) => section.items.map((item) => ({ ...item, section: section.label }))) : filterReferences(options.items(), query);
916
- var ReferenceTrigger = Extension3.create({
705
+ var ReferenceTrigger = Extension4.create({
917
706
  name: "referenceTrigger",
918
707
  addOptions() {
919
708
  return { items: () => [], nodeName: "captionedReference", ariaLabel: "References", sectionLabel: "References", icon: null };
@@ -924,7 +713,7 @@ var ReferenceTrigger = Extension3.create({
924
713
  Suggestion({
925
714
  editor: this.editor,
926
715
  char: "[",
927
- pluginKey: new PluginKey3("referenceTrigger"),
716
+ pluginKey: new PluginKey4("referenceTrigger"),
928
717
  ...listening("]"),
929
718
  // As a mention's: only where the chip may go.
930
719
  allow: ({ state, range }) => {
@@ -942,130 +731,488 @@ var ReferenceTrigger = Extension3.create({
942
731
  })
943
732
  ];
944
733
  }
945
- });
946
- var offers = (editor, command) => !command.needs || command.needs.every((name) => editor.extensionManager.extensions.some((extension) => extension.name === name));
947
- var FORMATS = [
948
- { id: "paragraph", label: "Text" },
949
- { id: "heading", label: "Heading", trigger: "#" },
950
- { id: "subheading", label: "Subheading", trigger: "##" },
951
- { id: "quote", label: "Quote", trigger: ">" },
952
- { id: "code", label: "Code", trigger: "`", blockTrigger: "```" },
953
- { id: "bulletList", label: "Bulleted list", trigger: "-" },
954
- { id: "orderedList", label: "Numbered list", trigger: "1." }
955
- ];
956
- var DOCUMENT_FORMATS = [
957
- { id: "checklist", label: "Checklist", trigger: "[]" },
958
- { id: "note", label: "Note" },
959
- { id: "warning", label: "Warning" }
960
- ];
961
- function applyFormat(editor, id, code = "inline") {
962
- const chain = editor.chain().focus();
963
- const run = (command, ...args) => {
964
- const named2 = chain[command];
965
- return named2 ? named2(...args).run() : false;
966
- };
967
- switch (id) {
968
- case "paragraph":
969
- return run("setParagraph");
970
- case "heading":
971
- return run("setHeading", { level: 1 });
972
- case "subheading":
973
- return run("setHeading", { level: 2 });
974
- case "quote":
975
- return run("toggleBlockquote");
976
- case "bulletList":
977
- return run("toggleBulletList");
978
- case "orderedList":
979
- return run("toggleOrderedList");
980
- case "code":
981
- return run(code === "block" ? "toggleCodeBlock" : "toggleCode");
982
- case "checklist": {
983
- if (!editor.isActive("listItem") && !run("toggleBulletList")) return false;
984
- if (typeof editor.getAttributes("listItem").checked === "boolean") return true;
985
- return editor.chain().updateAttributes("listItem", { checked: false }).run();
986
- }
987
- case "note":
988
- case "warning": {
989
- if (!editor.isActive("blockquote") && !run("toggleBlockquote")) return false;
990
- return editor.chain().updateAttributes("blockquote", { tone: id }).run();
734
+ });
735
+ var offers = (editor, command) => !command.needs || command.needs.every((name) => editor.extensionManager.extensions.some((extension) => extension.name === name));
736
+ var FORMATS = [
737
+ { id: "paragraph", label: "Text" },
738
+ { id: "heading", label: "Heading", trigger: "#" },
739
+ { id: "subheading", label: "Subheading", trigger: "##" },
740
+ { id: "quote", label: "Quote", trigger: ">" },
741
+ { id: "code", label: "Code", trigger: "`", blockTrigger: "```" },
742
+ { id: "bulletList", label: "Bulleted list", trigger: "-" },
743
+ { id: "orderedList", label: "Numbered list", trigger: "1." }
744
+ ];
745
+ var DOCUMENT_FORMATS = [
746
+ { id: "checklist", label: "Checklist", trigger: "[]" },
747
+ { id: "note", label: "Note" },
748
+ { id: "warning", label: "Warning" }
749
+ ];
750
+ function applyFormat(editor, id, code = "inline") {
751
+ const chain = editor.chain().focus();
752
+ const run = (command, ...args) => {
753
+ const named2 = chain[command];
754
+ return named2 ? named2(...args).run() : false;
755
+ };
756
+ switch (id) {
757
+ case "paragraph":
758
+ return run("setParagraph");
759
+ case "heading":
760
+ return run("setHeading", { level: 1 });
761
+ case "subheading":
762
+ return run("setHeading", { level: 2 });
763
+ case "quote":
764
+ return run("toggleBlockquote");
765
+ case "bulletList":
766
+ return run("toggleBulletList");
767
+ case "orderedList":
768
+ return run("toggleOrderedList");
769
+ case "code":
770
+ return run(code === "block" ? "toggleCodeBlock" : "toggleCode");
771
+ case "checklist": {
772
+ if (!editor.isActive("listItem") && !run("toggleBulletList")) return false;
773
+ if (typeof editor.getAttributes("listItem").checked === "boolean") return true;
774
+ return editor.chain().updateAttributes("listItem", { checked: false }).run();
775
+ }
776
+ case "note":
777
+ case "warning": {
778
+ if (!editor.isActive("blockquote") && !run("toggleBlockquote")) return false;
779
+ return editor.chain().updateAttributes("blockquote", { tone: id }).run();
780
+ }
781
+ default:
782
+ return false;
783
+ }
784
+ }
785
+ var KEYWORDS = {
786
+ checklist: ["todo", "task"],
787
+ note: ["callout"],
788
+ warning: ["callout"]
789
+ };
790
+ var NEEDS = {
791
+ checklist: ["checklist"],
792
+ note: ["callout"],
793
+ warning: ["callout"]
794
+ };
795
+ function formatSection(code = "inline") {
796
+ return {
797
+ label: "Format",
798
+ commands: (code === "block" ? [...FORMATS, ...DOCUMENT_FORMATS] : FORMATS).map((format) => ({
799
+ key: `format-${format.id}`,
800
+ row: { label: format.label, shortcut: code === "block" && format.blockTrigger || format.trigger },
801
+ keywords: [format.id, ...KEYWORDS[format.id] ?? []],
802
+ ...NEEDS[format.id] ? { needs: NEEDS[format.id] } : {},
803
+ run: (editor) => {
804
+ applyFormat(editor, format.id, code);
805
+ }
806
+ }))
807
+ };
808
+ }
809
+ function typeTrigger(input) {
810
+ return (editor) => {
811
+ requestAnimationFrame(() => editor.commands.insertContent(input));
812
+ };
813
+ }
814
+ function matches(command, q) {
815
+ return [command.row.label, ...command.keywords ?? []].some((text) => text.toLowerCase().includes(q));
816
+ }
817
+ function filterSlashSections(sections, query) {
818
+ const q = query.toLowerCase();
819
+ return sections.map((section) => ({ ...section, commands: q ? section.commands.filter((command) => matches(command, q)) : section.commands }));
820
+ }
821
+ var SlashCommands = Extension4.create({
822
+ name: "slashCommands",
823
+ addOptions() {
824
+ return { sections: [formatSection()] };
825
+ },
826
+ addProseMirrorPlugins() {
827
+ const sections = this.options.sections;
828
+ return [
829
+ Suggestion({
830
+ editor: this.editor,
831
+ char: "/",
832
+ startOfLine: false,
833
+ pluginKey: new PluginKey4("slashCommands"),
834
+ items: ({ query, editor }) => filterSlashSections(sections, query).flatMap(
835
+ (section, at) => section.commands.filter((command) => offers(editor, command)).map((command) => ({ ...command, section: at }))
836
+ ),
837
+ command: ({ editor, range, props }) => {
838
+ editor.chain().focus().deleteRange(range).run();
839
+ props.run(editor);
840
+ },
841
+ render: suggestionPopup({
842
+ ariaLabel: "Commands",
843
+ width: "w-[300px]",
844
+ maxHeight: "max-h-[400px]",
845
+ sections: (rows) => sections.map((section, at) => ({ label: section.label, items: rows.filter((row) => row.section === at) })),
846
+ itemKey: (row) => row.key,
847
+ row: (row) => row.row
848
+ })
849
+ })
850
+ ];
851
+ }
852
+ });
853
+
854
+ // src/EditorBlockHandle.tsx
855
+ import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
856
+ function editorBlocks(view) {
857
+ const out = [];
858
+ view.state.doc.forEach((node, pos) => {
859
+ const element = view.nodeDOM(pos);
860
+ if (element instanceof HTMLElement) out.push({ id: node.attrs.blockId || void 0, element, pos });
861
+ });
862
+ return out;
863
+ }
864
+ function duplicateBlock(editor, pos) {
865
+ const node = editor.state.doc.nodeAt(pos);
866
+ if (!node) return false;
867
+ editor.view.dispatch(editor.state.tr.insert(pos + node.nodeSize, node).scrollIntoView());
868
+ editor.commands.focus(pos + node.nodeSize + 1);
869
+ return true;
870
+ }
871
+ function deleteBlock(editor, pos) {
872
+ const { state } = editor;
873
+ const node = state.doc.nodeAt(pos);
874
+ if (!node) return false;
875
+ const tr = state.doc.childCount === 1 ? state.tr.replaceWith(pos, pos + node.nodeSize, state.schema.nodes.paragraph.create()) : state.tr.delete(pos, pos + node.nodeSize);
876
+ editor.view.dispatch(tr);
877
+ editor.commands.focus(Math.min(pos + 1, editor.state.doc.content.size));
878
+ return true;
879
+ }
880
+ function moveBlock(editor, from, to) {
881
+ const node = editor.state.doc.nodeAt(from);
882
+ if (!node || to >= from && to <= from + node.nodeSize) return false;
883
+ const tr = editor.state.tr.delete(from, from + node.nodeSize);
884
+ const at = tr.mapping.map(to);
885
+ editor.view.dispatch(tr.insert(at, node).scrollIntoView());
886
+ editor.commands.focus(at + 1);
887
+ return true;
888
+ }
889
+ function startBlockDrag(editor, pos, event, image) {
890
+ editor.commands.setNodeSelection(pos);
891
+ const { view } = editor;
892
+ const slice = view.state.selection.content();
893
+ const { dom, text } = view.serializeForClipboard(slice);
894
+ const transfer = event.dataTransfer;
895
+ if (transfer) {
896
+ transfer.clearData();
897
+ transfer.setData("text/html", dom.innerHTML);
898
+ transfer.setData("text/plain", text);
899
+ transfer.effectAllowed = "copyMove";
900
+ if (image) transfer.setDragImage(image, 0, 0);
901
+ }
902
+ view.dragging = { slice, move: true };
903
+ }
904
+ var blockDropCursor = { color: false, class: "bg-accent-primary" };
905
+ var NOTHING_YET = { find: () => null, rows: () => null, startDrag: () => {
906
+ }, drop: () => {
907
+ }, endDrag: () => {
908
+ }, onChange: () => () => {
909
+ } };
910
+ function EditorBlockHandle({ editor, turnInto = turnIntoRows, actions, children, className }) {
911
+ const editing = useMemo(() => {
912
+ if (!editor) return NOTHING_YET;
913
+ let dragged;
914
+ return {
915
+ find: (y) => blockAtY(editorBlocks(editor.view), y),
916
+ rows: (block) => {
917
+ const pos = block.pos;
918
+ if (!editor.isEditable) return null;
919
+ const node = pos === void 0 ? null : editor.state.doc.nodeAt(pos);
920
+ if (pos === void 0 || !node) return null;
921
+ const turnRows = typeof turnInto === "function" ? turnInto(node, editor) : node.isTextblock ? turnInto : [];
922
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
923
+ turnRows.length > 0 && /* @__PURE__ */ jsx4(MenuSub, { label: "Turn into", children: turnRows.map((row) => /* @__PURE__ */ jsx4(
924
+ MenuItem,
925
+ {
926
+ label: row.label,
927
+ selected: row.active?.(node) ?? false,
928
+ onClick: () => {
929
+ if (node.isTextblock) editor.chain().focus().setTextSelection(pos + 1).run();
930
+ row.run(editor, pos);
931
+ }
932
+ },
933
+ row.label
934
+ )) }),
935
+ /* @__PURE__ */ jsx4(MenuItem, { label: "Duplicate", leading: /* @__PURE__ */ jsx4(IconCopy, { size: 16, stroke: 1.5, className: "text-text-secondary" }), onClick: () => duplicateBlock(editor, pos) }),
936
+ /* @__PURE__ */ jsx4(MenuItem, { label: "Delete", destructive: true, leading: /* @__PURE__ */ jsx4(IconTrash, { size: 16, stroke: 1.5, className: "text-error-default" }), onClick: () => deleteBlock(editor, pos) })
937
+ ] });
938
+ },
939
+ startDrag: (block, event) => {
940
+ dragged = block.pos;
941
+ if (block.pos !== void 0) startBlockDrag(editor, block.pos, event, block.element);
942
+ },
943
+ drop: (target, after) => {
944
+ const node = target.pos === void 0 ? null : editor.state.doc.nodeAt(target.pos);
945
+ if (dragged === void 0 || target.pos === void 0 || !node) return;
946
+ moveBlock(editor, dragged, after ? target.pos + node.nodeSize : target.pos);
947
+ },
948
+ // ProseMirror clears `dragging` on its own dragend, which a drag started outside its element never reaches.
949
+ endDrag: () => setTimeout(() => editor.view.dragging = null, 50),
950
+ onChange: (changed) => {
951
+ editor.on("update", changed);
952
+ return () => {
953
+ editor.off("update", changed);
954
+ };
955
+ }
956
+ };
957
+ }, [editor, turnInto]);
958
+ return /* @__PURE__ */ jsx4(BlockHandle, { editing, actions, className, children });
959
+ }
960
+ var LISTS = ["bulletList", "orderedList"];
961
+ var isList = (node) => LISTS.includes(node.type.name);
962
+ function listItems(list, at) {
963
+ const out = [];
964
+ list.descendants((node, pos) => {
965
+ if (node.type.name === "listItem") out.push({ node, pos: at + 1 + pos });
966
+ });
967
+ return out;
968
+ }
969
+ var isChecklistItem = ({ node }) => typeof node.attrs.checked === "boolean";
970
+ var isChecklist = (list) => listItems(list, 0).every(isChecklistItem);
971
+ var isPlainList = (list, type) => list.type.name === type && !listItems(list, 0).some(isChecklistItem);
972
+ var hasExtension = (editor, name) => editor.extensionManager.extensions.some((e) => e.name === name);
973
+ function caretInto(editor, pos) {
974
+ const { state } = editor;
975
+ editor.view.dispatch(state.tr.setSelection(Selection.near(state.doc.resolve(Math.min(pos + 1, state.doc.content.size)))));
976
+ editor.commands.focus();
977
+ }
978
+ function turnListInto(editor, pos, kind) {
979
+ const { state } = editor;
980
+ const list = state.doc.nodeAt(pos);
981
+ if (!list || !isList(list)) return false;
982
+ const tr = state.tr;
983
+ if (kind === "text") {
984
+ const blocks = [];
985
+ for (const { node: item } of listItems(list, pos)) {
986
+ item.forEach((child, _offset, index) => {
987
+ if (isList(child)) return;
988
+ blocks.push(child.type.create({ ...child.attrs, blockId: index === 0 ? item.attrs.blockId : null }, child.content, child.marks));
989
+ });
990
+ }
991
+ tr.replaceWith(pos, pos + list.nodeSize, blocks);
992
+ } else {
993
+ const type = kind === "checklist" ? list.type : state.schema.nodes[kind];
994
+ if (list.type !== type) tr.setNodeMarkup(pos, type, list.attrs);
995
+ list.descendants((node, at) => {
996
+ if (isList(node) && node.type !== type) tr.setNodeMarkup(pos + 1 + at, type, node.attrs);
997
+ });
998
+ for (const { node, pos: at } of listItems(list, pos)) {
999
+ const checked = kind === "checklist" ? typeof node.attrs.checked === "boolean" ? node.attrs.checked : false : null;
1000
+ if (node.attrs.checked !== checked) tr.setNodeAttribute(at, "checked", checked);
1001
+ }
1002
+ }
1003
+ if (tr.docChanged) editor.view.dispatch(tr.scrollIntoView());
1004
+ caretInto(editor, pos);
1005
+ return true;
1006
+ }
1007
+ function turnQuoteInto(editor, pos, kind) {
1008
+ const { state } = editor;
1009
+ const quote = state.doc.nodeAt(pos);
1010
+ if (!quote || quote.type.name !== "blockquote") return false;
1011
+ const tr = state.tr;
1012
+ if (kind === "text") {
1013
+ const blocks = [];
1014
+ quote.forEach((child) => blocks.push(blocks.length === 0 ? child.type.create({ ...child.attrs, blockId: quote.attrs.blockId }, child.content, child.marks) : child));
1015
+ tr.replaceWith(pos, pos + quote.nodeSize, blocks);
1016
+ } else {
1017
+ const tone = kind === "quote" ? null : kind;
1018
+ if ((quote.attrs.tone ?? null) !== tone) tr.setNodeAttribute(pos, "tone", tone);
1019
+ }
1020
+ if (tr.docChanged) editor.view.dispatch(tr.scrollIntoView());
1021
+ caretInto(editor, pos);
1022
+ return true;
1023
+ }
1024
+ function isFormat(node, id) {
1025
+ if (id === "paragraph") return node.type.name === "paragraph";
1026
+ if (id === "heading") return node.type.name === "heading" && node.attrs.level === 1;
1027
+ if (id === "subheading") return node.type.name === "heading" && node.attrs.level === 2;
1028
+ if (id === "code") return node.type.name === "codeBlock";
1029
+ return false;
1030
+ }
1031
+ function turnIntoRows(node, editor) {
1032
+ const checklist = hasExtension(editor, "checklist");
1033
+ const callout = hasExtension(editor, "callout");
1034
+ if (node.isTextblock) {
1035
+ const document2 = DOCUMENT_FORMATS.filter((f) => f.id === "checklist" ? checklist : callout);
1036
+ return [...FORMATS, ...document2].map((format) => ({
1037
+ label: format.label,
1038
+ run: (e) => void applyFormat(e, format.id, "block"),
1039
+ active: (n) => isFormat(n, format.id)
1040
+ }));
1041
+ }
1042
+ if (isList(node)) {
1043
+ const rows = [
1044
+ { label: "Bulleted list", run: (e, pos) => void turnListInto(e, pos, "bulletList"), active: (n) => isPlainList(n, "bulletList") },
1045
+ { label: "Numbered list", run: (e, pos) => void turnListInto(e, pos, "orderedList"), active: (n) => isPlainList(n, "orderedList") }
1046
+ ];
1047
+ if (checklist) rows.push({ label: "Checklist", run: (e, pos) => void turnListInto(e, pos, "checklist"), active: isChecklist });
1048
+ rows.push({ label: "Text", run: (e, pos) => void turnListInto(e, pos, "text") });
1049
+ return rows;
1050
+ }
1051
+ if (node.type.name === "blockquote") {
1052
+ const rows = [{ label: "Quote", run: (e, pos) => void turnQuoteInto(e, pos, "quote"), active: (n) => !n.attrs.tone }];
1053
+ if (callout) {
1054
+ rows.push({ label: "Note", run: (e, pos) => void turnQuoteInto(e, pos, "note"), active: (n) => n.attrs.tone === "note" });
1055
+ rows.push({ label: "Warning", run: (e, pos) => void turnQuoteInto(e, pos, "warning"), active: (n) => n.attrs.tone === "warning" });
991
1056
  }
992
- default:
993
- return false;
1057
+ rows.push({ label: "Text", run: (e, pos) => void turnQuoteInto(e, pos, "text") });
1058
+ return rows;
994
1059
  }
1060
+ return [];
995
1061
  }
996
- var KEYWORDS = {
997
- checklist: ["todo", "task"],
998
- note: ["callout"],
999
- warning: ["callout"]
1000
- };
1001
- var NEEDS = {
1002
- checklist: ["checklist"],
1003
- note: ["callout"],
1004
- warning: ["callout"]
1005
- };
1006
- function formatSection(code = "inline") {
1062
+
1063
+ // src/EditorTableControls.tsx
1064
+ import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef3, useState as useState2 } from "react";
1065
+ import { TextSelection } from "@tiptap/pm/state";
1066
+ import { addColumnAfter, addColumnBefore, addRowAfter, addRowBefore, deleteColumn, deleteRow, deleteTable, toggleHeader } from "@tiptap/pm/tables";
1067
+ import { IconColumnInsertLeft, IconColumnInsertRight, IconGripHorizontal, IconGripVertical, IconPlus, IconRowInsertBottom, IconRowInsertTop, IconTrash as IconTrash2 } from "@tabler/icons-react";
1068
+ import { cn } from "@estiva-app/ui";
1069
+ import { IconButton } from "@estiva-app/ui";
1070
+
1071
+ // src/looks.ts
1072
+ var FLOATING_SURFACE_CLASSES = "rounded-lg border border-border-default bg-bg-elevated shadow-lg";
1073
+
1074
+ // src/EditorTableControls.tsx
1075
+ import { Menu, MenuItem as MenuItem2, MenuSeparator } from "@estiva-app/ui";
1076
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1077
+ function inCell(editor, cell, command) {
1078
+ const { view } = editor;
1079
+ if (!view.dom.contains(cell)) return false;
1080
+ const at = view.posAtDOM(cell, 0);
1081
+ view.dispatch(view.state.tr.setSelection(TextSelection.near(view.state.doc.resolve(at))));
1082
+ const done = command(view.state, view.dispatch, view);
1083
+ editor.commands.focus();
1084
+ return done;
1085
+ }
1086
+ function isHeaderRow(row) {
1087
+ return row.cells.length > 0 && Array.from(row.cells).every((c) => c.tagName === "TH");
1088
+ }
1089
+ function deleteTableRow(editor, cell) {
1090
+ const row = cell.closest("tr");
1091
+ const table = cell.closest("table");
1092
+ if (!row || !table || table.rows.length <= 1) return inCell(editor, cell, deleteTable);
1093
+ const header = isHeaderRow(row);
1094
+ return inCell(editor, cell, header ? (state, dispatch, view) => deleteRow(state, dispatch) && (!view || toggleHeader("row")(view.state, view.dispatch)) : deleteRow);
1095
+ }
1096
+ function deleteTableColumn(editor, cell) {
1097
+ const row = cell.closest("tr");
1098
+ return inCell(editor, cell, row && row.cells.length > 1 ? deleteColumn : deleteTable);
1099
+ }
1100
+ function appendTableRow(editor, table) {
1101
+ const last = table.rows[table.rows.length - 1]?.cells[0];
1102
+ return !!last && inCell(editor, last, addRowAfter);
1103
+ }
1104
+ function appendTableColumn(editor, table) {
1105
+ const first = table.rows[0];
1106
+ const last = first?.cells[first.cells.length - 1];
1107
+ return !!last && inCell(editor, last, addColumnAfter);
1108
+ }
1109
+ function place(frame, { table, row, cell }) {
1110
+ const at = frame.getBoundingClientRect();
1111
+ const t = table.getBoundingClientRect();
1112
+ const r = row.getBoundingClientRect();
1113
+ const c = cell.getBoundingClientRect();
1007
1114
  return {
1008
- label: "Format",
1009
- commands: (code === "block" ? [...FORMATS, ...DOCUMENT_FORMATS] : FORMATS).map((format) => ({
1010
- key: `format-${format.id}`,
1011
- row: { label: format.label, shortcut: code === "block" && format.blockTrigger || format.trigger },
1012
- keywords: [format.id, ...KEYWORDS[format.id] ?? []],
1013
- ...NEEDS[format.id] ? { needs: NEEDS[format.id] } : {},
1014
- run: (editor) => {
1015
- applyFormat(editor, format.id, code);
1016
- }
1017
- }))
1115
+ tableBox: { top: t.top - at.top, left: t.left - at.left, width: t.width, height: t.height },
1116
+ rowBox: { top: r.top - at.top, height: r.height },
1117
+ columnBox: { left: c.left - at.left, width: c.width }
1018
1118
  };
1019
1119
  }
1020
- function typeTrigger(input) {
1021
- return (editor) => {
1022
- requestAnimationFrame(() => editor.commands.insertContent(input));
1120
+ var ICON = { size: 16, stroke: 1.5, className: "text-text-secondary" };
1121
+ var keepFocus = (e) => e.preventDefault();
1122
+ var HANDLE_BOX = cn(FLOATING_SURFACE_CLASSES, "shadow-none");
1123
+ function EditorTableControls({ editor, children, className }) {
1124
+ const frame = useRef3(null);
1125
+ const [hovered, setHovered] = useState2(null);
1126
+ const [menu, setMenu] = useState2(null);
1127
+ const editable = !!editor && editor.isEditable;
1128
+ const follow = useCallback2(
1129
+ (target) => {
1130
+ if (!editor || !frame.current || !(target instanceof Element)) return;
1131
+ if (target.closest("[data-table-control]")) return;
1132
+ const cell = target.closest("td, th");
1133
+ const row = cell?.closest("tr");
1134
+ const table = row?.closest("table");
1135
+ if (!cell || !row || !table || !editor.view.dom.contains(table)) {
1136
+ setHovered(null);
1137
+ return;
1138
+ }
1139
+ const next = { table, row, cell, ...place(frame.current, { table, row, cell }) };
1140
+ setHovered((was) => was && was.cell === cell && JSON.stringify([was.tableBox, was.rowBox, was.columnBox]) === JSON.stringify([next.tableBox, next.rowBox, next.columnBox]) ? was : next);
1141
+ },
1142
+ [editor]
1143
+ );
1144
+ useEffect2(() => {
1145
+ if (!editor) return;
1146
+ const changed = ({ transaction }) => {
1147
+ if (transaction.docChanged && !menu) setHovered(null);
1148
+ };
1149
+ editor.on("transaction", changed);
1150
+ return () => {
1151
+ editor.off("transaction", changed);
1152
+ };
1153
+ }, [editor, menu]);
1154
+ const shown = editable ? hovered : null;
1155
+ const run = (act) => () => {
1156
+ if (editor) act();
1157
+ setMenu(null);
1158
+ setHovered(null);
1023
1159
  };
1160
+ return /* @__PURE__ */ jsxs4(
1161
+ "div",
1162
+ {
1163
+ ref: frame,
1164
+ className: cn("relative", className),
1165
+ onMouseMove: (e) => {
1166
+ if (!menu) follow(e.target);
1167
+ },
1168
+ onMouseLeave: () => {
1169
+ if (!menu) setHovered(null);
1170
+ },
1171
+ children: [
1172
+ children,
1173
+ shown && editor && /* @__PURE__ */ jsxs4(Fragment2, { children: [
1174
+ /* @__PURE__ */ jsx5("div", { "data-table-control": true, className: cn(HANDLE_BOX, "absolute -translate-x-1/2 -translate-y-1/2"), style: { left: shown.tableBox.left, top: shown.rowBox.top + shown.rowBox.height / 2 }, children: /* @__PURE__ */ jsxs4(
1175
+ Menu,
1176
+ {
1177
+ open: menu === "row",
1178
+ onOpenChange: (open) => setMenu(open ? "row" : null),
1179
+ trigger: /* @__PURE__ */ jsx5(IconButton, { "aria-label": "Row menu", tooltip: "Row", className: "h-6 w-4 p-0", children: /* @__PURE__ */ jsx5(IconGripVertical, { size: 16, stroke: 1.5 }) }),
1180
+ children: [
1181
+ !isHeaderRow(shown.row) && /* @__PURE__ */ jsx5(MenuItem2, { label: "Insert above", leading: /* @__PURE__ */ jsx5(IconRowInsertTop, { ...ICON }), onClick: run(() => inCell(editor, shown.cell, addRowBefore)) }),
1182
+ /* @__PURE__ */ jsx5(MenuItem2, { label: "Insert below", leading: /* @__PURE__ */ jsx5(IconRowInsertBottom, { ...ICON }), onClick: run(() => inCell(editor, shown.cell, addRowAfter)) }),
1183
+ /* @__PURE__ */ jsx5(MenuSeparator, {}),
1184
+ /* @__PURE__ */ jsx5(MenuItem2, { label: "Delete", destructive: true, leading: /* @__PURE__ */ jsx5(IconTrash2, { size: 16, stroke: 1.5, className: "text-error-default" }), onClick: run(() => deleteTableRow(editor, shown.cell)) }),
1185
+ /* @__PURE__ */ jsx5(MenuItem2, { label: "Delete table", destructive: true, leading: /* @__PURE__ */ jsx5(IconTrash2, { size: 16, stroke: 1.5, className: "text-error-default" }), onClick: run(() => inCell(editor, shown.cell, deleteTable)) })
1186
+ ]
1187
+ }
1188
+ ) }),
1189
+ /* @__PURE__ */ jsx5("div", { "data-table-control": true, className: cn(HANDLE_BOX, "absolute -translate-x-1/2 -translate-y-1/2"), style: { left: shown.columnBox.left + shown.columnBox.width / 2, top: shown.tableBox.top }, children: /* @__PURE__ */ jsxs4(
1190
+ Menu,
1191
+ {
1192
+ open: menu === "column",
1193
+ onOpenChange: (open) => setMenu(open ? "column" : null),
1194
+ trigger: /* @__PURE__ */ jsx5(IconButton, { "aria-label": "Column menu", tooltip: "Column", className: "h-4 w-6 p-0", children: /* @__PURE__ */ jsx5(IconGripHorizontal, { size: 16, stroke: 1.5 }) }),
1195
+ children: [
1196
+ /* @__PURE__ */ jsx5(MenuItem2, { label: "Insert left", leading: /* @__PURE__ */ jsx5(IconColumnInsertLeft, { ...ICON }), onClick: run(() => inCell(editor, shown.cell, addColumnBefore)) }),
1197
+ /* @__PURE__ */ jsx5(MenuItem2, { label: "Insert right", leading: /* @__PURE__ */ jsx5(IconColumnInsertRight, { ...ICON }), onClick: run(() => inCell(editor, shown.cell, addColumnAfter)) }),
1198
+ /* @__PURE__ */ jsx5(MenuSeparator, {}),
1199
+ /* @__PURE__ */ jsx5(MenuItem2, { label: "Delete", destructive: true, leading: /* @__PURE__ */ jsx5(IconTrash2, { size: 16, stroke: 1.5, className: "text-error-default" }), onClick: run(() => deleteTableColumn(editor, shown.cell)) }),
1200
+ /* @__PURE__ */ jsx5(MenuItem2, { label: "Delete table", destructive: true, leading: /* @__PURE__ */ jsx5(IconTrash2, { size: 16, stroke: 1.5, className: "text-error-default" }), onClick: run(() => inCell(editor, shown.cell, deleteTable)) })
1201
+ ]
1202
+ }
1203
+ ) }),
1204
+ shown.row === shown.table.rows[shown.table.rows.length - 1] && /* @__PURE__ */ jsx5("div", { "data-table-control": true, className: cn(HANDLE_BOX, "absolute flex h-4"), style: { left: shown.tableBox.left, top: shown.tableBox.top + shown.tableBox.height, width: shown.tableBox.width }, children: /* @__PURE__ */ jsx5(IconButton, { "aria-label": "Add row", tooltip: "Add row", className: "h-full w-full p-0", onMouseDown: keepFocus, onClick: run(() => appendTableRow(editor, shown.table)), children: /* @__PURE__ */ jsx5(IconPlus, { size: 12, stroke: 1.5 }) }) }),
1205
+ shown.cell === shown.row.cells[shown.row.cells.length - 1] && /* @__PURE__ */ jsx5("div", { "data-table-control": true, className: cn(HANDLE_BOX, "absolute flex w-4"), style: { left: shown.tableBox.left + shown.tableBox.width, top: shown.tableBox.top, height: shown.tableBox.height }, children: /* @__PURE__ */ jsx5(IconButton, { "aria-label": "Add column", tooltip: "Add column", className: "h-full w-full p-0", onMouseDown: keepFocus, onClick: run(() => appendTableColumn(editor, shown.table)), children: /* @__PURE__ */ jsx5(IconPlus, { size: 12, stroke: 1.5 }) }) })
1206
+ ] })
1207
+ ]
1208
+ }
1209
+ );
1024
1210
  }
1025
- function matches(command, q) {
1026
- return [command.row.label, ...command.keywords ?? []].some((text) => text.toLowerCase().includes(q));
1027
- }
1028
- function filterSlashSections(sections, query) {
1029
- const q = query.toLowerCase();
1030
- return sections.map((section) => ({ ...section, commands: q ? section.commands.filter((command) => matches(command, q)) : section.commands }));
1031
- }
1032
- var SlashCommands = Extension3.create({
1033
- name: "slashCommands",
1034
- addOptions() {
1035
- return { sections: [formatSection()] };
1036
- },
1037
- addProseMirrorPlugins() {
1038
- const sections = this.options.sections;
1039
- return [
1040
- Suggestion({
1041
- editor: this.editor,
1042
- char: "/",
1043
- startOfLine: false,
1044
- pluginKey: new PluginKey3("slashCommands"),
1045
- items: ({ query, editor }) => filterSlashSections(sections, query).flatMap(
1046
- (section, at) => section.commands.filter((command) => offers(editor, command)).map((command) => ({ ...command, section: at }))
1047
- ),
1048
- command: ({ editor, range, props }) => {
1049
- editor.chain().focus().deleteRange(range).run();
1050
- props.run(editor);
1051
- },
1052
- render: suggestionPopup({
1053
- ariaLabel: "Commands",
1054
- width: "w-[300px]",
1055
- maxHeight: "max-h-[400px]",
1056
- sections: (rows) => sections.map((section, at) => ({ label: section.label, items: rows.filter((row) => row.section === at) })),
1057
- itemKey: (row) => row.key,
1058
- row: (row) => row.row
1059
- })
1060
- })
1061
- ];
1062
- }
1063
- });
1064
1211
 
1065
1212
  // src/BlockNodes.tsx
1066
- import { Extension as Extension7, InputRule, Node as Node2 } from "@tiptap/core";
1213
+ import { Extension as Extension8, InputRule, Node as Node2 } from "@tiptap/core";
1067
1214
  import { DOMSerializer } from "@tiptap/pm/model";
1068
- import { Plugin as Plugin6, PluginKey as PluginKey7 } from "@tiptap/pm/state";
1215
+ import { Plugin as Plugin7, PluginKey as PluginKey8 } from "@tiptap/pm/state";
1069
1216
  import { Mapping, ReplaceAroundStep } from "@tiptap/pm/transform";
1070
1217
  import { NodeViewWrapper as NodeViewWrapper2, ReactNodeViewRenderer as ReactNodeViewRenderer2, ReactRenderer as ReactRenderer2 } from "@tiptap/react";
1071
1218
  import { Checkbox } from "@estiva-app/ui";
@@ -1073,17 +1220,17 @@ import { calloutLook } from "@estiva-app/ui";
1073
1220
 
1074
1221
  // src/ReferenceMenu.tsx
1075
1222
  import { useCallback as useCallback3 } from "react";
1076
- import { Extension as Extension6 } from "@tiptap/core";
1223
+ import { Extension as Extension7 } from "@tiptap/core";
1077
1224
  import { useEditorState } from "@tiptap/react";
1078
- import { NodeSelection as NodeSelection2, Plugin as Plugin5, PluginKey as PluginKey6, TextSelection as TextSelection2 } from "@tiptap/pm/state";
1225
+ import { NodeSelection as NodeSelection2, Plugin as Plugin6, PluginKey as PluginKey7, TextSelection as TextSelection2 } from "@tiptap/pm/state";
1079
1226
  import { ReplaceStep } from "@tiptap/pm/transform";
1080
1227
  import { IconCheck } from "@tabler/icons-react";
1081
1228
 
1082
1229
  // src/LinkChips.ts
1083
- import { Extension as Extension4 } from "@tiptap/core";
1230
+ import { Extension as Extension5 } from "@tiptap/core";
1084
1231
  import { closeHistory } from "@tiptap/pm/history";
1085
- import { Plugin as Plugin3, PluginKey as PluginKey4 } from "@tiptap/pm/state";
1086
- var key = new PluginKey4("linkChips");
1232
+ import { Plugin as Plugin4, PluginKey as PluginKey5 } from "@tiptap/pm/state";
1233
+ var key = new PluginKey5("linkChips");
1087
1234
  var CHIPS_MADE = "linkChipsMade";
1088
1235
  var PASTES_KEPT = 8;
1089
1236
  var LINK_OR_URL = /(\[[^[\]\n]+\]\((?:https?:\/\/|mailto:)[^\s)]+\))|(https?:\/\/[^\s<>"'`]+)/g;
@@ -1158,7 +1305,7 @@ function refreshLinkChips(editor) {
1158
1305
  const tr = apply(editor.state, findLinkChips(editor.state.doc, editor.state.selection, chips.options.referenceOf, chips.pasted));
1159
1306
  if (tr) editor.view.dispatch(tr);
1160
1307
  }
1161
- var LinkChips = Extension4.create({
1308
+ var LinkChips = Extension5.create({
1162
1309
  name: "linkChips",
1163
1310
  addOptions() {
1164
1311
  return { referenceOf: null, ask: null, limit: 24 };
@@ -1167,7 +1314,7 @@ var LinkChips = Extension4.create({
1167
1314
  const options = this.options;
1168
1315
  const editor = this.editor;
1169
1316
  return [
1170
- new Plugin3({
1317
+ new Plugin4({
1171
1318
  key,
1172
1319
  // The options, where `refreshLinkChips` can read them from the state alone, and where the last pastes landed.
1173
1320
  state: {
@@ -1231,8 +1378,8 @@ import { MenuItem as MenuItem3, MenuSection, MenuSeparator as MenuSeparator2 } f
1231
1378
  import { Popover as Popover2 } from "@estiva-app/ui";
1232
1379
 
1233
1380
  // src/ReferenceCards.ts
1234
- import { Extension as Extension5 } from "@tiptap/core";
1235
- import { NodeSelection, Plugin as Plugin4, PluginKey as PluginKey5 } from "@tiptap/pm/state";
1381
+ import { Extension as Extension6 } from "@tiptap/core";
1382
+ import { NodeSelection, Plugin as Plugin5, PluginKey as PluginKey6 } from "@tiptap/pm/state";
1236
1383
  import { Decoration as Decoration2, DecorationSet as DecorationSet2 } from "@tiptap/pm/view";
1237
1384
  import { useEffect as useEffect3, useState as useState3 } from "react";
1238
1385
  function layoutOf(raw) {
@@ -1261,7 +1408,7 @@ function referenceCardPlacements(doc) {
1261
1408
  });
1262
1409
  return out;
1263
1410
  }
1264
- var key2 = new PluginKey5("referenceCards");
1411
+ var key2 = new PluginKey6("referenceCards");
1265
1412
  function elementFor(storage, slotKey) {
1266
1413
  let element = storage.elements.get(slotKey);
1267
1414
  if (!element) {
@@ -1275,7 +1422,7 @@ function elementFor(storage, slotKey) {
1275
1422
  function storageOf(editor) {
1276
1423
  return editor.storage.referenceCards;
1277
1424
  }
1278
- var ReferenceCards = Extension5.create({
1425
+ var ReferenceCards = Extension6.create({
1279
1426
  name: "referenceCards",
1280
1427
  addStorage() {
1281
1428
  return { elements: /* @__PURE__ */ new Map() };
@@ -1295,7 +1442,7 @@ var ReferenceCards = Extension5.create({
1295
1442
  )
1296
1443
  );
1297
1444
  return [
1298
- new Plugin4({
1445
+ new Plugin5({
1299
1446
  key: key2,
1300
1447
  state: {
1301
1448
  init: (_, state) => decorate(state.doc),
@@ -1374,7 +1521,7 @@ function setReferenceCardAt(editor, pos, layout) {
1374
1521
 
1375
1522
  // src/ReferenceMenu.tsx
1376
1523
  import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1377
- var key3 = new PluginKey6("referenceMenu");
1524
+ var key3 = new PluginKey7("referenceMenu");
1378
1525
  var LAYOUTS = [
1379
1526
  { layout: "none", label: "None" },
1380
1527
  { layout: "compact", label: "Compact" },
@@ -1453,14 +1600,14 @@ function caretAfter(state, pos) {
1453
1600
  const node = state.doc.nodeAt(pos);
1454
1601
  return state.tr.setSelection(TextSelection2.create(state.doc, pos + (node?.nodeSize ?? 1)));
1455
1602
  }
1456
- var ReferenceMenu = Extension6.create({
1603
+ var ReferenceMenu = Extension7.create({
1457
1604
  name: "referenceMenu",
1458
1605
  // Before the editor's own keys, so Enter on a highlighted row is the row's, not a new line.
1459
1606
  priority: 1e3,
1460
1607
  addProseMirrorPlugins() {
1461
1608
  const editor = this.editor;
1462
1609
  return [
1463
- new Plugin5({
1610
+ new Plugin6({
1464
1611
  key: key3,
1465
1612
  state: {
1466
1613
  init: () => null,
@@ -1642,7 +1789,7 @@ function claimsIn(doc) {
1642
1789
  doc.descendants((node, pos, parent) => {
1643
1790
  const fixed = node.type.name === "unknownBlock";
1644
1791
  if (!fixed && !BLOCK_TYPE_SET.has(node.type.name)) return;
1645
- if (parent?.type.name === "listItem" && LISTS.has(node.type.name)) return;
1792
+ if (parent?.type.name === "listItem" && LISTS2.has(node.type.name)) return;
1646
1793
  const id = node.attrs.blockId;
1647
1794
  if (typeof id !== "string" || id === "") return;
1648
1795
  const copies = claims.get(id);
@@ -1689,11 +1836,11 @@ function repeatedBlockIds(doc, before, mapping) {
1689
1836
  }
1690
1837
  var BLOCK_CONTAINERS = /* @__PURE__ */ new Set(["bulletList", "orderedList", "table", "tableRow", "tableHeader", "tableCell"]);
1691
1838
  var FLATTENED = /* @__PURE__ */ new Set(["listItem", "blockquote"]);
1692
- var LISTS = /* @__PURE__ */ new Set(["bulletList", "orderedList"]);
1839
+ var LISTS2 = /* @__PURE__ */ new Set(["bulletList", "orderedList"]);
1693
1840
  function innerLists(item, pos) {
1694
1841
  const out = [];
1695
1842
  item.forEach((child, offset) => {
1696
- if (LISTS.has(child.type.name)) out.push({ pos: pos + 1 + offset, node: child });
1843
+ if (LISTS2.has(child.type.name)) out.push({ pos: pos + 1 + offset, node: child });
1697
1844
  });
1698
1845
  return out;
1699
1846
  }
@@ -1722,7 +1869,7 @@ function hiddenIds(doc) {
1722
1869
  doc.descendants((node, pos) => {
1723
1870
  if (!FLATTENED.has(node.type.name)) return;
1724
1871
  node.forEach((child, offset) => {
1725
- const inner = child.isTextblock || node.type.name === "listItem" && LISTS.has(child.type.name);
1872
+ const inner = child.isTextblock || node.type.name === "listItem" && LISTS2.has(child.type.name);
1726
1873
  if (inner && idOf(child) !== void 0) out.push(pos + 1 + offset);
1727
1874
  });
1728
1875
  });
@@ -1762,7 +1909,7 @@ function bringsInBlocks(transactions) {
1762
1909
  })
1763
1910
  );
1764
1911
  }
1765
- var BlockId = Extension7.create({
1912
+ var BlockId = Extension8.create({
1766
1913
  name: "blockId",
1767
1914
  onCreate() {
1768
1915
  const { state, view } = this.editor;
@@ -1774,8 +1921,8 @@ var BlockId = Extension7.create({
1774
1921
  },
1775
1922
  addProseMirrorPlugins() {
1776
1923
  return [
1777
- new Plugin6({
1778
- key: new PluginKey7("blockIdSplit"),
1924
+ new Plugin7({
1925
+ key: new PluginKey8("blockIdSplit"),
1779
1926
  appendTransaction: (transactions, old, state) => {
1780
1927
  if (!bringsInBlocks(transactions)) return null;
1781
1928
  const mapping = new Mapping();
@@ -2000,7 +2147,7 @@ function listItemView(editor, node, view, getPos) {
2000
2147
  }
2001
2148
  };
2002
2149
  }
2003
- var Checklist = Extension7.create({
2150
+ var Checklist = Extension8.create({
2004
2151
  name: "checklist",
2005
2152
  // Before the list item's own Enter and Backspace.
2006
2153
  priority: 101,
@@ -2065,14 +2212,14 @@ var Checklist = Extension7.create({
2065
2212
  addProseMirrorPlugins() {
2066
2213
  const editor = this.editor;
2067
2214
  return [
2068
- new Plugin6({
2069
- key: new PluginKey7("checklist"),
2215
+ new Plugin7({
2216
+ key: new PluginKey8("checklist"),
2070
2217
  props: { nodeViews: { listItem: (node, view, getPos) => listItemView(editor, node, view, getPos) } }
2071
2218
  })
2072
2219
  ];
2073
2220
  }
2074
2221
  });
2075
- var Callout = Extension7.create({
2222
+ var Callout = Extension8.create({
2076
2223
  name: "callout",
2077
2224
  addGlobalAttributes() {
2078
2225
  return [
@@ -2103,6 +2250,7 @@ export {
2103
2250
  KeptSelection,
2104
2251
  LinkChips,
2105
2252
  PastedImages,
2253
+ PastedMarkdown,
2106
2254
  PersonMention,
2107
2255
  ReferenceCards,
2108
2256
  ReferenceMenu,
@@ -2144,6 +2292,9 @@ export {
2144
2292
  setReferenceCardAt,
2145
2293
  startBlockDrag,
2146
2294
  suggestionPopup,
2295
+ turnIntoRows,
2296
+ turnListInto,
2297
+ turnQuoteInto,
2147
2298
  typeTrigger,
2148
2299
  urgentPubkeys,
2149
2300
  useReferenceCards