@rcarls/rc-textarea 0.1.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 +1311 -0
- package/dist/custom-elements.json +2103 -0
- package/dist/demo.css +85 -0
- package/dist/demo.js +40 -0
- package/dist/line-actions-controller-BsM6DJ0b.js +2334 -0
- package/dist/line-actions-controller-BsM6DJ0b.js.map +1 -0
- package/dist/rc-textarea-define.js +10 -0
- package/dist/rc-textarea-define.js.map +1 -0
- package/dist/rc-textarea.js +8 -0
- package/dist/rc-textarea.js.map +1 -0
- package/dist/types/packages/rc-textarea/src/blots.d.ts +39 -0
- package/dist/types/packages/rc-textarea/src/decoration.d.ts +65 -0
- package/dist/types/packages/rc-textarea/src/decoration.test.d.ts +1 -0
- package/dist/types/packages/rc-textarea/src/define.d.ts +1 -0
- package/dist/types/packages/rc-textarea/src/document.d.ts +19 -0
- package/dist/types/packages/rc-textarea/src/index.d.ts +7 -0
- package/dist/types/packages/rc-textarea/src/line-actions-controller.d.ts +91 -0
- package/dist/types/packages/rc-textarea/src/line-decorator.d.ts +57 -0
- package/dist/types/packages/rc-textarea/src/line-decorator.test.d.ts +1 -0
- package/dist/types/packages/rc-textarea/src/pattern-matcher.d.ts +23 -0
- package/dist/types/packages/rc-textarea/src/pattern-matcher.test.d.ts +1 -0
- package/dist/types/packages/rc-textarea/src/rc-textarea.d.ts +194 -0
- package/dist/types/packages/rc-textarea/src/rc-textarea.styles.d.ts +1 -0
- package/dist/types/packages/rc-textarea/src/rc-textarea.test.d.ts +0 -0
- package/dist/types/packages/rc-textarea/src/selection.d.ts +39 -0
- package/dist/types/packages/rc-textarea/src/test-helpers.d.ts +18 -0
- package/dist/types/packages/rc-textarea/src/types.d.ts +394 -0
- package/package.json +68 -0
package/README.md
ADDED
|
@@ -0,0 +1,1311 @@
|
|
|
1
|
+
# rc-textarea
|
|
2
|
+
|
|
3
|
+
A WAI-ARIA compliant enhanced textarea web component built with [Lit 3](https://lit.dev) and [Parchment](https://github.com/quilljs/parchment). Uses a `contenteditable` div as its editing surface, enabling inline visual formatting (bold, italic, color, underlines) that is impossible with a plain `<textarea>`.
|
|
4
|
+
|
|
5
|
+
**rc-textarea is not a rich text editor.** The underlying value is always plain text. Decorations are applied visually only — entirely through the JavaScript API, not through user action.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
1. [Key Capabilities](#key-capabilities)
|
|
12
|
+
2. [Architecture](#architecture)
|
|
13
|
+
3. [Installation](#installation)
|
|
14
|
+
4. [Basic Usage](#basic-usage)
|
|
15
|
+
5. [Attributes & Properties](#attributes--properties)
|
|
16
|
+
6. [Value Property](#value-property)
|
|
17
|
+
7. [Events](#events)
|
|
18
|
+
8. [CSS Customization](#css-customization)
|
|
19
|
+
9. [Plugin API](#plugin-api)
|
|
20
|
+
10. [Decoration Types](#decoration-types)
|
|
21
|
+
11. [Pattern API](#pattern-api)
|
|
22
|
+
12. [Plugin Helpers](#plugin-helpers)
|
|
23
|
+
13. [Selection API](#selection-api)
|
|
24
|
+
14. [Decoration Lifecycle](#decoration-lifecycle)
|
|
25
|
+
15. [Undo / Redo](#undo--redo)
|
|
26
|
+
16. [Keyboard Behavior](#keyboard-behavior)
|
|
27
|
+
17. [Form Integration](#form-integration)
|
|
28
|
+
18. [Accessibility](#accessibility)
|
|
29
|
+
19. [Performance Considerations](#performance-considerations)
|
|
30
|
+
20. [Troubleshooting](#troubleshooting)
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Key Capabilities
|
|
35
|
+
|
|
36
|
+
- **Mixed inline formatting** — bold, italic, color, background, underline styles on arbitrary character ranges
|
|
37
|
+
- **Error-lens style line annotations** — end-of-line messages (like VS Code's error lens)
|
|
38
|
+
- **Inline widgets** — non-editable DOM elements inserted at any character offset
|
|
39
|
+
- **Regex pattern decorations** — auto-decorate text matching regular expressions
|
|
40
|
+
- **Plugin API** — imperative decoration control + highlight.js / prism.js HTML compatibility bridge
|
|
41
|
+
- **Line numbers, word wrap, auto-grow** — feature flags for common use cases
|
|
42
|
+
- **Progressive enhancement** — wraps a native `<textarea>` for form submission; the textarea is hidden, the `contenteditable` div is the interaction surface
|
|
43
|
+
- **Custom undo/redo** — DOM rebuilds invalidate the browser's native undo stack; the component maintains its own
|
|
44
|
+
- **Accessible by default** — `role="textbox"`, `aria-multiline="true"`, spellcheck disabled, focus management
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Architecture
|
|
49
|
+
|
|
50
|
+
### DOM Structure
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
rc-textarea (LitElement shadow host, delegatesFocus=true)
|
|
54
|
+
├── #root (flex container)
|
|
55
|
+
│ ├── #gutter (line/list/custom gutter, optional)
|
|
56
|
+
│ │ └── #gutter-cells (container for .gutter-cell spans, one per line)
|
|
57
|
+
│ └── #editor-area (flex item, grows to fill)
|
|
58
|
+
│ ├── #editor (contenteditable div — Parchment ScrollBlot root)
|
|
59
|
+
│ │ └── .v2-line divs (one per logical line, V2BlockBlot)
|
|
60
|
+
│ │ ├── text nodes (plain text content)
|
|
61
|
+
│ │ ├── .v2-mark spans (V2InlineBlot — mark decorations)
|
|
62
|
+
│ │ ├── .v2-widget spans (V2WidgetBlot — inline widgets, contenteditable=false)
|
|
63
|
+
│ │ └── [data-message] attr (error-lens: used by ::after pseudo-element)
|
|
64
|
+
│ └── <slot> (lightDOM textarea — hidden, form-only)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Shadow DOM usage**: `delegatesFocus=true` ensures focus management works correctly. Line numbers are in the shadow DOM but read-only.
|
|
68
|
+
|
|
69
|
+
### Editing Loop (Data Flow)
|
|
70
|
+
|
|
71
|
+
1. **Browser handles edit** — User types/pastes/deletes text in the contenteditable div
|
|
72
|
+
2. **`input` event fires** → `_onInput()` handler:
|
|
73
|
+
- Save DOM selection to plain-text offsets via `saveSelection()`
|
|
74
|
+
- Extract plain text via `extractEditorText()`
|
|
75
|
+
- Update `this._value`
|
|
76
|
+
- Dispatch `rc-textarea-change` event
|
|
77
|
+
- Map existing decorations through the text change via `mapDecorationsThroughChange()`
|
|
78
|
+
- Schedule RAF render pass
|
|
79
|
+
3. **RAF render pass** → `_performRender()`:
|
|
80
|
+
- Extract plugin decorations
|
|
81
|
+
- Re-run patterns (rebuild all pattern decorations from regex)
|
|
82
|
+
- Ask plugin for new decorations (call `plugin.update()` or `plugin.highlight()`)
|
|
83
|
+
- Build `V2Document` from text + all decorations
|
|
84
|
+
- Tear down and fully rebuild the blot tree
|
|
85
|
+
- Restore DOM selection via `restoreSelection()`
|
|
86
|
+
- Update line number gutter if `lineNumbers=true`
|
|
87
|
+
4. **Decorations stay in sync** — All existing decorations persist through edits (mapped to new offsets)
|
|
88
|
+
|
|
89
|
+
**Key insight for LLM agents**: The blot tree is **rebuilt on every render frame**. This is necessary because decoration changes require DOM restructuring. The undo stack stores plain-text values, not DOM snapshots.
|
|
90
|
+
|
|
91
|
+
### Parchment Integration
|
|
92
|
+
|
|
93
|
+
- **`V2ScrollBlot`** wraps the `#editor` div and suppresses Parchment's `MutationObserver` (which would fight with our render loop)
|
|
94
|
+
- **`V2BlockBlot`** represents a single line (`.v2-line` div)
|
|
95
|
+
- **`V2InlineBlot`** represents a mark decoration (`.v2-mark` span)
|
|
96
|
+
- **`V2WidgetBlot`** represents an inline widget (`.v2-widget` span, `contenteditable=false`)
|
|
97
|
+
- **`V2Document`** class contains the build logic: `build(text, decorations) → BlotTree`
|
|
98
|
+
|
|
99
|
+
The blot tree is **immutable** — each render creates a new tree from scratch, then the DOM is replaced.
|
|
100
|
+
|
|
101
|
+
### Cursor Management
|
|
102
|
+
|
|
103
|
+
Plain-text offsets are essential for:
|
|
104
|
+
- Storing selection state across DOM rebuilds
|
|
105
|
+
- Decoration `from`/`to` ranges
|
|
106
|
+
- Plugin API (selection, cursor position)
|
|
107
|
+
- Undo/redo stack
|
|
108
|
+
|
|
109
|
+
**`selection.ts` module**:
|
|
110
|
+
- `saveSelection()` — convert DOM range to plain-text offsets, skipping widget spans
|
|
111
|
+
- `restoreSelection()` — convert plain-text offsets back to DOM range after rebuild
|
|
112
|
+
|
|
113
|
+
### File Organization
|
|
114
|
+
|
|
115
|
+
| File | Purpose |
|
|
116
|
+
|------|---------|
|
|
117
|
+
| `src/rc-textarea.ts` | Main LitElement: editing loop, plugin/pattern management, form wiring, undo/redo stack, gutter, event dispatch |
|
|
118
|
+
| `src/document.ts` | `V2Document` class — builds Parchment tree from text + decorations; `extractEditorText()` reverse operation |
|
|
119
|
+
| `src/blots.ts` | Parchment blot subclasses (`V2ScrollBlot`, `V2BlockBlot`, `V2InlineBlot`, `V2WidgetBlot`) + blot registry |
|
|
120
|
+
| `src/selection.ts` | `saveSelection()` / `restoreSelection()` for plain-text ↔ DOM range conversion |
|
|
121
|
+
| `src/decoration.ts` | `mapDecorationsThroughChange()` — map existing decorations through text edits; `isLargeChange()` heuristic |
|
|
122
|
+
| `src/pattern-matcher.ts` | `matchPatternResults()` — run `TextPattern` array against text, return mark + line decorations |
|
|
123
|
+
| `src/line-decorator.ts` | `createLineDecoratorPlugin()` factory — wraps per-line decoration logic in a full `RCTextareaPlugin` |
|
|
124
|
+
| `src/types.ts` | All exported TypeScript interfaces and utility types |
|
|
125
|
+
| `src/rc-textarea.styles.ts` | Component CSS (custom properties, parts, internal layout) |
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Installation
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
npm install @rcarls/rc-textarea
|
|
133
|
+
# or
|
|
134
|
+
yarn add @rcarls/rc-textarea
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Import to auto-register the custom element:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import '@rcarls/rc-textarea';
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Or import the class for typed access:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { RCTextarea } from '@rcarls/rc-textarea';
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Basic Usage
|
|
152
|
+
|
|
153
|
+
Slot a native `<textarea>` as the direct child. It is hidden from view and used only for form wiring.
|
|
154
|
+
|
|
155
|
+
```html
|
|
156
|
+
<rc-textarea>
|
|
157
|
+
<textarea name="body" rows="10" placeholder="Start typing…"></textarea>
|
|
158
|
+
</rc-textarea>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The component adopts the textarea's `name`, `required`, `disabled`, `maxlength`, `placeholder`, and initial `value`. Form submission reads the textarea's value, which is kept in sync by the component.
|
|
162
|
+
|
|
163
|
+
### JavaScript access
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
const editor = document.querySelector('rc-textarea');
|
|
167
|
+
|
|
168
|
+
// Read current plain text
|
|
169
|
+
console.log(editor.value);
|
|
170
|
+
|
|
171
|
+
// Set programmatically
|
|
172
|
+
editor.value = 'new content';
|
|
173
|
+
|
|
174
|
+
// Track changes
|
|
175
|
+
editor.addEventListener('rc-textarea-change', (e) => {
|
|
176
|
+
console.log('New value:', e.detail.value);
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Attributes & Properties
|
|
183
|
+
|
|
184
|
+
All attributes reflect to properties. Use attributes in HTML or properties in JS.
|
|
185
|
+
|
|
186
|
+
| Attribute | Property | Type | Default | Description |
|
|
187
|
+
|-----------|----------|------|---------|-------------|
|
|
188
|
+
| `line-numbers` | `lineNumbers` | `boolean` | `false` | Display line number gutter on the left |
|
|
189
|
+
| `list-numbers` | `listNumbers` | `boolean` | `false` | Display a numbered list gutter (skips blank lines, resets counter) |
|
|
190
|
+
| `gutter` | `gutter` | `boolean` | `false` | Show a gutter column without built-in content (plugins fill cells via `LineDecoration.gutterContent`) |
|
|
191
|
+
| `word-wrap` | `wordWrap` | `boolean` | `false` | Enable word wrapping (default: scroll horizontally) |
|
|
192
|
+
| `auto-grow` | `autoGrow` | `boolean` | `false` | Grow container height to fit content |
|
|
193
|
+
| `read-only` | `readOnly` | `boolean` | `false` | Disable text editing; still selectable |
|
|
194
|
+
| `label` | `label` | `string \| null` | `null` | Sets `aria-label` on the editor div |
|
|
195
|
+
|
|
196
|
+
### Example
|
|
197
|
+
|
|
198
|
+
```html
|
|
199
|
+
<rc-textarea
|
|
200
|
+
id="code-editor"
|
|
201
|
+
line-numbers
|
|
202
|
+
word-wrap
|
|
203
|
+
auto-grow
|
|
204
|
+
label="Code editor"
|
|
205
|
+
>
|
|
206
|
+
<textarea name="code" rows="20"></textarea>
|
|
207
|
+
</rc-textarea>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
const editor = document.querySelector('#code-editor');
|
|
212
|
+
console.log(editor.lineNumbers); // true
|
|
213
|
+
console.log(editor.wordWrap); // true
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Value Property
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
const editor = document.querySelector('rc-textarea');
|
|
222
|
+
|
|
223
|
+
// Read
|
|
224
|
+
const plainText = editor.value;
|
|
225
|
+
|
|
226
|
+
// Write — decorations are mapped through the change
|
|
227
|
+
editor.value = 'hello world';
|
|
228
|
+
|
|
229
|
+
// Write via textarea (if slotted)
|
|
230
|
+
const textarea = editor.querySelector('textarea');
|
|
231
|
+
textarea.value = 'test';
|
|
232
|
+
editor.value = textarea.value; // sync if needed
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Setting `value` programmatically:
|
|
236
|
+
1. Updates `this._value` and the slotted textarea
|
|
237
|
+
2. Dispatches `rc-textarea-change` event
|
|
238
|
+
3. Schedules a RAF render pass
|
|
239
|
+
4. Existing decorations are **mapped through the change** (shifted, clamped, or cleared)
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Events
|
|
244
|
+
|
|
245
|
+
All events bubble and are composed (cross shadow boundary).
|
|
246
|
+
|
|
247
|
+
| Event | Detail | Fires when |
|
|
248
|
+
|-------|--------|------------|
|
|
249
|
+
| `rc-textarea-change` | `{ value: string }` | Text changes (input, paste, undo/redo, programmatic `value` set) |
|
|
250
|
+
| `rc-textarea-focus` | (empty) | Editor receives focus |
|
|
251
|
+
| `rc-textarea-blur` | (empty) | Editor loses focus |
|
|
252
|
+
| `rc-textarea-select` | `{ selectionStart: number, selectionEnd: number }` | Selection changes (cursor move, click, keyboard nav) |
|
|
253
|
+
|
|
254
|
+
### Example
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
const editor = document.querySelector('rc-textarea');
|
|
258
|
+
|
|
259
|
+
editor.addEventListener('rc-textarea-change', (e) => {
|
|
260
|
+
console.log('Text changed to:', e.detail.value);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
editor.addEventListener('rc-textarea-select', (e) => {
|
|
264
|
+
const { selectionStart, selectionEnd } = e.detail;
|
|
265
|
+
console.log(`Selection: ${selectionStart}–${selectionEnd}`);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
editor.addEventListener('rc-textarea-focus', () => {
|
|
269
|
+
console.log('Editor focused');
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## CSS Customization
|
|
276
|
+
|
|
277
|
+
### Custom Properties
|
|
278
|
+
|
|
279
|
+
Set on the host element or any ancestor to style the editor.
|
|
280
|
+
|
|
281
|
+
| Property | Default | Description |
|
|
282
|
+
|----------|---------|-------------|
|
|
283
|
+
| `--rc-textarea-font-family` | `monospace` | Font family for text and line numbers |
|
|
284
|
+
| `--rc-textarea-font-size` | `1em` | Base font size |
|
|
285
|
+
| `--rc-textarea-line-height` | `1.5` | Line height affects gutter alignment |
|
|
286
|
+
| `--rc-textarea-padding` | `0.5em` | Inner padding of editor area |
|
|
287
|
+
| `--rc-textarea-background` | `Field` | Editor background color (system color keyword) |
|
|
288
|
+
| `--rc-textarea-color` | `FieldText` | Text color (system color keyword) |
|
|
289
|
+
| `--rc-textarea-caret-color` | (auto) | Cursor/caret color |
|
|
290
|
+
| `--rc-textarea-border` | `1px solid ButtonBorder` | Editor border (system color keyword) |
|
|
291
|
+
| `--rc-textarea-border-radius` | `2px` | Corner rounding |
|
|
292
|
+
| `--rc-textarea-focus-outline` | `2px solid AccentColor` | Focus ring (system color keyword) |
|
|
293
|
+
| `--rc-textarea-active-line-bg` | `transparent` | Background of the line containing the cursor |
|
|
294
|
+
| `--rc-textarea-gutter-color` | `GrayText` | Line number text color |
|
|
295
|
+
| `--rc-textarea-gutter-bg` | `Canvas` | Gutter background |
|
|
296
|
+
| `--rc-textarea-gutter-border` | `1px solid ButtonBorder` | Gutter right border |
|
|
297
|
+
| `--rc-textarea-gutter-padding-inline-end` | `0.75em` | Gap between gutter numbers and editor content |
|
|
298
|
+
|
|
299
|
+
### Example
|
|
300
|
+
|
|
301
|
+
```css
|
|
302
|
+
rc-textarea {
|
|
303
|
+
--rc-textarea-font-family: 'Fira Code', monospace;
|
|
304
|
+
--rc-textarea-font-size: 13px;
|
|
305
|
+
--rc-textarea-line-height: 1.6;
|
|
306
|
+
--rc-textarea-background: #1e1e2e;
|
|
307
|
+
--rc-textarea-color: #cdd6f4;
|
|
308
|
+
--rc-textarea-caret-color: #89b4fa;
|
|
309
|
+
--rc-textarea-border: 1px solid #313244;
|
|
310
|
+
--rc-textarea-border-radius: 4px;
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### CSS Parts
|
|
315
|
+
|
|
316
|
+
Use `::part()` pseudo-element for advanced styling.
|
|
317
|
+
|
|
318
|
+
| Part | Element | Supports |
|
|
319
|
+
|------|---------|----------|
|
|
320
|
+
| `root` | Outer flex container | All CSS |
|
|
321
|
+
| `gutter` | Gutter outer div | All CSS |
|
|
322
|
+
| `gutter-cells` | Inner container holding `.gutter-cell` spans | All CSS |
|
|
323
|
+
| `editor-area` | Container wrapping editor + slot | All CSS |
|
|
324
|
+
| `editor` | The `contenteditable` div | All CSS |
|
|
325
|
+
|
|
326
|
+
### Example
|
|
327
|
+
|
|
328
|
+
```css
|
|
329
|
+
rc-textarea::part(editor) {
|
|
330
|
+
border-radius: 4px;
|
|
331
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
rc-textarea::part(gutter) {
|
|
335
|
+
background: #f5f5f5;
|
|
336
|
+
font-size: 0.9em;
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Mark decoration styling
|
|
341
|
+
|
|
342
|
+
Inline decorations are applied to `.v2-mark` spans. Inline styles handle standard properties (bold, italic, color, etc.). For custom classes:
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
let api: RCTextareaPluginAPI;
|
|
346
|
+
editor.usePlugin({
|
|
347
|
+
mount(a) { api = a; },
|
|
348
|
+
update() {},
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Add decorations with a custom class
|
|
352
|
+
api.setDecorations([
|
|
353
|
+
{ type: 'mark', from: 5, to: 10, className: 'my-highlight' }
|
|
354
|
+
]);
|
|
355
|
+
|
|
356
|
+
// Style it in an adopted stylesheet
|
|
357
|
+
api.adoptStyleSheet(`
|
|
358
|
+
.v2-mark.my-highlight { background: yellow; }
|
|
359
|
+
`);
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Plugin API
|
|
365
|
+
|
|
366
|
+
Plugins are the primary mechanism for applying decorations. A plugin receives a `RCTextareaPluginAPI` during `mount()` — **save this reference** to set decorations from outside the plugin lifecycle.
|
|
367
|
+
|
|
368
|
+
### Interface Overview
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
interface RCTextareaPlugin {
|
|
372
|
+
/** Called once when the plugin is registered. Store `api` here for external use. */
|
|
373
|
+
mount?(api: RCTextareaPluginAPI): void;
|
|
374
|
+
/** Called when the plugin is replaced or the element disconnects. */
|
|
375
|
+
destroy?(): void;
|
|
376
|
+
/**
|
|
377
|
+
* Display-layer value transform — called before `update`/`highlight` in
|
|
378
|
+
* read-only mode. Return a non-null string to substitute it as the rendered
|
|
379
|
+
* text. The underlying `element.value` is never modified.
|
|
380
|
+
*/
|
|
381
|
+
transform?(value: string, api: RCTextareaPluginAPI): string | null | void;
|
|
382
|
+
/** Imperative decoration API — called on each value change. */
|
|
383
|
+
update?(value: string, api: RCTextareaPluginAPI): void | Promise<void>;
|
|
384
|
+
/**
|
|
385
|
+
* HTML-based compat — called on each value change.
|
|
386
|
+
* Return an HTML string (e.g. from hljs/prism); it will be parsed into
|
|
387
|
+
* mark decorations via `api.decorationsFromHtml()`.
|
|
388
|
+
*/
|
|
389
|
+
highlight?(value: string, api: RCTextareaPluginAPI):
|
|
390
|
+
string | null | void | Promise<string | null | void>;
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
At least one of `update` or `highlight` must be provided. `transform` is only
|
|
395
|
+
called in read-only mode and is typically used to substitute scaled or formatted
|
|
396
|
+
display text while preserving the raw underlying value.
|
|
397
|
+
|
|
398
|
+
### PluginAPI Methods
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
interface RCTextareaPluginAPI {
|
|
402
|
+
readonly host: HTMLElement; // rc-textarea element
|
|
403
|
+
readonly value: string; // Current plain text
|
|
404
|
+
readonly selectionStart: number; // Normalized selection start (≤ selectionEnd)
|
|
405
|
+
readonly selectionEnd: number; // Normalized selection end (≥ selectionStart)
|
|
406
|
+
|
|
407
|
+
getCursorRect(): DOMRect | null; // Cursor position in viewport coords (or null if not focused)
|
|
408
|
+
getWordAtCursor(): { word: string; from: number; to: number } | null;
|
|
409
|
+
onCursorMove(cb: (start: number, end: number) => void): () => void;
|
|
410
|
+
|
|
411
|
+
addDecoration(d: DecorationInput): string; // Add single decoration, return ID
|
|
412
|
+
removeDecoration(id: string): void; // Remove by ID
|
|
413
|
+
clearDecorations(): void; // Remove all plugin decorations
|
|
414
|
+
setDecorations(decorations: DecorationInput[]): void; // Replace all with new set
|
|
415
|
+
|
|
416
|
+
scheduleUpdate(): void; // Trigger render outside input events
|
|
417
|
+
|
|
418
|
+
adoptStyleSheet(sheetOrCssText: CSSStyleSheet | string): CSSStyleSheet;
|
|
419
|
+
removeStyleSheet(sheet: CSSStyleSheet): void;
|
|
420
|
+
decorationsFromHtml(html: string): Omit<MarkDecoration, 'id'>[];
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Mounting and Lifecycle
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
const editor = document.querySelector('rc-textarea');
|
|
428
|
+
|
|
429
|
+
let api: RCTextareaPluginAPI;
|
|
430
|
+
|
|
431
|
+
editor.usePlugin({
|
|
432
|
+
mount(a) {
|
|
433
|
+
api = a; // Save reference for external use
|
|
434
|
+
console.log('Plugin mounted');
|
|
435
|
+
},
|
|
436
|
+
update(value, a) {
|
|
437
|
+
// Called on each value change
|
|
438
|
+
const decorations = parseText(value);
|
|
439
|
+
a.setDecorations(decorations);
|
|
440
|
+
},
|
|
441
|
+
destroy() {
|
|
442
|
+
console.log('Plugin unmounting');
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// From outside plugin lifecycle, using saved reference
|
|
447
|
+
api.setDecorations([
|
|
448
|
+
{ type: 'mark', from: 0, to: 5, bold: true }
|
|
449
|
+
]);
|
|
450
|
+
api.scheduleUpdate();
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### Example 1: Synchronous Imperative Plugin
|
|
454
|
+
|
|
455
|
+
Manually find patterns and apply decorations:
|
|
456
|
+
|
|
457
|
+
```ts
|
|
458
|
+
const markdownLitePlugin = {
|
|
459
|
+
update(value, api) {
|
|
460
|
+
const decorations = [];
|
|
461
|
+
|
|
462
|
+
// Bold: **text**
|
|
463
|
+
const boldRe = /\*\*(.+?)\*\*/g;
|
|
464
|
+
let m;
|
|
465
|
+
while ((m = boldRe.exec(value)) !== null) {
|
|
466
|
+
decorations.push({
|
|
467
|
+
type: 'mark',
|
|
468
|
+
from: m.index,
|
|
469
|
+
to: m.index + m[0].length,
|
|
470
|
+
bold: true
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Italic: *text*
|
|
475
|
+
const italicRe = /\*([^*]+)\*/g;
|
|
476
|
+
while ((m = italicRe.exec(value)) !== null) {
|
|
477
|
+
decorations.push({
|
|
478
|
+
type: 'mark',
|
|
479
|
+
from: m.index,
|
|
480
|
+
to: m.index + m[0].length,
|
|
481
|
+
italic: true
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
api.setDecorations(decorations);
|
|
486
|
+
},
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
editor.usePlugin(markdownLitePlugin);
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Example 2: Asynchronous Plugin (WASM / Web Worker)
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
editor.usePlugin({
|
|
496
|
+
async update(value, api) {
|
|
497
|
+
const decorations = await myWasmParser.tokenize(value);
|
|
498
|
+
api.setDecorations(decorations);
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
Stale results are automatically discarded if a newer render starts before the promise resolves.
|
|
504
|
+
|
|
505
|
+
### Example 3: highlight.js Integration
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
import hljs from 'highlight.js/lib/core';
|
|
509
|
+
import javascript from 'highlight.js/lib/languages/javascript';
|
|
510
|
+
hljs.registerLanguage('javascript', javascript);
|
|
511
|
+
|
|
512
|
+
editor.usePlugin({
|
|
513
|
+
mount(api) {
|
|
514
|
+
// Inject theme CSS into shadow root (hljs classes live inside)
|
|
515
|
+
api.adoptStyleSheet(`
|
|
516
|
+
.hljs-keyword { color: #cba6f7; }
|
|
517
|
+
.hljs-string { color: #a6e3a1; }
|
|
518
|
+
.hljs-number { color: #fab387; }
|
|
519
|
+
.hljs-comment { color: #6c7086; font-style: italic; }
|
|
520
|
+
.hljs-title { color: #89b4fa; font-weight: bold; }
|
|
521
|
+
.hljs-built_in { color: #89dceb; }
|
|
522
|
+
.hljs-literal { color: #f5c2e7; }
|
|
523
|
+
`);
|
|
524
|
+
},
|
|
525
|
+
highlight(value, api) {
|
|
526
|
+
// hljs.highlight returns HTML with span wrapping syntax tokens
|
|
527
|
+
const { value: html } = hljs.highlight(value, { language: 'javascript' });
|
|
528
|
+
// Convert HTML token markup to decorations
|
|
529
|
+
api.setDecorations(api.decorationsFromHtml(html));
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
**Important**: hljs token class spans live inside the shadow root. Light-DOM stylesheets cannot pierce the shadow boundary, so theme CSS must be injected via `api.adoptStyleSheet()`.
|
|
535
|
+
|
|
536
|
+
### Example 4: Prism.js Integration
|
|
537
|
+
|
|
538
|
+
```ts
|
|
539
|
+
import Prism from 'prismjs';
|
|
540
|
+
import 'prismjs/components/prism-python';
|
|
541
|
+
|
|
542
|
+
editor.usePlugin({
|
|
543
|
+
mount(api) {
|
|
544
|
+
api.adoptStyleSheet(`
|
|
545
|
+
.token.keyword { color: #66d9ef; font-weight: bold; }
|
|
546
|
+
.token.string { color: #e6db74; }
|
|
547
|
+
.token.number { color: #ae81ff; }
|
|
548
|
+
.token.comment { color: #75715e; font-style: italic; }
|
|
549
|
+
.token.function { color: #a1efe4; }
|
|
550
|
+
`);
|
|
551
|
+
},
|
|
552
|
+
highlight(value, api) {
|
|
553
|
+
const html = Prism.highlight(value, Prism.languages.python, 'python');
|
|
554
|
+
api.setDecorations(api.decorationsFromHtml(html));
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### Switching / Removing Plugins
|
|
560
|
+
|
|
561
|
+
Only one plugin can be active at a time. Calling `usePlugin()` with a new plugin calls `destroy()` on the previous one.
|
|
562
|
+
|
|
563
|
+
```ts
|
|
564
|
+
editor.usePlugin(newPlugin); // previous plugin is destroyed
|
|
565
|
+
editor.removePlugin(); // current plugin destroyed, decorations cleared
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### Accessing Slotted Textarea
|
|
569
|
+
|
|
570
|
+
Plugins can read the textarea to access its attributes:
|
|
571
|
+
|
|
572
|
+
```ts
|
|
573
|
+
editor.usePlugin({
|
|
574
|
+
mount(api) {
|
|
575
|
+
const textarea = api.host.querySelector('textarea');
|
|
576
|
+
if (textarea) {
|
|
577
|
+
console.log('Form name:', textarea.name);
|
|
578
|
+
console.log('Max length:', textarea.maxLength);
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
586
|
+
## Decoration Types
|
|
587
|
+
|
|
588
|
+
Decorations are objects describing visual changes. All decorations are immutable after creation (API methods return new IDs or replace entire sets).
|
|
589
|
+
|
|
590
|
+
### MarkDecoration — styled character range
|
|
591
|
+
|
|
592
|
+
A visual style applied to a contiguous range of characters. Inline styles are rendered directly; `className` adds an extra CSS class for complex styling.
|
|
593
|
+
|
|
594
|
+
```ts
|
|
595
|
+
interface MarkDecoration {
|
|
596
|
+
id: string; // Auto-assigned — omit when passing to API
|
|
597
|
+
type: 'mark';
|
|
598
|
+
from: number; // Inclusive start (0-based character offset into plain text)
|
|
599
|
+
to: number; // Exclusive end
|
|
600
|
+
className?: string; // CSS class added to the span
|
|
601
|
+
bold?: boolean; // Inline: font-weight: bold
|
|
602
|
+
italic?: boolean; // Inline: font-style: italic
|
|
603
|
+
color?: string; // Inline: color (CSS color value)
|
|
604
|
+
background?: string; // Inline: background-color
|
|
605
|
+
underline?: 'solid' | 'wavy' | 'dotted' | 'dashed'; // text-decoration-style
|
|
606
|
+
underlineColor?: string; // text-decoration-color
|
|
607
|
+
attributes?: Record<string, string>; // Extra HTML attributes (data-*, title, etc.)
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### LineDecoration — whole-line styling + error-lens annotation
|
|
612
|
+
|
|
613
|
+
Applied to an entire logical line. Adds a CSS class to the line div and optionally renders an error-lens message at the end.
|
|
614
|
+
|
|
615
|
+
```ts
|
|
616
|
+
interface LineDecoration {
|
|
617
|
+
id: string;
|
|
618
|
+
type: 'line';
|
|
619
|
+
line: number; // 1-based logical line number
|
|
620
|
+
className?: string; // CSS class on the .v2-line div
|
|
621
|
+
message?: string; // Error-lens text (rendered via ::after, not selectable)
|
|
622
|
+
messageClassName?: string; // Space-separated class(es) set as data-message-class
|
|
623
|
+
attributes?: Record<string, string>;
|
|
624
|
+
gutterContent?: string | null; // Override gutter cell text for this line:
|
|
625
|
+
// string — custom label (e.g. "!", "▶")
|
|
626
|
+
// null — force empty (suppress built-in content)
|
|
627
|
+
// omitted — use the built-in mode default
|
|
628
|
+
}
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
Error-lens messages are rendered via CSS `::after` pseudo-element — they do not appear in selection or clipboard.
|
|
632
|
+
|
|
633
|
+
```css
|
|
634
|
+
/* in an adopted stylesheet */
|
|
635
|
+
.v2-line[data-message]::after {
|
|
636
|
+
content: attr(data-message);
|
|
637
|
+
margin-left: 1em;
|
|
638
|
+
color: #f38ba8;
|
|
639
|
+
font-style: italic;
|
|
640
|
+
}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### WidgetDecoration — non-editable inline element
|
|
644
|
+
|
|
645
|
+
A DOM element inserted at a character offset. Widgets are purely visual and do not appear in the plain text value.
|
|
646
|
+
|
|
647
|
+
```ts
|
|
648
|
+
interface WidgetDecoration {
|
|
649
|
+
id: string;
|
|
650
|
+
type: 'widget';
|
|
651
|
+
offset: number; // Character offset — widget placed before/after this position
|
|
652
|
+
create(): HTMLElement; // Factory called each render — must return a **new** element
|
|
653
|
+
side?: 'before' | 'after'; // Placement relative to character. Default: 'before'
|
|
654
|
+
}
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
**Important**: `create()` is called on every render frame. Return a new element each time; do not reuse the same DOM node.
|
|
658
|
+
|
|
659
|
+
```ts
|
|
660
|
+
api.setDecorations([
|
|
661
|
+
{
|
|
662
|
+
type: 'widget',
|
|
663
|
+
offset: 5,
|
|
664
|
+
create() {
|
|
665
|
+
const el = document.createElement('span');
|
|
666
|
+
el.textContent = '👉';
|
|
667
|
+
el.style.color = '#fab387';
|
|
668
|
+
return el;
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
]);
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
### DecorationInput (for API calls)
|
|
675
|
+
|
|
676
|
+
When calling `api.addDecoration()`, `api.setDecorations()`, etc., omit the `id` field:
|
|
677
|
+
|
|
678
|
+
```ts
|
|
679
|
+
type DecorationInput =
|
|
680
|
+
| Omit<MarkDecoration, 'id'>
|
|
681
|
+
| Omit<LineDecoration, 'id'>
|
|
682
|
+
| Omit<WidgetDecoration, 'id'>;
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
---
|
|
686
|
+
|
|
687
|
+
## Pattern API
|
|
688
|
+
|
|
689
|
+
Patterns automatically apply decorations to all regex matches on every value change, without manual event handling.
|
|
690
|
+
|
|
691
|
+
### Adding and Removing Patterns
|
|
692
|
+
|
|
693
|
+
```ts
|
|
694
|
+
const editor = document.querySelector('rc-textarea');
|
|
695
|
+
|
|
696
|
+
// Boolean pattern — bold + orange all "TODO" occurrences
|
|
697
|
+
const patternId = editor.addPattern({
|
|
698
|
+
pattern: /\bTODO\b/g,
|
|
699
|
+
bold: true,
|
|
700
|
+
color: '#fab387',
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
// Remove a specific pattern
|
|
704
|
+
editor.removePattern(patternId);
|
|
705
|
+
|
|
706
|
+
// Remove all patterns
|
|
707
|
+
editor.clearPatterns();
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
### TextPattern Interface
|
|
711
|
+
|
|
712
|
+
```ts
|
|
713
|
+
interface TextPattern {
|
|
714
|
+
id: string; // Auto-assigned — omit when calling addPattern()
|
|
715
|
+
pattern: RegExp; // Global flag is added automatically if missing
|
|
716
|
+
className?: string;
|
|
717
|
+
bold?: boolean;
|
|
718
|
+
italic?: boolean;
|
|
719
|
+
color?: string;
|
|
720
|
+
background?: string;
|
|
721
|
+
underline?: 'solid' | 'wavy' | 'dotted' | 'dashed';
|
|
722
|
+
underlineColor?: string;
|
|
723
|
+
attributes?: Record<string, string>;
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Per-named-capture-group styles. When set, one MarkDecoration is emitted
|
|
727
|
+
* per named group instead of one for the whole match. The 'd' flag
|
|
728
|
+
* (indices) is added to the pattern automatically.
|
|
729
|
+
* Unmatched optional groups are silently skipped.
|
|
730
|
+
*/
|
|
731
|
+
captureGroups?: Record<string, MarkDecorationStyle>;
|
|
732
|
+
|
|
733
|
+
// Callback to generate a LineDecoration for matching lines
|
|
734
|
+
createLineDecoration?: (match: RegExpMatchArray) =>
|
|
735
|
+
| Omit<LineDecoration, 'id' | 'type' | 'line'>
|
|
736
|
+
| null;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/** Subset of MarkDecoration properties used for styling (no id / from / to). */
|
|
740
|
+
type MarkDecorationStyle = Pick<MarkDecoration,
|
|
741
|
+
'className' | 'bold' | 'italic' | 'color' | 'background' |
|
|
742
|
+
'underline' | 'underlineColor' | 'attributes'
|
|
743
|
+
>;
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
### Example: Simple Pattern
|
|
747
|
+
|
|
748
|
+
```ts
|
|
749
|
+
// Red wavy underline for "FIXME"
|
|
750
|
+
editor.addPattern({
|
|
751
|
+
pattern: /\bFIXME\b/g,
|
|
752
|
+
color: '#f38ba8',
|
|
753
|
+
underline: 'wavy',
|
|
754
|
+
underlineColor: '#f38ba8',
|
|
755
|
+
});
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
### Example: Pattern with Error-Lens Annotation
|
|
759
|
+
|
|
760
|
+
```ts
|
|
761
|
+
editor.addPattern({
|
|
762
|
+
pattern: /\bFIXME\b/g,
|
|
763
|
+
bold: true,
|
|
764
|
+
color: '#f38ba8',
|
|
765
|
+
underline: 'wavy',
|
|
766
|
+
createLineDecoration: (match) => ({
|
|
767
|
+
className: 'fixme-line',
|
|
768
|
+
message: 'Review before release',
|
|
769
|
+
messageClassName: 'fixme-message',
|
|
770
|
+
}),
|
|
771
|
+
});
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
Then style the message:
|
|
775
|
+
|
|
776
|
+
```ts
|
|
777
|
+
api.adoptStyleSheet(`
|
|
778
|
+
.v2-line[data-message-class~="fixme-message"]::after {
|
|
779
|
+
color: #f38ba8;
|
|
780
|
+
font-weight: bold;
|
|
781
|
+
}
|
|
782
|
+
`);
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
### Example: Multi-line Regex (JavaScript comments)
|
|
786
|
+
|
|
787
|
+
```ts
|
|
788
|
+
editor.addPattern({
|
|
789
|
+
pattern: /\/\*[\s\S]*?\*\//g,
|
|
790
|
+
italic: true,
|
|
791
|
+
color: '#6c7086',
|
|
792
|
+
createLineDecoration: () => ({
|
|
793
|
+
message: '(comment)',
|
|
794
|
+
messageClassName: 'comment-marker',
|
|
795
|
+
}),
|
|
796
|
+
});
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
### Example: Named Capture Groups (`captureGroups`)
|
|
800
|
+
|
|
801
|
+
Use `captureGroups` to style different parts of a match independently without
|
|
802
|
+
manual offset arithmetic. One `MarkDecoration` is emitted per named group;
|
|
803
|
+
unmatched optional groups are skipped.
|
|
804
|
+
|
|
805
|
+
```ts
|
|
806
|
+
// key: value lines — key in purple, value in green
|
|
807
|
+
editor.addPattern({
|
|
808
|
+
pattern: /^(?<key>\w[\w-]*):\s*(?<value>.+)$/gm,
|
|
809
|
+
captureGroups: {
|
|
810
|
+
key: { bold: true, color: '#c792ea' },
|
|
811
|
+
value: { color: '#c3e88d' },
|
|
812
|
+
},
|
|
813
|
+
});
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
```ts
|
|
817
|
+
// Ingredient lines — quantity/measure bold, prep text muted
|
|
818
|
+
editor.addPattern({
|
|
819
|
+
pattern: /^(?<qty>[\d\s\/\u215B-\u215E]+\s+\w+)\s+(?<name>[^,]+?)(?<prep>,\s+.+)?$/gm,
|
|
820
|
+
captureGroups: {
|
|
821
|
+
qty: { bold: true },
|
|
822
|
+
name: { bold: true, color: 'var(--color-primary)' },
|
|
823
|
+
prep: { italic: true, color: 'var(--color-text-muted)' },
|
|
824
|
+
},
|
|
825
|
+
});
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
---
|
|
829
|
+
|
|
830
|
+
## Plugin Helpers
|
|
831
|
+
|
|
832
|
+
Two utilities are exported from the package to reduce plugin boilerplate.
|
|
833
|
+
|
|
834
|
+
### `matchPatternResults(value, patterns)`
|
|
835
|
+
|
|
836
|
+
Run a `TextPattern` array against `value` and return all matches as
|
|
837
|
+
decoration objects — without registering the patterns on the editor element.
|
|
838
|
+
Useful when a plugin needs to combine pattern-matched decorations with custom
|
|
839
|
+
decorations in a single `api.setDecorations()` call.
|
|
840
|
+
|
|
841
|
+
```ts
|
|
842
|
+
import { matchPatternResults } from '@rcarls/rc-textarea';
|
|
843
|
+
|
|
844
|
+
const KEYWORD_PATTERNS = [
|
|
845
|
+
{ id: 'kw-function', pattern: /\bfunction\b/g, bold: true, color: '#c792ea' },
|
|
846
|
+
{ id: 'kw-return', pattern: /\breturn\b/g, bold: true, color: '#89ddff' },
|
|
847
|
+
];
|
|
848
|
+
|
|
849
|
+
editor.usePlugin({
|
|
850
|
+
update(value, api) {
|
|
851
|
+
const { markDecorations, lineDecorations } =
|
|
852
|
+
matchPatternResults(value, KEYWORD_PATTERNS);
|
|
853
|
+
|
|
854
|
+
// Merge with custom diagnostics from a parser
|
|
855
|
+
const diagnostics = myParser.lint(value);
|
|
856
|
+
|
|
857
|
+
api.setDecorations([
|
|
858
|
+
...markDecorations,
|
|
859
|
+
...lineDecorations,
|
|
860
|
+
...diagnostics,
|
|
861
|
+
]);
|
|
862
|
+
},
|
|
863
|
+
});
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
### `createLineDecoratorPlugin(decorator, options?)`
|
|
867
|
+
|
|
868
|
+
Factory that wraps a `LineDecoratorPlugin` in a full `RCTextareaPlugin`. It handles:
|
|
869
|
+
|
|
870
|
+
- CSS injection via `api.adoptStyleSheet` on `mount()`
|
|
871
|
+
- `lineStart` offset bookkeeping — `decorateLine()` works with **line-relative** offsets (0 = start of the line), not absolute document offsets
|
|
872
|
+
- Optional `watch` subscriptions that call `api.scheduleUpdate()` when external values change (framework-agnostic)
|
|
873
|
+
- Cleanup of subscribers on `destroy()`
|
|
874
|
+
|
|
875
|
+
```ts
|
|
876
|
+
import { createLineDecoratorPlugin } from '@rcarls/rc-textarea';
|
|
877
|
+
import type { LineDecoratorPlugin } from '@rcarls/rc-textarea';
|
|
878
|
+
|
|
879
|
+
const KEYWORD_CSS = '.kw { font-weight: bold; color: #c792ea; }';
|
|
880
|
+
|
|
881
|
+
const keywordDecorator: LineDecoratorPlugin = {
|
|
882
|
+
styles: KEYWORD_CSS,
|
|
883
|
+
decorateLine(line) {
|
|
884
|
+
const results = [];
|
|
885
|
+
for (const m of line.matchAll(/\bfunction\b/g)) {
|
|
886
|
+
results.push({
|
|
887
|
+
type: 'mark' as const,
|
|
888
|
+
from: m.index!,
|
|
889
|
+
to: m.index! + m[0].length,
|
|
890
|
+
className: 'kw',
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
return results;
|
|
894
|
+
},
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
editor.usePlugin(createLineDecoratorPlugin(keywordDecorator));
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
#### With `extraDecorations` (merging whole-document results)
|
|
901
|
+
|
|
902
|
+
```ts
|
|
903
|
+
editor.usePlugin(createLineDecoratorPlugin(
|
|
904
|
+
myLineDecorator,
|
|
905
|
+
{
|
|
906
|
+
extraDecorations: (value) => {
|
|
907
|
+
// e.g. whole-document diagnostics from a parser
|
|
908
|
+
return myParser.lint(value).map(d => ({
|
|
909
|
+
type: 'line' as const,
|
|
910
|
+
line: d.line,
|
|
911
|
+
message: d.message,
|
|
912
|
+
messageClassName: 'diagnostic-error',
|
|
913
|
+
}));
|
|
914
|
+
},
|
|
915
|
+
},
|
|
916
|
+
));
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
#### With `watch` (react to external signal changes)
|
|
920
|
+
|
|
921
|
+
`watch` is an array of subscriber setup functions — each receives an `onChange`
|
|
922
|
+
callback and may return an optional cleanup function. This is
|
|
923
|
+
intentionally framework-agnostic.
|
|
924
|
+
|
|
925
|
+
```ts
|
|
926
|
+
// Vanilla JS: subscribe to a custom event
|
|
927
|
+
editor.usePlugin(createLineDecoratorPlugin(
|
|
928
|
+
myDecorator,
|
|
929
|
+
{
|
|
930
|
+
watch: [
|
|
931
|
+
(onChange) => {
|
|
932
|
+
window.addEventListener('theme-change', onChange);
|
|
933
|
+
return () => window.removeEventListener('theme-change', onChange);
|
|
934
|
+
},
|
|
935
|
+
],
|
|
936
|
+
},
|
|
937
|
+
));
|
|
938
|
+
|
|
939
|
+
// Solid.js: pass reactive signals
|
|
940
|
+
import { createEffect, on } from 'solid-js';
|
|
941
|
+
|
|
942
|
+
editor.usePlugin(createLineDecoratorPlugin(
|
|
943
|
+
myDecorator,
|
|
944
|
+
{
|
|
945
|
+
watch: [
|
|
946
|
+
(cb) => createEffect(on(mySignal, cb, { defer: true })),
|
|
947
|
+
],
|
|
948
|
+
},
|
|
949
|
+
));
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
---
|
|
953
|
+
|
|
954
|
+
## Selection API
|
|
955
|
+
|
|
956
|
+
The component tracks the current selection as plain-text offsets (not DOM nodes or ranges).
|
|
957
|
+
|
|
958
|
+
### Getting Selection
|
|
959
|
+
|
|
960
|
+
```ts
|
|
961
|
+
interface RCTextareaPluginAPI {
|
|
962
|
+
readonly selectionStart: number;
|
|
963
|
+
readonly selectionEnd: number;
|
|
964
|
+
|
|
965
|
+
getCursorRect(): DOMRect | null;
|
|
966
|
+
getWordAtCursor(): { word: string; from: number; to: number } | null;
|
|
967
|
+
onCursorMove(callback: (start: number, end: number) => void): () => void;
|
|
968
|
+
}
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
### Example: Tracking Cursor Moves
|
|
972
|
+
|
|
973
|
+
```ts
|
|
974
|
+
let unsub: () => void;
|
|
975
|
+
|
|
976
|
+
editor.usePlugin({
|
|
977
|
+
mount(api) {
|
|
978
|
+
unsub = api.onCursorMove((start, end) => {
|
|
979
|
+
if (start === end) {
|
|
980
|
+
// Cursor is collapsed (no selection)
|
|
981
|
+
const word = api.getWordAtCursor();
|
|
982
|
+
if (word) {
|
|
983
|
+
console.log(`Cursor on word: "${word.word}" at ${word.from}–${word.to}`);
|
|
984
|
+
}
|
|
985
|
+
} else {
|
|
986
|
+
// Selection active
|
|
987
|
+
console.log(`Selection: ${start}–${end}`);
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
},
|
|
991
|
+
destroy() {
|
|
992
|
+
unsub?.();
|
|
993
|
+
},
|
|
994
|
+
});
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
### Example: Anchoring Autocomplete Popup
|
|
998
|
+
|
|
999
|
+
```ts
|
|
1000
|
+
editor.usePlugin({
|
|
1001
|
+
mount(api) {
|
|
1002
|
+
const unsub = api.onCursorMove(() => {
|
|
1003
|
+
const rect = api.getCursorRect();
|
|
1004
|
+
if (rect) {
|
|
1005
|
+
const popup = document.querySelector('#autocomplete');
|
|
1006
|
+
popup.style.left = rect.left + 'px';
|
|
1007
|
+
popup.style.top = (rect.top + rect.height) + 'px';
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
return () => unsub?.();
|
|
1011
|
+
},
|
|
1012
|
+
});
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
---
|
|
1016
|
+
|
|
1017
|
+
## Decoration Lifecycle
|
|
1018
|
+
|
|
1019
|
+
Decorations undergo transformation as the text changes:
|
|
1020
|
+
|
|
1021
|
+
### Mapping Decorations Through Changes
|
|
1022
|
+
|
|
1023
|
+
When text is edited, existing plugin decorations are automatically adjusted:
|
|
1024
|
+
|
|
1025
|
+
- **Before the edit region**: start/end offsets unchanged
|
|
1026
|
+
- **After the edit region**: start/end offsets shifted by the character delta
|
|
1027
|
+
- **Overlapping the edit region**: start/end clamped to edit boundaries
|
|
1028
|
+
- **Zero-width after clamping**: decoration is dropped
|
|
1029
|
+
|
|
1030
|
+
### Large Change Heuristic
|
|
1031
|
+
|
|
1032
|
+
If a change is detected as **"large"** (heuristic: `changeSize > 50 chars AND changeSize > 50% of document`), all plugin decorations are cleared to avoid mapping errors. This handles:
|
|
1033
|
+
|
|
1034
|
+
- Paste of large text blocks
|
|
1035
|
+
- Select-all + type
|
|
1036
|
+
- Programmatic `value` set with very different content
|
|
1037
|
+
|
|
1038
|
+
Pattern decorations are **always** fully recomputed on each change (regex is re-run).
|
|
1039
|
+
|
|
1040
|
+
### Architecture Note for Agents
|
|
1041
|
+
|
|
1042
|
+
Decoration mapping occurs in `mapDecorationsThroughChange()` (in `decoration.ts`). The function:
|
|
1043
|
+
1. Calls `findEdit()` to locate insertion/deletion boundaries
|
|
1044
|
+
2. Applies geometry transformation to each decoration
|
|
1045
|
+
3. Returns a new decoration map (old map is not mutated)
|
|
1046
|
+
|
|
1047
|
+
---
|
|
1048
|
+
|
|
1049
|
+
## Undo / Redo
|
|
1050
|
+
|
|
1051
|
+
The component maintains its own undo/redo stack because DOM rebuilds (which happen on every render frame) invalidate the browser's native contenteditable undo history.
|
|
1052
|
+
|
|
1053
|
+
### Keyboard Shortcuts
|
|
1054
|
+
|
|
1055
|
+
| Key | Action |
|
|
1056
|
+
|-----|--------|
|
|
1057
|
+
| `Ctrl+Z` / `Cmd+Z` | Undo |
|
|
1058
|
+
| `Ctrl+Y` / `Cmd+Y` / `Ctrl+Shift+Z` | Redo |
|
|
1059
|
+
|
|
1060
|
+
### Stack Details
|
|
1061
|
+
|
|
1062
|
+
- **Capacity**: 100 entries (MAX_UNDO)
|
|
1063
|
+
- **Stored per entry**: plain text value + cursor position (anchorOffset, focusOffset)
|
|
1064
|
+
- **Decorations**: not stored; recomputed from the restored value
|
|
1065
|
+
- **Granularity**: one entry per input event (or scheduled update)
|
|
1066
|
+
|
|
1067
|
+
### Architecture
|
|
1068
|
+
|
|
1069
|
+
The undo stack is stored in:
|
|
1070
|
+
```ts
|
|
1071
|
+
private _undoStack: UndoEntry[] = [];
|
|
1072
|
+
private _undoIndex = -1; // Current position in stack
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
Each entry is created when text changes via input event. The stack is pruned to MAX_UNDO when full.
|
|
1076
|
+
|
|
1077
|
+
---
|
|
1078
|
+
|
|
1079
|
+
## Keyboard Behavior
|
|
1080
|
+
|
|
1081
|
+
| Key | Behavior |
|
|
1082
|
+
|-----|----------|
|
|
1083
|
+
| `Tab` | Insert `\t` character (does not move focus) |
|
|
1084
|
+
| `Ctrl/Cmd+Z` | Undo |
|
|
1085
|
+
| `Ctrl/Cmd+Y` / `Ctrl/Cmd+Shift+Z` | Redo |
|
|
1086
|
+
| `Paste` | HTML/rich text is stripped; only plain text is inserted |
|
|
1087
|
+
| `Enter` | Insert line break (`\n`) |
|
|
1088
|
+
|
|
1089
|
+
### Paste Handling
|
|
1090
|
+
|
|
1091
|
+
The component uses `input` event to detect and normalize pasted content. HTML markup is discarded; only plain text characters are inserted.
|
|
1092
|
+
|
|
1093
|
+
---
|
|
1094
|
+
|
|
1095
|
+
## Form Integration
|
|
1096
|
+
|
|
1097
|
+
The slotted `<textarea>` participates in form submission normally:
|
|
1098
|
+
|
|
1099
|
+
1. **Hidden visually** — inline styles: `position: absolute; left: -9999px; opacity: 0; clip: rect(0 0 0 0)`
|
|
1100
|
+
2. **DOM remains** — slotted at the light DOM so form traversal finds it
|
|
1101
|
+
3. **Value synced** — component keeps textarea's value in sync with `this._value`
|
|
1102
|
+
4. **Attributes adopted** — `name`, `required`, `disabled`, `maxlength`, `placeholder` are read from textarea on mount
|
|
1103
|
+
5. **Submission** — form submission reads textarea's value directly
|
|
1104
|
+
|
|
1105
|
+
### Example
|
|
1106
|
+
|
|
1107
|
+
```html
|
|
1108
|
+
<form id="myform">
|
|
1109
|
+
<rc-textarea>
|
|
1110
|
+
<textarea
|
|
1111
|
+
name="body"
|
|
1112
|
+
required
|
|
1113
|
+
maxlength="5000"
|
|
1114
|
+
placeholder="Enter your message…"
|
|
1115
|
+
></textarea>
|
|
1116
|
+
</rc-textarea>
|
|
1117
|
+
<button type="submit">Send</button>
|
|
1118
|
+
</form>
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
```ts
|
|
1122
|
+
document.getElementById('myform').addEventListener('submit', (e) => {
|
|
1123
|
+
e.preventDefault();
|
|
1124
|
+
const formData = new FormData(e.target);
|
|
1125
|
+
console.log(formData.get('body')); // Plain text value
|
|
1126
|
+
});
|
|
1127
|
+
```
|
|
1128
|
+
|
|
1129
|
+
---
|
|
1130
|
+
|
|
1131
|
+
## Accessibility
|
|
1132
|
+
|
|
1133
|
+
The component is designed with accessibility in mind:
|
|
1134
|
+
|
|
1135
|
+
### ARIA Roles & Attributes
|
|
1136
|
+
|
|
1137
|
+
- **`role="textbox"`** — identifies the editor as a text input
|
|
1138
|
+
- **`aria-multiline="true"`** — indicates multiline text support
|
|
1139
|
+
- **`aria-label`** — set via the `label` attribute (optional)
|
|
1140
|
+
- **`delegatesFocus=true`** — shadow DOM focus is delegated to the editable region
|
|
1141
|
+
|
|
1142
|
+
### Keyboard Navigation
|
|
1143
|
+
|
|
1144
|
+
- Tab and Shift+Tab focus/blur the editor normally
|
|
1145
|
+
- Arrow keys, Home/End, Ctrl+Arrow, etc. work natively
|
|
1146
|
+
- Screen readers can read selected text and cursor position
|
|
1147
|
+
|
|
1148
|
+
### Visual Accessibility
|
|
1149
|
+
|
|
1150
|
+
- High contrast by default (system colors)
|
|
1151
|
+
- `:focus-visible` indicator (can be styled via CSS custom property)
|
|
1152
|
+
- Line numbers are `aria-hidden="true"` (not part of screen reader navigation)
|
|
1153
|
+
|
|
1154
|
+
### Spellcheck Disabled
|
|
1155
|
+
|
|
1156
|
+
Spell checking is disabled to prevent vendor-specific squiggles from interfering with custom decorations. Set explicitly in the editor:
|
|
1157
|
+
|
|
1158
|
+
```ts
|
|
1159
|
+
editor.spellcheck = false;
|
|
1160
|
+
editor.autocorrect = 'off';
|
|
1161
|
+
editor.autocapitalize = 'off';
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
---
|
|
1165
|
+
|
|
1166
|
+
## Performance Considerations
|
|
1167
|
+
|
|
1168
|
+
### DOM Rebuild on Every Render
|
|
1169
|
+
|
|
1170
|
+
The blot tree is **completely rebuilt** on each render frame (RAF-batched). This is necessary because:
|
|
1171
|
+
- Decoration changes require DOM restructuring
|
|
1172
|
+
- Plain-text ↔ DOM offset conversion requires a stable tree
|
|
1173
|
+
- Cursor restoration requires rebuilding after changes
|
|
1174
|
+
|
|
1175
|
+
For documents **< 10,000 characters**, this is imperceptible. For very large documents, consider:
|
|
1176
|
+
- Truncating visible content (virtual scrolling)
|
|
1177
|
+
- Debouncing plugin updates
|
|
1178
|
+
- Using pattern decorations instead of plugin updates (patterns are optimized)
|
|
1179
|
+
|
|
1180
|
+
### Decoration Density
|
|
1181
|
+
|
|
1182
|
+
Large numbers of overlapping decorations (e.g., 1000+ marks on a single line) will impact performance. Keep decoration counts reasonable:
|
|
1183
|
+
- Syntax highlighting: typically 10–100 marks per line
|
|
1184
|
+
- Error markers: typically 1–5 per line
|
|
1185
|
+
- Patterns: typically < 50 matches per line
|
|
1186
|
+
|
|
1187
|
+
If density is high, profile with DevTools to identify bottlenecks.
|
|
1188
|
+
|
|
1189
|
+
### Memory
|
|
1190
|
+
|
|
1191
|
+
The undo/redo stack stores 100 entries of `{ value, anchorOffset, focusOffset }`. For a 10 KB document, this is ~1 MB. For very large documents, consider limiting undo depth or using an external undo manager.
|
|
1192
|
+
|
|
1193
|
+
---
|
|
1194
|
+
|
|
1195
|
+
## Troubleshooting
|
|
1196
|
+
|
|
1197
|
+
### Selection is lost after edit
|
|
1198
|
+
|
|
1199
|
+
Normal behavior — selection is saved before render and restored after. If you're updating text and immediately reading `selectionStart`, use `onCursorMove()` or `setTimeout()` to wait for the next render frame.
|
|
1200
|
+
|
|
1201
|
+
```ts
|
|
1202
|
+
editor.value = 'new text';
|
|
1203
|
+
// DON'T do this:
|
|
1204
|
+
console.log(editor.selectionStart); // May be stale
|
|
1205
|
+
|
|
1206
|
+
// DO this:
|
|
1207
|
+
await editor.updateComplete;
|
|
1208
|
+
const start = editor.value === 'new text' ? api.selectionStart : null;
|
|
1209
|
+
```
|
|
1210
|
+
|
|
1211
|
+
### Decorations disappear after large paste
|
|
1212
|
+
|
|
1213
|
+
If you paste a large block of text (> 50 chars and > 50% of document), plugin decorations are cleared by the "large change heuristic" to avoid mapping errors. Pattern decorations are reapplied. To preserve plugin decorations, update them in the plugin's `update()` method:
|
|
1214
|
+
|
|
1215
|
+
```ts
|
|
1216
|
+
editor.usePlugin({
|
|
1217
|
+
update(value, api) {
|
|
1218
|
+
// Recompute decorations from the new value
|
|
1219
|
+
const decs = parseText(value);
|
|
1220
|
+
api.setDecorations(decs);
|
|
1221
|
+
},
|
|
1222
|
+
});
|
|
1223
|
+
```
|
|
1224
|
+
|
|
1225
|
+
### Other's DOM change events fire inside editor
|
|
1226
|
+
|
|
1227
|
+
The component does not suppress mutation events on the editor. If you're listening for `MutationObserver` events on the editor element, you'll observe all blot tree rebuilds. To avoid this, listen outside the editor or throttle updates.
|
|
1228
|
+
|
|
1229
|
+
### cursor doesn't stay at expected position
|
|
1230
|
+
|
|
1231
|
+
Cursor restoration uses plain-text offsets. If decorations change (especially widgets, which take up no space in the value), the visual cursor position may shift. This is expected. Use `getCursorRect()` to anchor UI elements if precise positioning is critical.
|
|
1232
|
+
|
|
1233
|
+
### Text is very long (> 100 KB), editor is slow
|
|
1234
|
+
|
|
1235
|
+
The entire blot tree is rebuilt on each character input. For very large documents:
|
|
1236
|
+
- Profile with DevTools Performance tab to identify bottleneck
|
|
1237
|
+
- Consider truncating visible content (virtual scrolling)
|
|
1238
|
+
- Use `word-wrap: false` to reduce line breaks (fewer blots)
|
|
1239
|
+
- Limit undo depth: only keep last 10 entries instead of 100
|
|
1240
|
+
|
|
1241
|
+
### Plugin's `highlight()` result is overwritten immediately
|
|
1242
|
+
|
|
1243
|
+
If both `update()` and `highlight()` are provided, only one runs per render. The component runs `highlight()` if it returns a truthy value; otherwise, `update()` is called.
|
|
1244
|
+
|
|
1245
|
+
---
|
|
1246
|
+
|
|
1247
|
+
## API Reference Summary
|
|
1248
|
+
|
|
1249
|
+
### Properties
|
|
1250
|
+
|
|
1251
|
+
| Property | Type | Default | Reflects |
|
|
1252
|
+
|----------|------|---------|----------|
|
|
1253
|
+
| `value` | `string` | `''` | N/A |
|
|
1254
|
+
| `lineNumbers` | `boolean` | `false` | Yes (*line-numbers* attr) |
|
|
1255
|
+
| `listNumbers` | `boolean` | `false` | Yes (*list-numbers* attr) |
|
|
1256
|
+
| `gutter` | `boolean` | `false` | Yes (*gutter* attr) |
|
|
1257
|
+
| `wordWrap` | `boolean` | `false` | Yes (*word-wrap* attr) |
|
|
1258
|
+
| `autoGrow` | `boolean` | `false` | Yes (*auto-grow* attr) |
|
|
1259
|
+
| `readOnly` | `boolean` | `false` | Yes (*read-only* attr) |
|
|
1260
|
+
| `label` | `string \| null` | `null` | No |
|
|
1261
|
+
| `selectionStart` | `number` | `0` | No (readonly) |
|
|
1262
|
+
| `selectionEnd` | `number` | `0` | No (readonly) |
|
|
1263
|
+
|
|
1264
|
+
### Methods
|
|
1265
|
+
|
|
1266
|
+
| Method | Signature | Returns |
|
|
1267
|
+
|--------|-----------|---------|
|
|
1268
|
+
| `usePlugin()` | `(plugin: RCTextareaPlugin) => void` | — |
|
|
1269
|
+
| `removePlugin()` | `() => void` | — |
|
|
1270
|
+
| `addPattern()` | `(pattern: Omit<TextPattern, 'id'>) => string` | Pattern ID |
|
|
1271
|
+
| `removePattern()` | `(id: string) => void` | — |
|
|
1272
|
+
| `clearPatterns()` | `() => void` | — |
|
|
1273
|
+
|
|
1274
|
+
### Exported Helpers
|
|
1275
|
+
|
|
1276
|
+
| Export | Kind | Description |
|
|
1277
|
+
| ------ | ---- | ----------- |
|
|
1278
|
+
| `matchPatternResults` | function | Run `TextPattern[]` against a string; returns `{ markDecorations, lineDecorations }` |
|
|
1279
|
+
| `createLineDecoratorPlugin` | function | Wrap a `LineDecoratorPlugin` in a full `RCTextareaPlugin` |
|
|
1280
|
+
| `MarkDecorationStyle` | type | Styling-only subset of `MarkDecoration` (used in `TextPattern.captureGroups`) |
|
|
1281
|
+
| `LineDecoratorPlugin` | interface | Per-line decorator with line-relative offsets |
|
|
1282
|
+
| `LineDecoratorPluginOptions` | interface | Options for `createLineDecoratorPlugin` |
|
|
1283
|
+
|
|
1284
|
+
### Events
|
|
1285
|
+
|
|
1286
|
+
| Event | Bubbles | Composed | Detail |
|
|
1287
|
+
|-------|---------|----------|--------|
|
|
1288
|
+
| `rc-textarea-change` | Yes | Yes | `{ value: string }` |
|
|
1289
|
+
| `rc-textarea-focus` | Yes | Yes | — |
|
|
1290
|
+
| `rc-textarea-blur` | Yes | Yes | — |
|
|
1291
|
+
| `rc-textarea-select` | Yes | Yes | `{ selectionStart: number, selectionEnd: number }` |
|
|
1292
|
+
|
|
1293
|
+
### CSS Custom Properties
|
|
1294
|
+
|
|
1295
|
+
All custom properties are listed in [CSS Customization](#css-customization).
|
|
1296
|
+
|
|
1297
|
+
### CSS Parts
|
|
1298
|
+
|
|
1299
|
+
All parts are listed in [CSS Customization](#css-customization).
|
|
1300
|
+
|
|
1301
|
+
---
|
|
1302
|
+
|
|
1303
|
+
## Support & Contributing
|
|
1304
|
+
|
|
1305
|
+
For issues, feature requests, or contributions, visit the [rc-webcomponents repository](https://github.com/richardcarls/rc-webcomponents).
|
|
1306
|
+
|
|
1307
|
+
---
|
|
1308
|
+
|
|
1309
|
+
## License
|
|
1310
|
+
|
|
1311
|
+
[MIT](LICENSE) © Richard Carls
|