@estiva-app/ui 0.70.0 → 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
@@ -516,260 +516,8 @@ function suggestionPopup(options) {
516
516
  import { useMemo } from "react";
517
517
  import { IconCopy, IconTrash } from "@tabler/icons-react";
518
518
  import { BlockHandle, blockAtY } from "@estiva-app/ui";
519
+ import { Selection } from "@tiptap/pm/state";
519
520
  import { MenuItem, MenuSub } from "@estiva-app/ui";
520
- import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
521
- function editorBlocks(view) {
522
- const out = [];
523
- view.state.doc.forEach((node, pos) => {
524
- const element = view.nodeDOM(pos);
525
- if (element instanceof HTMLElement) out.push({ id: node.attrs.blockId || void 0, element, pos });
526
- });
527
- return out;
528
- }
529
- function duplicateBlock(editor, pos) {
530
- const node = editor.state.doc.nodeAt(pos);
531
- if (!node) return false;
532
- editor.view.dispatch(editor.state.tr.insert(pos + node.nodeSize, node).scrollIntoView());
533
- editor.commands.focus(pos + node.nodeSize + 1);
534
- return true;
535
- }
536
- function deleteBlock(editor, pos) {
537
- const { state } = editor;
538
- const node = state.doc.nodeAt(pos);
539
- if (!node) return false;
540
- 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);
541
- editor.view.dispatch(tr);
542
- editor.commands.focus(Math.min(pos + 1, editor.state.doc.content.size));
543
- return true;
544
- }
545
- function moveBlock(editor, from, to) {
546
- const node = editor.state.doc.nodeAt(from);
547
- if (!node || to >= from && to <= from + node.nodeSize) return false;
548
- const tr = editor.state.tr.delete(from, from + node.nodeSize);
549
- const at = tr.mapping.map(to);
550
- editor.view.dispatch(tr.insert(at, node).scrollIntoView());
551
- editor.commands.focus(at + 1);
552
- return true;
553
- }
554
- function startBlockDrag(editor, pos, event, image) {
555
- editor.commands.setNodeSelection(pos);
556
- const { view } = editor;
557
- const slice = view.state.selection.content();
558
- const { dom, text } = view.serializeForClipboard(slice);
559
- const transfer = event.dataTransfer;
560
- if (transfer) {
561
- transfer.clearData();
562
- transfer.setData("text/html", dom.innerHTML);
563
- transfer.setData("text/plain", text);
564
- transfer.effectAllowed = "copyMove";
565
- if (image) transfer.setDragImage(image, 0, 0);
566
- }
567
- view.dragging = { slice, move: true };
568
- }
569
- var blockDropCursor = { color: false, class: "bg-accent-primary" };
570
- var NOTHING_YET = { find: () => null, rows: () => null, startDrag: () => {
571
- }, drop: () => {
572
- }, endDrag: () => {
573
- }, onChange: () => () => {
574
- } };
575
- function EditorBlockHandle({ editor, turnInto = [], actions, children, className }) {
576
- const editing = useMemo(() => {
577
- if (!editor) return NOTHING_YET;
578
- let dragged;
579
- return {
580
- find: (y) => blockAtY(editorBlocks(editor.view), y),
581
- rows: (block) => {
582
- const pos = block.pos;
583
- if (!editor.isEditable) return null;
584
- const node = pos === void 0 ? null : editor.state.doc.nodeAt(pos);
585
- if (pos === void 0 || !node) return null;
586
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
587
- node.isTextblock && turnInto.length > 0 && /* @__PURE__ */ jsx3(MenuSub, { label: "Turn into", children: turnInto.map((row) => /* @__PURE__ */ jsx3(
588
- MenuItem,
589
- {
590
- label: row.label,
591
- selected: row.active?.(node) ?? false,
592
- onClick: () => {
593
- editor.chain().focus().setTextSelection(pos + 1).run();
594
- row.run(editor);
595
- }
596
- },
597
- row.label
598
- )) }),
599
- /* @__PURE__ */ jsx3(MenuItem, { label: "Duplicate", leading: /* @__PURE__ */ jsx3(IconCopy, { size: 16, stroke: 1.5, className: "text-text-secondary" }), onClick: () => duplicateBlock(editor, pos) }),
600
- /* @__PURE__ */ jsx3(MenuItem, { label: "Delete", destructive: true, leading: /* @__PURE__ */ jsx3(IconTrash, { size: 16, stroke: 1.5, className: "text-error-default" }), onClick: () => deleteBlock(editor, pos) })
601
- ] });
602
- },
603
- startDrag: (block, event) => {
604
- dragged = block.pos;
605
- if (block.pos !== void 0) startBlockDrag(editor, block.pos, event, block.element);
606
- },
607
- drop: (target, after) => {
608
- const node = target.pos === void 0 ? null : editor.state.doc.nodeAt(target.pos);
609
- if (dragged === void 0 || target.pos === void 0 || !node) return;
610
- moveBlock(editor, dragged, after ? target.pos + node.nodeSize : target.pos);
611
- },
612
- // ProseMirror clears `dragging` on its own dragend, which a drag started outside its element never reaches.
613
- endDrag: () => setTimeout(() => editor.view.dragging = null, 50),
614
- onChange: (changed) => {
615
- editor.on("update", changed);
616
- return () => {
617
- editor.off("update", changed);
618
- };
619
- }
620
- };
621
- }, [editor, turnInto]);
622
- return /* @__PURE__ */ jsx3(BlockHandle, { editing, actions, className, children });
623
- }
624
-
625
- // src/EditorTableControls.tsx
626
- import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef3, useState as useState2 } from "react";
627
- import { TextSelection } from "@tiptap/pm/state";
628
- import { addColumnAfter, addColumnBefore, addRowAfter, addRowBefore, deleteColumn, deleteRow, deleteTable, toggleHeader } from "@tiptap/pm/tables";
629
- import { IconColumnInsertLeft, IconColumnInsertRight, IconGripHorizontal, IconGripVertical, IconPlus, IconRowInsertBottom, IconRowInsertTop, IconTrash as IconTrash2 } from "@tabler/icons-react";
630
- import { cn } from "@estiva-app/ui";
631
- import { IconButton } from "@estiva-app/ui";
632
-
633
- // src/looks.ts
634
- var FLOATING_SURFACE_CLASSES = "rounded-lg border border-border-default bg-bg-elevated shadow-lg";
635
-
636
- // src/EditorTableControls.tsx
637
- import { Menu, MenuItem as MenuItem2, MenuSeparator } from "@estiva-app/ui";
638
- import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
639
- function inCell(editor, cell, command) {
640
- const { view } = editor;
641
- if (!view.dom.contains(cell)) return false;
642
- const at = view.posAtDOM(cell, 0);
643
- view.dispatch(view.state.tr.setSelection(TextSelection.near(view.state.doc.resolve(at))));
644
- const done = command(view.state, view.dispatch, view);
645
- editor.commands.focus();
646
- return done;
647
- }
648
- function isHeaderRow(row) {
649
- return row.cells.length > 0 && Array.from(row.cells).every((c) => c.tagName === "TH");
650
- }
651
- function deleteTableRow(editor, cell) {
652
- const row = cell.closest("tr");
653
- const table = cell.closest("table");
654
- if (!row || !table || table.rows.length <= 1) return inCell(editor, cell, deleteTable);
655
- const header = isHeaderRow(row);
656
- return inCell(editor, cell, header ? (state, dispatch, view) => deleteRow(state, dispatch) && (!view || toggleHeader("row")(view.state, view.dispatch)) : deleteRow);
657
- }
658
- function deleteTableColumn(editor, cell) {
659
- const row = cell.closest("tr");
660
- return inCell(editor, cell, row && row.cells.length > 1 ? deleteColumn : deleteTable);
661
- }
662
- function appendTableRow(editor, table) {
663
- const last = table.rows[table.rows.length - 1]?.cells[0];
664
- return !!last && inCell(editor, last, addRowAfter);
665
- }
666
- function appendTableColumn(editor, table) {
667
- const first = table.rows[0];
668
- const last = first?.cells[first.cells.length - 1];
669
- return !!last && inCell(editor, last, addColumnAfter);
670
- }
671
- function place(frame, { table, row, cell }) {
672
- const at = frame.getBoundingClientRect();
673
- const t = table.getBoundingClientRect();
674
- const r = row.getBoundingClientRect();
675
- const c = cell.getBoundingClientRect();
676
- return {
677
- tableBox: { top: t.top - at.top, left: t.left - at.left, width: t.width, height: t.height },
678
- rowBox: { top: r.top - at.top, height: r.height },
679
- columnBox: { left: c.left - at.left, width: c.width }
680
- };
681
- }
682
- var ICON = { size: 16, stroke: 1.5, className: "text-text-secondary" };
683
- var keepFocus = (e) => e.preventDefault();
684
- var HANDLE_BOX = cn(FLOATING_SURFACE_CLASSES, "shadow-none");
685
- function EditorTableControls({ editor, children, className }) {
686
- const frame = useRef3(null);
687
- const [hovered, setHovered] = useState2(null);
688
- const [menu, setMenu] = useState2(null);
689
- const editable = !!editor && editor.isEditable;
690
- const follow = useCallback2(
691
- (target) => {
692
- if (!editor || !frame.current || !(target instanceof Element)) return;
693
- if (target.closest("[data-table-control]")) return;
694
- const cell = target.closest("td, th");
695
- const row = cell?.closest("tr");
696
- const table = row?.closest("table");
697
- if (!cell || !row || !table || !editor.view.dom.contains(table)) {
698
- setHovered(null);
699
- return;
700
- }
701
- const next = { table, row, cell, ...place(frame.current, { table, row, cell }) };
702
- setHovered((was) => was && was.cell === cell && JSON.stringify([was.tableBox, was.rowBox, was.columnBox]) === JSON.stringify([next.tableBox, next.rowBox, next.columnBox]) ? was : next);
703
- },
704
- [editor]
705
- );
706
- useEffect2(() => {
707
- if (!editor) return;
708
- const changed = ({ transaction }) => {
709
- if (transaction.docChanged && !menu) setHovered(null);
710
- };
711
- editor.on("transaction", changed);
712
- return () => {
713
- editor.off("transaction", changed);
714
- };
715
- }, [editor, menu]);
716
- const shown = editable ? hovered : null;
717
- const run = (act) => () => {
718
- if (editor) act();
719
- setMenu(null);
720
- setHovered(null);
721
- };
722
- return /* @__PURE__ */ jsxs3(
723
- "div",
724
- {
725
- ref: frame,
726
- className: cn("relative", className),
727
- onMouseMove: (e) => {
728
- if (!menu) follow(e.target);
729
- },
730
- onMouseLeave: () => {
731
- if (!menu) setHovered(null);
732
- },
733
- children: [
734
- children,
735
- shown && editor && /* @__PURE__ */ jsxs3(Fragment2, { children: [
736
- /* @__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(
737
- Menu,
738
- {
739
- open: menu === "row",
740
- onOpenChange: (open) => setMenu(open ? "row" : null),
741
- 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 }) }),
742
- children: [
743
- !isHeaderRow(shown.row) && /* @__PURE__ */ jsx4(MenuItem2, { label: "Insert above", leading: /* @__PURE__ */ jsx4(IconRowInsertTop, { ...ICON }), onClick: run(() => inCell(editor, shown.cell, addRowBefore)) }),
744
- /* @__PURE__ */ jsx4(MenuItem2, { label: "Insert below", leading: /* @__PURE__ */ jsx4(IconRowInsertBottom, { ...ICON }), onClick: run(() => inCell(editor, shown.cell, addRowAfter)) }),
745
- /* @__PURE__ */ jsx4(MenuSeparator, {}),
746
- /* @__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)) }),
747
- /* @__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)) })
748
- ]
749
- }
750
- ) }),
751
- /* @__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(
752
- Menu,
753
- {
754
- open: menu === "column",
755
- onOpenChange: (open) => setMenu(open ? "column" : null),
756
- 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 }) }),
757
- children: [
758
- /* @__PURE__ */ jsx4(MenuItem2, { label: "Insert left", leading: /* @__PURE__ */ jsx4(IconColumnInsertLeft, { ...ICON }), onClick: run(() => inCell(editor, shown.cell, addColumnBefore)) }),
759
- /* @__PURE__ */ jsx4(MenuItem2, { label: "Insert right", leading: /* @__PURE__ */ jsx4(IconColumnInsertRight, { ...ICON }), onClick: run(() => inCell(editor, shown.cell, addColumnAfter)) }),
760
- /* @__PURE__ */ jsx4(MenuSeparator, {}),
761
- /* @__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)) }),
762
- /* @__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)) })
763
- ]
764
- }
765
- ) }),
766
- 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 }) }) }),
767
- 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 }) }) })
768
- ] })
769
- ]
770
- }
771
- );
772
- }
773
521
 
774
522
  // src/ComposerTriggers.tsx
775
523
  import { Extension as Extension4 } from "@tiptap/core";
@@ -780,7 +528,7 @@ import { NodeViewWrapper, ReactNodeViewRenderer } from "@tiptap/react";
780
528
  import { Avatar } from "@estiva-app/ui";
781
529
  import { EnterHint } from "@estiva-app/ui";
782
530
  import { inlineChipClassName } from "@estiva-app/ui";
783
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
531
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
784
532
  var PEOPLE_LIMIT = 6;
785
533
  function filterPeople(people, query) {
786
534
  const q = query.toLowerCase();
@@ -798,8 +546,8 @@ function peopleMenu(urgent) {
798
546
  row: (person) => ({
799
547
  label: person.name,
800
548
  description: person.description,
801
- leading: person.avatar ?? /* @__PURE__ */ jsx5(Avatar, { size: 32, alt: person.name }),
802
- hint: /* @__PURE__ */ jsx5(EnterHint, {}),
549
+ leading: person.avatar ?? /* @__PURE__ */ jsx3(Avatar, { size: 32, alt: person.name }),
550
+ hint: /* @__PURE__ */ jsx3(EnterHint, {}),
803
551
  size: "tall"
804
552
  })
805
553
  };
@@ -926,9 +674,9 @@ var CaptionedReference = Mention.extend({
926
674
  addNodeView() {
927
675
  const { icon, iconFor } = this.options;
928
676
  return ReactNodeViewRenderer(
929
- ({ node }) => /* @__PURE__ */ jsxs4(NodeViewWrapper, { as: "span", className: inlineChipClassName("neutral", "max-w-[24ch] cursor-default"), children: [
930
- /* @__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 }),
931
- /* @__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) })
932
680
  ] }),
933
681
  { as: "span" }
934
682
  );
@@ -950,7 +698,7 @@ function referenceMenu({ ariaLabel, sectionLabel, icon, iconFor }) {
950
698
  return groups;
951
699
  },
952
700
  itemKey: (item) => `reference-${item.id}`,
953
- 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, {}) })
954
702
  };
955
703
  }
956
704
  var listed = (options, query) => options.sections ? options.sections(query).flatMap((section) => section.items.map((item) => ({ ...item, section: section.label }))) : filterReferences(options.items(), query);
@@ -1103,6 +851,364 @@ var SlashCommands = Extension4.create({
1103
851
  }
1104
852
  });
1105
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" });
1056
+ }
1057
+ rows.push({ label: "Text", run: (e, pos) => void turnQuoteInto(e, pos, "text") });
1058
+ return rows;
1059
+ }
1060
+ return [];
1061
+ }
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();
1114
+ return {
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 }
1118
+ };
1119
+ }
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);
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
+ );
1210
+ }
1211
+
1106
1212
  // src/BlockNodes.tsx
1107
1213
  import { Extension as Extension8, InputRule, Node as Node2 } from "@tiptap/core";
1108
1214
  import { DOMSerializer } from "@tiptap/pm/model";
@@ -1683,7 +1789,7 @@ function claimsIn(doc) {
1683
1789
  doc.descendants((node, pos, parent) => {
1684
1790
  const fixed = node.type.name === "unknownBlock";
1685
1791
  if (!fixed && !BLOCK_TYPE_SET.has(node.type.name)) return;
1686
- if (parent?.type.name === "listItem" && LISTS.has(node.type.name)) return;
1792
+ if (parent?.type.name === "listItem" && LISTS2.has(node.type.name)) return;
1687
1793
  const id = node.attrs.blockId;
1688
1794
  if (typeof id !== "string" || id === "") return;
1689
1795
  const copies = claims.get(id);
@@ -1730,11 +1836,11 @@ function repeatedBlockIds(doc, before, mapping) {
1730
1836
  }
1731
1837
  var BLOCK_CONTAINERS = /* @__PURE__ */ new Set(["bulletList", "orderedList", "table", "tableRow", "tableHeader", "tableCell"]);
1732
1838
  var FLATTENED = /* @__PURE__ */ new Set(["listItem", "blockquote"]);
1733
- var LISTS = /* @__PURE__ */ new Set(["bulletList", "orderedList"]);
1839
+ var LISTS2 = /* @__PURE__ */ new Set(["bulletList", "orderedList"]);
1734
1840
  function innerLists(item, pos) {
1735
1841
  const out = [];
1736
1842
  item.forEach((child, offset) => {
1737
- 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 });
1738
1844
  });
1739
1845
  return out;
1740
1846
  }
@@ -1763,7 +1869,7 @@ function hiddenIds(doc) {
1763
1869
  doc.descendants((node, pos) => {
1764
1870
  if (!FLATTENED.has(node.type.name)) return;
1765
1871
  node.forEach((child, offset) => {
1766
- 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);
1767
1873
  if (inner && idOf(child) !== void 0) out.push(pos + 1 + offset);
1768
1874
  });
1769
1875
  });
@@ -2186,6 +2292,9 @@ export {
2186
2292
  setReferenceCardAt,
2187
2293
  startBlockDrag,
2188
2294
  suggestionPopup,
2295
+ turnIntoRows,
2296
+ turnListInto,
2297
+ turnQuoteInto,
2189
2298
  typeTrigger,
2190
2299
  urgentPubkeys,
2191
2300
  useReferenceCards