@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,109 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
3
|
+
import cn from "classnames";
|
|
4
|
+
|
|
5
|
+
import { Surface } from "../Surface";
|
|
6
|
+
|
|
7
|
+
export type PanelProps = {
|
|
8
|
+
spacing?: "medium" | "small";
|
|
9
|
+
noShadow?: boolean;
|
|
10
|
+
asChild?: boolean;
|
|
11
|
+
} & HTMLAttributes<HTMLDivElement>;
|
|
12
|
+
|
|
13
|
+
export const Panel = forwardRef<HTMLDivElement, PanelProps>(
|
|
14
|
+
({ asChild, className, children, spacing, noShadow, ...rest }, ref) => {
|
|
15
|
+
const panelClass = cn("p-2", spacing === "small" && "p-[0.2rem]", className);
|
|
16
|
+
|
|
17
|
+
const Comp = asChild ? Slot : "div";
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Comp ref={ref} {...rest}>
|
|
21
|
+
<Surface className={panelClass} withShadow={!noShadow}>
|
|
22
|
+
{children}
|
|
23
|
+
</Surface>
|
|
24
|
+
</Comp>
|
|
25
|
+
);
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
Panel.displayName = "Panel";
|
|
30
|
+
|
|
31
|
+
export const PanelDivider = forwardRef<HTMLDivElement, { asChild?: boolean } & HTMLAttributes<HTMLDivElement>>(
|
|
32
|
+
({ asChild, className, children, ...rest }, ref) => {
|
|
33
|
+
const dividerClass = cn("mb-2 border-b border-b-black/10 pb-2", className);
|
|
34
|
+
|
|
35
|
+
const Comp = asChild ? Slot : "div";
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Comp className={dividerClass} {...rest} ref={ref}>
|
|
39
|
+
{children}
|
|
40
|
+
</Comp>
|
|
41
|
+
);
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
PanelDivider.displayName = "PanelDivider";
|
|
46
|
+
|
|
47
|
+
export const PanelHeader = forwardRef<HTMLDivElement, { asChild?: boolean } & HTMLAttributes<HTMLDivElement>>(
|
|
48
|
+
({ asChild, className, children, ...rest }, ref) => {
|
|
49
|
+
const headerClass = cn("mb-2 border-b border-b-black/10 pb-2 text-sm", className);
|
|
50
|
+
|
|
51
|
+
const Comp = asChild ? Slot : "div";
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Comp className={headerClass} {...rest} ref={ref}>
|
|
55
|
+
{children}
|
|
56
|
+
</Comp>
|
|
57
|
+
);
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
PanelHeader.displayName = "PanelHeader";
|
|
62
|
+
|
|
63
|
+
export const PanelSection = forwardRef<HTMLDivElement, { asChild?: boolean } & HTMLAttributes<HTMLDivElement>>(
|
|
64
|
+
({ asChild, className, children, ...rest }, ref) => {
|
|
65
|
+
const sectionClass = cn("mt-4 first:mt-1", className);
|
|
66
|
+
|
|
67
|
+
const Comp = asChild ? Slot : "div";
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Comp className={sectionClass} {...rest} ref={ref}>
|
|
71
|
+
{children}
|
|
72
|
+
</Comp>
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
PanelSection.displayName = "PanelSection";
|
|
78
|
+
|
|
79
|
+
export const PanelHeadline = forwardRef<HTMLDivElement, { asChild?: boolean } & HTMLAttributes<HTMLDivElement>>(
|
|
80
|
+
({ asChild, className, children, ...rest }, ref) => {
|
|
81
|
+
const headlineClass = cn("mb-2 ml-1.5 font-medium text-black/80 text-xs dark:text-white/80", className);
|
|
82
|
+
|
|
83
|
+
const Comp = asChild ? Slot : "div";
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Comp className={headlineClass} {...rest} ref={ref}>
|
|
87
|
+
{children}
|
|
88
|
+
</Comp>
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
PanelHeadline.displayName = "PanelHeadline";
|
|
94
|
+
|
|
95
|
+
export const PanelFooter = forwardRef<HTMLDivElement, { asChild?: boolean } & HTMLAttributes<HTMLDivElement>>(
|
|
96
|
+
({ asChild, className, children, ...rest }, ref) => {
|
|
97
|
+
const footerClass = cn("mt-2 border-black/10 border-t pt-2 text-sm", className);
|
|
98
|
+
|
|
99
|
+
const Comp = asChild ? Slot : "div";
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<Comp className={footerClass} {...rest} ref={ref}>
|
|
103
|
+
{children}
|
|
104
|
+
</Comp>
|
|
105
|
+
);
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
PanelFooter.displayName = "PanelFooter";
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { forwardRef, type ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Close as PopoverClose,
|
|
4
|
+
Content as PopoverContent,
|
|
5
|
+
Portal as PopoverPortal,
|
|
6
|
+
Root as PopoverRoot,
|
|
7
|
+
Trigger as PopoverTrigger,
|
|
8
|
+
} from "@radix-ui/react-popover";
|
|
9
|
+
import { icons } from "lucide-react";
|
|
10
|
+
|
|
11
|
+
import { cn } from "../utils";
|
|
12
|
+
import { Surface } from "./Surface";
|
|
13
|
+
import { Toolbar } from "./Toolbar";
|
|
14
|
+
|
|
15
|
+
export const Trigger = PopoverTrigger;
|
|
16
|
+
export const Portal = PopoverPortal;
|
|
17
|
+
|
|
18
|
+
export type MenuProps = {
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
trigger: ReactNode;
|
|
21
|
+
triggerClassName?: string;
|
|
22
|
+
customTrigger?: boolean;
|
|
23
|
+
isOpen?: boolean;
|
|
24
|
+
onOpenChange?: (state: boolean) => void;
|
|
25
|
+
withPortal?: boolean;
|
|
26
|
+
tooltip?: string;
|
|
27
|
+
isActive?: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const Menu = ({
|
|
31
|
+
customTrigger,
|
|
32
|
+
trigger,
|
|
33
|
+
triggerClassName,
|
|
34
|
+
children,
|
|
35
|
+
isOpen,
|
|
36
|
+
withPortal,
|
|
37
|
+
tooltip,
|
|
38
|
+
onOpenChange,
|
|
39
|
+
}: MenuProps) => {
|
|
40
|
+
return (
|
|
41
|
+
<PopoverRoot onOpenChange={onOpenChange}>
|
|
42
|
+
{customTrigger ? (
|
|
43
|
+
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
|
44
|
+
) : (
|
|
45
|
+
<PopoverTrigger asChild>
|
|
46
|
+
<Toolbar.Button className={triggerClassName} tooltip={isOpen ? "" : tooltip}>
|
|
47
|
+
{trigger}
|
|
48
|
+
</Toolbar.Button>
|
|
49
|
+
</PopoverTrigger>
|
|
50
|
+
)}
|
|
51
|
+
{withPortal ? (
|
|
52
|
+
<PopoverPortal>
|
|
53
|
+
<PopoverContent asChild sideOffset={8}>
|
|
54
|
+
<Surface className="z-[9999] flex max-h-80 min-w-[15rem] flex-col gap-0.5 overflow-auto p-2">
|
|
55
|
+
{children}
|
|
56
|
+
</Surface>
|
|
57
|
+
</PopoverContent>
|
|
58
|
+
</PopoverPortal>
|
|
59
|
+
) : (
|
|
60
|
+
<PopoverContent asChild sideOffset={8}>
|
|
61
|
+
<Surface className="z-[9999] flex max-h-80 min-w-[15rem] flex-col gap-0.5 overflow-auto p-2">
|
|
62
|
+
{children}
|
|
63
|
+
</Surface>
|
|
64
|
+
</PopoverContent>
|
|
65
|
+
)}
|
|
66
|
+
</PopoverRoot>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
Menu.displayName = "Menu";
|
|
71
|
+
|
|
72
|
+
export const Item = ({
|
|
73
|
+
label,
|
|
74
|
+
close = true,
|
|
75
|
+
icon,
|
|
76
|
+
iconComponent,
|
|
77
|
+
disabled,
|
|
78
|
+
onClick,
|
|
79
|
+
isActive,
|
|
80
|
+
}: {
|
|
81
|
+
label: string | ReactNode;
|
|
82
|
+
icon?: keyof typeof icons;
|
|
83
|
+
iconComponent?: ReactNode;
|
|
84
|
+
close?: boolean;
|
|
85
|
+
disabled?: boolean;
|
|
86
|
+
onClick: () => void;
|
|
87
|
+
isActive?: boolean;
|
|
88
|
+
}) => {
|
|
89
|
+
const className = cn(
|
|
90
|
+
"flex w-full items-center gap-2 rounded bg-transparent p-1.5 text-left font-medium text-neutral-500 text-sm",
|
|
91
|
+
!(isActive || disabled),
|
|
92
|
+
"hover:bg-neutral-100 hover:text-neutral-800 dark:hover:bg-neutral-900 dark:hover:text-neutral-200",
|
|
93
|
+
isActive && !disabled && "bg-neutral-100 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200",
|
|
94
|
+
disabled && "cursor-not-allowed text-neutral-400 dark:text-neutral-600",
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const IconComponent = icon ? icons[icon] : null;
|
|
98
|
+
const IconCustomComponent = iconComponent || null;
|
|
99
|
+
|
|
100
|
+
const ItemComponent = close ? PopoverClose : "button";
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<ItemComponent className={className} disabled={disabled} onClick={onClick}>
|
|
104
|
+
{IconComponent && <IconComponent className="h-4 w-4" />}
|
|
105
|
+
{IconCustomComponent}
|
|
106
|
+
{label}
|
|
107
|
+
</ItemComponent>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export type CategoryTitle = {
|
|
112
|
+
children: ReactNode;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const CategoryTitle = ({ children }: CategoryTitle) => {
|
|
116
|
+
return (
|
|
117
|
+
<div className="mt-4 mb-1.5 select-none px-1 font-medium text-[0.625rem] text-neutral-400 uppercase first:mt-1.5 dark:text-neutral-600">
|
|
118
|
+
{children}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const Divider = forwardRef<HTMLHRElement>((props, ref) => {
|
|
124
|
+
return <hr {...props} className="my-1 border-neutral-200 dark:border-neutral-800" ref={ref} />;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
Divider.displayName = "Divider";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { forwardRef, type HTMLProps } from "react";
|
|
2
|
+
import cn from "classnames";
|
|
3
|
+
|
|
4
|
+
export const Spinner = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(({ className, ...rest }, ref) => {
|
|
5
|
+
const spinnerClass = cn("h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent", className);
|
|
6
|
+
|
|
7
|
+
return <div className={spinnerClass} ref={ref} {...rest} />;
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
Spinner.displayName = "Spinner";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Spinner } from "./Spinner";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { forwardRef, type HTMLProps } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../utils";
|
|
4
|
+
|
|
5
|
+
export type SurfaceProps = HTMLProps<HTMLDivElement> & {
|
|
6
|
+
withShadow?: boolean;
|
|
7
|
+
withBorder?: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const Surface = forwardRef<HTMLDivElement, SurfaceProps>(
|
|
11
|
+
({ children, className, withShadow = true, withBorder = true, ...props }, ref) => {
|
|
12
|
+
const surfaceClass = cn(
|
|
13
|
+
className,
|
|
14
|
+
"rounded-lg bg-white dark:bg-black",
|
|
15
|
+
withShadow ? "shadow-sm" : "",
|
|
16
|
+
withBorder ? "border border-neutral-200 dark:border-neutral-800" : "",
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className={surfaceClass} {...props} ref={ref}>
|
|
21
|
+
{children}
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
Surface.displayName = "Surface";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { forwardRef, type TextareaHTMLAttributes } from "react";
|
|
2
|
+
import cn from "classnames";
|
|
3
|
+
|
|
4
|
+
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaHTMLAttributes<HTMLTextAreaElement>>(
|
|
5
|
+
({ className, ...rest }, ref) => {
|
|
6
|
+
const textAreaClassName = cn(
|
|
7
|
+
"block h-[4.5rem] w-full rounded-lg border-0 bg-black/5 px-2 py-1 font-medium text-black text-sm caret-black",
|
|
8
|
+
"dark:bg-white/10 dark:text-white dark:caret-white",
|
|
9
|
+
"hover:bg-black/10",
|
|
10
|
+
"dark:hover:bg-white/20",
|
|
11
|
+
"focus:bg-transparent focus:outline focus:outline-black active:bg-transparent active:outline active:outline-black",
|
|
12
|
+
"dark:active:outline-white dark:focus:outline-white",
|
|
13
|
+
className,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
return <textarea className={textAreaClassName} ref={ref} {...rest} />;
|
|
17
|
+
},
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
Textarea.displayName = "Textarea";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Textarea } from "./Textarea";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import cn from "classnames";
|
|
3
|
+
|
|
4
|
+
export type ToggleProps = {
|
|
5
|
+
active?: boolean;
|
|
6
|
+
onChange: (active: boolean) => void;
|
|
7
|
+
size?: "small" | "large";
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const Toggle = ({ onChange, active = false, size = "large" }: ToggleProps) => {
|
|
11
|
+
const state = active ? "checked" : "unchecked";
|
|
12
|
+
const value = active ? "on" : "off";
|
|
13
|
+
|
|
14
|
+
const buttonClass = cn(
|
|
15
|
+
"inline-flex cursor-pointer items-center rounded-full border-transparent transition-colors",
|
|
16
|
+
active ? "bg-black" : "bg-neutral-200 hover:bg-neutral-300",
|
|
17
|
+
active ? "dark:bg-white" : "dark:bg-neutral-800 dark:hover:bg-neutral-700",
|
|
18
|
+
size === "small" && "h-3 w-6 px-0.5",
|
|
19
|
+
size === "large" && "h-5 w-9 px-0.5",
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const pinClass = cn(
|
|
23
|
+
"pointer-events-none block rounded-full transition-transform",
|
|
24
|
+
"bg-white dark:bg-black",
|
|
25
|
+
size === "small" && "h-2 w-2",
|
|
26
|
+
size === "large" && "h-4 w-4",
|
|
27
|
+
active ? cn(size === "small" ? "translate-x-3" : "", size === "large" ? "translate-x-4" : "") : "translate-x-0",
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const handleChange = useCallback(() => {
|
|
31
|
+
onChange(!active);
|
|
32
|
+
}, [active, onChange]);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<button className={buttonClass} data-state={state} onClick={handleChange} type="button" value={value}>
|
|
36
|
+
<span className={pinClass} data-state={state} />
|
|
37
|
+
</button>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Toggle } from "./Toggle";
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { type ButtonHTMLAttributes, forwardRef, type HTMLProps } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../utils";
|
|
4
|
+
import { Button, type ButtonProps } from "./Button";
|
|
5
|
+
import { Surface } from "./Surface";
|
|
6
|
+
import Tooltip from "./Tooltip";
|
|
7
|
+
|
|
8
|
+
export type ToolbarWrapperProps = {
|
|
9
|
+
shouldShowContent?: boolean;
|
|
10
|
+
isVertical?: boolean;
|
|
11
|
+
} & HTMLProps<HTMLDivElement>;
|
|
12
|
+
|
|
13
|
+
const ToolbarWrapper = forwardRef<HTMLDivElement, ToolbarWrapperProps>(
|
|
14
|
+
({ shouldShowContent = true, children, isVertical = false, className, ...rest }, ref) => {
|
|
15
|
+
const toolbarClassName = cn(
|
|
16
|
+
"inline-flex h-full gap-0.5 text-black leading-none",
|
|
17
|
+
isVertical ? "flex-col p-2" : "flex-row items-center p-1",
|
|
18
|
+
className,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
shouldShowContent && (
|
|
23
|
+
<Surface className={toolbarClassName} {...rest} ref={ref}>
|
|
24
|
+
{children}
|
|
25
|
+
</Surface>
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
ToolbarWrapper.displayName = "Toolbar";
|
|
32
|
+
|
|
33
|
+
export type ToolbarDividerProps = {
|
|
34
|
+
horizontal?: boolean;
|
|
35
|
+
} & HTMLProps<HTMLDivElement>;
|
|
36
|
+
|
|
37
|
+
const ToolbarDivider = forwardRef<HTMLDivElement, ToolbarDividerProps>(({ horizontal, className, ...rest }, ref) => {
|
|
38
|
+
const dividerClassName = cn(
|
|
39
|
+
"bg-neutral-200 dark:bg-neutral-800",
|
|
40
|
+
horizontal
|
|
41
|
+
? "my-1 h-[1px] w-full min-w-[1.5rem] first:mt-0 last:mt-0"
|
|
42
|
+
: "mx-1 h-full min-h-[1.5rem] w-[1px] first:ml-0 last:mr-0",
|
|
43
|
+
className,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return <div className={dividerClassName} ref={ref} {...rest} />;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
ToolbarDivider.displayName = "Toolbar.Divider";
|
|
50
|
+
|
|
51
|
+
export type ToolbarButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
52
|
+
active?: boolean;
|
|
53
|
+
activeClassname?: string;
|
|
54
|
+
tooltip?: string;
|
|
55
|
+
tooltipShortcut?: string[];
|
|
56
|
+
buttonSize?: ButtonProps["buttonSize"];
|
|
57
|
+
variant?: ButtonProps["variant"];
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const ToolbarButton = forwardRef<HTMLButtonElement, ToolbarButtonProps>(
|
|
61
|
+
(
|
|
62
|
+
{
|
|
63
|
+
children,
|
|
64
|
+
buttonSize = "icon",
|
|
65
|
+
variant = "ghost",
|
|
66
|
+
className,
|
|
67
|
+
tooltip,
|
|
68
|
+
tooltipShortcut,
|
|
69
|
+
activeClassname = "!bg-neutral-200 dark:!bg-neutral-800",
|
|
70
|
+
...rest
|
|
71
|
+
},
|
|
72
|
+
ref,
|
|
73
|
+
) => {
|
|
74
|
+
const buttonClass = cn("!w-auto min-w-[2rem] gap-1 px-2", className);
|
|
75
|
+
|
|
76
|
+
const content = (
|
|
77
|
+
<Button
|
|
78
|
+
activeClassname={activeClassname}
|
|
79
|
+
buttonSize={buttonSize}
|
|
80
|
+
className={buttonClass}
|
|
81
|
+
ref={ref}
|
|
82
|
+
variant={variant}
|
|
83
|
+
{...rest}
|
|
84
|
+
>
|
|
85
|
+
{children}
|
|
86
|
+
</Button>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (tooltip) {
|
|
90
|
+
return (
|
|
91
|
+
<Tooltip shortcut={tooltipShortcut} title={tooltip}>
|
|
92
|
+
{content}
|
|
93
|
+
</Tooltip>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return content;
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
ToolbarButton.displayName = "ToolbarButton";
|
|
102
|
+
|
|
103
|
+
export const Toolbar = {
|
|
104
|
+
Wrapper: ToolbarWrapper,
|
|
105
|
+
Divider: ToolbarDivider,
|
|
106
|
+
Button: ToolbarButton,
|
|
107
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type ReactElement, useCallback } from "react";
|
|
4
|
+
import Tippy from "rb-tippyjs-react/headless";
|
|
5
|
+
|
|
6
|
+
import type { TippyProps, TooltipProps } from "./types";
|
|
7
|
+
|
|
8
|
+
const isMac = typeof window !== "undefined" ? navigator.platform.toUpperCase().indexOf("MAC") >= 0 : false;
|
|
9
|
+
|
|
10
|
+
const ShortcutKey = ({ children }: { children: string }): ReactElement => {
|
|
11
|
+
const className =
|
|
12
|
+
"inline-flex items-center justify-center w-5 h-5 p-1 text-[0.625rem] rounded font-semibold leading-none border border-neutral-200 text-neutral-500 border-b-2";
|
|
13
|
+
|
|
14
|
+
if (children === "Mod") {
|
|
15
|
+
return <kbd className={className}>{isMac ? "⌘" : "Ctrl"}</kbd>; // ⌃
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (children === "Shift") {
|
|
19
|
+
return <kbd className={className}>⇧</kbd>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (children === "Alt") {
|
|
23
|
+
return <kbd className={className}>{isMac ? "⌥" : "Alt"}</kbd>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return <kbd className={className}>{children}</kbd>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const Tooltip = ({
|
|
30
|
+
children,
|
|
31
|
+
enabled = true,
|
|
32
|
+
title,
|
|
33
|
+
shortcut,
|
|
34
|
+
tippyOptions = {},
|
|
35
|
+
}: TooltipProps): ReactElement => {
|
|
36
|
+
const renderTooltip = useCallback(
|
|
37
|
+
(attrs: TippyProps) => (
|
|
38
|
+
<span
|
|
39
|
+
className="z-[999] flex items-center gap-2 rounded-lg border border-neutral-100 bg-white px-2.5 py-1 shadow-sm"
|
|
40
|
+
data-escaped={attrs["data-escaped"]}
|
|
41
|
+
data-placement={attrs["data-placement"]}
|
|
42
|
+
data-reference-hidden={attrs["data-reference-hidden"]}
|
|
43
|
+
tabIndex={-1}
|
|
44
|
+
>
|
|
45
|
+
{title && <span className="font-medium text-neutral-500 text-xs">{title}</span>}
|
|
46
|
+
{shortcut && (
|
|
47
|
+
<span className="flex items-center gap-0.5">
|
|
48
|
+
{shortcut.map((shortcutKey) => (
|
|
49
|
+
<ShortcutKey key={shortcutKey}>{shortcutKey}</ShortcutKey>
|
|
50
|
+
))}
|
|
51
|
+
</span>
|
|
52
|
+
)}
|
|
53
|
+
</span>
|
|
54
|
+
),
|
|
55
|
+
[shortcut, title],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (enabled) {
|
|
59
|
+
return (
|
|
60
|
+
<Tippy
|
|
61
|
+
appendTo={document.body}
|
|
62
|
+
delay={500}
|
|
63
|
+
offset={[0, 8]}
|
|
64
|
+
touch={false}
|
|
65
|
+
zIndex={99999}
|
|
66
|
+
{...tippyOptions}
|
|
67
|
+
render={renderTooltip}
|
|
68
|
+
>
|
|
69
|
+
<span>{children}</span>
|
|
70
|
+
</Tippy>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return <>{children}</>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export default Tooltip;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import type { Placement, Props } from "tippy.js";
|
|
3
|
+
|
|
4
|
+
export interface TooltipProps {
|
|
5
|
+
children?: string | React.ReactNode;
|
|
6
|
+
enabled?: boolean;
|
|
7
|
+
title?: string;
|
|
8
|
+
shortcut?: string[];
|
|
9
|
+
tippyOptions?: Omit<Partial<Props>, "content">;
|
|
10
|
+
content?: React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TippyProps {
|
|
14
|
+
"data-placement": Placement;
|
|
15
|
+
"data-reference-hidden"?: string;
|
|
16
|
+
"data-escaped"?: string;
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const cssVar = (name: string, value?: string) => {
|
|
2
|
+
let currentName = name;
|
|
3
|
+
if (name.substring(0, 2) !== "--") {
|
|
4
|
+
currentName = `--${currentName}`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
if (value) {
|
|
8
|
+
document.documentElement.style.setProperty(currentName, value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return getComputedStyle(document.body).getPropertyValue(currentName);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default cssVar;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Editor } from "@tiptap/react";
|
|
2
|
+
|
|
3
|
+
export const getRenderContainer = (editor: Editor, nodeType: string) => {
|
|
4
|
+
const {
|
|
5
|
+
view,
|
|
6
|
+
state: {
|
|
7
|
+
selection: { from },
|
|
8
|
+
},
|
|
9
|
+
} = editor;
|
|
10
|
+
|
|
11
|
+
const elements = document.querySelectorAll(".has-focus");
|
|
12
|
+
const elementCount = elements.length;
|
|
13
|
+
const innermostNode = elements[elementCount - 1];
|
|
14
|
+
const element = innermostNode;
|
|
15
|
+
|
|
16
|
+
if (
|
|
17
|
+
(element?.getAttribute("data-type") && element.getAttribute("data-type") === nodeType) ||
|
|
18
|
+
element?.classList?.contains(nodeType)
|
|
19
|
+
) {
|
|
20
|
+
return element;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const node = view.domAtPos(from).node as HTMLElement;
|
|
24
|
+
let container = node;
|
|
25
|
+
|
|
26
|
+
if (!container.tagName) {
|
|
27
|
+
container = node.parentElement!;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
while (
|
|
31
|
+
container &&
|
|
32
|
+
!(container.getAttribute("data-type") && container.getAttribute("data-type") === nodeType) &&
|
|
33
|
+
!container.classList.contains(nodeType)
|
|
34
|
+
) {
|
|
35
|
+
container = container.parentElement!;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return container;
|
|
39
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ClassValue, clsx } from "clsx";
|
|
2
|
+
import { twMerge } from "tailwind-merge";
|
|
3
|
+
|
|
4
|
+
export function cn(...inputs: ClassValue[]) {
|
|
5
|
+
return twMerge(clsx(inputs));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// biome-ignore lint/suspicious/noExplicitAny: <randomElement>
|
|
9
|
+
export function randomElement(array: any[]) {
|
|
10
|
+
return array[Math.floor(Math.random() * array.length)];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { cssVar } from "./cssVar";
|
|
14
|
+
export { getRenderContainer } from "./getRenderContainer";
|
|
15
|
+
export { isCustomNodeSelected } from "./isCustomNodeSelected";
|
|
16
|
+
export { isTextSelected } from "./isTextSelected";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import CodeBlock from "@tiptap/extension-code-block";
|
|
2
|
+
import type { Editor } from "@tiptap/react";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
// AiWriter, AiImage,
|
|
6
|
+
Figcaption,
|
|
7
|
+
HorizontalRule,
|
|
8
|
+
ImageBlock,
|
|
9
|
+
ImageUpload,
|
|
10
|
+
Link,
|
|
11
|
+
} from "../../editor/extension";
|
|
12
|
+
// import { TableOfContentsNode } from '@/extensions/TableOfContentsNode'
|
|
13
|
+
|
|
14
|
+
export const isTableGripSelected = (node: HTMLElement) => {
|
|
15
|
+
let container = node;
|
|
16
|
+
|
|
17
|
+
while (container && !["TD", "TH"].includes(container.tagName)) {
|
|
18
|
+
container = container.parentElement!;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const gripColumn = container?.querySelector?.("a.grip-column.selected");
|
|
22
|
+
const gripRow = container?.querySelector?.("a.grip-row.selected");
|
|
23
|
+
|
|
24
|
+
if (gripColumn || gripRow) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return false;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const isCustomNodeSelected = (editor: Editor, node: HTMLElement) => {
|
|
32
|
+
const customNodes = [
|
|
33
|
+
HorizontalRule.name,
|
|
34
|
+
ImageBlock.name,
|
|
35
|
+
ImageUpload.name,
|
|
36
|
+
CodeBlock.name,
|
|
37
|
+
Link.name,
|
|
38
|
+
// AiWriter.name,
|
|
39
|
+
// AiImage.name,
|
|
40
|
+
Figcaption.name,
|
|
41
|
+
// TableOfContentsNode.name,
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
return customNodes.some((type) => editor.isActive(type)) || isTableGripSelected(node);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default isCustomNodeSelected;
|