@dxpos/markdown 0.0.1

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.
Files changed (93) hide show
  1. package/README.md +77 -0
  2. package/dist/editor/ColorMenu.d.ts +18 -0
  3. package/dist/editor/ColorMenu.d.ts.map +1 -0
  4. package/dist/editor/ColorMenu.js +90 -0
  5. package/dist/editor/SlashMenu.d.ts +24 -0
  6. package/dist/editor/SlashMenu.d.ts.map +1 -0
  7. package/dist/editor/SlashMenu.js +179 -0
  8. package/dist/editor/TextStyleMenu.d.ts +45 -0
  9. package/dist/editor/TextStyleMenu.d.ts.map +1 -0
  10. package/dist/editor/TextStyleMenu.js +65 -0
  11. package/dist/editor/VideoEmbed.d.ts +15 -0
  12. package/dist/editor/VideoEmbed.d.ts.map +1 -0
  13. package/dist/editor/VideoEmbed.js +95 -0
  14. package/dist/editor/VideoMenu.d.ts +12 -0
  15. package/dist/editor/VideoMenu.d.ts.map +1 -0
  16. package/dist/editor/VideoMenu.js +35 -0
  17. package/dist/editor/color-menu/index.d.ts +19 -0
  18. package/dist/editor/color-menu/index.d.ts.map +1 -0
  19. package/dist/editor/color-menu/index.js +90 -0
  20. package/dist/editor/color-menu/text-color-presets.d.ts +9 -0
  21. package/dist/editor/color-menu/text-color-presets.d.ts.map +1 -0
  22. package/dist/editor/color-menu/text-color-presets.js +46 -0
  23. package/dist/editor/index.d.ts +12 -0
  24. package/dist/editor/index.d.ts.map +1 -0
  25. package/dist/editor/index.js +314 -0
  26. package/dist/editor/letter-spacing/index.d.ts +17 -0
  27. package/dist/editor/letter-spacing/index.d.ts.map +1 -0
  28. package/dist/editor/letter-spacing/index.js +35 -0
  29. package/dist/editor/letter-spacing.d.ts +17 -0
  30. package/dist/editor/letter-spacing.d.ts.map +1 -0
  31. package/dist/editor/letter-spacing.js +35 -0
  32. package/dist/editor/slash-menu/index.d.ts +25 -0
  33. package/dist/editor/slash-menu/index.d.ts.map +1 -0
  34. package/dist/editor/slash-menu/index.js +179 -0
  35. package/dist/editor/text-color-presets.d.ts +9 -0
  36. package/dist/editor/text-color-presets.d.ts.map +1 -0
  37. package/dist/editor/text-color-presets.js +46 -0
  38. package/dist/editor/text-style-menu/index.d.ts +46 -0
  39. package/dist/editor/text-style-menu/index.d.ts.map +1 -0
  40. package/dist/editor/text-style-menu/index.js +65 -0
  41. package/dist/editor/theme.d.ts +8 -0
  42. package/dist/editor/theme.d.ts.map +1 -0
  43. package/dist/editor/theme.js +18 -0
  44. package/dist/editor/toolbar/index.d.ts +17 -0
  45. package/dist/editor/toolbar/index.d.ts.map +1 -0
  46. package/dist/editor/toolbar/index.js +206 -0
  47. package/dist/editor/toolbar/items.d.ts +10 -0
  48. package/dist/editor/toolbar/items.d.ts.map +1 -0
  49. package/dist/editor/toolbar/items.js +34 -0
  50. package/dist/editor/toolbar/theme.d.ts +8 -0
  51. package/dist/editor/toolbar/theme.d.ts.map +1 -0
  52. package/dist/editor/toolbar/theme.js +18 -0
  53. package/dist/editor/toolbar-items.d.ts +10 -0
  54. package/dist/editor/toolbar-items.d.ts.map +1 -0
  55. package/dist/editor/toolbar-items.js +34 -0
  56. package/dist/editor/toolbar.d.ts +16 -0
  57. package/dist/editor/toolbar.d.ts.map +1 -0
  58. package/dist/editor/toolbar.js +206 -0
  59. package/dist/editor/use-dom-id.d.ts +3 -0
  60. package/dist/editor/use-dom-id.d.ts.map +1 -0
  61. package/dist/editor/use-dom-id.js +11 -0
  62. package/dist/editor/video-embed/index.d.ts +15 -0
  63. package/dist/editor/video-embed/index.d.ts.map +1 -0
  64. package/dist/editor/video-embed/index.js +95 -0
  65. package/dist/editor/video-embed/video-url.d.ts +8 -0
  66. package/dist/editor/video-embed/video-url.d.ts.map +1 -0
  67. package/dist/editor/video-embed/video-url.js +102 -0
  68. package/dist/editor/video-menu/index.d.ts +13 -0
  69. package/dist/editor/video-menu/index.d.ts.map +1 -0
  70. package/dist/editor/video-menu/index.js +35 -0
  71. package/dist/editor/video-url.d.ts +8 -0
  72. package/dist/editor/video-url.d.ts.map +1 -0
  73. package/dist/editor/video-url.js +102 -0
  74. package/dist/index.d.ts +5 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/dist/index.js +4 -0
  77. package/dist/plugin.d.ts +6 -0
  78. package/dist/plugin.d.ts.map +1 -0
  79. package/dist/plugin.js +1 -0
  80. package/dist/plugins/gfm/index.d.ts +3 -0
  81. package/dist/plugins/gfm/index.d.ts.map +1 -0
  82. package/dist/plugins/gfm/index.js +2 -0
  83. package/dist/plugins/highlight/index.d.ts +3 -0
  84. package/dist/plugins/highlight/index.d.ts.map +1 -0
  85. package/dist/plugins/highlight/index.js +2 -0
  86. package/dist/plugins/raw/index.d.ts +3 -0
  87. package/dist/plugins/raw/index.d.ts.map +1 -0
  88. package/dist/plugins/raw/index.js +2 -0
  89. package/dist/view/index.d.ts +9 -0
  90. package/dist/view/index.d.ts.map +1 -0
  91. package/dist/view/index.js +18 -0
  92. package/package.json +61 -0
  93. package/styles.css +760 -0
