@fragments-sdk/ui 0.10.0 → 0.11.0

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 (41) hide show
  1. package/dist/assets/ui.css +304 -0
  2. package/dist/blocks/BlogEditor.block.d.ts +3 -0
  3. package/dist/blocks/BlogEditor.block.d.ts.map +1 -0
  4. package/dist/components/Editor/Editor.module.scss.cjs +57 -0
  5. package/dist/components/Editor/Editor.module.scss.cjs.map +1 -0
  6. package/dist/components/Editor/Editor.module.scss.js +57 -0
  7. package/dist/components/Editor/Editor.module.scss.js.map +1 -0
  8. package/dist/components/Editor/index.cjs +548 -0
  9. package/dist/components/Editor/index.cjs.map +1 -0
  10. package/dist/components/Editor/index.d.ts +107 -0
  11. package/dist/components/Editor/index.d.ts.map +1 -0
  12. package/dist/components/Editor/index.js +531 -0
  13. package/dist/components/Editor/index.js.map +1 -0
  14. package/dist/components/Sidebar/index.cjs +6 -11
  15. package/dist/components/Sidebar/index.cjs.map +1 -1
  16. package/dist/components/Sidebar/index.d.ts.map +1 -1
  17. package/dist/components/Sidebar/index.js +6 -11
  18. package/dist/components/Sidebar/index.js.map +1 -1
  19. package/dist/index.cjs +22 -0
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +22 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/utils/keyboard-shortcuts.cjs +295 -0
  26. package/dist/utils/keyboard-shortcuts.cjs.map +1 -0
  27. package/dist/utils/keyboard-shortcuts.d.ts +293 -0
  28. package/dist/utils/keyboard-shortcuts.d.ts.map +1 -0
  29. package/dist/utils/keyboard-shortcuts.js +295 -0
  30. package/dist/utils/keyboard-shortcuts.js.map +1 -0
  31. package/fragments.json +1 -1
  32. package/package.json +27 -2
  33. package/src/blocks/BlogEditor.block.ts +34 -0
  34. package/src/components/Editor/Editor.fragment.tsx +322 -0
  35. package/src/components/Editor/Editor.module.scss +333 -0
  36. package/src/components/Editor/Editor.test.tsx +174 -0
  37. package/src/components/Editor/index.tsx +815 -0
  38. package/src/components/Sidebar/index.tsx +7 -14
  39. package/src/index.ts +43 -0
  40. package/src/utils/keyboard-shortcuts.test.ts +357 -0
  41. package/src/utils/keyboard-shortcuts.ts +502 -0
