html-wysiwyg-editor 1.0.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.
- package/README.md +207 -0
- package/dist/index.d.ts +107 -0
- package/dist/wysiwyg-editor.css +1 -0
- package/dist/wysiwyg-editor.js +2450 -0
- package/dist/wysiwyg-editor.js.map +1 -0
- package/dist/wysiwyg-editor.umd.cjs +15 -0
- package/dist/wysiwyg-editor.umd.cjs.map +1 -0
- package/package.json +69 -0
package/README.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# WYSIWYG HTML Editor
|
|
2
|
+
|
|
3
|
+
A lightweight, Canva-like WYSIWYG HTML editor for React with a smooth editing experience.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- ✨ **Rich Text Formatting** - Bold, italic, underline, strikethrough
|
|
12
|
+
- 🎨 **Colors** - Text and background color picker
|
|
13
|
+
- 🔤 **Typography** - Font family, size, and alignment
|
|
14
|
+
- 📝 **Block Elements** - Headings, lists, blockquotes, code blocks
|
|
15
|
+
- 🔗 **Links** - Insert and edit hyperlinks
|
|
16
|
+
- 🖼️ **Media** - Image/video handling with resize support
|
|
17
|
+
- 🔄 **History** - Undo/Redo with keyboard shortcuts
|
|
18
|
+
- 🎯 **Drag & Drop** - Reorder content blocks
|
|
19
|
+
- 🛡️ **Security** - XSS protection via DOMPurify
|
|
20
|
+
- ♿ **Accessible** - ARIA labels and keyboard navigation
|
|
21
|
+
- 📦 **Lightweight** - No heavy dependencies
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @your-org/wysiwyg-editor
|
|
27
|
+
# or
|
|
28
|
+
yarn add @your-org/wysiwyg-editor
|
|
29
|
+
# or
|
|
30
|
+
pnpm add @your-org/wysiwyg-editor
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import { WysiwygEditor } from '@your-org/wysiwyg-editor'
|
|
37
|
+
import '@your-org/wysiwyg-editor/style.css'
|
|
38
|
+
|
|
39
|
+
function App() {
|
|
40
|
+
const [html, setHtml] = useState('<p>Hello World!</p>')
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<WysiwygEditor
|
|
44
|
+
html={html}
|
|
45
|
+
onChange={setHtml}
|
|
46
|
+
placeholder="Start typing..."
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Props
|
|
53
|
+
|
|
54
|
+
| Prop | Type | Default | Description |
|
|
55
|
+
|------|------|---------|-------------|
|
|
56
|
+
| `html` | `string` | **required** | The HTML content to edit |
|
|
57
|
+
| `onChange` | `(html: string) => void` | **required** | Callback fired when content changes |
|
|
58
|
+
| `placeholder` | `string` | `"Start typing..."` | Placeholder text when editor is empty |
|
|
59
|
+
| `debounceMs` | `number` | `300` | Debounce delay for onChange in ms |
|
|
60
|
+
| `assets` | `Asset[]` | `[]` | Available assets for media replacement |
|
|
61
|
+
| `onUpload` | `(file: File) => Promise<string>` | `undefined` | Callback for file uploads |
|
|
62
|
+
|
|
63
|
+
### Asset Type
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
interface Asset {
|
|
67
|
+
id: string
|
|
68
|
+
url: string
|
|
69
|
+
name: string
|
|
70
|
+
type: 'image' | 'video'
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Usage Examples
|
|
75
|
+
|
|
76
|
+
### With File Upload
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { WysiwygEditor } from '@your-org/wysiwyg-editor'
|
|
80
|
+
import '@your-org/wysiwyg-editor/style.css'
|
|
81
|
+
|
|
82
|
+
function App() {
|
|
83
|
+
const [html, setHtml] = useState('')
|
|
84
|
+
|
|
85
|
+
const handleUpload = async (file: File): Promise<string> => {
|
|
86
|
+
const formData = new FormData()
|
|
87
|
+
formData.append('file', file)
|
|
88
|
+
|
|
89
|
+
const response = await fetch('/api/upload', {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
body: formData,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const { url } = await response.json()
|
|
95
|
+
return url
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<WysiwygEditor
|
|
100
|
+
html={html}
|
|
101
|
+
onChange={setHtml}
|
|
102
|
+
onUpload={handleUpload}
|
|
103
|
+
/>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### With Asset Library
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
const assets = [
|
|
112
|
+
{ id: '1', url: '/images/logo.png', name: 'Logo', type: 'image' },
|
|
113
|
+
{ id: '2', url: '/images/banner.jpg', name: 'Banner', type: 'image' },
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
<WysiwygEditor
|
|
117
|
+
html={html}
|
|
118
|
+
onChange={setHtml}
|
|
119
|
+
assets={assets}
|
|
120
|
+
/>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Keyboard Shortcuts
|
|
124
|
+
|
|
125
|
+
| Shortcut | Action |
|
|
126
|
+
|----------|--------|
|
|
127
|
+
| `⌘/Ctrl + B` | Bold |
|
|
128
|
+
| `⌘/Ctrl + I` | Italic |
|
|
129
|
+
| `⌘/Ctrl + U` | Underline |
|
|
130
|
+
| `⌘/Ctrl + K` | Insert link |
|
|
131
|
+
| `⌘/Ctrl + Z` | Undo |
|
|
132
|
+
| `⌘/Ctrl + Shift + Z` | Redo |
|
|
133
|
+
| `Delete` | Delete selected element |
|
|
134
|
+
| `Escape` | Deselect / Close menu |
|
|
135
|
+
|
|
136
|
+
## Customization
|
|
137
|
+
|
|
138
|
+
### CSS Variables
|
|
139
|
+
|
|
140
|
+
Override these CSS variables to customize the appearance:
|
|
141
|
+
|
|
142
|
+
```css
|
|
143
|
+
:root {
|
|
144
|
+
--wysiwyg-font-family: 'Inter', system-ui, sans-serif;
|
|
145
|
+
--wysiwyg-radius: 8px;
|
|
146
|
+
--wysiwyg-radius-sm: 4px;
|
|
147
|
+
--wysiwyg-color-accent: #6366f1;
|
|
148
|
+
--wysiwyg-color-bg: #f5f5f7;
|
|
149
|
+
--wysiwyg-color-surface: #ffffff;
|
|
150
|
+
--wysiwyg-color-border: #e5e5e5;
|
|
151
|
+
--wysiwyg-color-text: #1a1a1a;
|
|
152
|
+
--wysiwyg-color-text-muted: #6b7280;
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Advanced Usage
|
|
157
|
+
|
|
158
|
+
### Using Hooks Directly
|
|
159
|
+
|
|
160
|
+
For custom implementations, you can use the internal hooks:
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
import { useTextFormatting, useHistory, useDragAndDrop } from '@your-org/wysiwyg-editor'
|
|
164
|
+
|
|
165
|
+
function CustomEditor() {
|
|
166
|
+
const editorRef = useRef<HTMLDivElement>(null)
|
|
167
|
+
|
|
168
|
+
const { toggleBold, toggleItalic, setBlockType } = useTextFormatting({
|
|
169
|
+
editorRef,
|
|
170
|
+
onContentChange: () => { /* handle change */ }
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const { undo, redo, canUndo, canRedo } = useHistory(initialHtml)
|
|
174
|
+
|
|
175
|
+
// Build your custom UI...
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Sanitization Utilities
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
import { sanitizeHtml, containsDangerousContent } from '@your-org/wysiwyg-editor'
|
|
183
|
+
|
|
184
|
+
// Check if content has XSS vectors
|
|
185
|
+
if (containsDangerousContent(userInput)) {
|
|
186
|
+
console.warn('Dangerous content detected')
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Sanitize HTML
|
|
190
|
+
const safe = sanitizeHtml(userInput)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Browser Support
|
|
194
|
+
|
|
195
|
+
- Chrome 90+
|
|
196
|
+
- Firefox 88+
|
|
197
|
+
- Safari 14+
|
|
198
|
+
- Edge 90+
|
|
199
|
+
|
|
200
|
+
## Contributing
|
|
201
|
+
|
|
202
|
+
Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) first.
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT © [Your Organization]
|
|
207
|
+
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { JSX } from 'react/jsx-runtime';
|
|
2
|
+
import { RefObject } from 'react';
|
|
3
|
+
|
|
4
|
+
export declare interface Asset {
|
|
5
|
+
id: string;
|
|
6
|
+
url: string;
|
|
7
|
+
name: string;
|
|
8
|
+
type: "image" | "video";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare type BlockType = 'p' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
12
|
+
|
|
13
|
+
export declare function containsDangerousContent(html: string): boolean;
|
|
14
|
+
|
|
15
|
+
declare interface DragState {
|
|
16
|
+
isDragging: boolean;
|
|
17
|
+
draggedElement: HTMLElement | null;
|
|
18
|
+
dropIndicatorY: number | null;
|
|
19
|
+
dropTarget: HTMLElement | null;
|
|
20
|
+
dropPosition: 'before' | 'after' | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
declare type FormatCommand = 'bold' | 'italic' | 'underline' | 'strikeThrough';
|
|
24
|
+
|
|
25
|
+
export declare function sanitizeForOutput(html: string): string;
|
|
26
|
+
|
|
27
|
+
export declare function sanitizeHtml(html: string): string;
|
|
28
|
+
|
|
29
|
+
export declare function useDragAndDrop({ editorRef, onContentChange }: UseDragAndDropOptions): {
|
|
30
|
+
dragState: DragState;
|
|
31
|
+
handleDragStart: (element: HTMLElement, e: MouseEvent) => void;
|
|
32
|
+
findBlockParent: (element: HTMLElement) => HTMLElement | null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
declare interface UseDragAndDropOptions {
|
|
36
|
+
editorRef: RefObject<HTMLDivElement | null>;
|
|
37
|
+
onContentChange: () => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export declare function useHistory(initialValue: string, options?: UseHistoryOptions): {
|
|
41
|
+
pushToHistory: (value: string) => void;
|
|
42
|
+
undo: () => string | null;
|
|
43
|
+
redo: () => string | null;
|
|
44
|
+
reset: (value: string) => void;
|
|
45
|
+
canUndo: boolean;
|
|
46
|
+
canRedo: boolean;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
declare interface UseHistoryOptions {
|
|
50
|
+
maxHistory?: number;
|
|
51
|
+
debounceMs?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export declare function useTextFormatting({ editorRef, onContentChange }: UseTextFormattingOptions): {
|
|
55
|
+
toggleBold: () => void;
|
|
56
|
+
toggleItalic: () => void;
|
|
57
|
+
toggleUnderline: () => void;
|
|
58
|
+
toggleStrikethrough: () => void;
|
|
59
|
+
toggleBulletList: () => void;
|
|
60
|
+
toggleNumberedList: () => void;
|
|
61
|
+
setBlockType: (type: BlockType) => void;
|
|
62
|
+
getCurrentBlockType: () => BlockType;
|
|
63
|
+
isFormatActive: (command: FormatCommand) => boolean;
|
|
64
|
+
isInList: (listType: "ul" | "ol") => boolean;
|
|
65
|
+
insertLink: (url: string) => void;
|
|
66
|
+
removeLink: () => void;
|
|
67
|
+
getCurrentLink: () => string | null;
|
|
68
|
+
setTextColor: (color: string) => void;
|
|
69
|
+
setBackgroundColor: (color: string) => void;
|
|
70
|
+
getCurrentTextColor: () => string;
|
|
71
|
+
getCurrentBackgroundColor: () => string;
|
|
72
|
+
setFontFamily: (font: string) => void;
|
|
73
|
+
setFontSize: (size: string) => void;
|
|
74
|
+
getCurrentFontFamily: () => string;
|
|
75
|
+
getCurrentFontSize: () => string;
|
|
76
|
+
setAlignment: (alignment: "left" | "center" | "right" | "justify") => void;
|
|
77
|
+
getCurrentAlignment: () => "left" | "center" | "right" | "justify";
|
|
78
|
+
insertBlockquote: () => void;
|
|
79
|
+
insertCodeBlock: () => void;
|
|
80
|
+
insertHorizontalRule: () => void;
|
|
81
|
+
isInBlockquote: () => boolean;
|
|
82
|
+
isInCodeBlock: () => boolean;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
declare interface UseTextFormattingOptions {
|
|
86
|
+
editorRef: RefObject<HTMLDivElement | null>;
|
|
87
|
+
onContentChange: () => void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export declare function WysiwygEditor({ html, onChange, placeholder, debounceMs, assets, onUpload, }: WysiwygEditorProps): JSX.Element;
|
|
91
|
+
|
|
92
|
+
export declare interface WysiwygEditorProps {
|
|
93
|
+
/** The HTML content to edit */
|
|
94
|
+
html: string;
|
|
95
|
+
/** Callback fired when content changes */
|
|
96
|
+
onChange: (html: string) => void;
|
|
97
|
+
/** Placeholder text when editor is empty */
|
|
98
|
+
placeholder?: string;
|
|
99
|
+
/** Debounce delay for onChange in milliseconds */
|
|
100
|
+
debounceMs?: number;
|
|
101
|
+
/** Available assets for media replacement */
|
|
102
|
+
assets?: Asset[];
|
|
103
|
+
/** Callback for file uploads, returns the URL of the uploaded file */
|
|
104
|
+
onUpload?: (file: File) => Promise<string>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.link-popover{position:fixed;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius);box-shadow:0 4px 16px #0000001f,0 1px 3px #00000014;z-index:1001;padding:12px;min-width:300px;animation:popoverIn .15s ease-out}@keyframes popoverIn{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.link-form{display:flex;flex-direction:column;gap:10px}.link-input{width:100%;padding:10px 12px;font-size:14px;font-family:var(--font-family);background:var(--color-bg);color:var(--color-text);border:1px solid var(--color-border);border-radius:var(--radius-sm);outline:none;transition:border-color .15s ease,box-shadow .15s ease}.link-input:focus{border-color:var(--color-accent);box-shadow:0 0 0 3px var(--color-selection)}.link-input::placeholder{color:var(--color-text-muted)}.link-actions{display:flex;gap:6px}.link-btn{padding:8px 12px;font-size:13px;font-weight:500;font-family:var(--font-family);background:var(--color-surface-hover);color:var(--color-text);border:1px solid var(--color-border);border-radius:var(--radius-sm);cursor:pointer;transition:all .15s ease}.link-btn:hover:not(:disabled){background:var(--color-border)}.link-btn:disabled{opacity:.5;cursor:not-allowed}.link-btn-primary{background:var(--color-accent);color:#fff;border-color:var(--color-accent)}.link-btn-primary:hover:not(:disabled){background:var(--color-accent-hover);border-color:var(--color-accent-hover)}.link-btn-danger{color:#dc2626;border-color:#dc2626;background:transparent}.link-btn-danger:hover{background:#dc2626;color:#fff}.color-picker{position:fixed;width:280px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius);box-shadow:0 8px 32px #00000029,0 2px 8px #00000014;z-index:1002;overflow:hidden;animation:pickerIn .15s ease-out}.color-picker-header{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border-bottom:1px solid var(--color-border)}.color-picker-title{font-size:13px;font-weight:600}.color-picker-close{width:22px;height:22px;display:flex;align-items:center;justify-content:center;font-size:11px;background:transparent;color:var(--color-text-muted);border:none;border-radius:var(--radius-sm);cursor:pointer;transition:all .12s ease}.color-picker-close:hover{background:var(--color-surface-hover);color:var(--color-text)}.color-presets{padding:12px;display:flex;flex-direction:column;gap:6px}.color-row{display:flex;gap:4px}.color-swatch{width:32px;height:24px;border:none;border-radius:4px;cursor:pointer;transition:all .12s ease;position:relative}.color-swatch:hover{transform:scale(1.1);z-index:1}.color-swatch.active{outline:2px solid var(--color-accent);outline-offset:2px}.color-swatch.white{border:1px solid var(--color-border)}.color-custom{display:flex;gap:8px;padding:0 12px 12px}.color-input-native{width:36px;height:36px;padding:0;border:1px solid var(--color-border);border-radius:var(--radius-sm);cursor:pointer;background:transparent}.color-input-native::-webkit-color-swatch-wrapper{padding:2px}.color-input-native::-webkit-color-swatch{border:none;border-radius:4px}.color-input-text{flex:1;padding:8px 10px;font-size:13px;font-family:SF Mono,monospace;background:var(--color-bg);color:var(--color-text);border:1px solid var(--color-border);border-radius:var(--radius-sm);outline:none;transition:border-color .15s ease}.color-input-text:focus{border-color:var(--color-accent)}.color-apply-btn{padding:8px 12px;font-size:13px;font-weight:500;font-family:var(--font-family);background:var(--color-accent);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;transition:background .15s ease}.color-apply-btn:hover{background:var(--color-accent-hover)}.color-remove-btn{display:block;width:calc(100% - 24px);margin:0 12px 12px;padding:8px;font-size:12px;font-family:var(--font-family);background:transparent;color:var(--color-text-muted);border:1px dashed var(--color-border);border-radius:var(--radius-sm);cursor:pointer;transition:all .15s ease}.color-remove-btn:hover{background:var(--color-surface-hover);border-style:solid;color:var(--color-text)}.font-picker{position:fixed;width:220px;max-height:360px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius);box-shadow:0 8px 32px #00000029,0 2px 8px #00000014;z-index:1002;overflow:hidden;animation:fontPickerIn .15s ease-out}@keyframes fontPickerIn{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}.font-picker-header{display:flex;align-items:center;padding:4px;border-bottom:1px solid var(--color-border);gap:4px}.font-tab{flex:1;padding:8px 12px;font-size:13px;font-weight:500;font-family:var(--font-family);background:transparent;color:var(--color-text-muted);border:none;border-radius:var(--radius-sm);cursor:pointer;transition:all .15s ease}.font-tab:hover{background:var(--color-surface-hover);color:var(--color-text)}.font-tab.active{background:var(--color-accent);color:#fff}.font-picker-close{width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:11px;background:transparent;color:var(--color-text-muted);border:none;border-radius:var(--radius-sm);cursor:pointer;transition:all .12s ease}.font-picker-close:hover{background:var(--color-surface-hover);color:var(--color-text)}.font-picker-content{max-height:280px;overflow-y:auto}.font-list,.size-list{padding:8px;display:flex;flex-direction:column;gap:2px}.font-item{display:block;width:100%;padding:10px 12px;font-size:14px;text-align:left;background:transparent;color:var(--color-text);border:none;border-radius:var(--radius-sm);cursor:pointer;transition:background .12s ease}.font-item:hover{background:var(--color-surface-hover)}.font-item.active{background:var(--color-selection);color:var(--color-accent)}.size-item{display:flex;align-items:center;gap:12px;width:100%;padding:8px 12px;font-family:var(--font-family);text-align:left;background:transparent;color:var(--color-text);border:none;border-radius:var(--radius-sm);cursor:pointer;transition:background .12s ease}.size-item:hover{background:var(--color-surface-hover)}.size-item.active{background:var(--color-selection);color:var(--color-accent)}.size-preview{width:36px;font-weight:600}.size-label{flex:1;font-size:13px}.size-value{font-size:11px;color:var(--color-text-muted);font-family:SF Mono,monospace}.insert-menu{position:fixed;width:220px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius);box-shadow:0 8px 32px #00000029,0 2px 8px #00000014;z-index:1002;overflow:hidden;animation:insertMenuIn .15s ease-out}@keyframes insertMenuIn{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}.insert-menu-header{padding:10px 14px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--color-text-muted);border-bottom:1px solid var(--color-border)}.insert-menu-item{display:flex;align-items:center;gap:12px;width:100%;padding:10px 14px;font-family:var(--font-family);text-align:left;background:transparent;color:var(--color-text);border:none;cursor:pointer;transition:background .12s ease}.insert-menu-item:hover{background:var(--color-surface-hover)}.insert-menu-item:active{background:var(--color-selection)}.insert-icon{width:32px;height:32px;display:flex;align-items:center;justify-content:center;background:var(--color-bg);border-radius:var(--radius-sm);font-size:14px;color:var(--color-accent);flex-shrink:0}.insert-info{display:flex;flex-direction:column;gap:2px}.insert-label{font-size:13px;font-weight:500}.insert-desc{font-size:11px;color:var(--color-text-muted)}.floating-toolbar{position:fixed;display:flex;align-items:center;gap:2px;padding:6px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius);box-shadow:0 4px 16px #0000001f,0 1px 3px #00000014;z-index:1000;animation:floatingToolbarIn .15s ease-out}@keyframes floatingToolbarIn{0%{opacity:0;transform:translateY(4px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}.floating-btn{display:flex;align-items:center;justify-content:center;min-width:32px;height:32px;padding:0 8px;font-size:14px;font-family:var(--font-family);background:transparent;color:var(--color-text-muted);border:none;border-radius:var(--radius-sm);cursor:pointer;transition:all .12s ease;white-space:nowrap}.floating-btn:hover{background:var(--color-surface-hover);color:var(--color-text)}.floating-btn.active{background:var(--color-accent);color:#fff}.floating-btn strong{font-weight:700}.floating-btn em{font-style:italic}.floating-btn u{text-decoration:underline;text-underline-offset:2px}.floating-btn s{text-decoration:line-through}.color-btn{flex-direction:column;gap:2px;padding:4px 8px}.color-icon{font-size:14px;font-weight:700;line-height:1}.color-icon.bg-icon{background:var(--color-surface-hover);padding:1px 4px;border-radius:2px}.color-bar{width:16px;height:3px;border-radius:1px;border:1px solid var(--color-border)}.font-btn{font-weight:600;font-size:13px;padding:0 10px}.align-btn{padding:0 6px}.align-btn svg{display:block}.insert-btn{font-size:18px;font-weight:500;padding:0 8px}.block-menu-wrapper{position:relative}.block-btn{gap:4px;font-size:13px;font-weight:500}.dropdown-arrow{font-size:10px;opacity:.6}.block-menu{position:absolute;top:100%;left:0;margin-top:4px;min-width:140px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius);box-shadow:0 4px 16px #0000001f,0 1px 3px #00000014;overflow:hidden;animation:menuIn .12s ease-out}@keyframes menuIn{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.block-menu-item{display:block;width:100%;padding:8px 12px;font-size:13px;font-family:var(--font-family);text-align:left;background:transparent;color:var(--color-text);border:none;cursor:pointer;transition:background .1s ease}.block-menu-item:hover{background:var(--color-surface-hover)}.block-menu-item.active{background:var(--color-selection);color:var(--color-accent);font-weight:500}.floating-toolbar:after{display:none}.media-toolbar{position:fixed;display:flex;align-items:center;gap:2px;padding:6px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius);box-shadow:0 4px 16px #0000001f,0 1px 3px #00000014;z-index:1000;animation:mediaToolbarIn .15s ease-out}@keyframes mediaToolbarIn{0%{opacity:0;transform:translateY(4px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}.media-btn{display:flex;align-items:center;gap:6px;padding:8px 12px;font-size:13px;font-family:var(--font-family);font-weight:500;background:transparent;color:var(--color-text);border:none;border-radius:var(--radius-sm);cursor:pointer;transition:all .12s ease}.media-btn:hover{background:var(--color-surface-hover)}.media-btn-danger:hover{background:#fee2e2;color:#dc2626}.toolbar-divider{width:1px;height:20px;background:var(--color-border);margin:0 4px}.asset-picker{position:fixed;width:320px;max-height:400px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius);box-shadow:0 8px 32px #00000029,0 2px 8px #00000014;z-index:1001;overflow:hidden;animation:pickerIn .15s ease-out}@keyframes pickerIn{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}.asset-picker-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--color-border)}.asset-picker-title{font-size:14px;font-weight:600}.asset-picker-close{width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-size:12px;background:transparent;color:var(--color-text-muted);border:none;border-radius:var(--radius-sm);cursor:pointer;transition:all .12s ease}.asset-picker-close:hover{background:var(--color-surface-hover);color:var(--color-text)}.asset-upload{padding:12px 16px}.upload-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:12px;font-size:14px;font-family:var(--font-family);font-weight:500;background:var(--color-bg);color:var(--color-text);border:2px dashed var(--color-border);border-radius:var(--radius);cursor:pointer;transition:all .15s ease}.upload-btn:hover{border-color:var(--color-accent);background:var(--color-selection)}.asset-divider{padding:8px 16px;font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--color-text-muted);text-align:center}.asset-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;padding:0 16px 16px;max-height:240px;overflow-y:auto}.asset-item{aspect-ratio:1;background:var(--color-bg);border:2px solid transparent;border-radius:var(--radius-sm);overflow:hidden;cursor:pointer;transition:all .12s ease;padding:0}.asset-item:hover{border-color:var(--color-accent)}.asset-item.active{border-color:var(--color-accent);box-shadow:0 0 0 2px var(--color-selection)}.asset-item img,.asset-item video{width:100%;height:100%;object-fit:cover}.asset-empty{padding:24px 16px;text-align:center;color:var(--color-text-muted);font-size:13px}.drag-handle{position:fixed;width:28px;height:28px;display:flex;align-items:center;justify-content:center;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);color:var(--color-text-muted);cursor:grab;z-index:100;transition:all .15s ease;box-shadow:0 2px 8px #0000001a;animation:handleFadeIn .12s ease-out}@keyframes handleFadeIn{0%{opacity:0;transform:translate(-4px)}to{opacity:1;transform:translate(0)}}.drag-handle:hover{background:var(--color-accent);border-color:var(--color-accent);color:#fff;transform:scale(1.08);box-shadow:0 4px 12px #4f46e54d}.drag-handle:active{cursor:grabbing;transform:scale(1.02)}.drag-handle svg{width:14px;height:14px;pointer-events:none}.drop-indicator{position:fixed;left:0;right:0;height:4px;display:flex;align-items:center;pointer-events:none;z-index:999;transform:translateY(-50%);padding:0 20px}.drop-indicator-line{flex:1;height:2px;background:var(--color-accent);border-radius:1px}.drop-indicator-dot{width:8px;height:8px;background:var(--color-accent);border-radius:50%;flex-shrink:0}.image-resizer{position:fixed;pointer-events:none;z-index:99;border:2px solid var(--color-accent);border-radius:4px}.image-resizer.resizing{border-style:dashed}.resize-handle{position:absolute;width:12px;height:12px;background:var(--color-surface);border:2px solid var(--color-accent);border-radius:2px;pointer-events:auto;transition:transform .1s ease,background .1s ease}.resize-handle:hover{background:var(--color-accent);transform:scale(1.2)}.resize-handle.nw{top:-6px;left:-6px;cursor:nw-resize}.resize-handle.ne{top:-6px;right:-6px;cursor:ne-resize}.resize-handle.sw{bottom:-6px;left:-6px;cursor:sw-resize}.resize-handle.se{bottom:-6px;right:-6px;cursor:se-resize}.size-indicator{position:absolute;bottom:-28px;left:50%;transform:translate(-50%);padding:4px 8px;background:var(--color-text);color:var(--color-surface);font-size:11px;font-family:SF Mono,monospace;font-weight:500;border-radius:4px;white-space:nowrap;pointer-events:none}.shortcuts-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:2000;animation:overlayFadeIn .15s ease-out}@keyframes overlayFadeIn{0%{opacity:0}to{opacity:1}}.shortcuts-modal{width:480px;max-height:80vh;background:var(--color-surface);border-radius:var(--radius);box-shadow:0 20px 60px #0000004d;overflow:hidden;animation:modalSlideIn .2s ease-out}@keyframes modalSlideIn{0%{opacity:0;transform:translateY(-20px) scale(.95)}to{opacity:1;transform:translateY(0) scale(1)}}.shortcuts-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--color-border)}.shortcuts-title{font-size:16px;font-weight:600;margin:0}.shortcuts-close{width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:14px;background:transparent;color:var(--color-text-muted);border:none;border-radius:var(--radius-sm);cursor:pointer;transition:all .12s ease}.shortcuts-close:hover{background:var(--color-surface-hover);color:var(--color-text)}.shortcuts-content{padding:16px 20px;max-height:calc(80vh - 140px);overflow-y:auto}.shortcuts-section{margin-bottom:20px}.shortcuts-section:last-child{margin-bottom:0}.shortcuts-category{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--color-text-muted);margin:0 0 10px}.shortcuts-list{display:flex;flex-direction:column;gap:6px}.shortcut-item{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:var(--color-bg);border-radius:var(--radius-sm)}.shortcut-desc{font-size:13px;color:var(--color-text)}.shortcut-keys{display:flex;align-items:center;gap:4px}.shortcut-key{display:inline-flex;align-items:center;justify-content:center;min-width:24px;height:24px;padding:0 8px;font-size:11px;font-family:var(--font-family);font-weight:500;background:var(--color-surface);color:var(--color-text);border:1px solid var(--color-border);border-radius:4px;box-shadow:0 1px 2px #0000000d}.shortcut-plus{font-size:11px;color:var(--color-text-muted);margin:0 2px}.shortcuts-footer{padding:12px 20px;font-size:12px;color:var(--color-text-muted);text-align:center;border-top:1px solid var(--color-border);background:var(--color-bg)}.shortcuts-footer kbd{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 6px;font-size:10px;font-family:var(--font-family);background:var(--color-surface);border:1px solid var(--color-border);border-radius:3px;margin:0 4px}.wysiwyg-editor-wrapper{width:100%;max-width:800px;background:var(--color-surface);border-radius:var(--radius);border:1px solid var(--color-border);overflow:hidden;transition:border-color .15s ease,box-shadow .15s ease;position:relative}.wysiwyg-warning{position:absolute;top:12px;left:50%;transform:translate(-50%);background:#f59e0b;color:#000;padding:8px 16px;border-radius:var(--radius-sm);font-size:13px;font-weight:500;z-index:100;animation:fadeInOut 3s ease-in-out}@keyframes fadeInOut{0%{opacity:0;transform:translate(-50%) translateY(-10px)}10%{opacity:1;transform:translate(-50%) translateY(0)}90%{opacity:1;transform:translate(-50%) translateY(0)}to{opacity:0;transform:translate(-50%) translateY(-10px)}}.wysiwyg-editor-wrapper:focus-within{border-color:var(--color-accent);box-shadow:0 0 0 3px var(--color-selection)}.wysiwyg-hint-bar{display:flex;align-items:center;justify-content:space-between;padding:8px 16px;background:var(--color-bg);border-bottom:1px solid var(--color-border)}.history-buttons{display:flex;gap:4px}.history-btn{display:flex;align-items:center;gap:4px;padding:6px 10px;font-size:12px;font-family:var(--font-family);font-weight:500;background:transparent;color:var(--color-text-muted);border:1px solid var(--color-border);border-radius:var(--radius-sm);cursor:pointer;transition:all .15s ease}.history-btn:hover:not(.disabled){background:var(--color-surface-hover);color:var(--color-text);border-color:var(--color-text-muted)}.history-btn.disabled{opacity:.4;cursor:not-allowed}.hint-text{font-size:12px;color:var(--color-text-muted);flex:1}.shortcuts-btn{padding:4px 8px;font-size:14px;background:transparent;border:none;border-radius:var(--radius-sm);cursor:pointer;transition:background .12s ease}.shortcuts-btn:hover{background:var(--color-surface-hover)}.wysiwyg-editor blockquote{margin:16px 0;padding:12px 20px;border-left:4px solid var(--color-accent);background:var(--color-bg);font-style:italic;color:var(--color-text-muted)}.wysiwyg-editor pre{margin:16px 0;padding:16px;background:#1e1e2e;border-radius:var(--radius);overflow-x:auto}.wysiwyg-editor pre code{font-family:Fira Code,SF Mono,Menlo,Monaco,Courier New,monospace;font-size:13px;line-height:1.5;color:#cdd6f4;white-space:pre-wrap}.wysiwyg-editor code{font-family:Fira Code,SF Mono,Menlo,Monaco,Courier New,monospace;font-size:.9em;padding:2px 6px;background:var(--color-bg);border-radius:4px}.wysiwyg-editor hr{margin:24px 0;border:none;border-top:2px solid var(--color-border)}.wysiwyg-editor{min-height:400px;padding:32px 40px;outline:none;font-size:16px;line-height:1.7;color:var(--color-text)}.wysiwyg-editor--empty:before{content:attr(data-placeholder);color:var(--color-text-muted);pointer-events:none;position:absolute}.wysiwyg-editor h1{font-size:2.25rem;font-weight:700;line-height:1.2;margin:0 0 1rem;letter-spacing:-.03em;color:var(--color-text)}.wysiwyg-editor h2{font-size:1.75rem;font-weight:600;line-height:1.3;margin:1.5rem 0 .75rem;letter-spacing:-.02em;color:var(--color-text)}.wysiwyg-editor h3{font-size:1.375rem;font-weight:600;line-height:1.4;margin:1.25rem 0 .5rem;letter-spacing:-.01em;color:var(--color-text)}.wysiwyg-editor p{margin:0 0 1rem}.wysiwyg-editor p:last-child{margin-bottom:0}.wysiwyg-editor strong,.wysiwyg-editor b{font-weight:600;color:var(--color-text)}.wysiwyg-editor em,.wysiwyg-editor i{font-style:italic}.wysiwyg-editor u{text-decoration:underline;text-underline-offset:2px}.wysiwyg-editor a{color:var(--color-accent);text-decoration:underline;text-underline-offset:2px;transition:color .15s ease}.wysiwyg-editor a:hover{color:var(--color-accent-hover)}.wysiwyg-editor ul,.wysiwyg-editor ol{margin:0 0 1rem;padding-left:1.5rem}.wysiwyg-editor li{margin:.25rem 0}.wysiwyg-editor li::marker{color:var(--color-accent)}.wysiwyg-editor blockquote{margin:1rem 0;padding:.75rem 1.25rem;border-left:3px solid var(--color-accent);background:var(--color-surface-hover);border-radius:0 var(--radius-sm) var(--radius-sm) 0;font-style:italic;color:var(--color-text-muted)}.wysiwyg-editor code{font-family:SF Mono,Fira Code,monospace;font-size:.875em;background:var(--color-surface-hover);padding:.125rem .375rem;border-radius:var(--radius-sm);color:var(--color-accent)}.wysiwyg-editor pre{margin:1rem 0;padding:1rem;background:var(--color-bg);border-radius:var(--radius);overflow-x:auto}.wysiwyg-editor pre code{background:none;padding:0;font-size:.875rem;line-height:1.6;color:var(--color-text-muted)}.wysiwyg-editor hr{border:none;border-top:1px solid var(--color-border);margin:2rem 0}.wysiwyg-editor img,.wysiwyg-editor video{max-width:100%;height:auto;border-radius:var(--radius);margin:1rem 0;cursor:pointer;transition:outline .15s ease,box-shadow .15s ease}.wysiwyg-editor img:hover,.wysiwyg-editor video:hover{outline:2px solid var(--color-border);outline-offset:2px}.wysiwyg-editor img.media-selected,.wysiwyg-editor video.media-selected{outline:2px solid var(--color-accent);outline-offset:2px;box-shadow:0 0 0 4px var(--color-selection)}.wysiwyg-editor-wrapper.is-dragging{cursor:grabbing}.wysiwyg-editor-wrapper.is-dragging .wysiwyg-editor{-webkit-user-select:none;user-select:none}.wysiwyg-editor .dragging{opacity:.3;outline:2px dashed var(--color-border);outline-offset:2px}.wysiwyg-editor>*:hover{position:relative}.wysiwyg-editor ::selection{background:var(--color-selection)}.wysiwyg-editor table{width:100%;border-collapse:collapse;margin:1rem 0}.wysiwyg-editor th,.wysiwyg-editor td{border:1px solid var(--color-border);padding:.75rem 1rem;text-align:left}.wysiwyg-editor th{background:var(--color-surface-hover);font-weight:600}
|