@@ -0,0 +1,25 @@
1
+ import { Extension, type Editor, type Range } from "@tiptap/react";
2
+ import { type SuggestionKeyDownProps, type SuggestionProps } from "@tiptap/suggestion";
3
+ import { Heading1 } from "lucide-react";
4
+ export type SlashCommandContext = {
5
+ editor: Editor;
6
+ range: Range;
7
+ onInsertImage: () => void;
8
+ onInsertVideo: () => void;
9
+ };
10
+ export type SlashItem = {
11
+ title: string;
12
+ icon: typeof Heading1;
13
+ command: (ctx: SlashCommandContext) => void;
14
+ };
15
+ export declare const SLASH_ITEMS: SlashItem[];
16
+ interface SlashMenuProps extends SuggestionProps<SlashItem, SlashItem> {
17
+ }
18
+ export interface SlashMenuHandle {
19
+ onKeyDown: (props: SuggestionKeyDownProps) => boolean;
20
+ }
21
+ export declare const SlashMenu: import("react").ForwardRefExoticComponent<SlashMenuProps & import("react").RefAttributes<SlashMenuHandle>>;
22
+ /** 输入 / 唤起块类型菜单 */
23
+ export declare const createSlashCommand: (onInsertImage: () => void, onInsertVideo: () => void) => Extension<any, any>;
24
+ export {};
25
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/editor/slash-menu/index.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAiB,KAAK,MAAM,EAAE,KAAK,KAAK,EAAE,MAAM,eAAe,CAAC;AAClF,OAAmB,EAAE,KAAK,sBAAsB,EAAE,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACnG,OAAO,EAEL,QAAQ,EAcT,MAAM,cAAc,CAAC;AAGtB,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,KAAK,CAAC;IACb,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,aAAa,EAAE,MAAM,IAAI,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,QAAQ,CAAC;IACtB,OAAO,EAAE,CAAC,GAAG,EAAE,mBAAmB,KAAK,IAAI,CAAC;CAC7C,CAAC;AAqBF,eAAO,MAAM,WAAW,EAAE,SAAS,EAwElC,CAAC;AAEF,UAAU,cAAe,SAAQ,eAAe,CAAC,SAAS,EAAE,SAAS,CAAC;CAAG;AAEzE,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,OAAO,CAAC;CACvD;AAED,eAAO,MAAM,SAAS,4GAwDpB,CAAC;AAEH,mBAAmB;AACnB,eAAO,MAAM,kBAAkB,GAAI,eAAe,MAAM,IAAI,EAAE,eAAe,MAAM,IAAI,wBA+CtF,CAAC"}
@@ -0,0 +1,179 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Extension, ReactRenderer } from "@tiptap/react";
4
+ import Suggestion from "@tiptap/suggestion";
5
+ import { Code, Heading1, Heading2, Heading3, Heading4, Heading5, Heading6, ImageIcon, List, ListOrdered, ListTodo, Minus, Quote, Table, Video, } from "lucide-react";
6
+ import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
7
+ const HEADING_ICONS = {
8
+ 1: Heading1,
9
+ 2: Heading2,
10
+ 3: Heading3,
11
+ 4: Heading4,
12
+ 5: Heading5,
13
+ 6: Heading6,
14
+ };
15
+ const headingItem = (level) => {
16
+ return {
17
+ title: `标题 ${level}`,
18
+ icon: HEADING_ICONS[level],
19
+ command: ({ editor, range }) => {
20
+ editor.chain().focus().deleteRange(range).setHeading({ level }).run();
21
+ },
22
+ };
23
+ };
24
+ export const SLASH_ITEMS = [
25
+ headingItem(1),
26
+ headingItem(2),
27
+ headingItem(3),
28
+ headingItem(4),
29
+ headingItem(5),
30
+ headingItem(6),
31
+ {
32
+ title: "无序列表",
33
+ icon: List,
34
+ command: ({ editor, range }) => {
35
+ editor.chain().focus().deleteRange(range).toggleBulletList().run();
36
+ },
37
+ },
38
+ {
39
+ title: "有序列表",
40
+ icon: ListOrdered,
41
+ command: ({ editor, range }) => {
42
+ editor.chain().focus().deleteRange(range).toggleOrderedList().run();
43
+ },
44
+ },
45
+ {
46
+ title: "待办",
47
+ icon: ListTodo,
48
+ command: ({ editor, range }) => {
49
+ editor.chain().focus().deleteRange(range).toggleTaskList().run();
50
+ },
51
+ },
52
+ {
53
+ title: "代码块",
54
+ icon: Code,
55
+ command: ({ editor, range }) => {
56
+ editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
57
+ },
58
+ },
59
+ {
60
+ title: "引用",
61
+ icon: Quote,
62
+ command: ({ editor, range }) => {
63
+ editor.chain().focus().deleteRange(range).toggleBlockquote().run();
64
+ },
65
+ },
66
+ {
67
+ title: "表格",
68
+ icon: Table,
69
+ command: ({ editor, range }) => {
70
+ editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
71
+ },
72
+ },
73
+ {
74
+ title: "分割线",
75
+ icon: Minus,
76
+ command: ({ editor, range }) => {
77
+ editor.chain().focus().deleteRange(range).setHorizontalRule().run();
78
+ },
79
+ },
80
+ {
81
+ title: "图片",
82
+ icon: ImageIcon,
83
+ command: ({ editor, range, onInsertImage }) => {
84
+ editor.chain().focus().deleteRange(range).run();
85
+ onInsertImage();
86
+ },
87
+ },
88
+ {
89
+ title: "视频",
90
+ icon: Video,
91
+ command: ({ editor, range, onInsertVideo }) => {
92
+ editor.chain().focus().deleteRange(range).run();
93
+ onInsertVideo();
94
+ },
95
+ },
96
+ ];
97
+ export const SlashMenu = forwardRef(({ items, command }, ref) => {
98
+ const [selected, setSelected] = useState(0);
99
+ useEffect(() => {
100
+ setSelected(0);
101
+ }, [items]);
102
+ useImperativeHandle(ref, () => ({
103
+ onKeyDown: ({ event }) => {
104
+ if (items.length === 0) {
105
+ return false;
106
+ }
107
+ if (event.key === "ArrowUp") {
108
+ setSelected((index) => (index + items.length - 1) % items.length);
109
+ return true;
110
+ }
111
+ if (event.key === "ArrowDown") {
112
+ setSelected((index) => (index + 1) % items.length);
113
+ return true;
114
+ }
115
+ if (event.key === "Enter") {
116
+ const item = items[selected];
117
+ if (item) {
118
+ command(item);
119
+ }
120
+ return true;
121
+ }
122
+ return false;
123
+ },
124
+ }));
125
+ if (items.length === 0) {
126
+ return null;
127
+ }
128
+ return (_jsx("div", { className: "wsmd-slash-menu", role: "listbox", children: items.map((item, index) => {
129
+ const Icon = item.icon;
130
+ return (_jsxs("button", { className: `wsmd-slash-menu__item${index === selected ? " wsmd-slash-menu__item--active" : ""}`, type: "button", role: "option", "aria-selected": index === selected, onMouseEnter: () => setSelected(index), onClick: () => command(item), children: [_jsx(Icon, { "aria-hidden": true }), _jsx("span", { children: item.title })] }, item.title));
131
+ }) }));
132
+ });
133
+ /** 输入 / 唤起块类型菜单 */
134
+ export const createSlashCommand = (onInsertImage, onInsertVideo) => {
135
+ return Extension.create({
136
+ name: "slashCommand",
137
+ addProseMirrorPlugins() {
138
+ return [
139
+ Suggestion({
140
+ editor: this.editor,
141
+ char: "/",
142
+ items: ({ query }) => SLASH_ITEMS.filter((item) => item.title.toLowerCase().includes(query.toLowerCase())),
143
+ command: ({ editor, range, props }) => {
144
+ props.command({ editor, range, onInsertImage, onInsertVideo });
145
+ },
146
+ render: () => {
147
+ let component = null;
148
+ let unmount = null;
149
+ return {
150
+ onStart: (props) => {
151
+ component = new ReactRenderer(SlashMenu, {
152
+ editor: props.editor,
153
+ props,
154
+ });
155
+ unmount = props.mount(component.element);
156
+ },
157
+ onUpdate: (props) => {
158
+ component?.updateProps(props);
159
+ },
160
+ onKeyDown: (props) => {
161
+ if (props.event.key === "Escape") {
162
+ unmount?.();
163
+ return true;
164
+ }
165
+ return component?.ref?.onKeyDown(props) ?? false;
166
+ },
167
+ onExit: () => {
168
+ unmount?.();
169
+ component?.destroy();
170
+ unmount = null;
171
+ component = null;
172
+ },
173
+ };
174
+ },
175
+ }),
176
+ ];
177
+ },
178
+ });
179
+ };
@@ -0,0 +1,9 @@
1
+ export declare const TEXT_COLOR_PRESET_KEY = "kb-text-color-presets";
2
+ export declare const TEXT_COLOR_PRESET_LIMIT = 16;
3
+ export declare function normalizeTextColor(color: string): string | null;
4
+ /** 记住自定义色:去重后追加,超过上限丢掉最早的;内置色不写入 */
5
+ export declare function rememberCustomTextColor(current: readonly string[], color: string, builtin: readonly string[]): string[];
6
+ export declare function removeCustomTextColor(current: readonly string[], color: string): string[];
7
+ export declare function readCustomTextColors(): string[];
8
+ export declare function writeCustomTextColors(colors: readonly string[]): void;
9
+ //# sourceMappingURL=text-color-presets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-color-presets.d.ts","sourceRoot":"","sources":["../../src/editor/text-color-presets.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,0BAA0B,CAAC;AAC7D,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAI1C,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,iBAG/C;AAED,oCAAoC;AACpC,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,SAAS,MAAM,EAAE,YAe3B;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,YAM9E;AAED,wBAAgB,oBAAoB,aAYnC;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,QAE9D"}
@@ -0,0 +1,46 @@
1
+ export const TEXT_COLOR_PRESET_KEY = "kb-text-color-presets";
2
+ export const TEXT_COLOR_PRESET_LIMIT = 16;
3
+ const HEX = /^#[0-9a-f]{6}$/;
4
+ export function normalizeTextColor(color) {
5
+ const value = color.trim().toLowerCase();
6
+ return HEX.test(value) ? value : null;
7
+ }
8
+ /** 记住自定义色:去重后追加,超过上限丢掉最早的;内置色不写入 */
9
+ export function rememberCustomTextColor(current, color, builtin) {
10
+ const normalized = normalizeTextColor(color);
11
+ if (!normalized) {
12
+ return [...current];
13
+ }
14
+ if (builtin.some((item) => item.toLowerCase() === normalized)) {
15
+ return [...current];
16
+ }
17
+ const next = current.filter((item) => item !== normalized);
18
+ next.push(normalized);
19
+ if (next.length <= TEXT_COLOR_PRESET_LIMIT) {
20
+ return next;
21
+ }
22
+ return next.slice(next.length - TEXT_COLOR_PRESET_LIMIT);
23
+ }
24
+ export function removeCustomTextColor(current, color) {
25
+ const normalized = normalizeTextColor(color);
26
+ if (!normalized) {
27
+ return [...current];
28
+ }
29
+ return current.filter((item) => item !== normalized);
30
+ }
31
+ export function readCustomTextColors() {
32
+ const raw = localStorage.getItem(TEXT_COLOR_PRESET_KEY);
33
+ if (!raw) {
34
+ return [];
35
+ }
36
+ const parsed = JSON.parse(raw);
37
+ if (!Array.isArray(parsed)) {
38
+ return [];
39
+ }
40
+ return parsed
41
+ .filter((item) => typeof item === "string" && HEX.test(item))
42
+ .slice(-TEXT_COLOR_PRESET_LIMIT);
43
+ }
44
+ export function writeCustomTextColors(colors) {
45
+ localStorage.setItem(TEXT_COLOR_PRESET_KEY, JSON.stringify(colors));
46
+ }
@@ -0,0 +1,46 @@
1
+ import { type FC } from "react";
2
+ export declare const FONT_FAMILIES: readonly [{
3
+ readonly label: "宋体";
4
+ readonly value: "SimSun, Songti SC, serif";
5
+ }, {
6
+ readonly label: "黑体";
7
+ readonly value: "SimHei, Heiti SC, sans-serif";
8
+ }, {
9
+ readonly label: "楷体";
10
+ readonly value: "KaiTi, Kaiti SC, serif";
11
+ }, {
12
+ readonly label: "微软雅黑";
13
+ readonly value: "\"Microsoft YaHei\", \"PingFang SC\", sans-serif";
14
+ }, {
15
+ readonly label: "苹方";
16
+ readonly value: "\"PingFang SC\", \"Microsoft YaHei\", sans-serif";
17
+ }, {
18
+ readonly label: "Arial";
19
+ readonly value: "Arial, sans-serif";
20
+ }, {
21
+ readonly label: "Georgia";
22
+ readonly value: "Georgia, serif";
23
+ }, {
24
+ readonly label: "Times New Roman";
25
+ readonly value: "\"Times New Roman\", Times, serif";
26
+ }, {
27
+ readonly label: "Consolas";
28
+ readonly value: "Consolas, monospace";
29
+ }];
30
+ export declare const FONT_SIZES: readonly ["12px", "14px", "16px", "18px", "20px", "24px", "28px", "32px", "36px"];
31
+ export declare const LETTER_SPACINGS: readonly ["-1px", "-0.5px", "0px", "0.5px", "1px", "1.5px", "2px", "3px", "4px", "6px", "8px"];
32
+ export declare const LINE_HEIGHTS: readonly ["1", "1.15", "1.25", "1.5", "1.75", "2", "2.25", "2.5", "2.75", "3"];
33
+ interface TextStyleMenuProps {
34
+ fontFamily: string | null;
35
+ fontSize: string | null;
36
+ letterSpacing: string | null;
37
+ lineHeight: string | null;
38
+ onFontFamily: (value: string | null) => void;
39
+ onFontSize: (value: string | null) => void;
40
+ onLetterSpacing: (value: string | null) => void;
41
+ onLineHeight: (value: string | null) => void;
42
+ }
43
+ /** 工具栏文本样式:字体、字号、字间距、行距 */
44
+ export declare const TextStyleMenu: FC<TextStyleMenuProps>;
45
+ export {};
46
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/editor/text-style-menu/index.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAsC,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAEpE,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;EAUhB,CAAC;AAEX,eAAO,MAAM,UAAU,mFAAoF,CAAC;AAE5G,eAAO,MAAM,eAAe,gGAYlB,CAAC;AAEX,eAAO,MAAM,YAAY,gFAAiF,CAAC;AAS3G,UAAU,kBAAkB;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC7C,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC3C,eAAe,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAChD,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;CAC9C;AAwBD,2BAA2B;AAC3B,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,kBAAkB,CA0EhD,CAAC"}
@@ -0,0 +1,65 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Type } from "lucide-react";
4
+ import { useEffect, useId, useRef, useState } from "react";
5
+ export const FONT_FAMILIES = [
6
+ { label: "宋体", value: "SimSun, Songti SC, serif" },
7
+ { label: "黑体", value: "SimHei, Heiti SC, sans-serif" },
8
+ { label: "楷体", value: "KaiTi, Kaiti SC, serif" },
9
+ { label: "微软雅黑", value: '"Microsoft YaHei", "PingFang SC", sans-serif' },
10
+ { label: "苹方", value: '"PingFang SC", "Microsoft YaHei", sans-serif' },
11
+ { label: "Arial", value: "Arial, sans-serif" },
12
+ { label: "Georgia", value: "Georgia, serif" },
13
+ { label: "Times New Roman", value: '"Times New Roman", Times, serif' },
14
+ { label: "Consolas", value: "Consolas, monospace" },
15
+ ];
16
+ export const FONT_SIZES = ["12px", "14px", "16px", "18px", "20px", "24px", "28px", "32px", "36px"];
17
+ export const LETTER_SPACINGS = [
18
+ "-1px",
19
+ "-0.5px",
20
+ "0px",
21
+ "0.5px",
22
+ "1px",
23
+ "1.5px",
24
+ "2px",
25
+ "3px",
26
+ "4px",
27
+ "6px",
28
+ "8px",
29
+ ];
30
+ export const LINE_HEIGHTS = ["1", "1.15", "1.25", "1.5", "1.75", "2", "2.25", "2.5", "2.75", "3"];
31
+ const StyleRow = ({ label, value, options, onChange }) => {
32
+ const id = useId();
33
+ return (_jsxs("label", { className: "wsmd-text-style-menu__row", htmlFor: id, children: [_jsx("span", { children: label }), _jsxs("select", { id: id, className: "wsmd-text-style-menu__select", value: value ?? "", onChange: (event) => onChange(event.target.value || null), children: [_jsx("option", { value: "", children: "\u9ED8\u8BA4" }), options.map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value)))] })] }));
34
+ };
35
+ /** 工具栏文本样式:字体、字号、字间距、行距 */
36
+ export const TextStyleMenu = ({ fontFamily, fontSize, letterSpacing, lineHeight, onFontFamily, onFontSize, onLetterSpacing, onLineHeight }) => {
37
+ const [open, setOpen] = useState(false);
38
+ const rootRef = useRef(null);
39
+ const active = Boolean(fontFamily || fontSize || letterSpacing || lineHeight);
40
+ useEffect(() => {
41
+ if (!open) {
42
+ return;
43
+ }
44
+ const onPointerDown = (event) => {
45
+ if (!rootRef.current?.contains(event.target)) {
46
+ setOpen(false);
47
+ }
48
+ };
49
+ const onKeyDown = (event) => {
50
+ if (event.key === "Escape") {
51
+ setOpen(false);
52
+ }
53
+ };
54
+ document.addEventListener("pointerdown", onPointerDown);
55
+ document.addEventListener("keydown", onKeyDown);
56
+ return () => {
57
+ document.removeEventListener("pointerdown", onPointerDown);
58
+ document.removeEventListener("keydown", onKeyDown);
59
+ };
60
+ }, [open]);
61
+ return (_jsxs("div", { className: "wsmd-text-style-menu", ref: rootRef, children: [_jsx("button", { className: `wsmd-icon-btn wsmd-icon-btn--ghost wsmd-toolbar__btn${active ? " wsmd-toolbar__btn--active" : ""}`, type: "button", title: "\u6587\u672C", "aria-label": "\u6587\u672C", "aria-expanded": open, "aria-haspopup": "dialog", "aria-pressed": active, onClick: () => setOpen((value) => !value), children: _jsx(Type, { "aria-hidden": true }) }), open ? (_jsxs("div", { className: "wsmd-text-style-menu__panel", role: "dialog", "aria-label": "\u6587\u672C", children: [_jsx(StyleRow, { label: "\u5B57\u4F53", value: fontFamily, options: FONT_FAMILIES, onChange: onFontFamily }), _jsx(StyleRow, { label: "\u5B57\u53F7", value: fontSize, options: FONT_SIZES.map((size) => ({ label: size, value: size })), onChange: onFontSize }), _jsx(StyleRow, { label: "\u5B57\u95F4\u8DDD", value: letterSpacing, options: LETTER_SPACINGS.map((spacing) => ({
62
+ label: spacing === "0px" ? "0" : spacing,
63
+ value: spacing,
64
+ })), onChange: onLetterSpacing }), _jsx(StyleRow, { label: "\u884C\u8DDD", value: lineHeight, options: LINE_HEIGHTS.map((height) => ({ label: height, value: height })), onChange: onLineHeight })] })) : null] }));
65
+ };
@@ -0,0 +1,8 @@
1
+ export declare const THEME_STORAGE_KEY = "kb-theme";
2
+ export type ColorTheme = "light" | "dark";
3
+ export type ThemePreference = ColorTheme | "system";
4
+ export declare function isColorTheme(value: string | null): value is ColorTheme;
5
+ /** 未手动选择时不写 data-theme,交给系统 prefers-color-scheme */
6
+ export declare function applyThemePreference(preference: ThemePreference): void;
7
+ export declare function resolveColorTheme(preference: ThemePreference): ColorTheme;
8
+ //# sourceMappingURL=theme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/editor/theme.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,iBAAiB,aAAa,CAAC;AAE5C,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;AAC1C,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,QAAQ,CAAC;AAEpD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,KAAK,IAAI,UAAU,CAEtE;AAED,oDAAoD;AACpD,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,eAAe,QAM/D;AAED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,eAAe,GAAG,UAAU,CAKzE"}
@@ -0,0 +1,18 @@
1
+ export const THEME_STORAGE_KEY = "kb-theme";
2
+ export function isColorTheme(value) {
3
+ return value === "light" || value === "dark";
4
+ }
5
+ /** 未手动选择时不写 data-theme,交给系统 prefers-color-scheme */
6
+ export function applyThemePreference(preference) {
7
+ if (preference === "system") {
8
+ document.documentElement.removeAttribute("data-theme");
9
+ return;
10
+ }
11
+ document.documentElement.dataset.theme = preference;
12
+ }
13
+ export function resolveColorTheme(preference) {
14
+ if (preference !== "system") {
15
+ return preference;
16
+ }
17
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
18
+ }
@@ -0,0 +1,17 @@
1
+ import type { Editor } from "@tiptap/react";
2
+ import { type FC } from "react";
3
+ import { type ToolbarItem } from "./items";
4
+ interface ToolbarProps {
5
+ editor: Editor;
6
+ items: readonly ToolbarItem[];
7
+ uploading: boolean;
8
+ videoOpen: boolean;
9
+ videoError: string | null;
10
+ onInsertImage: () => void;
11
+ onVideoOpenChange: (open: boolean) => void;
12
+ onUploadVideo: () => void;
13
+ onInsertVideoUrl: (url: string) => void;
14
+ }
15
+ export declare const Toolbar: FC<ToolbarProps>;
16
+ export {};
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/editor/toolbar/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAgC5C,OAAO,EAAuB,KAAK,EAAE,EAAkB,MAAM,OAAO,CAAC;AAUrE,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAqB5D,UAAU,YAAY;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,iBAAiB,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3C,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAuED,eAAO,MAAM,OAAO,EAAE,EAAE,CAAC,YAAY,CAmXpC,CAAC"}
@@ -0,0 +1,206 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useEditorState } from "@tiptap/react";
4
+ import { AlignCenter, AlignJustify, AlignLeft, AlignRight, Baseline, Bold, Code, Heading1, Heading2, Heading3, Heading4, Heading5, Heading6, Highlighter, ImageIcon, Italic, Link as LinkIcon, List, ListOrdered, Loader2, Moon, Pilcrow, Quote, Redo2, Strikethrough, Sun, Table, Undo2, } from "lucide-react";
5
+ import { useEffect, useState } from "react";
6
+ import { ColorMenu, PRESET_HIGHLIGHT_COLORS, PRESET_TEXT_COLORS } from "../color-menu";
7
+ import { readCustomTextColors, rememberCustomTextColor, removeCustomTextColor, writeCustomTextColors, } from "../color-menu/text-color-presets";
8
+ import { TextStyleMenu } from "../text-style-menu";
9
+ import { VideoMenu } from "../video-menu";
10
+ import { DEFAULT_TOOLBAR } from "./items";
11
+ import { applyThemePreference, isColorTheme, resolveColorTheme, THEME_STORAGE_KEY, } from "./theme";
12
+ const HEADING_ICONS = {
13
+ 1: Heading1,
14
+ 2: Heading2,
15
+ 3: Heading3,
16
+ 4: Heading4,
17
+ 5: Heading5,
18
+ 6: Heading6,
19
+ };
20
+ const ToolbarButton = ({ title, active, disabled, onClick, children }) => {
21
+ return (_jsx("button", { className: `wsmd-icon-btn wsmd-icon-btn--ghost wsmd-toolbar__btn${active ? " wsmd-toolbar__btn--active" : ""}`, type: "button", title: title, "aria-label": title, "aria-pressed": active, disabled: disabled, onClick: onClick, children: children }));
22
+ };
23
+ const useToolbarTheme = () => {
24
+ const [preference, setPreference] = useState("system");
25
+ const [resolved, setResolved] = useState("light");
26
+ useEffect(() => {
27
+ const stored = localStorage.getItem(THEME_STORAGE_KEY);
28
+ const next = isColorTheme(stored) ? stored : "system";
29
+ setPreference(next);
30
+ setResolved(resolveColorTheme(next));
31
+ applyThemePreference(next);
32
+ }, []);
33
+ useEffect(() => {
34
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
35
+ const sync = () => setResolved(resolveColorTheme(preference));
36
+ media.addEventListener("change", sync);
37
+ return () => media.removeEventListener("change", sync);
38
+ }, [preference]);
39
+ const toggle = () => {
40
+ const next = resolved === "dark" ? "light" : "dark";
41
+ localStorage.setItem(THEME_STORAGE_KEY, next);
42
+ setPreference(next);
43
+ setResolved(next);
44
+ applyThemePreference(next);
45
+ };
46
+ return { resolved, toggle };
47
+ };
48
+ const textStyleValue = (value) => {
49
+ return typeof value === "string" && value ? value : null;
50
+ };
51
+ const headingLevel = (item) => {
52
+ if (!item.startsWith("h")) {
53
+ return null;
54
+ }
55
+ const level = Number(item.slice(1));
56
+ if (level < 1 || level > 6) {
57
+ return null;
58
+ }
59
+ return level;
60
+ };
61
+ export const Toolbar = ({ editor, items = DEFAULT_TOOLBAR, uploading, videoOpen, videoError, onInsertImage, onVideoOpenChange, onUploadVideo, onInsertVideoUrl }) => {
62
+ const { resolved, toggle } = useToolbarTheme();
63
+ const [customTextColors, setCustomTextColors] = useState([]);
64
+ const state = useEditorState({
65
+ editor,
66
+ selector: (ctx) => ({
67
+ canUndo: ctx.editor.can().undo(),
68
+ canRedo: ctx.editor.can().redo(),
69
+ isParagraph: ctx.editor.isActive("paragraph"),
70
+ headingLevels: [1, 2, 3, 4, 5, 6].map((level) => ctx.editor.isActive("heading", { level })),
71
+ isBold: ctx.editor.isActive("bold"),
72
+ isItalic: ctx.editor.isActive("italic"),
73
+ isStrike: ctx.editor.isActive("strike"),
74
+ isHighlight: ctx.editor.isActive("highlight"),
75
+ highlightColor: ctx.editor.getAttributes("highlight").color ?? null,
76
+ textColor: textStyleValue(ctx.editor.getAttributes("textStyle").color),
77
+ fontFamily: textStyleValue(ctx.editor.getAttributes("textStyle").fontFamily),
78
+ fontSize: textStyleValue(ctx.editor.getAttributes("textStyle").fontSize),
79
+ letterSpacing: textStyleValue(ctx.editor.getAttributes("textStyle").letterSpacing),
80
+ lineHeight: textStyleValue(ctx.editor.getAttributes("textStyle").lineHeight),
81
+ isBulletList: ctx.editor.isActive("bulletList"),
82
+ isOrderedList: ctx.editor.isActive("orderedList"),
83
+ isBlockquote: ctx.editor.isActive("blockquote"),
84
+ isCodeBlock: ctx.editor.isActive("codeBlock"),
85
+ isAlignLeft: ctx.editor.isActive({ textAlign: "left" }),
86
+ isAlignCenter: ctx.editor.isActive({ textAlign: "center" }),
87
+ isAlignRight: ctx.editor.isActive({ textAlign: "right" }),
88
+ isAlignJustify: ctx.editor.isActive({ textAlign: "justify" }),
89
+ isLink: ctx.editor.isActive("link"),
90
+ }),
91
+ });
92
+ useEffect(() => {
93
+ setCustomTextColors(readCustomTextColors());
94
+ }, []);
95
+ const setLink = () => {
96
+ const url = window.prompt("链接地址", editor.getAttributes("link").href ?? "");
97
+ if (url === null) {
98
+ return;
99
+ }
100
+ if (url === "") {
101
+ editor.chain().focus().unsetLink().run();
102
+ return;
103
+ }
104
+ editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
105
+ };
106
+ const renderItem = (item, index) => {
107
+ const level = headingLevel(item);
108
+ if (level) {
109
+ const Icon = HEADING_ICONS[level];
110
+ return (_jsx(ToolbarButton, { title: `标题 ${level}`, active: state.headingLevels[level - 1], onClick: () => editor.chain().focus().toggleHeading({ level }).run(), children: _jsx(Icon, { "aria-hidden": true }) }, `${item}-${index}`));
111
+ }
112
+ switch (item) {
113
+ case "divider":
114
+ return _jsx("span", { className: "wsmd-toolbar__divider" }, `${item}-${index}`);
115
+ case "spacer":
116
+ return _jsx("span", { className: "wsmd-toolbar__spacer" }, `${item}-${index}`);
117
+ case "paragraph":
118
+ return (_jsx(ToolbarButton, { title: "\u6BB5\u843D", active: state.isParagraph, onClick: () => editor.chain().focus().setParagraph().run(), children: _jsx(Pilcrow, { "aria-hidden": true }) }, item));
119
+ case "bold":
120
+ return (_jsx(ToolbarButton, { title: "\u7C97\u4F53", active: state.isBold, onClick: () => editor.chain().focus().toggleBold().run(), children: _jsx(Bold, { "aria-hidden": true }) }, item));
121
+ case "italic":
122
+ return (_jsx(ToolbarButton, { title: "\u659C\u4F53", active: state.isItalic, onClick: () => editor.chain().focus().toggleItalic().run(), children: _jsx(Italic, { "aria-hidden": true }) }, item));
123
+ case "strike":
124
+ return (_jsx(ToolbarButton, { title: "\u5220\u9664\u7EBF", active: state.isStrike, onClick: () => editor.chain().focus().toggleStrike().run(), children: _jsx(Strikethrough, { "aria-hidden": true }) }, item));
125
+ case "highlight":
126
+ return (_jsx(ColorMenu, { label: "\u9AD8\u4EAE", active: state.isHighlight, currentColor: state.highlightColor, presets: PRESET_HIGHLIGHT_COLORS, onPick: (color) => editor.chain().focus().setHighlight({ color }).run(), onClear: () => editor.chain().focus().unsetHighlight().run(), children: _jsx(Highlighter, { "aria-hidden": true }) }, item));
127
+ case "color":
128
+ return (_jsx(ColorMenu, { label: "\u6587\u5B57\u989C\u8272", active: Boolean(state.textColor), currentColor: state.textColor, presets: PRESET_TEXT_COLORS, customPresets: customTextColors, onPick: (color) => editor.chain().focus().setColor(color).run(), onRemember: (color) => {
129
+ setCustomTextColors((current) => {
130
+ const next = rememberCustomTextColor(current, color, PRESET_TEXT_COLORS);
131
+ writeCustomTextColors(next);
132
+ return next;
133
+ });
134
+ }, onRemoveCustom: (color) => {
135
+ setCustomTextColors((current) => {
136
+ const next = removeCustomTextColor(current, color);
137
+ writeCustomTextColors(next);
138
+ return next;
139
+ });
140
+ }, onClear: () => editor.chain().focus().unsetColor().run(), children: _jsx(Baseline, { "aria-hidden": true }) }, item));
141
+ case "textStyle":
142
+ return (_jsx(TextStyleMenu, { fontFamily: state.fontFamily, fontSize: state.fontSize, letterSpacing: state.letterSpacing, lineHeight: state.lineHeight, onFontFamily: (value) => {
143
+ const chain = editor.chain().focus();
144
+ if (value) {
145
+ chain.setFontFamily(value).run();
146
+ return;
147
+ }
148
+ chain.unsetFontFamily().run();
149
+ }, onFontSize: (value) => {
150
+ const chain = editor.chain().focus();
151
+ if (value) {
152
+ chain.setFontSize(value).run();
153
+ return;
154
+ }
155
+ chain.unsetFontSize().run();
156
+ }, onLetterSpacing: (value) => {
157
+ const chain = editor.chain().focus();
158
+ if (value) {
159
+ chain.setLetterSpacing(value).run();
160
+ return;
161
+ }
162
+ chain.unsetLetterSpacing().run();
163
+ }, onLineHeight: (value) => {
164
+ const chain = editor.chain().focus();
165
+ if (value) {
166
+ chain.setLineHeight(value).run();
167
+ return;
168
+ }
169
+ chain.unsetLineHeight().run();
170
+ } }, item));
171
+ case "list:bullet":
172
+ return (_jsx(ToolbarButton, { title: "\u65E0\u5E8F\u5217\u8868", active: state.isBulletList, onClick: () => editor.chain().focus().toggleBulletList().run(), children: _jsx(List, { "aria-hidden": true }) }, item));
173
+ case "list:ordered":
174
+ return (_jsx(ToolbarButton, { title: "\u6709\u5E8F\u5217\u8868", active: state.isOrderedList, onClick: () => editor.chain().focus().toggleOrderedList().run(), children: _jsx(ListOrdered, { "aria-hidden": true }) }, item));
175
+ case "quote":
176
+ return (_jsx(ToolbarButton, { title: "\u5F15\u7528", active: state.isBlockquote, onClick: () => editor.chain().focus().toggleBlockquote().run(), children: _jsx(Quote, { "aria-hidden": true }) }, item));
177
+ case "code":
178
+ return (_jsx(ToolbarButton, { title: "\u4EE3\u7801\u5757", active: state.isCodeBlock, onClick: () => editor.chain().focus().toggleCodeBlock().run(), children: _jsx(Code, { "aria-hidden": true }) }, item));
179
+ case "align:left":
180
+ return (_jsx(ToolbarButton, { title: "\u5DE6\u5BF9\u9F50", active: state.isAlignLeft, onClick: () => editor.chain().focus().setTextAlign("left").run(), children: _jsx(AlignLeft, { "aria-hidden": true }) }, item));
181
+ case "align:center":
182
+ return (_jsx(ToolbarButton, { title: "\u5C45\u4E2D", active: state.isAlignCenter, onClick: () => editor.chain().focus().setTextAlign("center").run(), children: _jsx(AlignCenter, { "aria-hidden": true }) }, item));
183
+ case "align:right":
184
+ return (_jsx(ToolbarButton, { title: "\u53F3\u5BF9\u9F50", active: state.isAlignRight, onClick: () => editor.chain().focus().setTextAlign("right").run(), children: _jsx(AlignRight, { "aria-hidden": true }) }, item));
185
+ case "align:justify":
186
+ return (_jsx(ToolbarButton, { title: "\u4E24\u7AEF\u5BF9\u9F50", active: state.isAlignJustify, onClick: () => editor.chain().focus().setTextAlign("justify").run(), children: _jsx(AlignJustify, { "aria-hidden": true }) }, item));
187
+ case "link":
188
+ return (_jsx(ToolbarButton, { title: "\u94FE\u63A5", active: state.isLink, onClick: setLink, children: _jsx(LinkIcon, { "aria-hidden": true }) }, item));
189
+ case "table":
190
+ return (_jsx(ToolbarButton, { title: "\u8868\u683C", onClick: () => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(), children: _jsx(Table, { "aria-hidden": true }) }, item));
191
+ case "image":
192
+ return (_jsx(ToolbarButton, { title: uploading ? "上传中" : "图片", disabled: uploading, onClick: onInsertImage, children: uploading ? _jsx(Loader2, { className: "wsmd-icon-btn__spin", "aria-hidden": true }) : _jsx(ImageIcon, { "aria-hidden": true }) }, item));
193
+ case "video":
194
+ return (_jsx(VideoMenu, { open: videoOpen, uploading: uploading, error: videoError, onOpenChange: onVideoOpenChange, onUpload: onUploadVideo, onSubmitUrl: onInsertVideoUrl }, item));
195
+ case "undo":
196
+ return (_jsx(ToolbarButton, { title: "\u64A4\u9500", disabled: !state.canUndo, onClick: () => editor.chain().focus().undo().run(), children: _jsx(Undo2, { "aria-hidden": true }) }, item));
197
+ case "redo":
198
+ return (_jsx(ToolbarButton, { title: "\u6062\u590D", disabled: !state.canRedo, onClick: () => editor.chain().focus().redo().run(), children: _jsx(Redo2, { "aria-hidden": true }) }, item));
199
+ case "theme":
200
+ return (_jsx(ToolbarButton, { title: resolved === "dark" ? "切换浅色" : "切换深色", onClick: toggle, children: resolved === "dark" ? _jsx(Sun, { "aria-hidden": true }) : _jsx(Moon, { "aria-hidden": true }) }, item));
201
+ default:
202
+ return null;
203
+ }
204
+ };
205
+ return (_jsx("div", { className: "wsmd-toolbar", role: "toolbar", "aria-label": "\u683C\u5F0F", children: items.map((item, index) => renderItem(item, index)) }));
206
+ };