@@ -0,0 +1,107 @@
1
+ import * as React from 'react';
2
+ export type EditorFormat = 'bold' | 'italic' | 'strikethrough' | 'link' | 'code' | 'bulletList' | 'orderedList' | 'heading1' | 'heading2' | 'heading3' | 'blockquote' | 'undo' | 'redo';
3
+ export type EditorSaveStatus = 'idle' | 'saving' | 'saved' | 'error';
4
+ export type EditorMode = 'rich' | 'markdown';
5
+ export type EditorSize = 'sm' | 'md' | 'lg';
6
+ export interface EditorProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue'> {
7
+ children?: React.ReactNode;
8
+ /** Controlled value */
9
+ value?: string;
10
+ /** Default value for uncontrolled usage */
11
+ defaultValue?: string;
12
+ /** Called when content changes */
13
+ onValueChange?: (value: string) => void;
14
+ /** Placeholder text */
15
+ placeholder?: string;
16
+ /** Disable the editor */
17
+ disabled?: boolean;
18
+ /** Read-only mode */
19
+ readOnly?: boolean;
20
+ /** Which format buttons to show */
21
+ formats?: EditorFormat[];
22
+ /** Show default toolbar */
23
+ toolbar?: boolean;
24
+ /** Show default status bar */
25
+ statusBar?: boolean;
26
+ /** Auto-save callback */
27
+ onAutoSave?: (value: string) => void;
28
+ /** Auto-save interval in ms */
29
+ autoSaveInterval?: number;
30
+ /** Editor size preset */
31
+ size?: EditorSize;
32
+ /** Maximum character count (shows indicator in status bar) */
33
+ maxLength?: number;
34
+ }
35
+ export interface EditorToolbarProps {
36
+ children: React.ReactNode;
37
+ className?: string;
38
+ }
39
+ export interface EditorToolbarGroupProps {
40
+ children: React.ReactNode;
41
+ 'aria-label'?: string;
42
+ className?: string;
43
+ }
44
+ export interface EditorToolbarButtonProps {
45
+ /** Which format this button toggles */
46
+ format: EditorFormat;
47
+ className?: string;
48
+ }
49
+ export interface EditorSeparatorProps {
50
+ className?: string;
51
+ }
52
+ export interface EditorStatusIndicatorProps {
53
+ /** Override the save status from context */
54
+ status?: EditorSaveStatus;
55
+ /** Custom labels per status */
56
+ labels?: Partial<Record<EditorSaveStatus, string>>;
57
+ className?: string;
58
+ }
59
+ export interface EditorContentProps {
60
+ className?: string;
61
+ }
62
+ export interface EditorStatusBarProps {
63
+ /** Show word count */
64
+ showWordCount?: boolean;
65
+ /** Show character count */
66
+ showCharCount?: boolean;
67
+ className?: string;
68
+ }
69
+ interface EditorContextValue {
70
+ value: string;
71
+ setValue: (v: string) => void;
72
+ placeholder: string;
73
+ disabled: boolean;
74
+ readOnly: boolean;
75
+ formats: EditorFormat[];
76
+ editor: any | null;
77
+ mode: EditorMode;
78
+ size: EditorSize;
79
+ maxLength?: number;
80
+ wordCount: number;
81
+ charCount: number;
82
+ toggleFormat: (f: EditorFormat) => void;
83
+ isFormatActive: (f: EditorFormat) => boolean;
84
+ saveStatus: EditorSaveStatus;
85
+ contentRef: React.RefObject<HTMLTextAreaElement | null>;
86
+ }
87
+ declare function useEditorContext(): EditorContextValue;
88
+ declare function EditorRoot({ children, value: controlledValue, defaultValue, onValueChange, placeholder, disabled, readOnly, formats, toolbar, statusBar, onAutoSave, autoSaveInterval, size, maxLength, className, ...htmlProps }: EditorProps): import("react/jsx-runtime").JSX.Element;
89
+ declare function EditorToolbar({ children, className }: EditorToolbarProps): import("react/jsx-runtime").JSX.Element;
90
+ declare function EditorToolbarGroup({ children, 'aria-label': ariaLabel, className }: EditorToolbarGroupProps): import("react/jsx-runtime").JSX.Element;
91
+ declare function EditorToolbarButton({ format, className }: EditorToolbarButtonProps): import("react/jsx-runtime").JSX.Element;
92
+ declare function EditorSeparator({ className }: EditorSeparatorProps): import("react/jsx-runtime").JSX.Element;
93
+ declare function EditorStatusIndicator({ status: statusOverride, labels, className }: EditorStatusIndicatorProps): import("react/jsx-runtime").JSX.Element | null;
94
+ declare function EditorContentArea({ className }: EditorContentProps): import("react/jsx-runtime").JSX.Element;
95
+ declare function EditorStatusBar({ showWordCount, showCharCount, className }: EditorStatusBarProps): import("react/jsx-runtime").JSX.Element;
96
+ export declare const Editor: typeof EditorRoot & {
97
+ Toolbar: typeof EditorToolbar;
98
+ ToolbarGroup: typeof EditorToolbarGroup;
99
+ ToolbarButton: typeof EditorToolbarButton;
100
+ Separator: typeof EditorSeparator;
101
+ StatusIndicator: typeof EditorStatusIndicator;
102
+ Content: typeof EditorContentArea;
103
+ StatusBar: typeof EditorStatusBar;
104
+ };
105
+ export { EditorRoot, EditorToolbar, EditorToolbarGroup, EditorToolbarButton, EditorSeparator, EditorStatusIndicator, EditorContentArea, EditorStatusBar, };
106
+ export { useEditorContext };
107
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Editor/index.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,531 @@
1
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import styles from "./Editor.module.scss.js";
4
+ import { ArrowClockwise, ArrowCounterClockwise, Quotes, TextHThree, TextHTwo, TextHOne, ListNumbers, ListBullets, Code, LinkSimple, TextStrikethrough, TextItalic, TextB } from "@phosphor-icons/react";
5
+ import { KEYBOARD_SHORTCUTS } from "../../utils/keyboard-shortcuts.js";
6
+ let _useEditor = null;
7
+ let _EditorContent = null;
8
+ let _StarterKit = null;
9
+ let _LinkExtension = null;
10
+ let _tiptapLoaded = false;
11
+ let _tiptapFailed = false;
12
+ function loadTipTapDeps() {
13
+ if (_tiptapLoaded) return;
14
+ _tiptapLoaded = true;
15
+ try {
16
+ const tiptapReact = require("@tiptap/react");
17
+ const starterKit = require("@tiptap/starter-kit");
18
+ const linkExt = require("@tiptap/extension-link");
19
+ _useEditor = tiptapReact.useEditor;
20
+ _EditorContent = tiptapReact.EditorContent;
21
+ _StarterKit = starterKit.default ?? starterKit.StarterKit ?? starterKit;
22
+ _LinkExtension = linkExt.default ?? linkExt.Link ?? linkExt;
23
+ } catch {
24
+ _tiptapFailed = true;
25
+ }
26
+ }
27
+ const FORMAT_META = {
28
+ bold: { icon: TextB, label: "Bold", shortcut: KEYBOARD_SHORTCUTS.EDITOR_BOLD.label },
29
+ italic: { icon: TextItalic, label: "Italic", shortcut: KEYBOARD_SHORTCUTS.EDITOR_ITALIC.label },
30
+ strikethrough: { icon: TextStrikethrough, label: "Strikethrough", shortcut: KEYBOARD_SHORTCUTS.EDITOR_STRIKETHROUGH.label },
31
+ link: { icon: LinkSimple, label: "Link", shortcut: KEYBOARD_SHORTCUTS.EDITOR_LINK.label },
32
+ code: { icon: Code, label: "Code", shortcut: KEYBOARD_SHORTCUTS.EDITOR_CODE.label },
33
+ bulletList: { icon: ListBullets, label: "Bullet list", shortcut: KEYBOARD_SHORTCUTS.EDITOR_BULLET_LIST.label },
34
+ orderedList: { icon: ListNumbers, label: "Ordered list", shortcut: KEYBOARD_SHORTCUTS.EDITOR_ORDERED_LIST.label },
35
+ heading1: { icon: TextHOne, label: "Heading 1", shortcut: KEYBOARD_SHORTCUTS.EDITOR_HEADING1.label },
36
+ heading2: { icon: TextHTwo, label: "Heading 2", shortcut: KEYBOARD_SHORTCUTS.EDITOR_HEADING2.label },
37
+ heading3: { icon: TextHThree, label: "Heading 3", shortcut: KEYBOARD_SHORTCUTS.EDITOR_HEADING3.label },
38
+ blockquote: { icon: Quotes, label: "Blockquote", shortcut: KEYBOARD_SHORTCUTS.EDITOR_BLOCKQUOTE.label },
39
+ undo: { icon: ArrowCounterClockwise, label: "Undo", shortcut: KEYBOARD_SHORTCUTS.EDITOR_UNDO.label },
40
+ redo: { icon: ArrowClockwise, label: "Redo", shortcut: KEYBOARD_SHORTCUTS.EDITOR_REDO.label }
41
+ };
42
+ const DEFAULT_FORMATS = ["bold", "italic", "strikethrough", "link", "code", "bulletList"];
43
+ const ACTION_FORMATS = /* @__PURE__ */ new Set(["undo", "redo"]);
44
+ const DEFAULT_STATUS_LABELS = {
45
+ idle: "",
46
+ saving: "SAVING...",
47
+ saved: "AUTO-SAVED",
48
+ error: "SAVE FAILED"
49
+ };
50
+ function getSelection(textarea) {
51
+ return {
52
+ start: textarea.selectionStart,
53
+ end: textarea.selectionEnd,
54
+ text: textarea.value.substring(textarea.selectionStart, textarea.selectionEnd)
55
+ };
56
+ }
57
+ function wrapSelection(textarea, prefix, suffix, setValue) {
58
+ const sel = getSelection(textarea);
59
+ const before = textarea.value.substring(0, sel.start);
60
+ const after = textarea.value.substring(sel.end);
61
+ const wrapped = `${prefix}${sel.text || "text"}${suffix}`;
62
+ const newValue = `${before}${wrapped}${after}`;
63
+ setValue(newValue);
64
+ requestAnimationFrame(() => {
65
+ textarea.focus();
66
+ const newStart = sel.start + prefix.length;
67
+ const newEnd = newStart + (sel.text || "text").length;
68
+ textarea.setSelectionRange(newStart, newEnd);
69
+ });
70
+ }
71
+ function applyMarkdownFormat(format, textarea, setValue) {
72
+ switch (format) {
73
+ case "bold":
74
+ wrapSelection(textarea, "**", "**", setValue);
75
+ break;
76
+ case "italic":
77
+ wrapSelection(textarea, "*", "*", setValue);
78
+ break;
79
+ case "strikethrough":
80
+ wrapSelection(textarea, "~~", "~~", setValue);
81
+ break;
82
+ case "code":
83
+ wrapSelection(textarea, "`", "`", setValue);
84
+ break;
85
+ case "link": {
86
+ const sel = getSelection(textarea);
87
+ const linkText = sel.text || "link text";
88
+ const before = textarea.value.substring(0, sel.start);
89
+ const after = textarea.value.substring(sel.end);
90
+ const newValue = `${before}[${linkText}](url)${after}`;
91
+ setValue(newValue);
92
+ requestAnimationFrame(() => {
93
+ textarea.focus();
94
+ const urlStart = sel.start + linkText.length + 3;
95
+ textarea.setSelectionRange(urlStart, urlStart + 3);
96
+ });
97
+ break;
98
+ }
99
+ case "bulletList": {
100
+ const sel = getSelection(textarea);
101
+ const before = textarea.value.substring(0, sel.start);
102
+ const after = textarea.value.substring(sel.end);
103
+ const lines = (sel.text || "item").split("\n");
104
+ const bulleted = lines.map((l) => `- ${l}`).join("\n");
105
+ const newValue = `${before}${bulleted}${after}`;
106
+ setValue(newValue);
107
+ break;
108
+ }
109
+ case "orderedList": {
110
+ const sel = getSelection(textarea);
111
+ const before = textarea.value.substring(0, sel.start);
112
+ const after = textarea.value.substring(sel.end);
113
+ const lines = (sel.text || "item").split("\n");
114
+ const numbered = lines.map((l, i) => `${i + 1}. ${l}`).join("\n");
115
+ const newValue = `${before}${numbered}${after}`;
116
+ setValue(newValue);
117
+ break;
118
+ }
119
+ case "heading1":
120
+ wrapSelection(textarea, "# ", "", setValue);
121
+ break;
122
+ case "heading2":
123
+ wrapSelection(textarea, "## ", "", setValue);
124
+ break;
125
+ case "heading3":
126
+ wrapSelection(textarea, "### ", "", setValue);
127
+ break;
128
+ case "blockquote": {
129
+ const sel = getSelection(textarea);
130
+ const before = textarea.value.substring(0, sel.start);
131
+ const after = textarea.value.substring(sel.end);
132
+ const lines = (sel.text || "quote").split("\n");
133
+ const quoted = lines.map((l) => `> ${l}`).join("\n");
134
+ const newValue = `${before}${quoted}${after}`;
135
+ setValue(newValue);
136
+ break;
137
+ }
138
+ }
139
+ }
140
+ const EditorContext = React.createContext(null);
141
+ function useEditorContext() {
142
+ const context = React.useContext(EditorContext);
143
+ if (!context) {
144
+ throw new Error("Editor compound components must be used within an Editor");
145
+ }
146
+ return context;
147
+ }
148
+ function useControllableState(controlledValue, defaultValue, onChange) {
149
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue);
150
+ const isControlled = controlledValue !== void 0;
151
+ const value = isControlled ? controlledValue : uncontrolledValue;
152
+ const setValue = React.useCallback(
153
+ (newValue) => {
154
+ if (!isControlled) {
155
+ setUncontrolledValue(newValue);
156
+ }
157
+ onChange == null ? void 0 : onChange(newValue);
158
+ },
159
+ [isControlled, onChange]
160
+ );
161
+ return [value, setValue];
162
+ }
163
+ function countWords(text) {
164
+ const trimmed = text.trim();
165
+ if (!trimmed) return 0;
166
+ return trimmed.split(/\s+/).length;
167
+ }
168
+ function EditorRoot({
169
+ children,
170
+ value: controlledValue,
171
+ defaultValue = "",
172
+ onValueChange,
173
+ placeholder = "Start typing...",
174
+ disabled = false,
175
+ readOnly = false,
176
+ formats = DEFAULT_FORMATS,
177
+ toolbar = true,
178
+ statusBar = true,
179
+ onAutoSave,
180
+ autoSaveInterval = 3e4,
181
+ size = "md",
182
+ maxLength,
183
+ className,
184
+ ...htmlProps
185
+ }) {
186
+ const contentRef = React.useRef(null);
187
+ const [value, setValue] = useControllableState(
188
+ controlledValue,
189
+ defaultValue,
190
+ onValueChange
191
+ );
192
+ const [saveStatus, setSaveStatus] = React.useState("idle");
193
+ loadTipTapDeps();
194
+ const hasTipTap = !_tiptapFailed && _useEditor && _EditorContent && _StarterKit;
195
+ const mode = hasTipTap ? "rich" : "markdown";
196
+ const tiptapEditor = hasTipTap ? (
197
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
198
+ _useEditor({
199
+ extensions: [
200
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
201
+ _StarterKit.configure({
202
+ heading: { levels: [1, 2, 3] },
203
+ blockquote: {},
204
+ codeBlock: false,
205
+ horizontalRule: false,
206
+ hardBreak: false
207
+ }),
208
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
209
+ _LinkExtension.configure({
210
+ openOnClick: false,
211
+ HTMLAttributes: { rel: "noopener noreferrer", target: "_blank" }
212
+ })
213
+ ],
214
+ editorProps: {
215
+ attributes: {
216
+ role: "textbox",
217
+ "aria-label": placeholder,
218
+ "aria-multiline": "true"
219
+ }
220
+ },
221
+ content: defaultValue || controlledValue || "",
222
+ editable: !disabled && !readOnly,
223
+ onUpdate: ({ editor: e }) => {
224
+ const html = e.getHTML();
225
+ setValue(html);
226
+ }
227
+ })
228
+ ) : null;
229
+ React.useEffect(() => {
230
+ if (tiptapEditor && controlledValue !== void 0) {
231
+ const currentContent = tiptapEditor.getHTML();
232
+ if (currentContent !== controlledValue) {
233
+ tiptapEditor.commands.setContent(controlledValue, false);
234
+ }
235
+ }
236
+ }, [controlledValue, tiptapEditor]);
237
+ React.useEffect(() => {
238
+ if (tiptapEditor) {
239
+ tiptapEditor.setEditable(!disabled && !readOnly);
240
+ }
241
+ }, [tiptapEditor, disabled, readOnly]);
242
+ React.useEffect(() => {
243
+ if (!onAutoSave || !value) return;
244
+ const timer = setTimeout(() => {
245
+ setSaveStatus("saving");
246
+ try {
247
+ onAutoSave(value);
248
+ setSaveStatus("saved");
249
+ } catch {
250
+ setSaveStatus("error");
251
+ }
252
+ }, autoSaveInterval);
253
+ return () => clearTimeout(timer);
254
+ }, [value, onAutoSave, autoSaveInterval]);
255
+ const toggleFormat = React.useCallback(
256
+ (format) => {
257
+ if (disabled || readOnly) return;
258
+ if (tiptapEditor) {
259
+ switch (format) {
260
+ case "bold":
261
+ tiptapEditor.chain().focus().toggleBold().run();
262
+ break;
263
+ case "italic":
264
+ tiptapEditor.chain().focus().toggleItalic().run();
265
+ break;
266
+ case "strikethrough":
267
+ tiptapEditor.chain().focus().toggleStrike().run();
268
+ break;
269
+ case "code":
270
+ tiptapEditor.chain().focus().toggleCode().run();
271
+ break;
272
+ case "bulletList":
273
+ tiptapEditor.chain().focus().toggleBulletList().run();
274
+ break;
275
+ case "orderedList":
276
+ tiptapEditor.chain().focus().toggleOrderedList().run();
277
+ break;
278
+ case "heading1":
279
+ tiptapEditor.chain().focus().toggleHeading({ level: 1 }).run();
280
+ break;
281
+ case "heading2":
282
+ tiptapEditor.chain().focus().toggleHeading({ level: 2 }).run();
283
+ break;
284
+ case "heading3":
285
+ tiptapEditor.chain().focus().toggleHeading({ level: 3 }).run();
286
+ break;
287
+ case "blockquote":
288
+ tiptapEditor.chain().focus().toggleBlockquote().run();
289
+ break;
290
+ case "undo":
291
+ tiptapEditor.chain().focus().undo().run();
292
+ break;
293
+ case "redo":
294
+ tiptapEditor.chain().focus().redo().run();
295
+ break;
296
+ case "link": {
297
+ const previousUrl = tiptapEditor.getAttributes("link").href;
298
+ if (previousUrl) {
299
+ tiptapEditor.chain().focus().unsetLink().run();
300
+ } else {
301
+ const url = window.prompt("Enter URL");
302
+ if (url) {
303
+ tiptapEditor.chain().focus().setLink({ href: url }).run();
304
+ }
305
+ }
306
+ break;
307
+ }
308
+ }
309
+ } else if (contentRef.current) {
310
+ applyMarkdownFormat(format, contentRef.current, setValue);
311
+ }
312
+ },
313
+ [disabled, readOnly, tiptapEditor, setValue]
314
+ );
315
+ const isFormatActive = React.useCallback(
316
+ (format) => {
317
+ if (!tiptapEditor) return false;
318
+ switch (format) {
319
+ case "bold":
320
+ return tiptapEditor.isActive("bold");
321
+ case "italic":
322
+ return tiptapEditor.isActive("italic");
323
+ case "strikethrough":
324
+ return tiptapEditor.isActive("strike");
325
+ case "code":
326
+ return tiptapEditor.isActive("code");
327
+ case "bulletList":
328
+ return tiptapEditor.isActive("bulletList");
329
+ case "orderedList":
330
+ return tiptapEditor.isActive("orderedList");
331
+ case "heading1":
332
+ return tiptapEditor.isActive("heading", { level: 1 });
333
+ case "heading2":
334
+ return tiptapEditor.isActive("heading", { level: 2 });
335
+ case "heading3":
336
+ return tiptapEditor.isActive("heading", { level: 3 });
337
+ case "blockquote":
338
+ return tiptapEditor.isActive("blockquote");
339
+ case "link":
340
+ return tiptapEditor.isActive("link");
341
+ case "undo":
342
+ case "redo":
343
+ return false;
344
+ // Actions don't have active state
345
+ default:
346
+ return false;
347
+ }
348
+ },
349
+ [tiptapEditor]
350
+ );
351
+ const wordCount = React.useMemo(() => {
352
+ var _a;
353
+ if (tiptapEditor) {
354
+ const text = ((_a = tiptapEditor.getText) == null ? void 0 : _a.call(tiptapEditor)) ?? "";
355
+ return countWords(text);
356
+ }
357
+ return countWords(value);
358
+ }, [value, tiptapEditor]);
359
+ const charCount = React.useMemo(() => {
360
+ var _a;
361
+ if (tiptapEditor) {
362
+ return (((_a = tiptapEditor.getText) == null ? void 0 : _a.call(tiptapEditor)) ?? "").length;
363
+ }
364
+ return value.length;
365
+ }, [value, tiptapEditor]);
366
+ const contextValue = {
367
+ value,
368
+ setValue,
369
+ placeholder,
370
+ disabled,
371
+ readOnly,
372
+ formats,
373
+ editor: tiptapEditor,
374
+ mode,
375
+ size,
376
+ maxLength,
377
+ wordCount,
378
+ charCount,
379
+ toggleFormat,
380
+ isFormatActive,
381
+ saveStatus,
382
+ contentRef
383
+ };
384
+ const classes = [
385
+ styles.editor,
386
+ disabled && styles.disabled,
387
+ readOnly && styles.readOnly,
388
+ className
389
+ ].filter(Boolean).join(" ");
390
+ const hasCustomChildren = children !== void 0;
391
+ return /* @__PURE__ */ jsx(EditorContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
392
+ "div",
393
+ {
394
+ ...htmlProps,
395
+ className: classes,
396
+ "data-disabled": disabled || void 0,
397
+ "data-readonly": readOnly || void 0,
398
+ "data-size": size,
399
+ children: hasCustomChildren ? children : /* @__PURE__ */ jsxs(Fragment, { children: [
400
+ toolbar && /* @__PURE__ */ jsx(EditorToolbar, { children: /* @__PURE__ */ jsx(EditorToolbarGroup, { "aria-label": "Text formatting", children: formats.map((f) => /* @__PURE__ */ jsx(EditorToolbarButton, { format: f }, f)) }) }),
401
+ /* @__PURE__ */ jsx(EditorContentArea, {}),
402
+ statusBar && /* @__PURE__ */ jsx(EditorStatusBar, { showWordCount: true, showCharCount: true })
403
+ ] })
404
+ }
405
+ ) });
406
+ }
407
+ function EditorToolbar({ children, className }) {
408
+ const classes = [styles.toolbar, className].filter(Boolean).join(" ");
409
+ return /* @__PURE__ */ jsx("div", { className: classes, role: "toolbar", "aria-label": "Editor formatting", children });
410
+ }
411
+ function EditorToolbarGroup({ children, "aria-label": ariaLabel, className }) {
412
+ const classes = [styles.toolbarGroup, className].filter(Boolean).join(" ");
413
+ return /* @__PURE__ */ jsx("div", { className: classes, role: "group", "aria-label": ariaLabel, children });
414
+ }
415
+ function EditorToolbarButton({ format, className }) {
416
+ const { toggleFormat, isFormatActive, disabled, readOnly, editor, mode } = useEditorContext();
417
+ const meta = FORMAT_META[format];
418
+ const isAction = ACTION_FORMATS.has(format);
419
+ const active = isAction ? false : isFormatActive(format);
420
+ const IconComponent = meta.icon;
421
+ let isDisabled = disabled || readOnly;
422
+ if (isAction && !isDisabled) {
423
+ if (mode === "markdown") {
424
+ isDisabled = true;
425
+ } else if (editor) {
426
+ isDisabled = format === "undo" ? !editor.can().undo() : !editor.can().redo();
427
+ }
428
+ }
429
+ const classes = [
430
+ styles.toolbarButton,
431
+ active && styles.toolbarButtonActive,
432
+ className
433
+ ].filter(Boolean).join(" ");
434
+ return /* @__PURE__ */ jsx(
435
+ "button",
436
+ {
437
+ type: "button",
438
+ className: classes,
439
+ onClick: () => toggleFormat(format),
440
+ disabled: isDisabled,
441
+ "aria-label": meta.label,
442
+ title: `${meta.label} (${meta.shortcut})`,
443
+ ...isAction ? {} : { "aria-pressed": active },
444
+ children: /* @__PURE__ */ jsx(IconComponent, { size: 16, weight: active ? "bold" : "regular" })
445
+ }
446
+ );
447
+ }
448
+ function EditorSeparator({ className }) {
449
+ const classes = [styles.separator, className].filter(Boolean).join(" ");
450
+ return /* @__PURE__ */ jsx("div", { className: classes, role: "separator", "aria-orientation": "vertical" });
451
+ }
452
+ function EditorStatusIndicator({ status: statusOverride, labels, className }) {
453
+ const { saveStatus } = useEditorContext();
454
+ const status = statusOverride ?? saveStatus;
455
+ const mergedLabels = { ...DEFAULT_STATUS_LABELS, ...labels };
456
+ const label = mergedLabels[status];
457
+ if (!label) return null;
458
+ const classes = [
459
+ styles.statusIndicator,
460
+ status === "error" && styles.statusError,
461
+ className
462
+ ].filter(Boolean).join(" ");
463
+ return /* @__PURE__ */ jsx("span", { className: classes, "aria-live": "polite", role: "status", children: label });
464
+ }
465
+ function EditorContentArea({ className }) {
466
+ const { value, setValue, placeholder, disabled, readOnly, editor, mode, contentRef } = useEditorContext();
467
+ if (mode === "rich" && editor && _EditorContent) {
468
+ const TipTapContent = _EditorContent;
469
+ const classes2 = [styles.content, styles.contentRich, className].filter(Boolean).join(" ");
470
+ return /* @__PURE__ */ jsx("div", { className: classes2, "data-placeholder": placeholder, children: /* @__PURE__ */ jsx(TipTapContent, { editor }) });
471
+ }
472
+ const classes = [styles.content, className].filter(Boolean).join(" ");
473
+ return /* @__PURE__ */ jsx("div", { className: classes, children: /* @__PURE__ */ jsx(
474
+ "textarea",
475
+ {
476
+ ref: contentRef,
477
+ className: styles.contentTextarea,
478
+ value,
479
+ onChange: (e) => setValue(e.target.value),
480
+ placeholder,
481
+ disabled,
482
+ readOnly,
483
+ "aria-label": placeholder
484
+ }
485
+ ) });
486
+ }
487
+ function EditorStatusBar({ showWordCount = true, showCharCount = true, className }) {
488
+ const { wordCount, charCount, maxLength } = useEditorContext();
489
+ const classes = [styles.statusBar, className].filter(Boolean).join(" ");
490
+ const isOverLimit = maxLength !== void 0 && charCount > maxLength;
491
+ const isNearLimit = maxLength !== void 0 && !isOverLimit && charCount >= maxLength * 0.9;
492
+ const charLimitClasses = [
493
+ styles.statusBarItem,
494
+ isNearLimit && styles.statusBarItemWarning,
495
+ isOverLimit && styles.statusBarItemError
496
+ ].filter(Boolean).join(" ");
497
+ return /* @__PURE__ */ jsxs("div", { className: classes, "aria-label": "Editor statistics", children: [
498
+ /* @__PURE__ */ jsx("div", { className: styles.statusBarLeft }),
499
+ /* @__PURE__ */ jsxs("div", { className: styles.statusBarRight, children: [
500
+ showWordCount && /* @__PURE__ */ jsxs("span", { className: styles.statusBarItem, children: [
501
+ wordCount,
502
+ " ",
503
+ wordCount === 1 ? "Word" : "Words"
504
+ ] }),
505
+ showWordCount && showCharCount && /* @__PURE__ */ jsx(EditorSeparator, {}),
506
+ showCharCount && /* @__PURE__ */ jsx("span", { className: charLimitClasses, children: maxLength !== void 0 ? `${charCount} / ${maxLength}` : `${charCount} ${charCount === 1 ? "Character" : "Characters"}` })
507
+ ] })
508
+ ] });
509
+ }
510
+ const Editor = Object.assign(EditorRoot, {
511
+ Toolbar: EditorToolbar,
512
+ ToolbarGroup: EditorToolbarGroup,
513
+ ToolbarButton: EditorToolbarButton,
514
+ Separator: EditorSeparator,
515
+ StatusIndicator: EditorStatusIndicator,
516
+ Content: EditorContentArea,
517
+ StatusBar: EditorStatusBar
518
+ });
519
+ export {
520
+ Editor,
521
+ EditorContentArea,
522
+ EditorRoot,
523
+ EditorSeparator,
524
+ EditorStatusBar,
525
+ EditorStatusIndicator,
526
+ EditorToolbar,
527
+ EditorToolbarButton,
528
+ EditorToolbarGroup,
529
+ useEditorContext
530
+ };
531
+ //# sourceMappingURL=index.js.map