inkflow-editor 1.3.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 (49) hide show
  1. package/ReadME.md +230 -0
  2. package/dist/EmojiList-B-C3-zN2.js +25 -0
  3. package/dist/core/Editor.d.ts +227 -0
  4. package/dist/core/HistoryManager.d.ts +36 -0
  5. package/dist/core/ImageManager.d.ts +28 -0
  6. package/dist/core/SelectionManager.d.ts +52 -0
  7. package/dist/core/plugins/ImageUploader.d.ts +15 -0
  8. package/dist/index.d.ts +12 -0
  9. package/dist/inkflow-editor.css +1 -0
  10. package/dist/inkflow-editor.mjs +2010 -0
  11. package/dist/style.d.ts +4 -0
  12. package/dist/ui/Toolbar.d.ts +26 -0
  13. package/dist/ui/toolbar/EmojiList.d.ts +6 -0
  14. package/dist/ui/toolbar/EmojiPicker.d.ts +21 -0
  15. package/dist/ui/toolbar/FloatingToolbar.d.ts +16 -0
  16. package/dist/ui/toolbar/InputModal.d.ts +24 -0
  17. package/dist/ui/toolbar/ToolbarItem.d.ts +16 -0
  18. package/dist/ui/toolbar/items/AlignCenter.d.ts +2 -0
  19. package/dist/ui/toolbar/items/AlignJustify.d.ts +2 -0
  20. package/dist/ui/toolbar/items/AlignLeft.d.ts +2 -0
  21. package/dist/ui/toolbar/items/AlignRight.d.ts +2 -0
  22. package/dist/ui/toolbar/items/Bold.d.ts +2 -0
  23. package/dist/ui/toolbar/items/BulletList.d.ts +2 -0
  24. package/dist/ui/toolbar/items/ClearFormatting.d.ts +2 -0
  25. package/dist/ui/toolbar/items/CodeBlock.d.ts +2 -0
  26. package/dist/ui/toolbar/items/Emoji.d.ts +2 -0
  27. package/dist/ui/toolbar/items/FontFamily.d.ts +2 -0
  28. package/dist/ui/toolbar/items/FontSize.d.ts +2 -0
  29. package/dist/ui/toolbar/items/Heading.d.ts +2 -0
  30. package/dist/ui/toolbar/items/HighlightColor.d.ts +2 -0
  31. package/dist/ui/toolbar/items/HorizontalRule.d.ts +2 -0
  32. package/dist/ui/toolbar/items/Image.d.ts +2 -0
  33. package/dist/ui/toolbar/items/Indent.d.ts +2 -0
  34. package/dist/ui/toolbar/items/Italic.d.ts +2 -0
  35. package/dist/ui/toolbar/items/LineHeight.d.ts +2 -0
  36. package/dist/ui/toolbar/items/Link.d.ts +2 -0
  37. package/dist/ui/toolbar/items/MagicFormat.d.ts +2 -0
  38. package/dist/ui/toolbar/items/OrderedList.d.ts +2 -0
  39. package/dist/ui/toolbar/items/Outdent.d.ts +2 -0
  40. package/dist/ui/toolbar/items/Redo.d.ts +2 -0
  41. package/dist/ui/toolbar/items/ResetMagicFormat.d.ts +2 -0
  42. package/dist/ui/toolbar/items/Strikethrough.d.ts +2 -0
  43. package/dist/ui/toolbar/items/Table.d.ts +2 -0
  44. package/dist/ui/toolbar/items/TableActions.d.ts +5 -0
  45. package/dist/ui/toolbar/items/TextColor.d.ts +2 -0
  46. package/dist/ui/toolbar/items/Underline.d.ts +2 -0
  47. package/dist/ui/toolbar/items/Undo.d.ts +2 -0
  48. package/dist/ui/toolbar/registry.d.ts +2 -0
  49. package/package.json +71 -0
