@react-email/editor 0.0.0-experimental.14 → 0.0.0-experimental.15

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/index.mjs CHANGED
@@ -9,11 +9,14 @@ import { Decoration, DecorationSet } from "@tiptap/pm/view";
9
9
  import { fromHtml } from "hast-util-from-html";
10
10
  import Prism from "prismjs";
11
11
  import TipTapPlaceholder from "@tiptap/extension-placeholder";
12
- import { useCurrentEditor, useEditorState } from "@tiptap/react";
13
- import { AlignCenterIcon, AlignLeftIcon, AlignRightIcon, BoldIcon, CaseUpperIcon, Check, ChevronDown, Code, CodeIcon, ExternalLinkIcon, Heading1, Heading2, Heading3, ItalicIcon, LinkIcon, List, ListOrdered, PencilIcon, StrikethroughIcon, TextIcon, TextQuote, UnderlineIcon, UnlinkIcon } from "lucide-react";
12
+ import { ReactRenderer, useCurrentEditor, useEditorState } from "@tiptap/react";
13
+ import { AlignCenterIcon, AlignLeftIcon, AlignRightIcon, BoldIcon, CaseUpperIcon, Check, ChevronDown, Code, CodeIcon, Columns2, Columns3, Columns4, ExternalLinkIcon, Heading1, Heading2, Heading3, ItalicIcon, LinkIcon, List, ListOrdered, MousePointer, PencilIcon, Rows2, SplitSquareVertical, SquareCode, StrikethroughIcon, Text, TextIcon, TextQuote, UnderlineIcon, UnlinkIcon } from "lucide-react";
14
14
  import * as React from "react";
