@meta-1/editor 0.0.27
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/README.md +458 -0
- package/package.json +100 -0
- package/src/editor/constants.tsx +66 -0
- package/src/editor/container.css +46 -0
- package/src/editor/control/character-count/index.tsx +39 -0
- package/src/editor/control/drag-handle/index.tsx +85 -0
- package/src/editor/control/drag-handle/use.content.actions.ts +71 -0
- package/src/editor/control/drag-handle/use.data.ts +29 -0
- package/src/editor/control/drag-handle/use.handle.id.ts +6 -0
- package/src/editor/control/index.tsx +35 -0
- package/src/editor/editor.css +626 -0
- package/src/editor/extension/block-quote-figure/BlockquoteFigure.ts +73 -0
- package/src/editor/extension/block-quote-figure/Quote/Quote.ts +31 -0
- package/src/editor/extension/block-quote-figure/Quote/index.ts +1 -0
- package/src/editor/extension/block-quote-figure/QuoteCaption/QuoteCaption.ts +54 -0
- package/src/editor/extension/block-quote-figure/QuoteCaption/index.ts +1 -0
- package/src/editor/extension/block-quote-figure/index.ts +1 -0
- package/src/editor/extension/document/index.ts +5 -0
- package/src/editor/extension/figcaption/Figcaption.ts +90 -0
- package/src/editor/extension/figcaption/index.ts +1 -0
- package/src/editor/extension/figure/Figure.ts +62 -0
- package/src/editor/extension/figure/index.ts +1 -0
- package/src/editor/extension/font-size/FontSize.ts +64 -0
- package/src/editor/extension/font-size/index.ts +1 -0
- package/src/editor/extension/global-drag-handle/clipboard-serializer.ts +28 -0
- package/src/editor/extension/global-drag-handle/index.ts +377 -0
- package/src/editor/extension/heading/index.ts +13 -0
- package/src/editor/extension/horizontal-rule/HorizontalRule.ts +10 -0
- package/src/editor/extension/horizontal-rule/index.ts +1 -0
- package/src/editor/extension/image/index.ts +5 -0
- package/src/editor/extension/image-block/ImageBlock.ts +103 -0
- package/src/editor/extension/image-block/components/ImageBlockMenu.tsx +100 -0
- package/src/editor/extension/image-block/components/ImageBlockView.tsx +47 -0
- package/src/editor/extension/image-block/components/ImageBlockWidth.tsx +40 -0
- package/src/editor/extension/image-block/index.ts +1 -0
- package/src/editor/extension/image-upload/ImageUpload.ts +58 -0
- package/src/editor/extension/image-upload/index.ts +1 -0
- package/src/editor/extension/image-upload/view/ImageUpload.tsx +27 -0
- package/src/editor/extension/image-upload/view/ImageUploader.tsx +64 -0
- package/src/editor/extension/image-upload/view/hooks.ts +109 -0
- package/src/editor/extension/image-upload/view/index.tsx +1 -0
- package/src/editor/extension/index.ts +30 -0
- package/src/editor/extension/link/Link.ts +39 -0
- package/src/editor/extension/link/index.ts +1 -0
- package/src/editor/extension/multi-column/Column.ts +33 -0
- package/src/editor/extension/multi-column/Columns.ts +65 -0
- package/src/editor/extension/multi-column/index.ts +2 -0
- package/src/editor/extension/multi-column/menus/ColumnsMenu.tsx +82 -0
- package/src/editor/extension/multi-column/menus/index.ts +1 -0
- package/src/editor/extension/selection/Selection.ts +36 -0
- package/src/editor/extension/selection/index.ts +1 -0
- package/src/editor/extension/slash-command/MenuList.tsx +145 -0
- package/src/editor/extension/slash-command/groups.ts +153 -0
- package/src/editor/extension/slash-command/index.ts +277 -0
- package/src/editor/extension/slash-command/types.ts +25 -0
- package/src/editor/extension/table/Cell.ts +126 -0
- package/src/editor/extension/table/Header.ts +89 -0
- package/src/editor/extension/table/Row.ts +8 -0
- package/src/editor/extension/table/Table.ts +9 -0
- package/src/editor/extension/table/index.ts +4 -0
- package/src/editor/extension/table/menus/TableColumn/index.tsx +73 -0
- package/src/editor/extension/table/menus/TableColumn/utils.ts +38 -0
- package/src/editor/extension/table/menus/TableRow/index.tsx +74 -0
- package/src/editor/extension/table/menus/TableRow/utils.ts +38 -0
- package/src/editor/extension/table/menus/index.tsx +2 -0
- package/src/editor/extension/table/utils.ts +258 -0
- package/src/editor/extension/task-item/index.ts +1 -0
- package/src/editor/extension/task-item/task-item.ts +225 -0
- package/src/editor/extension/task-list/index.ts +1 -0
- package/src/editor/extension/task-list/task-list.ts +81 -0
- package/src/editor/extension/trailing-node/index.ts +1 -0
- package/src/editor/extension/trailing-node/trailing-node.ts +70 -0
- package/src/editor/extension/unique-id/index.ts +1 -0
- package/src/editor/extension/unique-id/uniqueId.ts +123 -0
- package/src/editor/hooks.ts +264 -0
- package/src/editor/index.tsx +53 -0
- package/src/editor/menus/LinkMenu/LinkMenu.tsx +75 -0
- package/src/editor/menus/LinkMenu/index.tsx +1 -0
- package/src/editor/menus/TextMenu/TextMenu.tsx +193 -0
- package/src/editor/menus/TextMenu/components/AIDropdown.tsx +140 -0
- package/src/editor/menus/TextMenu/components/ContentTypePicker.tsx +76 -0
- package/src/editor/menus/TextMenu/components/EditLinkPopover.tsx +25 -0
- package/src/editor/menus/TextMenu/components/FontFamilyPicker.tsx +84 -0
- package/src/editor/menus/TextMenu/components/FontSizePicker.tsx +56 -0
- package/src/editor/menus/TextMenu/hooks/useTextmenuCommands.ts +96 -0
- package/src/editor/menus/TextMenu/hooks/useTextmenuContentTypes.ts +86 -0
- package/src/editor/menus/TextMenu/hooks/useTextmenuStates.ts +50 -0
- package/src/editor/menus/TextMenu/index.tsx +2 -0
- package/src/editor/menus/types.ts +21 -0
- package/src/editor/panels/Colorpicker/ColorButton.tsx +35 -0
- package/src/editor/panels/Colorpicker/Colorpicker.tsx +67 -0
- package/src/editor/panels/Colorpicker/index.tsx +2 -0
- package/src/editor/panels/LinkEditorPanel/LinkEditorPanel.tsx +76 -0
- package/src/editor/panels/LinkEditorPanel/index.tsx +1 -0
- package/src/editor/panels/LinkPreviewPanel/LinkPreviewPanel.tsx +32 -0
- package/src/editor/panels/LinkPreviewPanel/index.tsx +1 -0
- package/src/editor/panels/index.tsx +3 -0
- package/src/editor/types.tsx +38 -0
- package/src/editor/ui/Button/Button.tsx +70 -0
- package/src/editor/ui/Button/index.tsx +2 -0
- package/src/editor/ui/Dropdown/Dropdown.tsx +39 -0
- package/src/editor/ui/Dropdown/index.tsx +1 -0
- package/src/editor/ui/Icon.tsx +21 -0
- package/src/editor/ui/Loader/Loader.tsx +39 -0
- package/src/editor/ui/Loader/index.ts +1 -0
- package/src/editor/ui/Loader/types.ts +7 -0
- package/src/editor/ui/Panel/index.tsx +109 -0
- package/src/editor/ui/PopoverMenu.tsx +127 -0
- package/src/editor/ui/Spinner/Spinner.tsx +10 -0
- package/src/editor/ui/Spinner/index.tsx +1 -0
- package/src/editor/ui/Surface.tsx +27 -0
- package/src/editor/ui/Textarea/Textarea.tsx +20 -0
- package/src/editor/ui/Textarea/index.tsx +1 -0
- package/src/editor/ui/Toggle/Toggle.tsx +39 -0
- package/src/editor/ui/Toggle/index.tsx +1 -0
- package/src/editor/ui/Toolbar.tsx +107 -0
- package/src/editor/ui/Tooltip/index.tsx +77 -0
- package/src/editor/ui/Tooltip/types.ts +17 -0
- package/src/editor/utils/cssVar.ts +14 -0
- package/src/editor/utils/getRenderContainer.ts +39 -0
- package/src/editor/utils/index.ts +16 -0
- package/src/editor/utils/isCustomNodeSelected.ts +47 -0
- package/src/editor/utils/isTextSelected.ts +25 -0
- package/src/editor/utils/locale.ts +5 -0
- package/src/editor/viewer/index.tsx +26 -0
- package/src/globals.css +1 -0
- package/src/index.ts +7 -0
- package/src/locales/en-us.ts +133 -0
- package/src/locales/zh-cn.ts +133 -0
- package/src/locales/zh-tw.ts +133 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Editor } from "@tiptap/react";
|
|
2
|
+
|
|
3
|
+
import { i18n } from "../../../utils/locale";
|
|
4
|
+
import type { ContentPickerOptions } from "../components/ContentTypePicker";
|
|
5
|
+
|
|
6
|
+
export const useTextmenuContentTypes = (editor: Editor): ContentPickerOptions => {
|
|
7
|
+
return [
|
|
8
|
+
{
|
|
9
|
+
type: "category",
|
|
10
|
+
label: i18n("textMenu.hierarchy"),
|
|
11
|
+
id: "hierarchy",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
icon: "Pilcrow",
|
|
15
|
+
onClick: () => editor.chain().focus().lift("taskItem").liftListItem("listItem").setParagraph().run(),
|
|
16
|
+
id: "paragraph",
|
|
17
|
+
disabled: () => !editor.can().setParagraph(),
|
|
18
|
+
isActive: () =>
|
|
19
|
+
editor.isActive("paragraph") &&
|
|
20
|
+
!editor.isActive("orderedList") &&
|
|
21
|
+
!editor.isActive("bulletList") &&
|
|
22
|
+
!editor.isActive("taskList"),
|
|
23
|
+
label: i18n("textMenu.paragraph"),
|
|
24
|
+
type: "option",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
icon: "Heading1",
|
|
28
|
+
onClick: () => editor.chain().focus().lift("taskItem").liftListItem("listItem").setHeading({ level: 1 }).run(),
|
|
29
|
+
id: "heading1",
|
|
30
|
+
disabled: () => !editor.can().setHeading({ level: 1 }),
|
|
31
|
+
isActive: () => editor.isActive("heading", { level: 1 }),
|
|
32
|
+
label: i18n("textMenu.heading1"),
|
|
33
|
+
type: "option",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
icon: "Heading2",
|
|
37
|
+
onClick: () => editor.chain().focus().lift("taskItem").liftListItem("listItem").setHeading({ level: 2 }).run(),
|
|
38
|
+
id: "heading2",
|
|
39
|
+
disabled: () => !editor.can().setHeading({ level: 2 }),
|
|
40
|
+
isActive: () => editor.isActive("heading", { level: 2 }),
|
|
41
|
+
label: i18n("textMenu.heading2"),
|
|
42
|
+
type: "option",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
icon: "Heading3",
|
|
46
|
+
onClick: () => editor.chain().focus().lift("taskItem").liftListItem("listItem").setHeading({ level: 3 }).run(),
|
|
47
|
+
id: "heading3",
|
|
48
|
+
disabled: () => !editor.can().setHeading({ level: 3 }),
|
|
49
|
+
isActive: () => editor.isActive("heading", { level: 3 }),
|
|
50
|
+
label: i18n("textMenu.heading3"),
|
|
51
|
+
type: "option",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: "category",
|
|
55
|
+
label: i18n("textMenu.lists"),
|
|
56
|
+
id: "lists",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
icon: "List",
|
|
60
|
+
onClick: () => editor.chain().focus().toggleBulletList().run(),
|
|
61
|
+
id: "bulletList",
|
|
62
|
+
disabled: () => !editor.can().toggleBulletList(),
|
|
63
|
+
isActive: () => editor.isActive("bulletList"),
|
|
64
|
+
label: i18n("textMenu.bulletList"),
|
|
65
|
+
type: "option",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
icon: "ListOrdered",
|
|
69
|
+
onClick: () => editor.chain().focus().toggleOrderedList().run(),
|
|
70
|
+
id: "orderedList",
|
|
71
|
+
disabled: () => !editor.can().toggleOrderedList(),
|
|
72
|
+
isActive: () => editor.isActive("orderedList"),
|
|
73
|
+
label: i18n("textMenu.orderedList"),
|
|
74
|
+
type: "option",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
icon: "ListTodo",
|
|
78
|
+
onClick: () => editor.chain().focus().toggleTaskList().run(),
|
|
79
|
+
id: "todoList",
|
|
80
|
+
disabled: () => !editor.can().toggleTaskList(),
|
|
81
|
+
isActive: () => editor.isActive("taskList"),
|
|
82
|
+
label: i18n("textMenu.ListTodo"),
|
|
83
|
+
type: "option",
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import type { Editor } from "@tiptap/react";
|
|
3
|
+
|
|
4
|
+
import { isCustomNodeSelected, isTextSelected } from "../../../utils";
|
|
5
|
+
import type { ShouldShowProps } from "../../types";
|
|
6
|
+
|
|
7
|
+
export const useTextMenuStates = (editor: Editor) => {
|
|
8
|
+
const shouldShow = useCallback(
|
|
9
|
+
({ view, from }: ShouldShowProps) => {
|
|
10
|
+
if (!view) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const dragging = view.dom.classList.contains("dragging");
|
|
15
|
+
if (dragging) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const domAtPos = view.domAtPos(from || 0).node as HTMLElement;
|
|
20
|
+
const nodeDOM = view.nodeDOM(from || 0) as HTMLElement;
|
|
21
|
+
const node = nodeDOM || domAtPos;
|
|
22
|
+
|
|
23
|
+
if (isCustomNodeSelected(editor, node)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return isTextSelected({ editor });
|
|
28
|
+
},
|
|
29
|
+
[editor],
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
isBold: editor.isActive("bold"),
|
|
34
|
+
isItalic: editor.isActive("italic"),
|
|
35
|
+
isStrike: editor.isActive("strike"),
|
|
36
|
+
isUnderline: editor.isActive("underline"),
|
|
37
|
+
isCode: editor.isActive("code"),
|
|
38
|
+
isSubscript: editor.isActive("subscript"),
|
|
39
|
+
isSuperscript: editor.isActive("superscript"),
|
|
40
|
+
isAlignLeft: editor.isActive({ textAlign: "left" }),
|
|
41
|
+
isAlignCenter: editor.isActive({ textAlign: "center" }),
|
|
42
|
+
isAlignRight: editor.isActive({ textAlign: "right" }),
|
|
43
|
+
isAlignJustify: editor.isActive({ textAlign: "justify" }),
|
|
44
|
+
currentColor: editor.getAttributes("textStyle")?.color || undefined,
|
|
45
|
+
currentHighlight: editor.getAttributes("highlight")?.color || undefined,
|
|
46
|
+
currentFont: editor.getAttributes("textStyle")?.fontFamily || undefined,
|
|
47
|
+
currentSize: editor.getAttributes("textStyle")?.fontSize || undefined,
|
|
48
|
+
shouldShow,
|
|
49
|
+
};
|
|
50
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import type { Editor as CoreEditor } from "@tiptap/core";
|
|
3
|
+
import type { EditorState } from "@tiptap/pm/state";
|
|
4
|
+
import type { EditorView } from "@tiptap/pm/view";
|
|
5
|
+
import type { Editor } from "@tiptap/react";
|
|
6
|
+
|
|
7
|
+
export interface MenuProps {
|
|
8
|
+
editor: Editor;
|
|
9
|
+
// biome-ignore lint/suspicious/noExplicitAny: <appendTo>
|
|
10
|
+
appendTo?: React.RefObject<any>;
|
|
11
|
+
shouldHide?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ShouldShowProps {
|
|
15
|
+
editor?: CoreEditor;
|
|
16
|
+
view: EditorView;
|
|
17
|
+
state?: EditorState;
|
|
18
|
+
oldState?: EditorState;
|
|
19
|
+
from?: number;
|
|
20
|
+
to?: number;
|
|
21
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { memo, useCallback } from "react";
|
|
2
|
+
import cn from "classnames";
|
|
3
|
+
|
|
4
|
+
export type ColorButtonProps = {
|
|
5
|
+
color?: string;
|
|
6
|
+
active?: boolean;
|
|
7
|
+
onColorChange?: (color: string) => void;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const ColorButton = memo(({ color, active, onColorChange }: ColorButtonProps) => {
|
|
11
|
+
const wrapperClassName = cn(
|
|
12
|
+
"group flex items-center justify-center rounded px-1.5 py-1.5",
|
|
13
|
+
!active && "hover:bg-neutral-100",
|
|
14
|
+
active && "bg-neutral-100",
|
|
15
|
+
);
|
|
16
|
+
const bubbleClassName = cn(
|
|
17
|
+
"h-4 w-4 rounded bg-slate-100 shadow-sm ring-current ring-offset-2",
|
|
18
|
+
!active && "hover:ring-1",
|
|
19
|
+
active && "ring-1",
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const handleClick = useCallback(() => {
|
|
23
|
+
if (onColorChange) {
|
|
24
|
+
onColorChange(color || "");
|
|
25
|
+
}
|
|
26
|
+
}, [onColorChange, color]);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<button className={wrapperClassName} onClick={handleClick} type="button">
|
|
30
|
+
<div className={bubbleClassName} style={{ backgroundColor: color, color: color }} />
|
|
31
|
+
</button>
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
ColorButton.displayName = "ColorButton";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type ChangeEvent, useCallback, useState } from "react";
|
|
2
|
+
import { HexColorPicker } from "react-colorful";
|
|
3
|
+
|
|
4
|
+
import { themeColors } from "../../constants";
|
|
5
|
+
import { Icon } from "../../ui/Icon";
|
|
6
|
+
import { Toolbar } from "../../ui/Toolbar";
|
|
7
|
+
import { i18n } from "../../utils/locale";
|
|
8
|
+
import { ColorButton } from "./ColorButton";
|
|
9
|
+
|
|
10
|
+
const HEX_COLOR_REGEX = /^#([0-9A-F]{3}){1,2}$/i;
|
|
11
|
+
|
|
12
|
+
export type ColorPickerProps = {
|
|
13
|
+
color?: string;
|
|
14
|
+
onChange?: (color: string) => void;
|
|
15
|
+
onClear?: () => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const ColorPicker = ({ color, onChange, onClear }: ColorPickerProps) => {
|
|
19
|
+
const [colorInputValue, setColorInputValue] = useState(color || "");
|
|
20
|
+
|
|
21
|
+
const handleColorUpdate = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
|
22
|
+
setColorInputValue(event.target.value);
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
const handleColorChange = useCallback(() => {
|
|
26
|
+
const isCorrectColor = HEX_COLOR_REGEX.test(colorInputValue);
|
|
27
|
+
|
|
28
|
+
if (!isCorrectColor) {
|
|
29
|
+
if (onChange) {
|
|
30
|
+
onChange("");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (onChange) {
|
|
37
|
+
onChange(colorInputValue);
|
|
38
|
+
}
|
|
39
|
+
}, [colorInputValue, onChange]);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex flex-col gap-2">
|
|
43
|
+
<HexColorPicker className="!w-full" color={color || ""} onChange={onChange} />
|
|
44
|
+
<input
|
|
45
|
+
className="w-full rounded border border-neutral-200 bg-white p-2 text-black focus:outline-1 focus:outline-neutral-300 focus:ring-0 dark:border-neutral-800 dark:bg-black dark:text-white dark:focus:outline-neutral-700"
|
|
46
|
+
onBlur={handleColorChange}
|
|
47
|
+
onChange={handleColorUpdate}
|
|
48
|
+
placeholder="#000000"
|
|
49
|
+
type="text"
|
|
50
|
+
value={colorInputValue}
|
|
51
|
+
/>
|
|
52
|
+
<div className="flex max-w-[15rem] flex-wrap items-center gap-1">
|
|
53
|
+
{themeColors.map((currentColor) => (
|
|
54
|
+
<ColorButton
|
|
55
|
+
active={currentColor === color}
|
|
56
|
+
color={currentColor}
|
|
57
|
+
key={currentColor}
|
|
58
|
+
onColorChange={onChange}
|
|
59
|
+
/>
|
|
60
|
+
))}
|
|
61
|
+
<Toolbar.Button onClick={onClear} tooltip={i18n("colorPicker.undo")}>
|
|
62
|
+
<Icon name="Undo" />
|
|
63
|
+
</Toolbar.Button>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type ChangeEvent, type FormEvent, useCallback, useMemo, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { Button } from "../../ui/Button";
|
|
4
|
+
import { Icon } from "../../ui/Icon";
|
|
5
|
+
import { Surface } from "../../ui/Surface";
|
|
6
|
+
import { Toggle } from "../../ui/Toggle";
|
|
7
|
+
import { i18n } from "../../utils/locale";
|
|
8
|
+
|
|
9
|
+
const URL_REGEX = /^(\S+):(\/\/)?\S+$/;
|
|
10
|
+
|
|
11
|
+
export type LinkEditorPanelProps = {
|
|
12
|
+
initialUrl?: string;
|
|
13
|
+
initialOpenInNewTab?: boolean;
|
|
14
|
+
onSetLink: (url: string, openInNewTab?: boolean) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const useLinkEditorState = ({ initialUrl, initialOpenInNewTab, onSetLink }: LinkEditorPanelProps) => {
|
|
18
|
+
const [url, setUrl] = useState(initialUrl || "");
|
|
19
|
+
const [openInNewTab, setOpenInNewTab] = useState(initialOpenInNewTab);
|
|
20
|
+
|
|
21
|
+
const onChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
|
22
|
+
setUrl(event.target.value);
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
const isValidUrl = useMemo(() => URL_REGEX.test(url), [url]);
|
|
26
|
+
|
|
27
|
+
const handleSubmit = useCallback(
|
|
28
|
+
(e: FormEvent) => {
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
if (isValidUrl) {
|
|
31
|
+
onSetLink(url, openInNewTab);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
[url, isValidUrl, openInNewTab, onSetLink],
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
url,
|
|
39
|
+
setUrl,
|
|
40
|
+
openInNewTab,
|
|
41
|
+
setOpenInNewTab,
|
|
42
|
+
onChange,
|
|
43
|
+
handleSubmit,
|
|
44
|
+
isValidUrl,
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const LinkEditorPanel = ({ onSetLink, initialOpenInNewTab, initialUrl }: LinkEditorPanelProps) => {
|
|
49
|
+
const state = useLinkEditorState({ onSetLink, initialOpenInNewTab, initialUrl });
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Surface className="p-2">
|
|
53
|
+
<form className="flex items-center gap-2" onSubmit={state.handleSubmit}>
|
|
54
|
+
<label className="flex cursor-text items-center gap-2 rounded-lg bg-neutral-100 p-2 dark:bg-neutral-900">
|
|
55
|
+
<Icon className="flex-none text-black dark:text-white" name="Link" />
|
|
56
|
+
<input
|
|
57
|
+
className="min-w-[12rem] flex-1 bg-transparent text-black text-sm outline-none dark:text-white"
|
|
58
|
+
onChange={state.onChange}
|
|
59
|
+
placeholder={i18n("panel.linkEditor.input")}
|
|
60
|
+
type="url"
|
|
61
|
+
value={state.url}
|
|
62
|
+
/>
|
|
63
|
+
</label>
|
|
64
|
+
<Button buttonSize="small" disabled={!state.isValidUrl} type="submit" variant="primary">
|
|
65
|
+
{i18n("panel.linkEditor.submit")}
|
|
66
|
+
</Button>
|
|
67
|
+
</form>
|
|
68
|
+
<div className="mt-3">
|
|
69
|
+
<span className="flex cursor-pointer select-none items-center justify-start gap-2 font-semibold text-neutral-500 text-sm dark:text-neutral-400">
|
|
70
|
+
{i18n("panel.linkEditor.newTab")}
|
|
71
|
+
<Toggle active={state.openInNewTab} onChange={state.setOpenInNewTab} />
|
|
72
|
+
</span>
|
|
73
|
+
</div>
|
|
74
|
+
</Surface>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { LinkEditorPanel } from "./LinkEditorPanel";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Icon } from "../../ui/Icon";
|
|
2
|
+
import { Surface } from "../../ui/Surface";
|
|
3
|
+
import { Toolbar } from "../../ui/Toolbar";
|
|
4
|
+
import Tooltip from "../../ui/Tooltip";
|
|
5
|
+
import { i18n } from "../../utils/locale";
|
|
6
|
+
|
|
7
|
+
export type LinkPreviewPanelProps = {
|
|
8
|
+
url: string;
|
|
9
|
+
onEdit: () => void;
|
|
10
|
+
onClear: () => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const LinkPreviewPanel = ({ onClear, onEdit, url }: LinkPreviewPanelProps) => {
|
|
14
|
+
return (
|
|
15
|
+
<Surface className="flex items-center gap-2 p-2">
|
|
16
|
+
<a className="break-all text-sm underline" href={url} rel="noopener noreferrer" target="_blank">
|
|
17
|
+
{url}
|
|
18
|
+
</a>
|
|
19
|
+
<Toolbar.Divider />
|
|
20
|
+
<Tooltip title={i18n("panel.linkPreview.edit")}>
|
|
21
|
+
<Toolbar.Button onClick={onEdit}>
|
|
22
|
+
<Icon name="Pen" />
|
|
23
|
+
</Toolbar.Button>
|
|
24
|
+
</Tooltip>
|
|
25
|
+
<Tooltip title={i18n("panel.linkPreview.delete")}>
|
|
26
|
+
<Toolbar.Button onClick={onClear}>
|
|
27
|
+
<Icon name="Trash2" />
|
|
28
|
+
</Toolbar.Button>
|
|
29
|
+
</Tooltip>
|
|
30
|
+
</Surface>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { LinkPreviewPanel } from "./LinkPreviewPanel";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type EditorUser = {
|
|
2
|
+
clientId: string;
|
|
3
|
+
name: string;
|
|
4
|
+
color: string;
|
|
5
|
+
initials?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type LanguageOption = {
|
|
9
|
+
name: string;
|
|
10
|
+
label: string;
|
|
11
|
+
value: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type AiTone =
|
|
15
|
+
| "academic"
|
|
16
|
+
| "business"
|
|
17
|
+
| "casual"
|
|
18
|
+
| "childfriendly"
|
|
19
|
+
| "conversational"
|
|
20
|
+
| "emotional"
|
|
21
|
+
| "humorous"
|
|
22
|
+
| "informative"
|
|
23
|
+
| "inspirational"
|
|
24
|
+
| string;
|
|
25
|
+
|
|
26
|
+
export type AiPromptType = "SHORTEN" | "EXTEND" | "SIMPLIFY" | "TONE";
|
|
27
|
+
|
|
28
|
+
export type AiToneOption = {
|
|
29
|
+
name: string;
|
|
30
|
+
label: string;
|
|
31
|
+
value: AiTone;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type AiImageStyle = {
|
|
35
|
+
name: string;
|
|
36
|
+
label: string;
|
|
37
|
+
value: string;
|
|
38
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import cn from "classnames";
|
|
3
|
+
|
|
4
|
+
export type ButtonVariant = "primary" | "secondary" | "tertiary" | "quaternary" | "ghost";
|
|
5
|
+
export type ButtonSize = "medium" | "small" | "icon" | "iconSmall";
|
|
6
|
+
|
|
7
|
+
export type ButtonProps = {
|
|
8
|
+
variant?: ButtonVariant;
|
|
9
|
+
active?: boolean;
|
|
10
|
+
activeClassname?: string;
|
|
11
|
+
buttonSize?: ButtonSize;
|
|
12
|
+
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
|
|
13
|
+
|
|
14
|
+
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
15
|
+
(
|
|
16
|
+
{ active, buttonSize = "medium", children, disabled, variant = "primary", className, activeClassname, ...rest },
|
|
17
|
+
ref,
|
|
18
|
+
) => {
|
|
19
|
+
const buttonClassName = cn(
|
|
20
|
+
"group flex items-center justify-center gap-2 whitespace-nowrap rounded-md border border-transparent font-semibold text-sm disabled:opacity-50",
|
|
21
|
+
|
|
22
|
+
variant === "primary" &&
|
|
23
|
+
cn(
|
|
24
|
+
"border-black bg-black text-white dark:border-white dark:bg-white dark:text-black",
|
|
25
|
+
!(disabled || active) &&
|
|
26
|
+
"hover:bg-neutral-800 active:bg-neutral-900 dark:active:bg-neutral-300 dark:hover:bg-neutral-200",
|
|
27
|
+
active && cn("bg-neutral-900 dark:bg-neutral-300", activeClassname),
|
|
28
|
+
),
|
|
29
|
+
|
|
30
|
+
variant === "secondary" &&
|
|
31
|
+
cn(
|
|
32
|
+
"text-neutral-900 dark:text-white",
|
|
33
|
+
!(disabled || active) &&
|
|
34
|
+
"hover:bg-neutral-100 active:bg-neutral-200 dark:active:bg-neutral-800 dark:hover:bg-neutral-900",
|
|
35
|
+
active && "bg-neutral-200 dark:bg-neutral-800",
|
|
36
|
+
),
|
|
37
|
+
|
|
38
|
+
variant === "tertiary" &&
|
|
39
|
+
cn(
|
|
40
|
+
"bg-neutral-50 text-neutral-900 dark:border-neutral-900 dark:bg-neutral-900 dark:text-white",
|
|
41
|
+
!(disabled || active) &&
|
|
42
|
+
"hover:bg-neutral-100 active:bg-neutral-200 dark:active:bg-neutral-700 dark:hover:bg-neutral-800",
|
|
43
|
+
active && cn("bg-neutral-200 dark:bg-neutral-800", activeClassname),
|
|
44
|
+
),
|
|
45
|
+
|
|
46
|
+
variant === "ghost" &&
|
|
47
|
+
cn(
|
|
48
|
+
"border-transparent bg-transparent text-neutral-500 dark:text-neutral-400",
|
|
49
|
+
!(disabled || active) &&
|
|
50
|
+
"hover:bg-black/5 hover:text-neutral-700 active:bg-black/10 active:text-neutral-800 dark:active:text-neutral-200 dark:hover:bg-white/10 dark:hover:text-neutral-300",
|
|
51
|
+
active && cn("bg-black/10 text-neutral-800 dark:bg-white/20 dark:text-neutral-200", activeClassname),
|
|
52
|
+
),
|
|
53
|
+
|
|
54
|
+
buttonSize === "medium" && "px-3 py-2",
|
|
55
|
+
buttonSize === "small" && "px-2 py-1",
|
|
56
|
+
buttonSize === "icon" && "h-8 w-8",
|
|
57
|
+
buttonSize === "iconSmall" && "h-6 w-6",
|
|
58
|
+
|
|
59
|
+
className,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<button className={buttonClassName} disabled={disabled} ref={ref} {...rest}>
|
|
64
|
+
{children}
|
|
65
|
+
</button>
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
Button.displayName = "Button";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import cn from "classnames";
|
|
3
|
+
|
|
4
|
+
export const DropdownCategoryTitle = ({ children }: { children: ReactNode }) => {
|
|
5
|
+
return (
|
|
6
|
+
<div className="mb-1 px-1.5 font-semibold text-[.65rem] text-neutral-500 uppercase dark:text-neutral-400">
|
|
7
|
+
{children}
|
|
8
|
+
</div>
|
|
9
|
+
);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const DropdownButton = ({
|
|
13
|
+
children,
|
|
14
|
+
isActive,
|
|
15
|
+
onClick,
|
|
16
|
+
disabled,
|
|
17
|
+
className,
|
|
18
|
+
}: {
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
isActive?: boolean;
|
|
21
|
+
onClick?: () => void;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
className?: string;
|
|
24
|
+
}) => {
|
|
25
|
+
const buttonClass = cn(
|
|
26
|
+
"flex w-full items-center gap-2 rounded bg-transparent p-1.5 text-left font-medium text-neutral-500 text-sm dark:text-neutral-400",
|
|
27
|
+
!(isActive || disabled),
|
|
28
|
+
"hover:bg-neutral-100 hover:text-neutral-800 dark:hover:bg-neutral-900 dark:hover:text-neutral-200",
|
|
29
|
+
isActive && !disabled && "bg-neutral-100 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200",
|
|
30
|
+
disabled && "cursor-not-allowed text-neutral-400 dark:text-neutral-600",
|
|
31
|
+
className,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<button className={buttonClass} disabled={disabled} onClick={onClick} type="button">
|
|
36
|
+
{children}
|
|
37
|
+
</button>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DropdownButton, DropdownCategoryTitle } from "./Dropdown";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { memo } from "react";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
import { icons } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
export type IconProps = {
|
|
6
|
+
name: keyof typeof icons;
|
|
7
|
+
className?: string;
|
|
8
|
+
strokeWidth?: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const Icon = memo(({ name, className, strokeWidth }: IconProps) => {
|
|
12
|
+
const IconComponent = icons[name];
|
|
13
|
+
|
|
14
|
+
if (!IconComponent) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return <IconComponent className={classNames("h-4 w-4", className)} strokeWidth={strokeWidth || 2.5} />;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
Icon.displayName = "Icon";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createPortal } from "react-dom";
|
|
2
|
+
|
|
3
|
+
import type { LoaderProps, LoadingWrapperProps } from "./types";
|
|
4
|
+
|
|
5
|
+
const LoadingWrapper = ({ label }: LoadingWrapperProps) => {
|
|
6
|
+
return (
|
|
7
|
+
<div className="flex flex-col items-center justify-center gap-2 rounded-lg bg-black p-4 text-white shadow-2xl dark:bg-white dark:text-black">
|
|
8
|
+
<svg
|
|
9
|
+
className="h-8 w-8 animate-spin"
|
|
10
|
+
fill="none"
|
|
11
|
+
stroke="currentColor"
|
|
12
|
+
strokeLinecap="round"
|
|
13
|
+
strokeLinejoin="round"
|
|
14
|
+
strokeWidth="2"
|
|
15
|
+
viewBox="0 0 24 24"
|
|
16
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
17
|
+
>
|
|
18
|
+
<title>Loading</title>
|
|
19
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
20
|
+
</svg>
|
|
21
|
+
{label && <p className="font-semibold text-sm text-white leading-tight dark:text-black">{label}</p>}
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const Loader = ({ hasOverlay = true, label }: LoaderProps) => {
|
|
27
|
+
return hasOverlay ? (
|
|
28
|
+
createPortal(
|
|
29
|
+
<div className="fixed top-0 left-0 z-[9999] flex h-full w-full select-none items-center justify-center bg-black/60">
|
|
30
|
+
<LoadingWrapper label={label} />
|
|
31
|
+
</div>,
|
|
32
|
+
document.body,
|
|
33
|
+
)
|
|
34
|
+
) : (
|
|
35
|
+
<LoadingWrapper label={label} />
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default Loader;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Loader } from "./Loader";
|