@bizapps/design-system-testing 1.6.0 → 1.7.2

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
@@ -1,16 +1,19 @@
1
1
  "use client";
2
2
  import { createContext, useId, useState, useCallback, useMemo, useRef, useEffect, useContext } from 'react';
3
- import { EditorContent, ReactNodeViewRenderer, useEditorState, NodeViewWrapper } from '@tiptap/react';
3
+ import { EditorContent, ReactNodeViewRenderer, useEditor, NodeViewWrapper, useEditorState } from '@tiptap/react';
4
4
  import { clsx } from 'clsx';
5
5
  import { twMerge } from 'tailwind-merge';
6
6
  import { Slot, Label as Label$1, DropdownMenu as DropdownMenu$1, Dialog as Dialog$1 } from 'radix-ui';
7
7
  import { jsx, jsxs } from 'react/jsx-runtime';
8
8
  import { cva } from 'class-variance-authority';
9
- import { Trash2, Ban, ChevronDown, Check, LoaderCircle, XIcon, UploadIcon, ExternalLink, Pencil, Unlink } from 'lucide-react';
9
+ import { Trash2, ChevronDown, Check, Bold, Italic, Heading2, Highlighter, Underline, Strikethrough, Quote, List, ListOrdered, Link, ImagePlus, Table, Ban, LoaderCircle, XIcon, UploadIcon, ExternalLink, Pencil, Unlink } from 'lucide-react';
10
10
  import { motion, AnimatePresence } from 'motion/react';
11
11
  import { useDropzone } from 'react-dropzone';
12
- import { BubbleMenu } from '@tiptap/react/menus';
12
+ import Highlight from '@tiptap/extension-highlight';
13
+ import { TableKit } from '@tiptap/extension-table';
14
+ import StarterKit from '@tiptap/starter-kit';
13
15
  import { Image } from '@tiptap/extension-image';
16
+ import { BubbleMenu } from '@tiptap/react/menus';
14
17
 
15
18
  // src/components/compounds/RichTextEditor/rich-text-editor.tsx
16
19
  function cn(...inputs) {
@@ -814,6 +817,430 @@ function InsertImageDialog({
814
817
  ] })
815
818
  ] }) });
816
819
  }