15
+ import { useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
15
16
  import * as Popover from "@radix-ui/react-popover";
16
17
  import { BubbleMenu as BubbleMenu$1 } from "@tiptap/react/menus";
18
+ import Suggestion from "@tiptap/suggestion";
19
+ import tippy from "tippy.js";
17
20
 
18
21
  //#region src/core/email-node.ts
19
22
  var EmailNode = class EmailNode extends Node {
@@ -2881,5 +2884,464 @@ const LinkBubbleMenu = {
2881
2884
  };
2882
2885
 
2883
2886
  //#endregion
2884
- export { AlignmentAttribute, Body, Bold, BubbleMenu, BubbleMenuAlignCenter, BubbleMenuAlignLeft, BubbleMenuAlignRight, BubbleMenuBold, BubbleMenuCode, BubbleMenuDefault, BubbleMenuItalic, BubbleMenuItem, BubbleMenuItemGroup, BubbleMenuLinkSelector, BubbleMenuNodeSelector, BubbleMenuRoot, BubbleMenuSeparator, BubbleMenuStrike, BubbleMenuUnderline, BubbleMenuUppercase, Button, ButtonBubbleMenu, ButtonBubbleMenuDefault, ButtonBubbleMenuEditLink, ButtonBubbleMenuRoot, ButtonBubbleMenuToolbar, COLUMN_PARENT_TYPES, ClassAttribute, CodeBlockPrism, ColumnsColumn, Div, EmailNode, FourColumns, ImageBubbleMenu, ImageBubbleMenuDefault, ImageBubbleMenuEditLink, ImageBubbleMenuRoot, ImageBubbleMenuToolbar, LinkBubbleMenu, LinkBubbleMenuDefault, LinkBubbleMenuEditLink, LinkBubbleMenuForm, LinkBubbleMenuOpenLink, LinkBubbleMenuRoot, LinkBubbleMenuToolbar, LinkBubbleMenuUnlink, MAX_COLUMNS_DEPTH, MaxNesting, NodeSelectorContent, NodeSelectorRoot, NodeSelectorTrigger, Placeholder, PreservedStyle, PreviewText, Section, StyleAttribute, Sup, Table, TableCell, TableHeader, TableRow, ThreeColumns, TwoColumns, Uppercase, coreExtensions, editorEventBus, getColumnsDepth, processStylesForUnlink, setTextAlignment, useButtonBubbleMenuContext, useImageBubbleMenuContext, useLinkBubbleMenuContext };
2887
+ //#region src/ui/slash-command/utils.ts
2888
+ function isInsideNode(editor, type) {
2889
+ const { $from } = editor.state.selection;
2890
+ for (let d = $from.depth; d > 0; d--) if ($from.node(d).type.name === type) return true;
2891
+ return false;
2892
+ }
2893
+ function isAtMaxColumnsDepth(editor) {
2894
+ const { from } = editor.state.selection;
2895
+ return getColumnsDepth(editor.state.doc, from) >= MAX_COLUMNS_DEPTH;
2896
+ }
2897
+ function updateScrollView(container, item) {
2898
+ const containerRect = container.getBoundingClientRect();
2899
+ const itemRect = item.getBoundingClientRect();
2900
+ if (itemRect.top < containerRect.top) container.scrollTop -= containerRect.top - itemRect.top;
2901
+ else if (itemRect.bottom > containerRect.bottom) container.scrollTop += itemRect.bottom - containerRect.bottom;
2902
+ }
2903
+
2904
+ //#endregion
2905
+ //#region src/ui/slash-command/command-list.tsx
2906
+ const CATEGORY_ORDER = [
2907
+ "Text",
2908
+ "Media",
2909
+ "Layout",
2910
+ "Utility"
2911
+ ];
2912
+ function groupByCategory(items) {
2913
+ const seen = /* @__PURE__ */ new Map();
2914
+ for (const item of items) {
2915
+ const existing = seen.get(item.category);
2916
+ if (existing) existing.push(item);
2917
+ else seen.set(item.category, [item]);
2918
+ }
2919
+ const ordered = [];
2920
+ for (const cat of CATEGORY_ORDER) {
2921
+ const group = seen.get(cat);
2922
+ if (group) {
2923
+ ordered.push({
2924
+ category: cat,
2925
+ items: group
2926
+ });
2927
+ seen.delete(cat);
2928
+ }
2929
+ }
2930
+ for (const [category, group] of seen) ordered.push({
2931
+ category,
2932
+ items: group
2933
+ });
2934
+ return ordered;
2935
+ }
2936
+ function CommandItem({ item, selected, onSelect }) {
2937
+ const Icon = item.icon;
2938
+ return /* @__PURE__ */ jsxs("button", {
2939
+ "data-re-slash-command-item": "",
2940
+ "data-selected": selected || void 0,
2941
+ onClick: onSelect,
2942
+ type: "button",
2943
+ children: [/* @__PURE__ */ jsx(Icon, { size: 20 }), /* @__PURE__ */ jsx("span", { children: item.title })]
2944
+ });
2945
+ }
2946
+ function CommandList({ items, command, query, ref }) {
2947
+ const [selectedIndex, setSelectedIndex] = useState(0);
2948
+ const containerRef = useRef(null);
2949
+ useEffect(() => {
2950
+ setSelectedIndex(0);
2951
+ }, [items]);
2952
+ useLayoutEffect(() => {
2953
+ const container = containerRef.current;
2954
+ if (!container) return;
2955
+ const selected = container.querySelector("[data-selected]");
2956
+ if (selected) updateScrollView(container, selected);
2957
+ }, [selectedIndex]);
2958
+ const selectItem = useCallback((index) => {
2959
+ const item = items[index];
2960
+ if (item) command(item);
2961
+ }, [items, command]);
2962
+ useImperativeHandle(ref, () => ({ onKeyDown: ({ event }) => {
2963
+ if (items.length === 0) return false;
2964
+ if (event.key === "ArrowUp") {
2965
+ setSelectedIndex((i) => (i + items.length - 1) % items.length);
2966
+ return true;
2967
+ }
2968
+ if (event.key === "ArrowDown") {
2969
+ setSelectedIndex((i) => (i + 1) % items.length);
2970
+ return true;
2971
+ }
2972
+ if (event.key === "Enter") {
2973
+ selectItem(selectedIndex);
2974
+ return true;
2975
+ }
2976
+ return false;
2977
+ } }), [
2978
+ items.length,
2979
+ selectItem,
2980
+ selectedIndex
2981
+ ]);
2982
+ if (items.length === 0) return /* @__PURE__ */ jsx("div", {
2983
+ "data-re-slash-command": "",
2984
+ children: /* @__PURE__ */ jsx("div", {
2985
+ "data-re-slash-command-empty": "",
2986
+ children: "No results"
2987
+ })
2988
+ });
2989
+ if (query.trim().length > 0) return /* @__PURE__ */ jsx("div", {
2990
+ "data-re-slash-command": "",
2991
+ ref: containerRef,
2992
+ children: items.map((item, index) => /* @__PURE__ */ jsx(CommandItem, {
2993
+ item,
2994
+ onSelect: () => selectItem(index),
2995
+ selected: index === selectedIndex
2996
+ }, item.title))
2997
+ });
2998
+ const groups = groupByCategory(items);
2999
+ let flatIndex = 0;
3000
+ return /* @__PURE__ */ jsx("div", {
3001
+ "data-re-slash-command": "",
3002
+ ref: containerRef,
3003
+ children: groups.map((group) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
3004
+ "data-re-slash-command-category": "",
3005
+ children: group.category
3006
+ }), group.items.map((item) => {
3007
+ const currentIndex = flatIndex++;
3008
+ return /* @__PURE__ */ jsx(CommandItem, {
3009
+ item,
3010
+ onSelect: () => selectItem(currentIndex),
3011
+ selected: currentIndex === selectedIndex
3012
+ }, item.title);
3013
+ })] }, group.category))
3014
+ });
3015
+ }
3016
+
3017
+ //#endregion
3018
+ //#region src/ui/slash-command/commands.ts
3019
+ const TEXT = {
3020
+ title: "Text",
3021
+ description: "Plain text block",
3022
+ icon: Text,
3023
+ category: "Text",
3024
+ searchTerms: ["p", "paragraph"],
3025
+ command: ({ editor, range }) => {
3026
+ editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run();
3027
+ }
3028
+ };
3029
+ const H1 = {
3030
+ title: "Title",
3031
+ description: "Large heading",
3032
+ icon: Heading1,
3033
+ category: "Text",
3034
+ searchTerms: [
3035
+ "title",
3036
+ "big",
3037
+ "large",
3038
+ "h1"
3039
+ ],
3040
+ command: ({ editor, range }) => {
3041
+ editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
3042
+ }
3043
+ };
3044
+ const H2 = {
3045
+ title: "Subtitle",
3046
+ description: "Medium heading",
3047
+ icon: Heading2,
3048
+ category: "Text",
3049
+ searchTerms: [
3050
+ "subtitle",
3051
+ "medium",
3052
+ "h2"
3053
+ ],
3054
+ command: ({ editor, range }) => {
3055
+ editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run();
3056
+ }
3057
+ };
3058
+ const H3 = {
3059
+ title: "Heading",
3060
+ description: "Small heading",
3061
+ icon: Heading3,
3062
+ category: "Text",
3063
+ searchTerms: [
3064
+ "subtitle",
3065
+ "small",
3066
+ "h3"
3067
+ ],
3068
+ command: ({ editor, range }) => {
3069
+ editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run();
3070
+ }
3071
+ };
3072
+ const BULLET_LIST = {
3073
+ title: "Bullet list",
3074
+ description: "Unordered list",
3075
+ icon: List,
3076
+ category: "Text",
3077
+ searchTerms: ["unordered", "point"],
3078
+ command: ({ editor, range }) => {
3079
+ editor.chain().focus().deleteRange(range).toggleBulletList().run();
3080
+ }
3081
+ };
3082
+ const NUMBERED_LIST = {
3083
+ title: "Numbered list",
3084
+ description: "Ordered list",
3085
+ icon: ListOrdered,
3086
+ category: "Text",
3087
+ searchTerms: ["ordered"],
3088
+ command: ({ editor, range }) => {
3089
+ editor.chain().focus().deleteRange(range).toggleOrderedList().run();
3090
+ }
3091
+ };
3092
+ const QUOTE = {
3093
+ title: "Quote",
3094
+ description: "Block quote",
3095
+ icon: TextQuote,
3096
+ category: "Text",
3097
+ searchTerms: ["blockquote"],
3098
+ command: ({ editor, range }) => {
3099
+ editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run();
3100
+ }
3101
+ };
3102
+ const CODE = {
3103
+ title: "Code block",
3104
+ description: "Code snippet",
3105
+ icon: SquareCode,
3106
+ category: "Text",
3107
+ searchTerms: ["codeblock"],
3108
+ command: ({ editor, range }) => {
3109
+ editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
3110
+ }
3111
+ };
3112
+ const BUTTON = {
3113
+ title: "Button",
3114
+ description: "Clickable button",
3115
+ icon: MousePointer,
3116
+ category: "Layout",
3117
+ searchTerms: ["button"],
3118
+ command: ({ editor, range }) => {
3119
+ editor.chain().focus().deleteRange(range).setButton().run();
3120
+ }
3121
+ };
3122
+ const DIVIDER = {
3123
+ title: "Divider",
3124
+ description: "Horizontal separator",
3125
+ icon: SplitSquareVertical,
3126
+ category: "Layout",
3127
+ searchTerms: [
3128
+ "hr",
3129
+ "divider",
3130
+ "separator"
3131
+ ],
3132
+ command: ({ editor, range }) => {
3133
+ editor.chain().focus().deleteRange(range).setHorizontalRule().run();
3134
+ }
3135
+ };
3136
+ const SECTION = {
3137
+ title: "Section",
3138
+ description: "Content section",
3139
+ icon: Rows2,
3140
+ category: "Layout",
3141
+ searchTerms: [
3142
+ "section",
3143
+ "row",
3144
+ "container"
3145
+ ],
3146
+ command: ({ editor, range }) => {
3147
+ editor.chain().focus().deleteRange(range).insertSection().run();
3148
+ }
3149
+ };
3150
+ const TWO_COLUMNS = {
3151
+ title: "2 columns",
3152
+ description: "Two column layout",
3153
+ icon: Columns2,
3154
+ category: "Layout",
3155
+ searchTerms: [
3156
+ "columns",
3157
+ "column",
3158
+ "layout",
3159
+ "grid",
3160
+ "split",
3161
+ "side-by-side",
3162
+ "multi-column",
3163
+ "row",
3164
+ "two",
3165
+ "2"
3166
+ ],
3167
+ command: ({ editor, range }) => {
3168
+ editor.chain().focus().deleteRange(range).insertColumns(2).run();
3169
+ }
3170
+ };
3171
+ const THREE_COLUMNS = {
3172
+ title: "3 columns",
3173
+ description: "Three column layout",
3174
+ icon: Columns3,
3175
+ category: "Layout",
3176
+ searchTerms: [
3177
+ "columns",
3178
+ "column",
3179
+ "layout",
3180
+ "grid",
3181
+ "split",
3182
+ "multi-column",
3183
+ "row",
3184
+ "three",
3185
+ "3"
3186
+ ],
3187
+ command: ({ editor, range }) => {
3188
+ editor.chain().focus().deleteRange(range).insertColumns(3).run();
3189
+ }
3190
+ };
3191
+ const FOUR_COLUMNS = {
3192
+ title: "4 columns",
3193
+ description: "Four column layout",
3194
+ icon: Columns4,
3195
+ category: "Layout",
3196
+ searchTerms: [
3197
+ "columns",
3198
+ "column",
3199
+ "layout",
3200
+ "grid",
3201
+ "split",
3202
+ "multi-column",
3203
+ "row",
3204
+ "four",
3205
+ "4"
3206
+ ],
3207
+ command: ({ editor, range }) => {
3208
+ editor.chain().focus().deleteRange(range).insertColumns(4).run();
3209
+ }
3210
+ };
3211
+ const defaultSlashCommands = [
3212
+ TEXT,
3213
+ H1,
3214
+ H2,
3215
+ H3,
3216
+ BULLET_LIST,
3217
+ NUMBERED_LIST,
3218
+ QUOTE,
3219
+ CODE,
3220
+ BUTTON,
3221
+ DIVIDER,
3222
+ SECTION,
3223
+ TWO_COLUMNS,
3224
+ THREE_COLUMNS,
3225
+ FOUR_COLUMNS
3226
+ ];
3227
+
3228
+ //#endregion
3229
+ //#region src/ui/slash-command/extension.ts
3230
+ const SlashCommandExtension = Extension.create({
3231
+ name: "slash-command",
3232
+ addOptions() {
3233
+ return { suggestion: {
3234
+ char: "/",
3235
+ allow: ({ editor }) => !editor.isActive("codeBlock"),
3236
+ command: ({ editor, range, props }) => {
3237
+ props.command({
3238
+ editor,
3239
+ range
3240
+ });
3241
+ }
3242
+ } };
3243
+ },
3244
+ addProseMirrorPlugins() {
3245
+ return [Suggestion({
3246
+ pluginKey: new PluginKey("slash-command"),
3247
+ editor: this.editor,
3248
+ ...this.options.suggestion
3249
+ })];
3250
+ }
3251
+ });
3252
+
3253
+ //#endregion
3254
+ //#region src/ui/slash-command/render.tsx
3255
+ function createRenderItems(component = CommandList) {
3256
+ return () => {
3257
+ let renderer = null;
3258
+ let popup = null;
3259
+ return {
3260
+ onStart: (props) => {
3261
+ renderer = new ReactRenderer(component, {
3262
+ props,
3263
+ editor: props.editor
3264
+ });
3265
+ if (!props.clientRect) return;
3266
+ popup = tippy("body", {
3267
+ getReferenceClientRect: props.clientRect,
3268
+ appendTo: () => document.body,
3269
+ content: renderer.element,
3270
+ showOnCreate: true,
3271
+ interactive: true,
3272
+ trigger: "manual",
3273
+ placement: "bottom-start"
3274
+ });
3275
+ },
3276
+ onUpdate: (props) => {
3277
+ if (!renderer) return;
3278
+ renderer.updateProps(props);
3279
+ if (popup?.[0] && props.clientRect) popup[0].setProps({ getReferenceClientRect: props.clientRect });
3280
+ },
3281
+ onKeyDown: (props) => {
3282
+ if (props.event.key === "Escape") {
3283
+ popup?.[0]?.hide();
3284
+ return true;
3285
+ }
3286
+ return renderer?.ref?.onKeyDown(props) ?? false;
3287
+ },
3288
+ onExit: () => {
3289
+ popup?.[0]?.destroy();
3290
+ renderer?.destroy();
3291
+ popup = null;
3292
+ renderer = null;
3293
+ }
3294
+ };
3295
+ };
3296
+ }
3297
+
3298
+ //#endregion
3299
+ //#region src/ui/slash-command/search.ts
3300
+ function scoreItem(item, query) {
3301
+ if (!query) return 100;
3302
+ const q = query.toLowerCase();
3303
+ const title = item.title.toLowerCase();
3304
+ const description = item.description.toLowerCase();
3305
+ const terms = item.searchTerms?.map((t) => t.toLowerCase()) ?? [];
3306
+ if (title === q) return 100;
3307
+ if (title.startsWith(q)) return 90;
3308
+ if (title.split(/\s+/).some((w) => w.startsWith(q))) return 80;
3309
+ if (terms.some((t) => t === q)) return 70;
3310
+ if (terms.some((t) => t.startsWith(q))) return 60;
3311
+ if (title.includes(q)) return 40;
3312
+ if (terms.some((t) => t.includes(q))) return 30;
3313
+ if (description.includes(q)) return 20;
3314
+ return 0;
3315
+ }
3316
+ function filterAndRankItems(items, query) {
3317
+ const trimmed = query.trim();
3318
+ if (!trimmed) return items;
3319
+ const scored = items.map((item) => ({
3320
+ item,
3321
+ score: scoreItem(item, trimmed)
3322
+ })).filter(({ score }) => score > 0);
3323
+ scored.sort((a, b) => b.score - a.score);
3324
+ return scored.map(({ item }) => item);
3325
+ }
3326
+
3327
+ //#endregion
3328
+ //#region src/ui/slash-command/create-slash-command.ts
3329
+ function defaultFilterItems(items, query, editor) {
3330
+ return filterAndRankItems(isAtMaxColumnsDepth(editor) ? items.filter((item) => item.category !== "Layout" || !item.title.includes("column")) : items, query);
3331
+ }
3332
+ function createSlashCommand(options) {
3333
+ const items = options?.items ?? defaultSlashCommands;
3334
+ const filterFn = options?.filterItems ?? defaultFilterItems;
3335
+ return SlashCommandExtension.configure({ suggestion: {
3336
+ items: ({ query, editor }) => filterFn(items, query, editor),
3337
+ render: createRenderItems(options?.component)
3338
+ } });
3339
+ }
3340
+
3341
+ //#endregion
3342
+ //#region src/ui/slash-command/index.ts
3343
+ const SlashCommand = createSlashCommand();
3344
+
3345
+ //#endregion
3346
+ export { AlignmentAttribute, BULLET_LIST, BUTTON, Body, Bold, BubbleMenu, BubbleMenuAlignCenter, BubbleMenuAlignLeft, BubbleMenuAlignRight, BubbleMenuBold, BubbleMenuCode, BubbleMenuDefault, BubbleMenuItalic, BubbleMenuItem, BubbleMenuItemGroup, BubbleMenuLinkSelector, BubbleMenuNodeSelector, BubbleMenuRoot, BubbleMenuSeparator, BubbleMenuStrike, BubbleMenuUnderline, BubbleMenuUppercase, Button, ButtonBubbleMenu, ButtonBubbleMenuDefault, ButtonBubbleMenuEditLink, ButtonBubbleMenuRoot, ButtonBubbleMenuToolbar, CODE, COLUMN_PARENT_TYPES, ClassAttribute, CodeBlockPrism, ColumnsColumn, CommandList, DIVIDER, Div, EmailNode, FOUR_COLUMNS, FourColumns, H1, H2, H3, ImageBubbleMenu, ImageBubbleMenuDefault, ImageBubbleMenuEditLink, ImageBubbleMenuRoot, ImageBubbleMenuToolbar, LinkBubbleMenu, LinkBubbleMenuDefault, LinkBubbleMenuEditLink, LinkBubbleMenuForm, LinkBubbleMenuOpenLink, LinkBubbleMenuRoot, LinkBubbleMenuToolbar, LinkBubbleMenuUnlink, MAX_COLUMNS_DEPTH, MaxNesting, NUMBERED_LIST, NodeSelectorContent, NodeSelectorRoot, NodeSelectorTrigger, Placeholder, PreservedStyle, PreviewText, QUOTE, SECTION, Section, SlashCommand, StyleAttribute, Sup, TEXT, THREE_COLUMNS, TWO_COLUMNS, Table, TableCell, TableHeader, TableRow, ThreeColumns, TwoColumns, Uppercase, coreExtensions, createSlashCommand, defaultSlashCommands, editorEventBus, filterAndRankItems, getColumnsDepth, isAtMaxColumnsDepth, isInsideNode, processStylesForUnlink, scoreItem, setTextAlignment, useButtonBubbleMenuContext, useImageBubbleMenuContext, useLinkBubbleMenuContext };
2885
3347
  //# sourceMappingURL=index.mjs.map