package/ReadME.md ADDED
@@ -0,0 +1,230 @@
1
+ # inkflow-editor ๐Ÿš€ | Premium WYSIWYG Editor
2
+
3
+ [![NPM Downloads](https://img.shields.io/npm/dw/inkflow-editor.svg)](https://www.npmjs.com/package/inkflow-editor)
4
+ [![NPM Version](https://img.shields.io/npm/v/inkflow-editor.svg)](https://www.npmjs.com/package/inkflow-editor)
5
+ [![Beta Status](https://img.shields.io/badge/status-beta-orange.svg)](https://www.npmjs.com/package/inkflow-editor)
6
+
7
+ A premium, ultra-lightweight, and framework-agnostic **WYSIWYG rich text editor** built entirely with Vanilla TypeScript. Featuring a sophisticated **Slate & Indigo** design system, it provides a flawless writing experience for React, Next.js, and modern web applications.
8
+
9
+ > [!IMPORTANT]
10
+ > **Beta Version**: Inkflow is currently in its beta phase. We are actively refining the API and performance. Community feedback is welcome! ๐Ÿš€
11
+
12
+ ### ๐Ÿ’ก What is WYSIWYG?
13
+ **WYSIWYG** stands for **"What You See Is What You Get"**.
14
+ Unlike markdown or code editors, what you see while typing in Inkflowโ€”the bold text, centered headings, and interactive tablesโ€”is exactly how it will appear when published. It bridges the gap between editing and the final result, making rich-text creation accessible and predictable.
15
+
16
+ ## ๐Ÿš€ Recent Performance & Security Breakthrough (v1.1.2)
17
+ We recently completed an aggressive optimization and security hardening pass:
18
+ - **79% Size Reduction:** Packed weight dropped from **132kB to 28kB**.
19
+ - **9.8/10 Security Score:** Internal audit confirmed world-class XSS protection.
20
+ - **Pure ESM Architecture:** Zero legacy CommonJS bloat for modern bundlers.
21
+
22
+ ---
23
+
24
+ ## ๐ŸŽฎ Live React Preview
25
+ Wanna see it in action? Try the **Interactive React Demo** on StackBlitz:
26
+ [**Run Demo on StackBlitz**](https://stackblitz.com/edit/vitejs-vite-e8u5yntq?embed=1&view=preview)
27
+
28
+ ## โœจ Premium Features & Why Choose This Editor?
29
+
30
+ ### ๐Ÿ‘ Key Pros & Capabilities
31
+ - **Microscopic Footprint**: Only **~28kB** packed weight. Total initial load is incredibly light.
32
+ - **Secure By Design**: Rated **9.8/10** in security audits with forced XSS sanitization.
33
+ - **Pure ESM Build**: Optimized for modern bundlers (Vite, Webpack 5, etc.) with zero CJS bloat.
34
+ - **Performance Optimized**: Heavy components like the Emoji Picker are **dynamic-imported** only when clicked.
35
+ - **Framework Agnostic**: Native support for **React**, **Next.js**, **Vue**, **Angular**, and **Svelte**.
36
+ - **Auto-Formatting Magic**: Intelligently parses pasted HTML strings into clean, formatted rich text.
37
+ - **Professional UI/UX**: Modern aesthetics curated with a polished Slate & Indigo color palette.
38
+ - **Table Support**: Natively insert and style interactive HTML tables.
39
+ - **Emoji Picker**: Integrated searchable emoji library for expressive content.
40
+ - **Dark Mode**: Sophisticated dark theme for premium developer experiences.
41
+ - **Customizable Toolbar**: Granular control over tool visibility and layout.
42
+ - **Smart Image Management**: Built-in client-side compression (WebP), loading states, custom upload adapters, live resizing, and native captions.
43
+
44
+ ---
45
+
46
+ ## ๐ŸŒ Documentation Website (Coming Soon!)
47
+ We are currently building a dedicated official website to provide the best possible developer experience.
48
+
49
+ **What to expect:**
50
+ - **Interactive Playground**: Test all features live in your browser.
51
+ - **Deep-Dive Guides**: Detailed integration steps for React, Next.js, Vue, and more.
52
+ - **Full API Reference**: Comprehensive documentation for every method and option.
53
+ - **Custom Theme Builder**: Visually design your editor's look and feel.
54
+
55
+ ๐Ÿš€ **Stay tuned for the official launch!**
56
+
57
+ ---
58
+
59
+ ## ๐Ÿ›ก Security & XSS Protection
60
+ Inkflow takes security seriously. It features a hard-coded strict whitelist in `DOMPurify` to ensure:
61
+ - **Malicious Scripts:** Automatically stripped from pastes and API inputs.
62
+ - **URI Blocking:** Blocks `javascript:`, `data:`, and `vbscript:` schemes.
63
+ - **Link Hardening:** Every link is forced to have `rel="noopener noreferrer"`.
64
+ - **Normalization:** Every structural cleanup is followed by a final sanitization pass.
65
+
66
+ ---
67
+
68
+ > [!TIP]
69
+ > The editor is optimized for performance. Features like the **Emoji Picker** are only loaded when needed, keeping your initial page load lightning fast.
70
+
71
+ ### ๐Ÿ‘Ž Cons (Current Limitations)
72
+ - Markdown shortcut typing (e.g., typing `#` for H1) is not natively supported yet.
73
+
74
+ ---
75
+
76
+ ## ๐Ÿ“š Technical Guides
77
+ For deep-dive documentation, check out our local guides:
78
+ - [**Usage Guide**](./USAGE_GUIDE.md): Configuration, API methods, and feature customization.
79
+ - [**Technical Integration Guide**](./INTEGRATION_GUIDE.md): Step-by-step setup and advanced patterns.
80
+ - [**Security Report**](./SECURITY_REPORT.md): Full breakdown of our XSS protection and hardening.
81
+
82
+ ---
83
+
84
+ ## ๐Ÿ“ฆ Installation
85
+
86
+ ```bash
87
+ npm install inkflow-editor
88
+ ```
89
+
90
+ ## ๐Ÿš€ Quick Start
91
+
92
+ ### Basic Usage (Vanilla JS)
93
+
94
+ ```javascript
95
+ import { InkflowEditor } from 'inkflow-editor';
96
+ import 'inkflow-editor/style'; // Simple style import
97
+
98
+ const container = document.getElementById('editor');
99
+ const editor = new InkflowEditor(container, {
100
+ placeholder: 'Type something beautiful...',
101
+ autofocus: true,
102
+ showStatus: true,
103
+ toolbarItems: ['bold', 'italic', 'heading', 'table', 'link'] // Customize tools
104
+ });
105
+ ```
106
+
107
+ ### In React (Preventing Duplicates)
108
+ In React **Strict Mode**, components mount twice in development. Always use the cleanup function to destroy the editor instance.
109
+
110
+ ```tsx
111
+ import { useEffect, useRef } from 'react';
112
+ import { InkflowEditor } from 'inkflow-editor';
113
+ import 'inkflow-editor/style';
114
+
115
+ export default function App() {
116
+ const containerRef = useRef<HTMLDivElement>(null);
117
+ const editorRef = useRef<InkflowEditor | null>(null);
118
+
119
+ useEffect(() => {
120
+ if (containerRef.current && !editorRef.current) {
121
+ editorRef.current = new InkflowEditor(containerRef.current, {
122
+ placeholder: 'Start writing...',
123
+ });
124
+ }
125
+
126
+ return () => {
127
+ if (editorRef.current) {
128
+ editorRef.current.destroy();
129
+ editorRef.current = null;
130
+ }
131
+ };
132
+ }, []);
133
+
134
+ return <div ref={containerRef} />;
135
+ }
136
+ ```
137
+
138
+ ### In Next.js (Safe Implementation)
139
+ For Next.js, ensure the editor is only initialized on the client side using `useEffect`.
140
+
141
+ ```tsx
142
+ "use client";
143
+ import { useEffect, useRef } from 'react';
144
+ import { InkflowEditor } from 'inkflow-editor';
145
+ import 'inkflow-editor/style';
146
+
147
+ export default function MyEditor() {
148
+ const containerRef = useRef<HTMLDivElement>(null);
149
+
150
+ useEffect(() => {
151
+ if (!containerRef.current) return;
152
+
153
+ const editor = new InkflowEditor(containerRef.current, {
154
+ onSave: (html) => console.log(html)
155
+ });
156
+
157
+ return () => editor.destroy(); // Crucial for HMR and Strict Mode
158
+ }, []);
159
+
160
+ return <div ref={containerRef} className="editor-shell" />;
161
+ }
162
+ ```
163
+
164
+ ---
165
+
166
+ ## โš™๏ธ Configuration Options
167
+
168
+ | Option | Type | Default | Description |
169
+ | :--- | :--- | :--- | :--- |
170
+ | `placeholder` | `string` | `undefined` | The placeholder text when the editor is empty. |
171
+ | `autofocus` | `boolean` | `false` | Focus the editor automatically on initialization. |
172
+ | `dark` | `boolean` | `false` | Enable sophisticated Dark Mode theme. |
173
+ | `showStatus` | `boolean` | `true` | Show/hide the "Saved at..." status in the toolbar. |
174
+ | `toolbarItems` | `string[]` | `all` | Array of tool IDs to display (e.g., `['bold', 'table']`). |
175
+ | `onSave` | `function` | `undefined` | Callback triggered when content is saved. |
176
+ | `autoSaveInterval` | `number` | `1000` | Delay in ms before auto-save triggers after typing. |
177
+ | `imageEndpoints` | `object` | `undefined` | Custom upload endpoint configuration: `{ upload: string }`. |
178
+ | `cloudinaryFallback` | `object` | `undefined` | Cloudinary settings: `{ cloudName: string, uploadPreset: string }`. |
179
+ | `maxImageSizeMB` | `number` | `5` | Maximum image size in MB (enforced pre and post compression). |
180
+
181
+ ## ๐Ÿ›  API Methods
182
+
183
+ - `destroy()`: **Crucial** - Cleans up DOM, event listeners, and memory leaks.
184
+ - `getHTML()`: Returns the content as a sanitized HTML string.
185
+ - `setHTML(html)`: Programmatically sets the editor content.
186
+ - `focus()`: Forces focus onto the editor.
187
+ - `setDarkMode(boolean)`: Dynamically toggle dark mode.
188
+ - `insertTable(rows, cols)`: Programmatically insert a table.
189
+ - `insertImage(url, id, isLoading)`: Programmatically insert an image with optional loading state.
190
+
191
+ ## ๐Ÿ’ก Troubleshooting: Duplicate Editors?
192
+ If you see multiple toolbars or editors, it's likely because:
193
+ 1. **React Strict Mode**: Ensure you call `editor.destroy()` in the `useEffect` cleanup.
194
+ 2. **Missing Cleanup**: The editor injects elements into the DOM; if you don't destroy it when the component unmounts, those elements remain.
195
+
196
+ ---
197
+
198
+ ## ๐Ÿ“ Patch Notes
199
+
200
+ ### v1.3.0 (The Inkflow Rebrand)
201
+ - **New Name & Package**: Rebranded as **Inkflow** (`inkflow-editor`).
202
+ - **Toolbar Positioning**: Support for `Top`, `Bottom`, `Left`, `Right`, and `Floating` positions.
203
+ - **Premium Table UI**: Redesigned tables with rounded corners, zebra stripes, and better interactive states.
204
+ - **Magic Format 2.0**: Added Typography and Accents themes with a dedicated Reset button.
205
+ - **Refined Sider UI**: Centered vertical toolbars (76px) with pill-shaped controls and premium scrollbars.
206
+ - **Paste Sanitization**: Hardened character limits to prevent bypass via pasting.
207
+
208
+ ### v1.2.0 (Premium UI & Image Power-Up)
209
+ - **Lucide Icon Upgrade**: Replaced all 27 toolbar icons with high-quality, professional Lucide-styled SVGs.
210
+ - **Initialization Loader**: Added a sophisticated shimmering glassmorphism loader for a smoother startup experience.
211
+ - **Advanced Image Pipeline**: Added drag-and-drop support with automatic client-side **WebP compression**.
212
+ - **Interactive UX**: Added loading state previews, 4-corner resizing handles, and native `<figcaption>` support.
213
+ - **Glassmorphism Design**: Enhanced modals and loaders with modern backdrop-blur effects.
214
+
215
+ ### v1.1.2 (Security & Performance)
216
+ - **Aggressive Size Optimization**: Reduced packed size to **28kB** by moving to ESM-only and pruning datasets.
217
+ - **Hardened Sanitization**: Centralized all HTML processing through a unified security layer (Rating 9.8/10).
218
+ - **Safe UI Rendering**: Eliminated `innerHTML` usage in all UI components for zero-trust text rendering.
219
+ - **CJS Build Deprecation**: Removed CommonJS versions to optimize for modern ESM-based environments.
220
+
221
+ ### v1.1.1 (Quick Fixes)
222
+ - Fixed missing `destroy()` export.
223
+ - Resolved memory leaks in image resizer.
224
+ - Prevented duplicate editors in React Strict Mode.
225
+
226
+ ---
227
+
228
+ ## ๐Ÿ“„ License
229
+
230
+ MIT ยฉ [Anuj Nainwal](https://github.com/anujnainwal)
@@ -0,0 +1,25 @@
1
+ const e = [
2
+ { emoji: "๐Ÿ˜€", name: "grinning", category: "Smileys" },
3
+ { emoji: "๐Ÿ˜ƒ", name: "smiley", category: "Smileys" },
4
+ { emoji: "๐Ÿ˜„", name: "smile", category: "Smileys" },
5
+ { emoji: "๐Ÿ˜", name: "grin", category: "Smileys" },
6
+ { emoji: "๐Ÿ˜†", name: "laughing", category: "Smileys" },
7
+ { emoji: "๐Ÿ˜…", name: "sweat smile", category: "Smileys" },
8
+ { emoji: "๐Ÿ˜‚", name: "joy", category: "Smileys" },
9
+ { emoji: "๐Ÿ™‚", name: "slight smile", category: "Smileys" },
10
+ { emoji: "๐Ÿ˜‰", name: "wink", category: "Smileys" },
11
+ { emoji: "๐Ÿ˜Š", name: "blush", category: "Smileys" },
12
+ { emoji: "๐Ÿ˜", name: "heart eyes", category: "Smileys" },
13
+ { emoji: "๐Ÿ˜˜", name: "kissing heart", category: "Smileys" },
14
+ { emoji: "๐Ÿ‘", name: "thumbs up", category: "Hands" },
15
+ { emoji: "๐Ÿ‘Ž", name: "thumbs down", category: "Hands" },
16
+ { emoji: "โค๏ธ", name: "heart", category: "Symbols" },
17
+ { emoji: "โœจ", name: "sparkles", category: "Symbols" },
18
+ { emoji: "๐Ÿ”ฅ", name: "fire", category: "Symbols" },
19
+ { emoji: "โœ…", name: "check", category: "Symbols" },
20
+ { emoji: "๐ŸŽ‰", name: "party", category: "Activities" },
21
+ { emoji: "๐Ÿš€", name: "rocket", category: "Travel" }
22
+ ];
23
+ export {
24
+ e as EMOJI_LIST
25
+ };
@@ -0,0 +1,227 @@
1
+ import { SelectionManager } from './SelectionManager';
2
+ import { ImageManager } from './ImageManager';
3
+ export type ToolbarPosition = 'top' | 'bottom' | 'left' | 'right' | 'floating';
4
+ export interface ThemeConfig {
5
+ primaryColor?: string;
6
+ primaryHover?: string;
7
+ bgApp?: string;
8
+ bgEditor?: string;
9
+ toolbarBg?: string;
10
+ borderColor?: string;
11
+ borderFocus?: string;
12
+ textMain?: string;
13
+ textMuted?: string;
14
+ placeholder?: string;
15
+ btnHover?: string;
16
+ btnActive?: string;
17
+ radiusLg?: string;
18
+ radiusMd?: string;
19
+ radiusSm?: string;
20
+ shadowSm?: string;
21
+ shadowMd?: string;
22
+ shadowLg?: string;
23
+ }
24
+ export interface EditorOptions {
25
+ placeholder?: string;
26
+ autofocus?: boolean;
27
+ theme?: ThemeConfig;
28
+ dark?: boolean;
29
+ onSave?: (html: string) => void;
30
+ onSaving?: () => void;
31
+ onChange?: (html: string) => void;
32
+ autoSaveInterval?: number;
33
+ autoSave?: boolean;
34
+ showStatus?: boolean;
35
+ showLoader?: boolean;
36
+ toolbarItems?: string[];
37
+ imageEndpoints?: {
38
+ upload: string;
39
+ delete: string;
40
+ };
41
+ cloudinaryFallback?: {
42
+ cloudName: string;
43
+ uploadPreset: string;
44
+ };
45
+ maxImageSizeMB?: number;
46
+ onImageDelete?: (imageId?: string, imageUrl?: string) => void;
47
+ maxCharCount?: number;
48
+ showCharCount?: boolean;
49
+ strictCharLimit?: boolean;
50
+ toolbarPosition?: ToolbarPosition;
51
+ }
52
+ export declare class CoreEditor {
53
+ protected container: HTMLElement;
54
+ protected editableElement: HTMLElement;
55
+ selection: SelectionManager;
56
+ protected imageManager: ImageManager;
57
+ private history;
58
+ protected options: EditorOptions;
59
+ private saveTimeout;
60
+ private historyTimeout;
61
+ private pendingStyles;
62
+ private observer;
63
+ private floatingToolbar;
64
+ private magicStateMap;
65
+ private eventListeners;
66
+ private loaderElement;
67
+ private isUndoingRedoing;
68
+ constructor(container: HTMLElement, options?: EditorOptions);
69
+ /**
70
+ * Applies custom theme variables to the editor container.
71
+ */
72
+ private applyTheme;
73
+ /**
74
+ * Toggles dark mode on the editor.
75
+ */
76
+ setDarkMode(enabled: boolean): void;
77
+ /**
78
+ * Destroys the editor instance and cleans up.
79
+ */
80
+ destroy(): void;
81
+ protected checkPlaceholder(): void;
82
+ private addEventListener;
83
+ protected setupImageObserver(): void;
84
+ /**
85
+ * Wraps a raw <img> element in the interactive container
86
+ */
87
+ private wrapImage;
88
+ protected setupInputHandlers(): void;
89
+ /**
90
+ * Sets up strict character limit enforcement.
91
+ */
92
+ private setupLimitEnforcement;
93
+ /**
94
+ * Immediately records a history state if one is pending.
95
+ */
96
+ private flushHistoryRecord;
97
+ private handleInput;
98
+ private scheduleHistoryRecord;
99
+ private scheduleAutoSave;
100
+ save(): void;
101
+ undo(): void;
102
+ redo(): void;
103
+ protected triggerChange(): void;
104
+ private createLoader;
105
+ private hideLoader;
106
+ protected createEditableElement(): HTMLElement;
107
+ /**
108
+ * Focuses the editor.
109
+ */
110
+ focus(): void;
111
+ /**
112
+ * Executes a command on the current selection.
113
+ */
114
+ execute(command: string, value?: string | null): void;
115
+ /**
116
+ * Special handler for links to open them in a new tab when clicked.
117
+ */
118
+ private setupLinkClickHandlers;
119
+ /**
120
+ * Magic Format logic: Cycles through aesthetic presets for the entire document.
121
+ */
122
+ magicFormat(): void;
123
+ /**
124
+ * Resets all magic formatting (inline styles) from the document.
125
+ */
126
+ resetMagicFormat(): void;
127
+ private formatMagicTable;
128
+ private formatMagicImage;
129
+ private formatMagicHeading;
130
+ private formatMagicText;
131
+ /**
132
+ * Enriches text nodes within a block with emojis without breaking HTML structure.
133
+ */
134
+ private enrichBlockWithEmojis;
135
+ /**
136
+ * Inserts a table at the current selection.
137
+ */
138
+ insertTable(rows?: number, cols?: number): void;
139
+ /**
140
+ * Adds a row to the currently selected table.
141
+ */
142
+ addRow(): void;
143
+ /**
144
+ * Deletes the currently selected row.
145
+ */
146
+ deleteRow(): void;
147
+ /**
148
+ * Adds a column to the currently selected table.
149
+ */
150
+ addColumn(): void;
151
+ /**
152
+ * Deletes the currently selected column.
153
+ */
154
+ deleteColumn(): void;
155
+ private getSelectedTd;
156
+ private getSelectedTable;
157
+ /**
158
+ * Recursively removes a style property from all elements in a fragment.
159
+ */
160
+ private clearStyleRecursive;
161
+ /**
162
+ * Applies an inline style to the selection.
163
+ * This is used for properties like font-size (px) and font-family
164
+ * where execCommand is outdated or limited.
165
+ */
166
+ /**
167
+ * Applies an inline style to the selection.
168
+ * This is used for properties like font-size (px) and font-family
169
+ * where execCommand is outdated or limited.
170
+ */
171
+ setStyle(property: string, value: string, range?: Range): Range | null;
172
+ /**
173
+ * Applies a style to the block-level containers within the range.
174
+ */
175
+ private setBlockStyle;
176
+ /**
177
+ * Creates a link at the current selection.
178
+ * Ensures the link opens in a new tab with proper security attributes.
179
+ */
180
+ createLink(url: string): void;
181
+ /**
182
+ * Inserts an image at the current selection.
183
+ */
184
+ insertImage(url: string, id?: string, isLoading?: boolean): HTMLElement | null;
185
+ /**
186
+ * Returns the clean and optimized HTML content of the editor.
187
+ */
188
+ getHTML(): string;
189
+ /**
190
+ * Returns the plain text content of the editor.
191
+ */
192
+ getText(): string;
193
+ /**
194
+ * Returns the current character count based on plain text.
195
+ */
196
+ getCharCount(): number;
197
+ /**
198
+ * Normalizes the editor's content in-place.
199
+ */
200
+ normalize(): void;
201
+ private normalizationContainer;
202
+ /**
203
+ * Internal helper to strictly sanitize HTML strings.
204
+ */
205
+ private sanitize;
206
+ /**
207
+ * Optimizes HTML by fixing invalid nesting and removing redundant tags.
208
+ */
209
+ private normalizeHTML;
210
+ private handlePaste;
211
+ /**
212
+ * Sets the HTML content of the editor.
213
+ */
214
+ setHTML(html: string): void;
215
+ /**
216
+ * Internal access to the editable element.
217
+ */
218
+ get el(): HTMLElement;
219
+ /**
220
+ * Returns the editor options.
221
+ */
222
+ getOptions(): EditorOptions;
223
+ /**
224
+ * Internal helper to handle multiple files.
225
+ */
226
+ handleFiles(files: File[]): Promise<void>;
227
+ }
@@ -0,0 +1,36 @@
1
+ export interface HistoryState {
2
+ html: string;
3
+ selection: {
4
+ startPath: number[];
5
+ startOffset: number;
6
+ endPath: number[];
7
+ endOffset: number;
8
+ } | null;
9
+ }
10
+ export declare class HistoryManager {
11
+ private stack;
12
+ private index;
13
+ private maxDepth;
14
+ constructor(initialState?: string);
15
+ /**
16
+ * Records a new state in the history stack.
17
+ * Clears any "redo" states if we record a new action.
18
+ */
19
+ record(html: string, selection: HistoryState['selection'] | null): void;
20
+ /**
21
+ * Returns the previous state if available.
22
+ */
23
+ undo(): HistoryState | null;
24
+ /**
25
+ * Returns the next state if available.
26
+ */
27
+ redo(): HistoryState | null;
28
+ /**
29
+ * Checks if undo is possible.
30
+ */
31
+ canUndo(): boolean;
32
+ /**
33
+ * Checks if redo is possible.
34
+ */
35
+ canRedo(): boolean;
36
+ }
@@ -0,0 +1,28 @@
1
+ import { CoreEditor } from './Editor';
2
+ export declare class ImageManager {
3
+ private editor;
4
+ private activeContainer;
5
+ private isResizing;
6
+ private startX;
7
+ private startY;
8
+ private startWidth;
9
+ private startHeight;
10
+ private currentHandle;
11
+ private aspectRatio;
12
+ private boundMouseDown;
13
+ private boundMouseMove;
14
+ private boundMouseUp;
15
+ private boundKeyDown;
16
+ constructor(editor: CoreEditor);
17
+ private setupListeners;
18
+ private handleMouseDown;
19
+ private handleMouseMove;
20
+ private handleMouseUp;
21
+ private handleKeyDown;
22
+ destroy(): void;
23
+ private selectImage;
24
+ private deselectImage;
25
+ private startResize;
26
+ private handleResize;
27
+ private stopResize;
28
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * SelectionManager handles wrapping the native browser Selection and Range APIs.
3
+ * This ensures consistent behavior across different environments and simplifies
4
+ * interaction with the editor's cursor and selected text.
5
+ */
6
+ export declare class SelectionManager {
7
+ /**
8
+ * Returns the current selection object.
9
+ */
10
+ getSelection(): Selection | null;
11
+ /**
12
+ * Returns the first range of the current selection.
13
+ */
14
+ getRange(): Range | null;
15
+ /**
16
+ * Serializes the current selection into a path-based format relative to a root element.
17
+ * This allows restoring selection even if the DOM nodes are replaced but the structure is similar.
18
+ */
19
+ getSelectionPath(root: HTMLElement): {
20
+ startPath: number[];
21
+ startOffset: number;
22
+ endPath: number[];
23
+ endOffset: number;
24
+ } | null;
25
+ /**
26
+ * Restores selection from a path-based serialization.
27
+ */
28
+ restoreSelectionPath(root: HTMLElement, path: {
29
+ startPath: number[];
30
+ startOffset: number;
31
+ endPath: number[];
32
+ endOffset: number;
33
+ } | null): void;
34
+ private getNodePath;
35
+ private getNodeByPath;
36
+ /**
37
+ * Saves the current selection range.
38
+ */
39
+ saveSelection(): Range | null;
40
+ /**
41
+ * Restores a previously saved range.
42
+ */
43
+ restoreSelection(range: Range | null): void;
44
+ /**
45
+ * Checks if the selection is within a specific element.
46
+ */
47
+ isSelectionInElement(element: HTMLElement): boolean;
48
+ /**
49
+ * Clears the current selection.
50
+ */
51
+ clearSelection(): void;
52
+ }
@@ -0,0 +1,15 @@
1
+ import { EditorOptions } from '../Editor';
2
+ export interface UploadResult {
3
+ imageUrl: string;
4
+ imageId?: string;
5
+ }
6
+ export declare class ImageUploader {
7
+ /**
8
+ * Compresses an image file using HTML5 Canvas.
9
+ */
10
+ static compressImage(file: File, maxSizeMB: number): Promise<File | Blob>;
11
+ /**
12
+ * Uploads a file based on editor configuration.
13
+ */
14
+ static uploadFile(file: File | Blob, options: EditorOptions): Promise<UploadResult | null>;
15
+ }
@@ -0,0 +1,12 @@
1
+ import { CoreEditor, EditorOptions } from './core/Editor';
2
+ import { Toolbar } from './ui/Toolbar';
3
+ export declare class InkflowEditor extends CoreEditor {
4
+ private toolbar;
5
+ constructor(container: HTMLElement, options?: EditorOptions);
6
+ getToolbar(): Toolbar;
7
+ destroy(): void;
8
+ }
9
+ export { CoreEditor, type EditorOptions } from './core/Editor';
10
+ export { SelectionManager } from './core/SelectionManager';
11
+ export { Toolbar } from './ui/Toolbar';
12
+ export { HistoryManager } from './core/HistoryManager';
@@ -0,0 +1 @@
1
+ .te-container,.te-emoji-picker,.te-modal,.te-floating-toolbar{--te-primary-color: #6366f1;--te-primary-hover: #4f46e5;--te-bg-app: #f8fafc;--te-bg-editor: #ffffff;--te-toolbar-bg: #ffffff;--te-border-color: #e2e8f0;--te-border-focus: #cbd5e1;--te-text-main: #1e293b;--te-text-muted: #64748b;--te-placeholder: #94a3b8;--te-btn-hover: #f1f5f9;--te-btn-active: #e2e8f0;--te-radius-lg: 12px;--te-radius-md: 8px;--te-radius-sm: 6px;--te-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / .05);--te-shadow-md: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--te-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--te-transition: all .2s cubic-bezier(.4, 0, .2, 1)}.te-container.te-dark,.te-emoji-picker.te-dark,.te-modal.te-dark,.te-floating-toolbar.te-dark{--te-primary-color: #818cf8 !important;--te-primary-hover: #6366f1 !important;--te-bg-app: #0f172a !important;--te-bg-editor: #1e293b !important;--te-toolbar-bg: #1e293b !important;--te-border-color: #334155 !important;--te-border-focus: #475569 !important;--te-text-main: #f8fafc !important;--te-text-muted: #94a3b8 !important;--te-placeholder: #64748b !important;--te-btn-hover: #334155 !important;--te-btn-active: #475569 !important;--te-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / .3) !important;--te-shadow-md: 0 4px 6px -1px rgb(0 0 0 / .5), 0 2px 4px -2px rgb(0 0 0 / .5) !important;--te-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / .5), 0 4px 6px -4px rgb(0 0 0 / .5) !important}.te-container,.te-container *,.te-emoji-picker,.te-emoji-picker *,.te-modal,.te-modal *,.te-floating-toolbar,.te-floating-toolbar *{box-sizing:border-box}.te-container{display:flex;flex-direction:column;height:500px;border:1px solid var(--te-border-color);border-radius:var(--te-radius-lg);background:var(--te-bg-editor);box-shadow:var(--te-shadow-sm);overflow:hidden;font-family:Inter,-apple-system,system-ui,sans-serif;color:var(--te-text-main)!important;transition:var(--te-transition);max-width:100%;position:relative}.te-container:focus-within{border-color:var(--te-primary-color);box-shadow:0 0 0 4px #6366f11a,var(--te-shadow-md)}.te-toolbar{display:flex;flex-wrap:wrap;gap:6px;padding:10px 12px;border-bottom:1px solid var(--te-border-color);background:var(--te-toolbar-bg);align-items:center;-webkit-user-select:none;user-select:none;position:sticky;top:0;z-index:10}.te-container.te-toolbar-bottom .te-toolbar{top:auto;bottom:0;border-bottom:none;border-top:1px solid var(--te-border-color);order:2}.te-container.te-toolbar-left,.te-container.te-toolbar-right{flex-direction:row}.te-container.te-toolbar-left .te-toolbar,.te-container.te-toolbar-right .te-toolbar{flex-direction:column;width:76px;height:100%;border-bottom:none;overflow-y:auto;position:relative;flex-wrap:nowrap;padding:16px 6px;align-items:center;scrollbar-width:thin;scrollbar-color:var(--te-border-color) transparent}.te-container.te-toolbar-left .te-toolbar::-webkit-scrollbar,.te-container.te-toolbar-right .te-toolbar::-webkit-scrollbar{width:4px}.te-container.te-toolbar-left .te-toolbar::-webkit-scrollbar-thumb,.te-container.te-toolbar-right .te-toolbar::-webkit-scrollbar-thumb{background:var(--te-border-color);border-radius:4px}.te-container.te-toolbar-left .te-toolbar{border-right:1px solid var(--te-border-color)}.te-container.te-toolbar-right .te-toolbar{border-left:1px solid var(--te-border-color);order:2}.te-container.te-toolbar-left .te-toolbar-status,.te-container.te-toolbar-right .te-toolbar-status{flex-direction:column;margin-top:auto;margin-left:0;padding:12px 0;border-top:1px solid var(--te-border-color);width:100%}.te-container.te-toolbar-left .te-divider,.te-container.te-toolbar-right .te-divider{width:36px;height:1px;margin:12px 0;opacity:.6}.te-container.te-toolbar-left .te-select,.te-container.te-toolbar-right .te-select{min-width:64px;width:64px;height:26px;padding:0 4px;background-image:none;text-align:center;font-size:10px;margin-bottom:6px;border-radius:13px;border-color:var(--te-border-color);background:var(--te-bg-editor)!important;color:var(--te-text-main)!important;box-shadow:var(--te-shadow-sm)}.te-container.te-toolbar-left .te-select:hover,.te-container.te-toolbar-right .te-select:hover{border-color:var(--te-primary-main);background:var(--te-bg-editor)!important}.te-container.te-toolbar-left .te-input,.te-container.te-toolbar-right .te-input{width:64px;height:26px;font-size:11px;margin-bottom:6px;border-radius:13px;text-align:center;border-color:var(--te-border-color);background:var(--te-bg-editor)!important;box-shadow:var(--te-shadow-sm)}.te-container.te-toolbar-left .te-input:hover,.te-container.te-toolbar-right .te-input:hover{border-color:var(--te-primary-main)}.te-container.te-toolbar-left .te-button+.te-button,.te-container.te-toolbar-right .te-button+.te-button{margin-top:4px}.te-container.te-toolbar-left .te-toolbar-status,.te-container.te-toolbar-right .te-toolbar-status{flex-direction:column;margin-top:auto;margin-left:0;padding:16px 4px;border-top:1px dashed var(--te-border-color);width:100%;gap:14px;align-items:center;font-size:10px;text-align:center;color:var(--te-text-muted)}.te-container.te-toolbar-left .te-toolbar-status>span,.te-container.te-toolbar-right .te-toolbar-status>span{margin:0!important;padding:0!important;border:none!important}.te-container.te-toolbar-floating .te-toolbar{display:none!important}.te-divider{width:1px;height:20px;background-color:var(--te-border-color);margin:0 6px}.te-button,.te-select,.te-input{display:flex;align-items:center;justify-content:center;height:32px;border:1px solid transparent;background:transparent;border-radius:var(--te-radius-sm);cursor:pointer;color:var(--te-text-muted);font-size:13px;font-weight:500;transition:var(--te-transition);box-sizing:border-box}.te-button{width:32px;padding:0}.te-button svg{width:16px;height:16px;stroke:currentColor;stroke-width:2.2;fill:none}.te-select{padding:0 24px 0 8px;min-width:120px;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2364748b'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2.5' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 6px center;background-size:12px;border:1px solid var(--te-border-color);background-color:var(--te-bg-editor)!important;color:var(--te-text-main)!important}.te-input{width:56px;padding:0 4px;text-align:center;cursor:text;border:1px solid var(--te-border-color);background-color:var(--te-bg-editor)!important;color:var(--te-text-main)!important}.te-color-picker{width:32px;height:32px;padding:4px;border:1px solid var(--te-border-color);border-radius:var(--te-radius-sm);background:var(--te-bg-editor);cursor:pointer;box-sizing:border-box}.te-color-picker:hover{border-color:var(--te-border-focus)}.te-color-picker-wrapper{position:relative;display:inline-flex;width:32px;height:32px;border-radius:var(--te-radius-sm);transition:var(--te-transition)}.te-color-picker-wrapper:hover{background:var(--te-btn-hover)}.te-color-picker-input{position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;cursor:pointer;z-index:2}.te-color-icon{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center;z-index:1}.te-color-indicator{position:absolute;bottom:4px;left:6px;right:6px;height:3px;border-radius:2px;box-shadow:0 1px 1px var(--te-shadow-sm)}.te-emoji-picker{position:absolute;width:280px;height:320px;background:var(--te-bg-editor);border:1px solid var(--te-border-color);border-radius:var(--te-radius-md);box-shadow:var(--te-shadow-lg);display:flex;flex-direction:column;z-index:1000;animation:te-fade-in .2s ease-out}@keyframes te-fade-in{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}.te-emoji-header{padding:12px;border-bottom:1px solid var(--te-border-color)}.te-emoji-search{width:100%;padding:8px 12px;border:1px solid var(--te-border-color);border-radius:var(--te-radius-sm);background-color:var(--te-bg-editor)!important;color:var(--te-text-main)!important;font-size:13px;outline:none;transition:var(--te-transition)}.te-emoji-search:focus{border-color:var(--te-primary-color);box-shadow:0 0 0 2px #6366f11a}.te-emoji-body{flex:1;overflow-y:auto;overflow-x:hidden;padding:8px}.te-emoji-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:4px}.te-emoji-category-title{grid-column:span 7;padding:8px 4px 4px;font-size:11px;font-weight:600;color:var(--te-text-muted);text-transform:uppercase;letter-spacing:.05em}.te-emoji-item{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;border:none;background:transparent;cursor:pointer;border-radius:var(--te-radius-sm);transition:var(--te-transition)}.te-emoji-item:hover{background:var(--te-btn-hover);transform:scale(1.1)}.te-emoji-empty{grid-column:span 7;text-align:center;padding:20px;color:var(--te-text-muted);font-size:13px}.te-button:hover{background:var(--te-btn-hover);color:var(--te-text-main)!important}.te-select:hover,.te-input:hover{border-color:var(--te-border-focus)}.te-button.active{background:var(--te-btn-active);color:var(--te-primary-color);border-color:#6366f133}.te-select:focus,.te-input:focus{border-color:var(--te-primary-color);box-shadow:0 0 0 2px #6366f11a;outline:none;background:var(--te-bg-editor);color:var(--te-text-main)!important}.te-content{flex:1;overflow-y:auto;min-height:240px;padding:24px;line-height:1.25;outline:none;font-size:16px;position:relative}.te-content p{margin:0 0 1.25em}.te-content h1,.te-content h2,.te-content h3,.te-content h4,.te-content h5,.te-content h6{color:var(--te-text-main)!important;font-weight:700;margin:1rem 0 .5rem;line-height:1.1}.te-content h1:first-child,.te-content p:first-child{margin-top:0}.te-content h1{font-size:2.25em}.te-content h2{font-size:1.875em}.te-content h3{font-size:1.5em}.te-content h4{font-size:1.25em}.te-content[data-placeholder].is-empty:before{content:attr(data-placeholder);color:var(--te-placeholder);pointer-events:none;position:absolute;left:24px;right:24px;text-align:inherit;white-space:pre-wrap;overflow:hidden}.te-content pre{background-color:var(--te-bg-app);border:1px solid var(--te-border-color);border-radius:var(--te-radius-md);padding:16px;margin:1.5rem 0;overflow-x:auto;font-family:Fira Code,JetBrains Mono,Cascadia Code,monospace;font-size:14px;line-height:1.6;color:var(--te-text-main);tab-size:4}.te-content code{font-family:inherit;background:none;padding:0;color:inherit}.te-content :not(pre)>code{background-color:var(--te-bg-app);padding:2px 6px;border-radius:4px;font-size:.9em;border:1px solid var(--te-border-color)}.te-image-container{position:relative;display:inline-block;margin:1.5rem 0;max-width:100%;border-radius:var(--te-radius-lg);overflow:visible;transition:box-shadow var(--te-transition-fast);line-height:0}.te-image-container:hover{box-shadow:0 0 0 2px var(--te-primary-color)}.te-image-container.active{box-shadow:0 0 0 2px var(--te-primary-color),var(--te-shadow-lg)}.te-image{display:block;max-width:100%;height:auto;-webkit-user-select:none;user-select:none}.te-image-caption{font-size:.875rem;color:var(--te-text-muted);text-align:center;padding:.75rem;background:var(--te-bg-app);line-height:normal;border-bottom-left-radius:var(--te-radius-lg);border-bottom-right-radius:var(--te-radius-lg)}.te-image-caption:empty:before{content:attr(data-placeholder);color:var(--te-placeholder)}.te-image-resizer{position:absolute;width:10px;height:10px;background:var(--te-primary-color);border:1px solid var(--te-bg-editor);z-index:10;display:none}.te-image-container:hover .te-image-resizer,.te-image-container.active .te-image-resizer{display:block}.te-resizer-top-left{top:-5px;left:-5px;cursor:nwse-resize}.te-resizer-top-right{top:-5px;right:-5px;cursor:nesw-resize}.te-resizer-bottom-left{bottom:-5px;left:-5px;cursor:nesw-resize}.te-resizer-bottom-right{bottom:-5px;right:-5px;cursor:nwse-resize}.te-content.dragover{background:var(--te-btn-hover);outline:2px dashed var(--te-primary-color);outline-offset:-4px}.te-image-container.is-loading{position:relative;overflow:hidden;background:#f1f5f9;min-width:200px;min-height:150px}.te-image-container.is-loading .te-image{opacity:.3;filter:blur(4px);transition:opacity .5s ease,filter .5s ease}.te-image-container.is-loading:after{content:"";position:absolute;top:0;left:0;width:100%;height:100%;background:linear-gradient(90deg,#fff0 0,#fff6,#fff0);animation:te-skeleton-pulse 1.5s infinite}@keyframes te-skeleton-pulse{0%{transform:translate(-100%)}to{transform:translate(100%)}}.te-dark .te-image-container.is-loading{background:#334155}.te-table{width:100%;border-collapse:separate;border-spacing:0;margin:1.5rem 0;border:1px solid var(--te-border-color);border-radius:var(--te-radius-lg);table-layout:fixed;overflow:hidden;box-shadow:var(--te-shadow-sm)}.te-table th,.te-table td{border:1px solid var(--te-border-color);border-top:none;border-left:none;padding:14px 16px;min-height:48px;vertical-align:top;position:relative;transition:all .2s cubic-bezier(.4,0,.2,1);color:var(--te-text-main);background:var(--te-bg-editor)}.te-table th:last-child,.te-table td:last-child{border-right:none}.te-table tr:last-child td{border-bottom:none}.te-table th{background:var(--te-bg-app);font-size:11px;font-weight:800;color:var(--te-text-muted);text-transform:uppercase;letter-spacing:.1em;text-align:left;border-bottom:2px solid var(--te-border-color)}.te-table tr:nth-child(2n) td{background:#f1f5f94d}.te-dark .te-table tr:nth-child(2n) td{background:#1e293b4d}.te-table tr:hover td{background:var(--te-btn-hover)}.te-table td:focus-within{background:var(--te-bg-editor);box-shadow:inset 0 0 0 2px var(--te-primary-color);z-index:1}.te-dark .te-table{border-color:var(--te-border-color);box-shadow:0 4px 6px -1px #0003}.te-dark .te-table th{background:#0f172a}.te-toolbar-loader{width:12px;height:12px;border:1.5px solid var(--te-primary-color);border-bottom-color:transparent;border-radius:50%;display:inline-block;animation:te-spin 1s linear infinite}@keyframes te-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.te-toolbar-status{display:flex;align-items:center;gap:8px;transition:opacity .3s ease}.te-modal{position:absolute;width:280px;background:#fffc;backdrop-filter:blur(12px) saturate(180%);-webkit-backdrop-filter:blur(12px) saturate(180%);border:1px solid rgba(226,232,240,.7);border-radius:var(--te-radius-lg);box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a,0 0 0 1px #0000000d;display:flex;flex-direction:column;z-index:1000;animation:te-fade-in .25s cubic-bezier(.4,0,.2,1);padding:20px}.te-dark .te-modal{background:#1e293bcc;border-color:#334155b3;box-shadow:0 20px 25px -5px #0000004d,0 8px 10px -6px #0000004d,0 0 0 1px #ffffff0d}.te-modal-header{font-size:16px;font-weight:700;color:var(--te-text-main);margin-bottom:20px;letter-spacing:-.01em}.te-modal-field{margin-bottom:16px}.te-modal-field label{display:block;font-size:11px;font-weight:700;color:var(--te-text-muted);text-transform:uppercase;margin-bottom:6px;letter-spacing:.05em}.te-modal-input{width:100%;padding:10px 12px;border:1px solid var(--te-border-color);border-radius:var(--te-radius-md);background:var(--te-bg-editor);color:var(--te-text-main);font-size:14px;outline:none;transition:var(--te-transition)}.te-modal-input:focus{border-color:var(--te-primary-color);box-shadow:0 0 0 3px #6366f126;background:var(--te-bg-editor)}.te-modal-file-input{padding:8px;font-size:13px;cursor:pointer}.te-modal-file-input::-webkit-file-upload-button{background:var(--te-primary-color);color:#fff;border:none;padding:6px 12px;border-radius:6px;font-weight:600;margin-right:10px;cursor:pointer;transition:all .2s;font-family:inherit}.te-modal-file-input::-webkit-file-upload-button:hover{background:var(--te-primary-hover)}.te-modal-footer{display:flex;justify-content:flex-end;gap:12px;margin-top:8px}.te-modal-btn{padding:8px 16px;font-size:13px;font-weight:600;border-radius:var(--te-radius-md);cursor:pointer;transition:var(--te-transition);border:1px solid transparent;display:inline-flex;align-items:center;justify-content:center}.te-modal-btn-cancel{background:transparent;color:var(--te-text-muted);border:1px solid var(--te-border-color)}.te-modal-btn-cancel:hover{background:var(--te-btn-hover);color:var(--te-text-main);border-color:var(--te-border-focus)}.te-modal-btn-confirm{background:var(--te-primary-color);color:#fff;box-shadow:0 4px 6px -1px #6366f133}.te-modal-btn-confirm:hover{background:var(--te-primary-hover);transform:translateY(-1px);box-shadow:0 6px 8px -1px #6366f14d}.te-modal-btn-confirm:active{transform:translateY(0)}.te-floating-toolbar{background:var(--te-bg-editor);border:1px solid var(--te-border-color);border-radius:var(--te-radius-md);padding:4px;box-shadow:var(--te-shadow-lg);display:flex;gap:2px;pointer-events:auto;opacity:0;transform:translateY(10px);transition:opacity .2s ease,transform .2s ease,background .3s ease}.te-floating-toolbar.te-floating-visible{opacity:1;transform:translateY(0)}.te-floating-btn{background:transparent;border:none;cursor:pointer;padding:6px;border-radius:var(--te-radius-sm);color:var(--te-text-muted);display:flex;align-items:center;justify-content:center;transition:all .2s ease}.te-floating-btn:hover{background:var(--te-btn-hover);color:var(--te-text-main)}.te-floating-btn svg{width:18px;height:18px;stroke:currentColor}.te-loader-overlay{position:absolute;inset:0;background:#f1f5f9e6;backdrop-filter:blur(12px) saturate(200%);-webkit-backdrop-filter:blur(12px) saturate(200%);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:1000;transition:opacity .4s cubic-bezier(.4,0,.2,1),visibility .4s;border-radius:var(--te-radius-lg)}.te-dark .te-loader-overlay{background:#0f172ae6}.te-loader-overlay.hidden{opacity:0;visibility:hidden;pointer-events:none}.te-loader-spinner{width:48px;height:48px;border:4px solid var(--te-border-color);border-top-color:var(--te-primary-color);border-radius:50%;animation:te-spin 1s cubic-bezier(.4,0,.2,1) infinite;margin-bottom:24px;filter:drop-shadow(0 0 10px rgba(99,102,241,.2))}.te-loader-shimmer{width:140px;height:4px;background:var(--te-border-color);border-radius:2px;overflow:hidden;position:relative}.te-loader-shimmer:after{content:"";position:absolute;top:0;left:0;width:100%;height:100%;background:linear-gradient(90deg,#6366f100 0,#6366f166,#6366f100);animation:te-skeleton-pulse 1.5s infinite}.te-loader-text{font-size:13px;font-weight:600;color:var(--te-text-muted);letter-spacing:.02em;margin-top:12px}