820
+ var DEFAULT_MIN_WIDTH = 120;
821
+ var DEFAULT_START_WIDTH = 400;
822
+ var DEFAULT_RATIO = 4 / 3;
823
+ var KEYBOARD_STEP = 20;
824
+ function ResizableImageView({ node, updateAttributes, selected, extension }) {
825
+ const { src, alt, width, height } = node.attrs;
826
+ const sizeRef = useRef({ width, height });
827
+ sizeRef.current = { width, height };
828
+ const { resizeLabel, minWidth = DEFAULT_MIN_WIDTH } = extension.options;
829
+ const dragRef = useRef(null);
830
+ const [dragging, setDragging] = useState(false);
831
+ const resizeTo = useCallback(
832
+ (nextWidth, ratio, maxWidth) => {
833
+ const clamped = Math.max(minWidth, Math.min(Math.round(nextWidth), maxWidth));
834
+ updateAttributes({ width: clamped, height: Math.round(clamped / ratio) });
835
+ },
836
+ [minWidth, updateAttributes]
837
+ );
838
+ const measure = useCallback((handle) => {
839
+ const image = handle.parentElement?.querySelector("img") ?? null;
840
+ const current = sizeRef.current;
841
+ const editable = handle.closest(".ProseMirror");
842
+ const available = editable?.clientWidth ?? 0;
843
+ const naturalWidth = image?.naturalWidth ?? 0;
844
+ const naturalHeight = image?.naturalHeight ?? 0;
845
+ const ratio = current.width > 0 && current.height > 0 ? current.width / current.height : naturalWidth > 0 && naturalHeight > 0 ? naturalWidth / naturalHeight : DEFAULT_RATIO;
846
+ const unbounded = current.width > 0 ? current.width : naturalWidth || DEFAULT_START_WIDTH;
847
+ const startWidth = available > 0 ? Math.min(unbounded, available) : unbounded;
848
+ return {
849
+ width: startWidth,
850
+ ratio,
851
+ // jsdom reports clientWidth as 0, so a zero measure means "no
852
+ // constraint" — the real clamp only applies in a browser.
853
+ maxWidth: available > 0 ? available : Infinity
854
+ };
855
+ }, []);
856
+ const imageRef = useRef(null);
857
+ const fittedSrcRef = useRef(null);
858
+ useEffect(() => {
859
+ const img = imageRef.current;
860
+ if (!img) return;
861
+ if (sizeRef.current.width > 0 || fittedSrcRef.current === src) return;
862
+ const fit = () => {
863
+ const available = img.closest(".ProseMirror")?.clientWidth ?? 0;
864
+ if (!img.naturalWidth || !img.naturalHeight || available <= 0) return;
865
+ fittedSrcRef.current = src;
866
+ const nextWidth = Math.min(img.naturalWidth, available);
867
+ updateAttributes({
868
+ width: nextWidth,
869
+ height: Math.round(nextWidth / (img.naturalWidth / img.naturalHeight))
870
+ });
871
+ };
872
+ if (img.complete) {
873
+ fit();
874
+ return;
875
+ }
876
+ img.addEventListener("load", fit, { once: true });
877
+ return () => img.removeEventListener("load", fit);
878
+ }, [src, updateAttributes]);
879
+ const onPointerDown = useCallback(
880
+ (event) => {
881
+ event.preventDefault();
882
+ event.stopPropagation();
883
+ const start = measure(event.currentTarget);
884
+ dragRef.current = { startX: event.clientX, ...start };
885
+ setDragging(true);
886
+ const onMove = (moveEvent) => {
887
+ const drag = dragRef.current;
888
+ if (!drag) return;
889
+ resizeTo(drag.width + (moveEvent.clientX - drag.startX), drag.ratio, drag.maxWidth);
890
+ };
891
+ const onUp = () => {
892
+ dragRef.current = null;
893
+ setDragging(false);
894
+ window.removeEventListener("pointermove", onMove);
895
+ window.removeEventListener("pointerup", onUp);
896
+ };
897
+ window.addEventListener("pointermove", onMove);
898
+ window.addEventListener("pointerup", onUp);
899
+ },
900
+ [measure, resizeTo]
901
+ );
902
+ const onKeyDown = useCallback(
903
+ (event) => {
904
+ const step = event.key === "ArrowRight" ? KEYBOARD_STEP : event.key === "ArrowLeft" ? -KEYBOARD_STEP : 0;
905
+ if (step === 0) return;
906
+ event.preventDefault();
907
+ const start = measure(event.currentTarget);
908
+ resizeTo(start.width + step, start.ratio, start.maxWidth);
909
+ },
910
+ [measure, resizeTo]
911
+ );
912
+ const sizeStyle = width > 0 ? { width, height: height > 0 ? height : void 0, maxWidth: "100%" } : void 0;
913
+ return /* @__PURE__ */ jsxs(
914
+ NodeViewWrapper,
915
+ {
916
+ "data-slot": "resizable-image",
917
+ className: "group relative inline-block max-w-full",
918
+ children: [
919
+ /* @__PURE__ */ jsx(
920
+ "img",
921
+ {
922
+ ref: imageRef,
923
+ src,
924
+ alt,
925
+ className: "max-w-full rounded-md",
926
+ style: sizeStyle,
927
+ draggable: false
928
+ }
929
+ ),
930
+ /* @__PURE__ */ jsx(
931
+ "div",
932
+ {
933
+ role: "button",
934
+ tabIndex: 0,
935
+ "aria-label": resizeLabel,
936
+ title: resizeLabel,
937
+ "data-slot": "resizable-image-handle",
938
+ onPointerDown,
939
+ onKeyDown,
940
+ className: cn(
941
+ "border-primary absolute -right-1 -bottom-1 h-4 w-4 cursor-se-resize touch-none rounded-full border-2 bg-white transition-opacity",
942
+ "focus-visible:ring-ring/50 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:outline-none",
943
+ dragging || selected ? "opacity-100" : "opacity-0 group-hover:opacity-100"
944
+ )
945
+ }
946
+ )
947
+ ]
948
+ }
949
+ );
950
+ }
951
+ function createResizableImage(options) {
952
+ return Image.extend({
953
+ addOptions() {
954
+ return { ...this.parent(), ...options };
955
+ },
956
+ // The size attributes belong to the extension, not to the consumer: the
957
+ // stock Image extension keeps width/height too, but they default to null
958
+ // and render as HTML attributes, which pins the image to a fixed size.
959
+ //
960
+ // A numeric default with `rendered: false` is what makes resizing work
961
+ // at all — Tiptap v3 gates DOM emission on `rendered`, so the size stays
962
+ // out of the markup and lives only in the document JSON, where a
963
+ // serializer can read it. Without these declarations the schema drops
964
+ // every updateAttributes({ width }) call and the handle does nothing.
965
+ addAttributes() {
966
+ return {
967
+ ...this.parent?.(),
968
+ width: { default: 0, rendered: false },
969
+ height: { default: 0, rendered: false }
970
+ };
971
+ },
972
+ addNodeView() {
973
+ return ReactNodeViewRenderer(ResizableImageView);
974
+ }
975
+ });
976
+ }
977
+ function ListMenu({ editor, triggerLabel, icon, options }) {
978
+ const active = options.find((option) => editor?.isActive(option.node));
979
+ return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
980
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
981
+ Button,
982
+ {
983
+ type: "button",
984
+ size: "sm",
985
+ variant: active ? "secondary" : "ghost",
986
+ disabled: !editor,
987
+ title: active ? active.label : triggerLabel,
988
+ "aria-label": active ? active.label : triggerLabel,
989
+ "data-slot": "list-menu-trigger",
990
+ children: [
991
+ active ? active.icon : icon,
992
+ /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 opacity-60" })
993
+ ]
994
+ }
995
+ ) }),
996
+ /* @__PURE__ */ jsx(DropdownMenuContent, { align: "start", children: options.map((option) => {
997
+ const isActive = editor?.isActive(option.node) ?? false;
998
+ return /* @__PURE__ */ jsxs(
999
+ DropdownMenuItem,
1000
+ {
1001
+ disabled: !editor,
1002
+ className: "cursor-pointer",
1003
+ onSelect: () => editor && option.run(editor),
1004
+ children: [
1005
+ option.icon,
1006
+ /* @__PURE__ */ jsx("span", { className: "flex-1", children: option.label }),
1007
+ /* @__PURE__ */ jsx(
1008
+ Check,
1009
+ {
1010
+ "aria-hidden": "true",
1011
+ className: cn("h-4 w-4", isActive ? "opacity-100" : "opacity-0")
1012
+ }
1013
+ )
1014
+ ]
1015
+ },
1016
+ option.node
1017
+ );
1018
+ }) })
1019
+ ] });
1020
+ }
1021
+ function useRichTextEditor({
1022
+ labels,
1023
+ onUpdate,
1024
+ content,
1025
+ onInsertLink,
1026
+ onInsertImage,
1027
+ extraExtensions = [],
1028
+ extraToolbar = [],
1029
+ contentClassName,
1030
+ allowBase64Images = false,
1031
+ editorProps
1032
+ }) {
1033
+ const [linkNeedsText, setLinkNeedsText] = useState(false);
1034
+ const editor = useEditor({
1035
+ extensions: [
1036
+ // openOnClick false: a plain click has to place the caret so the
1037
+ // author can edit link text. Left at its default, Tiptap navigates
1038
+ // the current tab and throws the author out of the editor.
1039
+ StarterKit.configure({ link: { openOnClick: false } }),
1040
+ Highlight,
1041
+ TableKit.configure({ table: { resizable: true } }),
1042
+ createResizableImage({ resizeLabel: labels.resizeImage }).configure({
1043
+ inline: false,
1044
+ allowBase64: allowBase64Images
1045
+ }),
1046
+ ...extraExtensions
1047
+ ],
1048
+ content,
1049
+ immediatelyRender: false,
1050
+ // Required for the toolbar's pressed state and the image node view to
1051
+ // repaint as the selection and attributes change.
1052
+ shouldRerenderOnTransaction: true,
1053
+ editorProps: {
1054
+ ...editorProps,
1055
+ attributes: {
1056
+ // ProseMirror's editable div needs its own accessible name: a
1057
+ // visible <Label> beside it is decoration, and a screen reader
1058
+ // would announce an unnamed box.
1059
+ role: "textbox",
1060
+ "aria-label": labels.editor,
1061
+ class: contentClassName ? `${EDITOR_CONTENT_STYLE} ${contentClassName}` : EDITOR_CONTENT_STYLE
1062
+ },
1063
+ // Ctrl/Cmd-click opens the link in a new tab and leaves the author
1064
+ // where they were: the shortcut is a deliberate gesture, and keeping
1065
+ // the editor open means no unsaved work is at risk. A plain click
1066
+ // falls through to ProseMirror and just moves the caret.
1067
+ handleClick(_view, _pos, event) {
1068
+ if (!event.ctrlKey && !event.metaKey) return false;
1069
+ const href = event.target?.closest("a")?.getAttribute("href");
1070
+ if (!href) return false;
1071
+ event.preventDefault();
1072
+ window.open(href, "_blank", "noopener,noreferrer");
1073
+ return true;
1074
+ }
1075
+ },
1076
+ onUpdate: onUpdate ? ({ editor: current }) => onUpdate(current.getJSON()) : void 0
1077
+ });
1078
+ const currentLinkHref = useCallback(
1079
+ () => editor?.getAttributes("link").href ?? "",
1080
+ [editor]
1081
+ );
1082
+ const openLink = useCallback(() => {
1083
+ if (editor) {
1084
+ const { from, to } = editor.state.selection;
1085
+ setLinkNeedsText(from === to && !editor.isActive("link"));
1086
+ }
1087
+ onInsertLink?.();
1088
+ }, [editor, onInsertLink]);
1089
+ const applyLink = useCallback(
1090
+ (href, text) => {
1091
+ if (!editor) return;
1092
+ const url = href.trim();
1093
+ if (linkNeedsText) {
1094
+ if (url && text?.trim()) {
1095
+ editor.chain().focus().insertContent({
1096
+ type: "text",
1097
+ text: text.trim(),
1098
+ marks: [{ type: "link", attrs: { href: url } }]
1099
+ }).run();
1100
+ }
1101
+ return;
1102
+ }
1103
+ if (url) editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
1104
+ else editor.chain().focus().extendMarkRange("link").unsetLink().run();
1105
+ },
1106
+ [editor, linkNeedsText]
1107
+ );
1108
+ const toolbar = useMemo(() => {
1109
+ const builtIn = [
1110
+ {
1111
+ kind: "button",
1112
+ id: "bold",
1113
+ icon: /* @__PURE__ */ jsx(Bold, { className: "h-4 w-4" }),
1114
+ label: labels.bold,
1115
+ active: { name: "bold" },
1116
+ run: (e) => e.chain().focus().toggleBold().run()
1117
+ },
1118
+ {
1119
+ kind: "button",
1120
+ id: "italic",
1121
+ icon: /* @__PURE__ */ jsx(Italic, { className: "h-4 w-4" }),
1122
+ label: labels.italic,
1123
+ active: { name: "italic" },
1124
+ run: (e) => e.chain().focus().toggleItalic().run()
1125
+ },
1126
+ {
1127
+ kind: "button",
1128
+ id: "heading",
1129
+ icon: /* @__PURE__ */ jsx(Heading2, { className: "h-4 w-4" }),
1130
+ label: labels.heading,
1131
+ active: { name: "heading", attrs: { level: 2 } },
1132
+ run: (e) => e.chain().focus().toggleHeading({ level: 2 }).run()
1133
+ },
1134
+ { kind: "separator" },
1135
+ {
1136
+ kind: "button",
1137
+ id: "highlight",
1138
+ icon: /* @__PURE__ */ jsx(Highlighter, { className: "h-4 w-4" }),
1139
+ label: labels.highlight,
1140
+ active: { name: "highlight" },
1141
+ run: (e) => e.chain().focus().toggleHighlight().run()
1142
+ },
1143
+ {
1144
+ kind: "button",
1145
+ id: "underline",
1146
+ icon: /* @__PURE__ */ jsx(Underline, { className: "h-4 w-4" }),
1147
+ label: labels.underline,
1148
+ active: { name: "underline" },
1149
+ run: (e) => e.chain().focus().toggleMark("underline").run()
1150
+ },
1151
+ {
1152
+ kind: "button",
1153
+ id: "strike",
1154
+ icon: /* @__PURE__ */ jsx(Strikethrough, { className: "h-4 w-4" }),
1155
+ label: labels.strikethrough,
1156
+ active: { name: "strike" },
1157
+ run: (e) => e.chain().focus().toggleMark("strike").run()
1158
+ },
1159
+ { kind: "separator" },
1160
+ {
1161
+ kind: "button",
1162
+ id: "quote",
1163
+ icon: /* @__PURE__ */ jsx(Quote, { className: "h-4 w-4" }),
1164
+ label: labels.quote,
1165
+ active: { name: "blockquote" },
1166
+ run: (e) => e.chain().focus().toggleBlockquote().run()
1167
+ },
1168
+ {
1169
+ kind: "custom",
1170
+ id: "lists",
1171
+ render: (current) => /* @__PURE__ */ jsx(
1172
+ ListMenu,
1173
+ {
1174
+ editor: current,
1175
+ triggerLabel: labels.lists.trigger,
1176
+ icon: /* @__PURE__ */ jsx(List, { className: "h-4 w-4" }),
1177
+ options: [
1178
+ {
1179
+ node: "bulletList",
1180
+ label: labels.lists.bullet,
1181
+ icon: /* @__PURE__ */ jsx(List, { className: "h-4 w-4" }),
1182
+ run: (e) => e.chain().focus().toggleBulletList().run()
1183
+ },
1184
+ {
1185
+ node: "orderedList",
1186
+ label: labels.lists.ordered,
1187
+ icon: /* @__PURE__ */ jsx(ListOrdered, { className: "h-4 w-4" }),
1188
+ run: (e) => e.chain().focus().toggleOrderedList().run()
1189
+ }
1190
+ ]
1191
+ }
1192
+ )
1193
+ },
1194
+ { kind: "separator" },
1195
+ {
1196
+ kind: "button",
1197
+ id: "link",
1198
+ icon: /* @__PURE__ */ jsx(Link, { className: "h-4 w-4" }),
1199
+ label: labels.insertLink,
1200
+ active: { name: "link" },
1201
+ run: () => openLink()
1202
+ },
1203
+ {
1204
+ kind: "button",
1205
+ id: "image",
1206
+ icon: /* @__PURE__ */ jsx(ImagePlus, { className: "h-4 w-4" }),
1207
+ label: labels.insertImage,
1208
+ // No `active`: this opens a dialog, it is never a pressed state.
1209
+ run: () => onInsertImage?.()
1210
+ },
1211
+ {
1212
+ kind: "custom",
1213
+ id: "table",
1214
+ render: (current) => /* @__PURE__ */ jsx(
1215
+ TableMenu,
1216
+ {
1217
+ editor: current,
1218
+ icon: /* @__PURE__ */ jsx(Table, { className: "h-4 w-4" }),
1219
+ labels: labels.table
1220
+ }
1221
+ )
1222
+ }
1223
+ ];
1224
+ let items = builtIn;
1225
+ const appended = [];
1226
+ for (const group of extraToolbar) {
1227
+ if (!group.after) {
1228
+ appended.push(...group.items);
1229
+ continue;
1230
+ }
1231
+ const at = items.findIndex(
1232
+ (item) => item.kind !== "separator" && item.id === group.after
1233
+ );
1234
+ if (at === -1) {
1235
+ appended.push(...group.items);
1236
+ continue;
1237
+ }
1238
+ items = [...items.slice(0, at + 1), ...group.items, ...items.slice(at + 1)];
1239
+ }
1240
+ return [...items, ...appended];
1241
+ }, [labels, extraToolbar, onInsertImage, openLink]);
1242
+ return { editor, toolbar, linkNeedsText, currentLinkHref, applyLink };
1243
+ }
817
1244
  function ColorMenu({ editor, labels, icon, swatches }) {
818
1245
  const active = swatches.find((swatch) => editor?.isActive(swatch.mark));
819
1246
  function apply(mark) {
@@ -883,50 +1310,6 @@ function ColorMenu({ editor, labels, icon, swatches }) {
883
1310
  ) })
