torch-glare 1.2.8 → 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.
- package/apps/lib/components/TableDnDWrapper.ts +495 -0
- package/apps/lib/components/TextEditor.tsx +53 -1
- package/dist/bin/index.js +5 -0
- package/dist/bin/index.js.map +1 -1
- package/dist/src/commands/mcp.d.ts +2 -0
- package/dist/src/commands/mcp.d.ts.map +1 -0
- package/dist/src/commands/mcp.js +91 -0
- package/dist/src/commands/mcp.js.map +1 -0
- package/dist/src/shared/configureFonts.d.ts +6 -0
- package/dist/src/shared/configureFonts.d.ts.map +1 -0
- package/dist/src/shared/configureFonts.js +106 -0
- package/dist/src/shared/configureFonts.js.map +1 -0
- package/dist/src/shared/configureGlobalCss.d.ts +6 -0
- package/dist/src/shared/configureGlobalCss.d.ts.map +1 -0
- package/dist/src/shared/configureGlobalCss.js +154 -0
- package/dist/src/shared/configureGlobalCss.js.map +1 -0
- package/dist/src/shared/configureTailwind.d.ts +7 -0
- package/dist/src/shared/configureTailwind.d.ts.map +1 -0
- package/dist/src/shared/configureTailwind.js +163 -0
- package/dist/src/shared/configureTailwind.js.map +1 -0
- package/dist/src/shared/detectFramework.d.ts +23 -0
- package/dist/src/shared/detectFramework.d.ts.map +1 -0
- package/dist/src/shared/detectFramework.js +119 -0
- package/dist/src/shared/detectFramework.js.map +1 -0
- package/dist/src/shared/getDependenciesAndInstallNestedComponents.d.ts.map +1 -1
- package/dist/src/shared/getDependenciesAndInstallNestedComponents.js +18 -2
- package/dist/src/shared/getDependenciesAndInstallNestedComponents.js.map +1 -1
- package/dist/src/shared/installBaseUtils.d.ts +5 -0
- package/dist/src/shared/installBaseUtils.d.ts.map +1 -0
- package/dist/src/shared/installBaseUtils.js +79 -0
- package/dist/src/shared/installBaseUtils.js.map +1 -0
- package/dist/src/shared/resolveAliases.d.ts +24 -0
- package/dist/src/shared/resolveAliases.d.ts.map +1 -0
- package/dist/src/shared/resolveAliases.js +98 -0
- package/dist/src/shared/resolveAliases.js.map +1 -0
- package/docs/components/breadcrumb.md +955 -0
- package/docs/components/button-group.md +647 -0
- package/docs/components/text-editor.md +711 -0
- package/docs/components/toggle-button.md +640 -0
- package/docs/tutorials/getting-started.md +123 -431
- package/package.json +1 -1
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: TextEditor
|
|
3
|
+
version: 1.1.15
|
|
4
|
+
status: stable
|
|
5
|
+
category: components/editors
|
|
6
|
+
tags: [editor, rich-text, block-editor, editorjs, markdown, rtl, accessible]
|
|
7
|
+
last-reviewed: 2024-11-05
|
|
8
|
+
bundle-size: 45kb
|
|
9
|
+
dependencies:
|
|
10
|
+
- "@editorjs/editorjs": "^2.28.0"
|
|
11
|
+
- "@editorjs/header": "^2.8.0"
|
|
12
|
+
- "@editorjs/list": "^1.9.0"
|
|
13
|
+
- "@editorjs/nested-list": "^1.4.0"
|
|
14
|
+
- "@editorjs/checklist": "^1.6.0"
|
|
15
|
+
- "@editorjs/quote": "^2.6.0"
|
|
16
|
+
- "@editorjs/warning": "^1.4.0"
|
|
17
|
+
- "@editorjs/code": "^2.9.0"
|
|
18
|
+
- "@editorjs/delimiter": "^1.4.0"
|
|
19
|
+
- "@editorjs/embed": "^2.7.0"
|
|
20
|
+
- "@editorjs/table": "^2.3.0"
|
|
21
|
+
- "@editorjs/link": "^2.6.0"
|
|
22
|
+
- "@editorjs/simple-image": "^1.6.0"
|
|
23
|
+
- "@editorjs/raw": "^2.5.0"
|
|
24
|
+
- "@editorjs/marker": "^1.4.0"
|
|
25
|
+
- "@editorjs/inline-code": "^1.5.0"
|
|
26
|
+
- "@editorjs/underline": "^1.1.0"
|
|
27
|
+
- "@editorjs/text-variant-tune": "^1.0.0"
|
|
28
|
+
- "class-variance-authority": "^0.7.0"
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
# TextEditor
|
|
32
|
+
|
|
33
|
+
> A feature-rich block-style text editor built on Editor.js with 18+ block tools, automatic RTL/LTR detection, markdown paste support, dark mode theming, and imperative ref methods for programmatic control.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install torch-glare
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Import
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { TextEditor } from 'torch-glare/lib/components/TextEditor'
|
|
45
|
+
// or
|
|
46
|
+
import { TextEditor } from 'torch-glare/lib/components'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick Examples
|
|
50
|
+
|
|
51
|
+
### Basic Usage
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { TextEditor } from 'torch-glare/lib/components/TextEditor'
|
|
55
|
+
|
|
56
|
+
function Example() {
|
|
57
|
+
return (
|
|
58
|
+
<TextEditor
|
|
59
|
+
placeholder="Start writing..."
|
|
60
|
+
onChange={(data) => console.log(data)}
|
|
61
|
+
/>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### With Initial Data
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { TextEditor } from 'torch-glare/lib/components/TextEditor'
|
|
70
|
+
import type { OutputData } from '@editorjs/editorjs'
|
|
71
|
+
|
|
72
|
+
function Example() {
|
|
73
|
+
const initialData: OutputData = {
|
|
74
|
+
time: Date.now(),
|
|
75
|
+
blocks: [
|
|
76
|
+
{
|
|
77
|
+
type: 'header',
|
|
78
|
+
data: { text: 'Welcome', level: 2 }
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: 'paragraph',
|
|
82
|
+
data: { text: 'Start editing this document.' }
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<TextEditor
|
|
89
|
+
data={initialData}
|
|
90
|
+
onChange={(data) => console.log('Saved:', data)}
|
|
91
|
+
/>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Controlled with Ref
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { useRef } from 'react'
|
|
100
|
+
import { TextEditor, TextEditorRef } from 'torch-glare/lib/components/TextEditor'
|
|
101
|
+
|
|
102
|
+
function EditorWithControls() {
|
|
103
|
+
const editorRef = useRef<TextEditorRef>(null)
|
|
104
|
+
|
|
105
|
+
const handleSave = async () => {
|
|
106
|
+
if (editorRef.current) {
|
|
107
|
+
const data = await editorRef.current.save()
|
|
108
|
+
console.log('Editor content:', data)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const handleClear = () => {
|
|
113
|
+
editorRef.current?.clear()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div>
|
|
118
|
+
<div className="flex gap-2 mb-4">
|
|
119
|
+
<button onClick={handleSave}>Save</button>
|
|
120
|
+
<button onClick={handleClear}>Clear</button>
|
|
121
|
+
</div>
|
|
122
|
+
<TextEditor ref={editorRef} size="L" />
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Different Sizes
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
<TextEditor size="S" placeholder="Small editor (200px)" />
|
|
132
|
+
<TextEditor size="M" placeholder="Medium editor (300px)" />
|
|
133
|
+
<TextEditor size="L" placeholder="Large editor (400px)" />
|
|
134
|
+
<TextEditor size="XL" placeholder="Extra large editor (500px)" />
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Custom Min Height
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
<TextEditor
|
|
141
|
+
minHeight={600}
|
|
142
|
+
placeholder="Custom height editor"
|
|
143
|
+
/>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Read-Only Mode
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
function ReadOnlyEditor({ content }: { content: OutputData }) {
|
|
150
|
+
return (
|
|
151
|
+
<TextEditor
|
|
152
|
+
data={content}
|
|
153
|
+
readOnly={true}
|
|
154
|
+
size="L"
|
|
155
|
+
/>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### With Theme Override
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
<TextEditor theme="dark" size="M" placeholder="Dark theme editor" />
|
|
164
|
+
<TextEditor theme="light" size="M" placeholder="Light theme editor" />
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Disabled State
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
<TextEditor
|
|
171
|
+
disabled
|
|
172
|
+
data={someData}
|
|
173
|
+
size="M"
|
|
174
|
+
/>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Custom Tools Override
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import Header from '@editorjs/header'
|
|
181
|
+
import List from '@editorjs/list'
|
|
182
|
+
|
|
183
|
+
function MinimalEditor() {
|
|
184
|
+
const customTools = {
|
|
185
|
+
header: {
|
|
186
|
+
class: Header,
|
|
187
|
+
inlineToolbar: true,
|
|
188
|
+
config: { levels: [2, 3], defaultLevel: 2 }
|
|
189
|
+
},
|
|
190
|
+
list: {
|
|
191
|
+
class: List,
|
|
192
|
+
inlineToolbar: true
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<TextEditor
|
|
198
|
+
tools={customTools}
|
|
199
|
+
placeholder="Only headers and lists allowed"
|
|
200
|
+
/>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### With onReady Callback
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
function EditorWithReadyState() {
|
|
209
|
+
const [isReady, setIsReady] = useState(false)
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<div>
|
|
213
|
+
{!isReady && <div>Loading editor...</div>}
|
|
214
|
+
<TextEditor
|
|
215
|
+
onReady={() => setIsReady(true)}
|
|
216
|
+
placeholder="Editor loads asynchronously"
|
|
217
|
+
/>
|
|
218
|
+
</div>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Markdown Paste Support
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
function MarkdownEditor() {
|
|
227
|
+
return (
|
|
228
|
+
<TextEditor
|
|
229
|
+
size="L"
|
|
230
|
+
placeholder="Try pasting markdown content here..."
|
|
231
|
+
onChange={(data) => console.log(data)}
|
|
232
|
+
/>
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
// Pasting markdown like "# Heading\n- item 1\n- item 2" will
|
|
236
|
+
// auto-convert to Editor.js header and list blocks.
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## API Reference
|
|
240
|
+
|
|
241
|
+
### Props
|
|
242
|
+
|
|
243
|
+
| Prop | Type | Default | Description |
|
|
244
|
+
|------|------|---------|-------------|
|
|
245
|
+
| `variant` | `'PresentationStyle'` | `'PresentationStyle'` | Visual style variant |
|
|
246
|
+
| `size` | `'S' \| 'M' \| 'L' \| 'XL'` | - | Size preset controlling min-height and padding |
|
|
247
|
+
| `theme` | `'light' \| 'dark' \| 'default'` | - | Override theme for this component |
|
|
248
|
+
| `data` | `OutputData` | - | Initial editor content (Editor.js OutputData format) |
|
|
249
|
+
| `onChange` | `(data: OutputData) => void` | - | Called when content changes (debounced) |
|
|
250
|
+
| `onReady` | `() => void` | - | Called when editor finishes initialization |
|
|
251
|
+
| `readOnly` | `boolean` | `false` | Makes the editor non-editable |
|
|
252
|
+
| `placeholder` | `string` | `'Write something or press / to select a tool'` | Placeholder text for empty editor |
|
|
253
|
+
| `autofocus` | `boolean` | `false` | Auto-focus the editor on mount |
|
|
254
|
+
| `tools` | `Record<string, any>` | Default 18+ tools | Override the Editor.js tools configuration |
|
|
255
|
+
| `minHeight` | `number` | - | Custom min-height in pixels (overrides size) |
|
|
256
|
+
| `disabled` | `boolean` | `false` | Disables interaction with reduced opacity |
|
|
257
|
+
| `className` | `string` | - | Additional CSS classes |
|
|
258
|
+
|
|
259
|
+
### Ref Methods (TextEditorRef)
|
|
260
|
+
|
|
261
|
+
| Method | Signature | Description |
|
|
262
|
+
|--------|-----------|-------------|
|
|
263
|
+
| `save` | `() => Promise<OutputData>` | Saves and returns the current editor content |
|
|
264
|
+
| `clear` | `() => void` | Clears all editor content |
|
|
265
|
+
| `render` | `(data: OutputData) => Promise<void>` | Renders the provided data into the editor |
|
|
266
|
+
| `focus` | `(atEnd?: boolean) => boolean` | Focuses the editor; optionally at the end of content |
|
|
267
|
+
| `getInstance` | `() => EditorJS \| null` | Returns the raw Editor.js instance |
|
|
268
|
+
|
|
269
|
+
### Size Variants
|
|
270
|
+
|
|
271
|
+
| Size | Min Height | Padding |
|
|
272
|
+
|------|-----------|---------|
|
|
273
|
+
| S | 200px | 8px (p-2) |
|
|
274
|
+
| M | 300px | 12px (p-3) |
|
|
275
|
+
| L | 400px | 16px (p-4) |
|
|
276
|
+
| XL | 500px | 20px (p-5) |
|
|
277
|
+
|
|
278
|
+
### Default Block Tools
|
|
279
|
+
|
|
280
|
+
| Tool | Shortcut | Description |
|
|
281
|
+
|------|----------|-------------|
|
|
282
|
+
| Header | `Cmd+Shift+H` | H1-H6 headings |
|
|
283
|
+
| List | `Cmd+Shift+L` | Ordered/unordered lists |
|
|
284
|
+
| NestedList | - | Multi-level nested lists |
|
|
285
|
+
| Checklist | - | Interactive checkboxes |
|
|
286
|
+
| Quote | `Cmd+Shift+O` | Block quotes with caption |
|
|
287
|
+
| Warning | - | Warning/notice blocks |
|
|
288
|
+
| Code | `Cmd+Shift+C` | Code snippets |
|
|
289
|
+
| Delimiter | - | Horizontal divider |
|
|
290
|
+
| Embed | - | YouTube, Vimeo, CodePen, Twitter, Instagram, GitHub |
|
|
291
|
+
| Table | `Cmd+Alt+T` | Data tables (default 2x3) |
|
|
292
|
+
| LinkTool | - | Link previews |
|
|
293
|
+
| SimpleImage | - | Image blocks via URL |
|
|
294
|
+
| Raw | - | Raw HTML blocks |
|
|
295
|
+
| Chart | - | Interactive chart blocks (bar, line, pie, doughnut, radar, polar) |
|
|
296
|
+
| Marker | `Cmd+Shift+M` | Inline text highlighting |
|
|
297
|
+
| InlineCode | `Cmd+Shift+I` | Inline code formatting |
|
|
298
|
+
| Underline | - | Inline underline formatting |
|
|
299
|
+
| TextVariant | - | Block-level text tune (callout, citation, details) |
|
|
300
|
+
|
|
301
|
+
### TypeScript
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
import { HTMLAttributes } from 'react'
|
|
305
|
+
import { VariantProps } from 'class-variance-authority'
|
|
306
|
+
import { OutputData } from '@editorjs/editorjs'
|
|
307
|
+
|
|
308
|
+
type Themes = 'light' | 'dark' | 'default'
|
|
309
|
+
|
|
310
|
+
export interface TextEditorRef {
|
|
311
|
+
save: () => Promise<OutputData>
|
|
312
|
+
clear: () => void
|
|
313
|
+
render: (data: OutputData) => Promise<void>
|
|
314
|
+
focus: (atEnd?: boolean) => boolean
|
|
315
|
+
getInstance: () => EditorJS | null
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
interface TextEditorProps
|
|
319
|
+
extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'>,
|
|
320
|
+
VariantProps<typeof textEditorStyles> {
|
|
321
|
+
theme?: Themes
|
|
322
|
+
data?: OutputData
|
|
323
|
+
onChange?: (data: OutputData) => void
|
|
324
|
+
onReady?: () => void
|
|
325
|
+
readOnly?: boolean
|
|
326
|
+
placeholder?: string
|
|
327
|
+
autofocus?: boolean
|
|
328
|
+
tools?: Record<string, any>
|
|
329
|
+
minHeight?: number
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export const TextEditor: React.ForwardRefExoticComponent<
|
|
333
|
+
TextEditorProps & React.RefAttributes<TextEditorRef>
|
|
334
|
+
>
|
|
335
|
+
|
|
336
|
+
export type { TextEditorProps, OutputData }
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
## Common Patterns
|
|
340
|
+
|
|
341
|
+
### Blog Post Editor
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
import { useRef, useState } from 'react'
|
|
345
|
+
import { TextEditor, TextEditorRef } from 'torch-glare/lib/components/TextEditor'
|
|
346
|
+
import { Button } from 'torch-glare/lib/components/Button'
|
|
347
|
+
import type { OutputData } from '@editorjs/editorjs'
|
|
348
|
+
|
|
349
|
+
function BlogPostEditor() {
|
|
350
|
+
const editorRef = useRef<TextEditorRef>(null)
|
|
351
|
+
const [isSaving, setIsSaving] = useState(false)
|
|
352
|
+
|
|
353
|
+
const handlePublish = async () => {
|
|
354
|
+
if (!editorRef.current) return
|
|
355
|
+
setIsSaving(true)
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const content = await editorRef.current.save()
|
|
359
|
+
await publishPost(content)
|
|
360
|
+
} finally {
|
|
361
|
+
setIsSaving(false)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return (
|
|
366
|
+
<div className="max-w-3xl mx-auto">
|
|
367
|
+
<TextEditor
|
|
368
|
+
ref={editorRef}
|
|
369
|
+
size="XL"
|
|
370
|
+
autofocus
|
|
371
|
+
placeholder="Write your blog post..."
|
|
372
|
+
onChange={(data) => autoSaveDraft(data)}
|
|
373
|
+
/>
|
|
374
|
+
<div className="mt-4 flex justify-end">
|
|
375
|
+
<Button
|
|
376
|
+
variant="PrimeStyle"
|
|
377
|
+
is_loading={isSaving}
|
|
378
|
+
onClick={handlePublish}
|
|
379
|
+
>
|
|
380
|
+
Publish
|
|
381
|
+
</Button>
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Preview Mode Toggle
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
function EditorWithPreview() {
|
|
392
|
+
const editorRef = useRef<TextEditorRef>(null)
|
|
393
|
+
const [readOnly, setReadOnly] = useState(false)
|
|
394
|
+
|
|
395
|
+
return (
|
|
396
|
+
<div>
|
|
397
|
+
<div className="flex gap-2 mb-4">
|
|
398
|
+
<Button
|
|
399
|
+
variant={readOnly ? 'BorderStyle' : 'PrimeStyle'}
|
|
400
|
+
onClick={() => setReadOnly(false)}
|
|
401
|
+
>
|
|
402
|
+
Edit
|
|
403
|
+
</Button>
|
|
404
|
+
<Button
|
|
405
|
+
variant={readOnly ? 'PrimeStyle' : 'BorderStyle'}
|
|
406
|
+
onClick={() => setReadOnly(true)}
|
|
407
|
+
>
|
|
408
|
+
Preview
|
|
409
|
+
</Button>
|
|
410
|
+
</div>
|
|
411
|
+
<TextEditor
|
|
412
|
+
ref={editorRef}
|
|
413
|
+
readOnly={readOnly}
|
|
414
|
+
size="L"
|
|
415
|
+
/>
|
|
416
|
+
</div>
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Load and Render Saved Content
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
function DocumentViewer({ documentId }: { documentId: string }) {
|
|
425
|
+
const editorRef = useRef<TextEditorRef>(null)
|
|
426
|
+
|
|
427
|
+
useEffect(() => {
|
|
428
|
+
async function loadDocument() {
|
|
429
|
+
const data = await fetchDocument(documentId)
|
|
430
|
+
await editorRef.current?.render(data)
|
|
431
|
+
}
|
|
432
|
+
loadDocument()
|
|
433
|
+
}, [documentId])
|
|
434
|
+
|
|
435
|
+
return (
|
|
436
|
+
<TextEditor
|
|
437
|
+
ref={editorRef}
|
|
438
|
+
readOnly
|
|
439
|
+
size="XL"
|
|
440
|
+
/>
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Form Integration
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
import { useForm, Controller } from 'react-hook-form'
|
|
449
|
+
|
|
450
|
+
function ArticleForm() {
|
|
451
|
+
const { control, handleSubmit } = useForm()
|
|
452
|
+
|
|
453
|
+
return (
|
|
454
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
455
|
+
<Controller
|
|
456
|
+
name="content"
|
|
457
|
+
control={control}
|
|
458
|
+
render={({ field }) => (
|
|
459
|
+
<TextEditor
|
|
460
|
+
data={field.value}
|
|
461
|
+
onChange={field.onChange}
|
|
462
|
+
size="L"
|
|
463
|
+
placeholder="Article content..."
|
|
464
|
+
/>
|
|
465
|
+
)}
|
|
466
|
+
/>
|
|
467
|
+
<Button type="submit" variant="PrimeStyle">
|
|
468
|
+
Save Article
|
|
469
|
+
</Button>
|
|
470
|
+
</form>
|
|
471
|
+
)
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
## Testing
|
|
476
|
+
|
|
477
|
+
### Unit Test Example
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
import { render, screen, waitFor } from '@testing-library/react'
|
|
481
|
+
import { TextEditor, TextEditorRef } from 'torch-glare/lib/components/TextEditor'
|
|
482
|
+
|
|
483
|
+
describe('TextEditor', () => {
|
|
484
|
+
it('renders the editor container', () => {
|
|
485
|
+
const { container } = render(
|
|
486
|
+
<TextEditor size="M" />
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
expect(container.querySelector('.torch-text-editor')).toBeInTheDocument()
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
it('applies size class', () => {
|
|
493
|
+
const { container } = render(
|
|
494
|
+
<TextEditor size="L" />
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
const editor = container.querySelector('.torch-text-editor')
|
|
498
|
+
expect(editor).toHaveClass('min-h-[400px]')
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
it('applies disabled state', () => {
|
|
502
|
+
const { container } = render(
|
|
503
|
+
<TextEditor disabled />
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
const editor = container.querySelector('.torch-text-editor')
|
|
507
|
+
expect(editor).toHaveClass('cursor-not-allowed', 'pointer-events-none')
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
it('calls onReady when editor initializes', async () => {
|
|
511
|
+
const onReady = jest.fn()
|
|
512
|
+
render(<TextEditor onReady={onReady} />)
|
|
513
|
+
|
|
514
|
+
await waitFor(() => {
|
|
515
|
+
expect(onReady).toHaveBeenCalledTimes(1)
|
|
516
|
+
})
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
it('exposes ref methods', async () => {
|
|
520
|
+
const ref = React.createRef<TextEditorRef>()
|
|
521
|
+
render(<TextEditor ref={ref} />)
|
|
522
|
+
|
|
523
|
+
await waitFor(() => {
|
|
524
|
+
expect(ref.current).toBeDefined()
|
|
525
|
+
expect(ref.current?.save).toBeDefined()
|
|
526
|
+
expect(ref.current?.clear).toBeDefined()
|
|
527
|
+
expect(ref.current?.render).toBeDefined()
|
|
528
|
+
expect(ref.current?.focus).toBeDefined()
|
|
529
|
+
expect(ref.current?.getInstance).toBeDefined()
|
|
530
|
+
})
|
|
531
|
+
})
|
|
532
|
+
})
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
## Accessibility
|
|
536
|
+
|
|
537
|
+
### Keyboard Support
|
|
538
|
+
|
|
539
|
+
- **Tab**: Navigate between block tools and editor content
|
|
540
|
+
- **Enter**: Create new block
|
|
541
|
+
- **/**: Open block tool selector (slash command)
|
|
542
|
+
- **Cmd+Shift+H**: Insert header
|
|
543
|
+
- **Cmd+Shift+L**: Insert list
|
|
544
|
+
- **Cmd+Shift+O**: Insert quote
|
|
545
|
+
- **Cmd+Shift+C**: Insert code block
|
|
546
|
+
- **Cmd+Alt+T**: Insert table
|
|
547
|
+
- **Cmd+Shift+M**: Apply marker highlight
|
|
548
|
+
- **Cmd+Shift+I**: Apply inline code
|
|
549
|
+
|
|
550
|
+
### ARIA Attributes
|
|
551
|
+
|
|
552
|
+
The TextEditor renders semantic HTML through Editor.js:
|
|
553
|
+
|
|
554
|
+
```html
|
|
555
|
+
<div
|
|
556
|
+
data-theme="dark"
|
|
557
|
+
class="torch-text-editor"
|
|
558
|
+
spellcheck="false"
|
|
559
|
+
translate="no"
|
|
560
|
+
>
|
|
561
|
+
<div id="torch-editor-xxxxx">
|
|
562
|
+
<!-- Editor.js renders accessible content blocks -->
|
|
563
|
+
</div>
|
|
564
|
+
</div>
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
### RTL/LTR Support
|
|
568
|
+
|
|
569
|
+
- Automatic per-block direction detection based on first visible character
|
|
570
|
+
- Supports Arabic, Hebrew, Farsi, and other RTL scripts
|
|
571
|
+
- List bullets and checkboxes flip correctly in RTL mode
|
|
572
|
+
- Mixed-direction documents supported (each block detects independently)
|
|
573
|
+
|
|
574
|
+
### Screen Reader Support
|
|
575
|
+
|
|
576
|
+
- Editor.js blocks render as semantic HTML (headings, lists, paragraphs)
|
|
577
|
+
- Block tools accessible via keyboard navigation
|
|
578
|
+
- Inline toolbar supports keyboard shortcuts
|
|
579
|
+
|
|
580
|
+
## Styling
|
|
581
|
+
|
|
582
|
+
### Custom Styles with className
|
|
583
|
+
|
|
584
|
+
```typescript
|
|
585
|
+
<TextEditor
|
|
586
|
+
className="shadow-lg border border-gray-200 rounded-lg"
|
|
587
|
+
size="L"
|
|
588
|
+
/>
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### Theme Customization
|
|
592
|
+
|
|
593
|
+
```css
|
|
594
|
+
/* Custom theme variables */
|
|
595
|
+
[data-theme="custom"] .torch-text-editor {
|
|
596
|
+
--color-background: #your-bg;
|
|
597
|
+
--color-text-primary: #your-text;
|
|
598
|
+
--color-border: #your-border;
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### Dark Mode
|
|
603
|
+
|
|
604
|
+
The TextEditor includes comprehensive dark mode CSS variable overrides for:
|
|
605
|
+
|
|
606
|
+
- Editor.js core popover menus
|
|
607
|
+
- Inline toolbar
|
|
608
|
+
- Table plugin cells, borders, and toolbox
|
|
609
|
+
- Search fields in popovers
|
|
610
|
+
- Chart block tool elements
|
|
611
|
+
- Scrollbar styling
|
|
612
|
+
|
|
613
|
+
Dark mode activates automatically via `data-theme="dark"` on the component or a parent element.
|
|
614
|
+
|
|
615
|
+
### Design Token Classes
|
|
616
|
+
|
|
617
|
+
The PresentationStyle variant maps to the following design tokens:
|
|
618
|
+
|
|
619
|
+
- Background: `bg-background-presentation-form-field-primary`
|
|
620
|
+
- Text: `text-content-presentation-global-primary`
|
|
621
|
+
- Selection: `bg-background-presentation-action-hover`
|
|
622
|
+
- Marker: `bg-background-presentation-state-warning-primary`
|
|
623
|
+
- Toolbar: `text-content-presentation-action-light-primary`
|
|
624
|
+
- Popover: `bg-background-presentation-form-field-primary`
|
|
625
|
+
- Code: `bg-background-presentation-action-secondary`
|
|
626
|
+
|
|
627
|
+
## Performance
|
|
628
|
+
|
|
629
|
+
| Metric | Value |
|
|
630
|
+
|--------|-------|
|
|
631
|
+
| Bundle size (gzip) | ~45kb (with all tools) |
|
|
632
|
+
| First render | ~50ms |
|
|
633
|
+
| onChange debounce | 500ms + requestIdleCallback |
|
|
634
|
+
| Tree-shakeable | Partially (tools are bundled) |
|
|
635
|
+
|
|
636
|
+
### Optimization Tips
|
|
637
|
+
|
|
638
|
+
1. Use `tools` prop to include only needed block tools for smaller bundles
|
|
639
|
+
2. The `onChange` callback is debounced (500ms) and deferred to idle time via `requestIdleCallback`
|
|
640
|
+
3. Auto-direction uses `requestAnimationFrame` batching to avoid layout thrashing
|
|
641
|
+
4. Markdown paste uses a single `render()` call instead of per-block insertion
|
|
642
|
+
5. MutationObserver watches only direct children of the redactor (not full subtree)
|
|
643
|
+
|
|
644
|
+
## Troubleshooting
|
|
645
|
+
|
|
646
|
+
### Common Issues
|
|
647
|
+
|
|
648
|
+
#### Editor not initializing
|
|
649
|
+
|
|
650
|
+
**Solution:** Ensure the component is mounted in a client component with `"use client"` directive. TextEditor requires browser APIs.
|
|
651
|
+
|
|
652
|
+
```typescript
|
|
653
|
+
"use client"
|
|
654
|
+
|
|
655
|
+
import { TextEditor } from 'torch-glare/lib/components/TextEditor'
|
|
656
|
+
|
|
657
|
+
export default function Page() {
|
|
658
|
+
return <TextEditor />
|
|
659
|
+
}
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
#### onChange fires too frequently
|
|
663
|
+
|
|
664
|
+
**Solution:** The onChange is already debounced at 500ms + `requestIdleCallback`. For additional throttling, wrap your handler:
|
|
665
|
+
|
|
666
|
+
```typescript
|
|
667
|
+
const debouncedSave = useMemo(
|
|
668
|
+
() => debounce((data: OutputData) => saveToServer(data), 2000),
|
|
669
|
+
[]
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
<TextEditor onChange={debouncedSave} />
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
#### RTL text not detected
|
|
676
|
+
|
|
677
|
+
**Solution:** The auto-direction detects RTL based on the first visible character. If your text starts with LTR characters (numbers, punctuation), the block will be LTR. Start with an RTL character for correct detection.
|
|
678
|
+
|
|
679
|
+
#### Tailwind resets strip heading styles
|
|
680
|
+
|
|
681
|
+
**Solution:** The TextEditor injects heading styles automatically via an internal stylesheet that restores h1-h6 sizing within the `.torch-text-editor` container. No additional configuration needed.
|
|
682
|
+
|
|
683
|
+
## Related Components
|
|
684
|
+
|
|
685
|
+
- [Input](/docs/components/input.md) - Single-line text input
|
|
686
|
+
- [Textarea](/docs/components/textarea.md) - Multi-line plain text input
|
|
687
|
+
- [Form](/docs/components/form.md) - Form wrapper for validation
|
|
688
|
+
|
|
689
|
+
## Browser Support
|
|
690
|
+
|
|
691
|
+
- Chrome 90+ (requestIdleCallback supported)
|
|
692
|
+
- Firefox 88+ (requestIdleCallback supported)
|
|
693
|
+
- Safari 14+ (fallback to setTimeout for requestIdleCallback)
|
|
694
|
+
- Edge 90+
|
|
695
|
+
- Mobile browsers (touch-friendly block controls)
|
|
696
|
+
|
|
697
|
+
## Changelog
|
|
698
|
+
|
|
699
|
+
### v1.1.15
|
|
700
|
+
- Added ChartBlockTool for interactive chart creation
|
|
701
|
+
- Added markdown paste support with auto-conversion
|
|
702
|
+
- Added auto RTL/LTR detection per block
|
|
703
|
+
- Comprehensive dark mode CSS variable overrides
|
|
704
|
+
- Debounced onChange with requestIdleCallback scheduling
|
|
705
|
+
- Performance-optimized MutationObserver for direction tracking
|
|
706
|
+
|
|
707
|
+
### v1.1.0
|
|
708
|
+
- Initial stable release with 18 block tools
|
|
709
|
+
- PresentationStyle variant
|
|
710
|
+
- 4 size presets
|
|
711
|
+
- Imperative ref API
|