884
1311
  ] });
885
1312
  }
886
- function ListMenu({ editor, triggerLabel, icon, options }) {
887
- const active = options.find((option) => editor?.isActive(option.node));
888
- return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
889
- /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
890
- Button,
891
- {
892
- type: "button",
893
- size: "sm",
894
- variant: active ? "secondary" : "ghost",
895
- disabled: !editor,
896
- title: active ? active.label : triggerLabel,
897
- "aria-label": active ? active.label : triggerLabel,
898
- "data-slot": "list-menu-trigger",
899
- children: [
900
- active ? active.icon : icon,
901
- /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 opacity-60" })
902
- ]
903
- }
904
- ) }),
905
- /* @__PURE__ */ jsx(DropdownMenuContent, { align: "start", children: options.map((option) => {
906
- const isActive = editor?.isActive(option.node) ?? false;
907
- return /* @__PURE__ */ jsxs(
908
- DropdownMenuItem,
909
- {
910
- disabled: !editor,
911
- className: "cursor-pointer",
912
- onSelect: () => editor && option.run(editor),
913
- children: [
914
- option.icon,
915
- /* @__PURE__ */ jsx("span", { className: "flex-1", children: option.label }),
916
- /* @__PURE__ */ jsx(
917
- Check,
918
- {
919
- "aria-hidden": "true",
920
- className: cn("h-4 w-4", isActive ? "opacity-100" : "opacity-0")
921
- }
922
- )
923
- ]
924
- },
925
- option.node
926
- );
927
- }) })
928
- ] });
929
- }
930
1313
  function InsertLinkDialog({
931
1314
  open,
932
1315
  onOpenChange,
@@ -1068,164 +1451,7 @@ function LinkBubbleMenu({ editor, labels, onEdit, onOpen }) {
1068
1451
  if (!editor) return null;
1069
1452
  return /* @__PURE__ */ jsx(BubbleMenu, { editor, shouldShow, options, children: /* @__PURE__ */ jsx(LinkActions, { editor, labels, onEdit, onOpen }) });
1070
1453
  }
1071
- var DEFAULT_MIN_WIDTH = 120;
1072
- var DEFAULT_START_WIDTH = 400;
1073
- var DEFAULT_RATIO = 4 / 3;
1074
- var KEYBOARD_STEP = 20;
1075
- function ResizableImageView({ node, updateAttributes, selected, extension }) {
1076
- const { src, alt, width, height } = node.attrs;
1077
- const sizeRef = useRef({ width, height });
1078
- sizeRef.current = { width, height };
1079
- const { resizeLabel, minWidth = DEFAULT_MIN_WIDTH } = extension.options;
1080
- const dragRef = useRef(null);
1081
- const [dragging, setDragging] = useState(false);
1082
- const resizeTo = useCallback(
1083
- (nextWidth, ratio, maxWidth) => {
1084
- const clamped = Math.max(minWidth, Math.min(Math.round(nextWidth), maxWidth));
1085
- updateAttributes({ width: clamped, height: Math.round(clamped / ratio) });
1086
- },
1087
- [minWidth, updateAttributes]
1088
- );
1089
- const measure = useCallback((handle) => {
1090
- const image = handle.parentElement?.querySelector("img") ?? null;
1091
- const current = sizeRef.current;
1092
- const editable = handle.closest(".ProseMirror");
1093
- const available = editable?.clientWidth ?? 0;
1094
- const naturalWidth = image?.naturalWidth ?? 0;
1095
- const naturalHeight = image?.naturalHeight ?? 0;
1096
- const ratio = current.width > 0 && current.height > 0 ? current.width / current.height : naturalWidth > 0 && naturalHeight > 0 ? naturalWidth / naturalHeight : DEFAULT_RATIO;
1097
- const unbounded = current.width > 0 ? current.width : naturalWidth || DEFAULT_START_WIDTH;
1098
- const startWidth = available > 0 ? Math.min(unbounded, available) : unbounded;
1099
- return {
1100
- width: startWidth,
1101
- ratio,
1102
- // jsdom reports clientWidth as 0, so a zero measure means "no
1103
- // constraint" — the real clamp only applies in a browser.
1104
- maxWidth: available > 0 ? available : Infinity
1105
- };
1106
- }, []);
1107
- const imageRef = useRef(null);
1108
- const fittedSrcRef = useRef(null);
1109
- useEffect(() => {
1110
- const img = imageRef.current;
1111
- if (!img) return;
1112
- if (sizeRef.current.width > 0 || fittedSrcRef.current === src) return;
1113
- const fit = () => {
1114
- const available = img.closest(".ProseMirror")?.clientWidth ?? 0;
1115
- if (!img.naturalWidth || !img.naturalHeight || available <= 0) return;
1116
- fittedSrcRef.current = src;
1117
- const nextWidth = Math.min(img.naturalWidth, available);
1118
- updateAttributes({
1119
- width: nextWidth,
1120
- height: Math.round(nextWidth / (img.naturalWidth / img.naturalHeight))
1121
- });
1122
- };
1123
- if (img.complete) {
1124
- fit();
1125
- return;
1126
- }
1127
- img.addEventListener("load", fit, { once: true });
1128
- return () => img.removeEventListener("load", fit);
1129
- }, [src, updateAttributes]);
1130
- const onPointerDown = useCallback(
1131
- (event) => {
1132
- event.preventDefault();
1133
- event.stopPropagation();
1134
- const start = measure(event.currentTarget);
1135
- dragRef.current = { startX: event.clientX, ...start };
1136
- setDragging(true);
1137
- const onMove = (moveEvent) => {
1138
- const drag = dragRef.current;
1139
- if (!drag) return;
1140
- resizeTo(drag.width + (moveEvent.clientX - drag.startX), drag.ratio, drag.maxWidth);
1141
- };
1142
- const onUp = () => {
1143
- dragRef.current = null;
1144
- setDragging(false);
1145
- window.removeEventListener("pointermove", onMove);
1146
- window.removeEventListener("pointerup", onUp);
1147
- };
1148
- window.addEventListener("pointermove", onMove);
1149
- window.addEventListener("pointerup", onUp);
1150
- },
1151
- [measure, resizeTo]
1152
- );
1153
- const onKeyDown = useCallback(
1154
- (event) => {
1155
- const step = event.key === "ArrowRight" ? KEYBOARD_STEP : event.key === "ArrowLeft" ? -KEYBOARD_STEP : 0;
1156
- if (step === 0) return;
1157
- event.preventDefault();
1158
- const start = measure(event.currentTarget);
1159
- resizeTo(start.width + step, start.ratio, start.maxWidth);
1160
- },
1161
- [measure, resizeTo]
1162
- );
1163
- const sizeStyle = width > 0 ? { width, height: height > 0 ? height : void 0, maxWidth: "100%" } : void 0;
1164
- return /* @__PURE__ */ jsxs(
1165
- NodeViewWrapper,
1166
- {
1167
- "data-slot": "resizable-image",
1168
- className: "group relative inline-block max-w-full",
1169
- children: [
1170
- /* @__PURE__ */ jsx(
1171
- "img",
1172
- {
1173
- ref: imageRef,
1174
- src,
1175
- alt,
1176
- className: "max-w-full rounded-md",
1177
- style: sizeStyle,
1178
- draggable: false
1179
- }
1180
- ),
1181
- /* @__PURE__ */ jsx(
1182
- "div",
1183
- {
1184
- role: "button",
1185
- tabIndex: 0,
1186
- "aria-label": resizeLabel,
1187
- title: resizeLabel,
1188
- "data-slot": "resizable-image-handle",
1189
- onPointerDown,
1190
- onKeyDown,
1191
- className: cn(
1192
- "border-primary absolute -right-1 -bottom-1 h-4 w-4 cursor-se-resize touch-none rounded-full border-2 bg-white transition-opacity",
1193
- "focus-visible:ring-ring/50 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:outline-none",
1194
- dragging || selected ? "opacity-100" : "opacity-0 group-hover:opacity-100"
1195
- )
1196
- }
1197
- )
1198
- ]
1199
- }
1200
- );
1201
- }
1202
- function createResizableImage(options) {
1203
- return Image.extend({
1204
- addOptions() {
1205
- return { ...this.parent(), ...options };
1206
- },
1207
- // The size attributes belong to the extension, not to the consumer: the
1208
- // stock Image extension keeps width/height too, but they default to null
1209
- // and render as HTML attributes, which pins the image to a fixed size.
1210
- //
1211
- // A numeric default with `rendered: false` is what makes resizing work
1212
- // at all — Tiptap v3 gates DOM emission on `rendered`, so the size stays
1213
- // out of the markup and lives only in the document JSON, where a
1214
- // serializer can read it. Without these declarations the schema drops
1215
- // every updateAttributes({ width }) call and the handle does nothing.
1216
- addAttributes() {
1217
- return {
1218
- ...this.parent?.(),
1219
- width: { default: 0, rendered: false },
1220
- height: { default: 0, rendered: false }
1221
- };
1222
- },
1223
- addNodeView() {
1224
- return ReactNodeViewRenderer(ResizableImageView);
1225
- }
1226
- });
1227
- }
1228
1454
 
1229
- export { ColorMenu, EDITOR_CONTENT_STYLE, InsertImageDialog, InsertLinkDialog, LinkBubbleMenu, ListMenu, RichTextEditor, RichTextToolbar, TableMenu, createResizableImage };
1455
+ export { ColorMenu, EDITOR_CONTENT_STYLE, InsertImageDialog, InsertLinkDialog, LinkBubbleMenu, ListMenu, RichTextEditor, RichTextToolbar, TableMenu, createResizableImage, useRichTextEditor };
1230
1456
  //# sourceMappingURL=editor.js.map
1231
1457
  //# sourceMappingURL=editor.js.map