bestricheditor 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 ADDED
@@ -0,0 +1,271 @@
1
+ # Best Rich Editor
2
+
3
+ **One library. Every editing style.**
4
+
5
+ Most rich text editors force you to pick one paradigm and stick with it. Best Rich Editor ships all three — structured block editing, Markdown with live preview, and WYSIWYG — behind a single, consistent API. Switch modes per editor instance. Mix them on the same page.
6
+
7
+ | Mode | What it is | When to use it |
8
+ |---|---|---|
9
+ | **BRE** | Block editor — drag-and-drop blocks, slash menu | Documents, wikis, structured content |
10
+ | **BREM** | Markdown editor — textarea + live preview | Developers, technical writers |
11
+ | **BREW** | WYSIWYG editor — formatting toolbar | Non-technical users, prose-heavy content |
12
+
13
+ - **Zero framework dependencies** — works with React, Vue, Svelte, Angular, or plain HTML
14
+ - **Two runtime deps only** — [DOMPurify](https://github.com/cure53/DOMPurify) (XSS sanitization) + [KaTeX](https://katex.org/) (math rendering)
15
+ - **14 block types** — headings, lists, quotes, code, tables, images, audio, video, KaTeX formulas, 2-column layouts
16
+ - **Fully serializable** — `getJSON()` / `setJSON()` / `getHTML()` on every mode
17
+ - **Safe by default** — all HTML output sanitized via DOMPurify; URL injection prevented
18
+
19
+ ---
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install bestricheditor
25
+ ```
26
+
27
+ If you use the **ESM build**, also import the stylesheet:
28
+
29
+ ```js
30
+ import 'bestricheditor/dist/bre.css';
31
+ ```
32
+
33
+ The **UMD build** (`dist/bre.umd.js`) injects CSS automatically — no separate import needed.
34
+
35
+ ---
36
+
37
+ ## Quick start
38
+
39
+ ### BRE — Block editor
40
+
41
+ Drag-and-drop blocks, slash menu, keyboard navigation, 2-column layout.
42
+
43
+ ```js
44
+ import { createEditor } from 'bestricheditor';
45
+ import 'bestricheditor/dist/bre.css';
46
+
47
+ const editor = createEditor(document.getElementById('editor'), {
48
+ mode: 'BRE', // default, can be omitted
49
+ onChange: (doc) => console.log('changed:', doc),
50
+ });
51
+
52
+ editor.setJSON(myDocument);
53
+
54
+ const doc = editor.getJSON(); // structured JSON document
55
+ const html = editor.getHTML(); // sanitized HTML string
56
+
57
+ editor.destroy();
58
+ ```
59
+
60
+ ### BREM — Markdown editor
61
+
62
+ Write Markdown source; blur or click **Preview** to render. Click the preview to go back.
63
+
64
+ ```js
65
+ const editor = createEditor(container, { mode: 'BREM' });
66
+ ```
67
+
68
+ Supports: headings, bold/italic/code, lists, blockquotes, code fences, tables, links, images, audio, video, `$inline$` and `$$block$$` KaTeX math.
69
+
70
+ ### BREW — WYSIWYG editor
71
+
72
+ Formatting toolbar, no Markdown knowledge required.
73
+
74
+ ```js
75
+ const editor = createEditor(container, { mode: 'BREW' });
76
+ ```
77
+
78
+ Toolbar includes: paragraph/heading selector, bold/italic/underline, bulleted list, numbered list, quote, code block, divider, link, KaTeX formula, table, image, audio, video.
79
+
80
+ ---
81
+
82
+ ## Options
83
+
84
+ ```js
85
+ createEditor(container, {
86
+ mode: 'BRE', // 'BRE' | 'BREM' | 'BREW'
87
+ onChange: (doc) => {}, // debounced callback on every change
88
+ embedAllowlist: ['youtube.com', 'youtu.be', 'vimeo.com'],
89
+ virtualize: false, // true = render only visible blocks
90
+ });
91
+ ```
92
+
93
+ | Option | Type | Default | Description |
94
+ |---|---|---|---|
95
+ | `mode` | `string` | `'BRE'` | Editor mode |
96
+ | `onChange` | `function` | `null` | Debounced callback receiving the current document |
97
+ | `embedAllowlist` | `string[]` | `['youtube.com','youtu.be','vimeo.com']` | Domains allowed as iframe embeds |
98
+ | `virtualize` | `boolean` | `false` | Virtualized rendering for large documents |
99
+
100
+ ---
101
+
102
+ ## API
103
+
104
+ Every mode exposes the same four methods:
105
+
106
+ ```js
107
+ editor.getJSON() // → Document object
108
+ editor.setJSON(doc) // loads a Document object
109
+ editor.getHTML() // → sanitized HTML string
110
+ editor.destroy() // removes DOM, cleans up listeners
111
+ ```
112
+
113
+ ---
114
+
115
+ ## Data model
116
+
117
+ `getJSON()` and `setJSON()` use this structure:
118
+
119
+ ```js
120
+ // Document
121
+ {
122
+ id: string,
123
+ version: number,
124
+ created: number, // Unix ms
125
+ updated: number,
126
+ blocks: Block[],
127
+ }
128
+
129
+ // Block
130
+ { id: string, type: string, data: object }
131
+ ```
132
+
133
+ ### Block types
134
+
135
+ | Type | Data shape |
136
+ |---|---|
137
+ | `paragraph` | `{ text: string }` |
138
+ | `heading` | `{ level: 1–6, text: string }` |
139
+ | `quote` | `{ text: string }` |
140
+ | `divider` | `{}` |
141
+ | `code` | `{ language?: string, code: string }` |
142
+ | `bulleted_list` | `{ text: string }` |
143
+ | `numbered_list` | `{ text: string }` |
144
+ | `formula` | `{ latex: string, displayMode: boolean }` |
145
+ | `table` | `{ rows: string[][] }` — first row is the header |
146
+ | `image` | `{ src: string, alt?: string, caption?: string }` |
147
+ | `audio` | `{ src: string, caption?: string }` |
148
+ | `video` | `{ src: string, caption?: string }` |
149
+ | `columns` | `{ children: Block[][] }` — 2-column layout |
150
+ | `markdown` | `{ markdown: string }` — BREM mode only |
151
+
152
+ ```js
153
+ // Example document
154
+ const doc = {
155
+ id: 'my-doc',
156
+ version: 1,
157
+ created: Date.now(),
158
+ updated: Date.now(),
159
+ blocks: [
160
+ { id: '1', type: 'heading', data: { level: 1, text: 'Hello world' } },
161
+ { id: '2', type: 'paragraph', data: { text: 'Rich text in the browser.' } },
162
+ { id: '3', type: 'formula', data: { latex: 'E = mc^2', displayMode: true } },
163
+ { id: '4', type: 'table', data: { rows: [['Name','Score'],['Alice','98'],['Bob','87']] } },
164
+ { id: '5', type: 'image', data: { src: 'https://example.com/photo.jpg', alt: 'Photo', caption: 'My photo' } },
165
+ { id: '6', type: 'video', data: { src: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' } },
166
+ ],
167
+ };
168
+
169
+ editor.setJSON(doc);
170
+ ```
171
+
172
+ The same document object works with `setJSON` in any mode — BRE, BREM, and BREW all read from and write to the same schema.
173
+
174
+ ---
175
+
176
+ ## BRE — Block editor features
177
+
178
+ | Feature | Detail |
179
+ |---|---|
180
+ | Slash menu | Press `/` to open a searchable block inserter |
181
+ | Drag reorder | Drag the handle on the left of any block |
182
+ | Keyboard splitting | `Enter` splits a block; `Backspace` at start merges |
183
+ | Arrow navigation | `↑` / `↓` moves between blocks |
184
+ | 2-column layout | Insert a **Columns** block; stacks on mobile |
185
+ | Virtualized rendering | Pass `{ virtualize: true }` for 500+ block documents |
186
+
187
+ ---
188
+
189
+ ## BREW — WYSIWYG keyboard shortcuts
190
+
191
+ | Shortcut | Action |
192
+ |---|---|
193
+ | `⌘B` / `Ctrl+B` | Bold |
194
+ | `⌘I` / `Ctrl+I` | Italic |
195
+ | `⌘U` / `Ctrl+U` | Underline |
196
+
197
+ ---
198
+
199
+ ## Math (KaTeX)
200
+
201
+ Supported in all three modes.
202
+
203
+ **BRE / BREW** — use the **∑ Formula** toolbar button, or supply a `formula` block via `setJSON`.
204
+
205
+ **BREM** — write `$inline$` or `$$display$$` in the textarea.
206
+
207
+ ```js
208
+ { type: 'formula', data: { latex: '\\int_0^\\infty e^{-x}\\,dx = 1', displayMode: true } }
209
+ ```
210
+
211
+ ---
212
+
213
+ ## Video embeds
214
+
215
+ YouTube and Vimeo URLs are automatically converted to privacy-respecting iframes:
216
+
217
+ - YouTube → `youtube-nocookie.com/embed/…`
218
+ - Vimeo → `player.vimeo.com/video/…`
219
+
220
+ Other video URLs render as a native `<video>` element. Extend the allowlist with the `embedAllowlist` option.
221
+
222
+ ---
223
+
224
+ ## Virtualized rendering
225
+
226
+ For documents with hundreds of blocks:
227
+
228
+ ```js
229
+ const editor = createEditor(container, { mode: 'BRE', virtualize: true });
230
+ ```
231
+
232
+ Only blocks visible in the viewport (± 30 blocks) are in the DOM. A prefix-sum array gives O(log n) window lookups and O(1) spacer height calculations.
233
+
234
+ ---
235
+
236
+ ## Styling
237
+
238
+ The editor uses CSS custom properties. Override on `:root` or your container:
239
+
240
+ ```css
241
+ :root {
242
+ --bre-font-family: system-ui, sans-serif;
243
+ --bre-color-text: #1a1a1a;
244
+ --bre-color-surface: #ffffff;
245
+ --bre-color-border: #e0e0e0;
246
+ --bre-color-accent: #2563eb;
247
+ --bre-color-muted: #6b7280;
248
+ --bre-radius: 6px;
249
+ }
250
+ ```
251
+
252
+ ---
253
+
254
+ ## Security
255
+
256
+ - All HTML output sanitized by **DOMPurify** before any `innerHTML` write
257
+ - All `href` / `src` values validated — `javascript:` and `data:` URLs are rejected
258
+ - YouTube/Vimeo iframes use `sandbox`, `referrerpolicy`, and `loading="lazy"`
259
+ - No `eval()` anywhere in the codebase
260
+
261
+ ---
262
+
263
+ ## Browser support
264
+
265
+ Modern evergreen browsers (Chrome, Firefox, Safari, Edge). Requires ES2020+ and native ES Modules.
266
+
267
+ ---
268
+
269
+ ## License
270
+
271
+ MIT
package/dist/bre.css ADDED
@@ -0,0 +1 @@
1
+ :root{--bre-color-primary:#111827;--bre-color-bg:#fff;--bre-color-surface:#f9fafb;--bre-color-border:#e5e7eb;--bre-color-text:#111827;--bre-color-text-muted:#6b7280;--bre-color-accent:#3b82f6;--bre-color-placeholder:#9ca3af;--bre-color-code-bg:#1e1e2e;--bre-color-code-text:#cdd6f4;--bre-color-quote-border:#3b82f6;--bre-font-sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;--bre-font-mono:"SFMono-Regular",Consolas,"Liberation Mono",Menlo,monospace;--bre-radius:6px;--bre-editor-max-width:720px;--bre-editor-padding:40px 24px}.bre-editor{background:var(--bre-color-bg);box-sizing:border-box;color:var(--bre-color-text);font-family:var(--bre-font-sans);font-size:16px;line-height:1.6;margin:0 auto;max-width:var(--bre-editor-max-width);min-height:300px;outline:none;padding:var(--bre-editor-padding);position:relative}.bre-block{align-items:flex-start;display:flex;gap:4px;margin:2px 0;position:relative}.bre-block:hover .bre-drag-handle{opacity:1}.bre-drag-handle{align-items:center;border-radius:3px;color:var(--bre-color-text-muted);cursor:grab;display:flex;flex-shrink:0;height:24px;justify-content:center;left:-28px;opacity:0;position:absolute;top:3px;transition:opacity .15s ease;user-select:none;width:20px}.bre-drag-handle:hover{background:var(--bre-color-surface);opacity:1}.bre-drag-handle:active{cursor:grabbing}.bre-block-content{flex:1;min-width:0;outline:none;width:100%;word-break:break-word}.bre-block-content[data-bre-placeholder]:empty:before{color:var(--bre-color-placeholder);content:attr(data-bre-placeholder);pointer-events:none;user-select:none}code[data-bre-placeholder]:empty:before{color:#6b7280;content:attr(data-bre-placeholder);pointer-events:none;user-select:none}.bre-paragraph{margin:0;padding:3px 0}.bre-heading{font-weight:700;line-height:1.3;margin:0}.bre-heading--1{font-size:2em;padding:6px 0 4px}.bre-heading--2{font-size:1.5em;padding:5px 0 3px}.bre-heading--3{font-size:1.25em;padding:4px 0 2px}.bre-heading--4{font-size:1.1em;padding:3px 0 2px}.bre-heading--5{font-size:1em;padding:3px 0 2px}.bre-heading--6{font-size:.9em;padding:3px 0 2px}.bre-heading--6,.bre-quote{color:var(--bre-color-text-muted)}.bre-quote{background:var(--bre-color-surface);border-left:3px solid var(--bre-color-quote-border);border-radius:0 var(--bre-radius) var(--bre-radius) 0;font-style:italic;margin:0;padding:6px 16px}.bre-divider{border:none;border-top:2px solid var(--bre-color-border);margin:12px 0;width:100%}.bre-code-wrapper{background:var(--bre-color-code-bg);border-radius:var(--bre-radius);flex:1;min-width:0;overflow:hidden}.bre-code-lang{color:#89b4fa;font-family:var(--bre-font-mono);font-size:.75em;padding:6px 14px 2px;text-transform:lowercase;user-select:none}.bre-code{margin:0;overflow-x:auto;padding:12px 14px}.bre-code code{color:var(--bre-color-code-text);display:block;font-family:var(--bre-font-mono);font-size:.875em;min-height:1.5em;outline:none;tab-size:2;white-space:pre}.bre-bulleted-list,.bre-numbered-list{list-style:none;margin:0;padding:3px 0}.bre-bulleted-list:before{color:var(--bre-color-text);content:"•";font-size:1.2em;left:-14px;line-height:1.5;position:absolute}.bre-block--bulleted_list{padding-left:20px}.bre-block--numbered_list{counter-increment:bre-numbered;padding-left:28px}.bre-block--bulleted_list~.bre-block--numbered_list:first-of-type,.bre-editor>.bre-block--numbered_list:first-child{counter-reset:bre-numbered}.bre-numbered-list:before{color:var(--bre-color-text);content:counter(bre-numbered) ".";font-variant-numeric:tabular-nums;left:0;min-width:24px;position:absolute}.bre-block--columns{align-items:stretch}.bre-columns{display:grid;flex:1;gap:16px;min-width:0}.bre-columns[data-bre-cols="2"]{grid-template-columns:1fr 1fr}.bre-columns[data-bre-cols="3"]{grid-template-columns:1fr 1fr 1fr}.bre-columns[data-bre-cols="4"]{grid-template-columns:1fr 1fr 1fr 1fr}@media (max-width:1000px){.bre-columns[data-bre-cols="4"]{grid-template-columns:1fr 1fr}}@media (max-width:600px){.bre-columns,.bre-columns[data-bre-cols="2"],.bre-columns[data-bre-cols="3"],.bre-columns[data-bre-cols="4"]{grid-template-columns:1fr}}.bre-column{background:var(--bre-color-surface);border:1px solid var(--bre-color-border);border-radius:var(--bre-radius);min-width:0;padding:4px 8px}.bre-column .bre-block{margin:2px 0}.bre-column .bre-drag-handle{left:-24px}.bre-table-wrapper{flex:1;min-width:0;overflow-x:auto}.bre-table{border-collapse:collapse;table-layout:auto;width:100%}.bre-table td,.bre-table th{border:1px solid var(--bre-color-border);min-width:80px;padding:6px 10px;text-align:left;vertical-align:top}.bre-table th{background:var(--bre-color-surface);font-weight:600}.bre-table-cell[contenteditable]{cursor:text;outline:none;white-space:pre-wrap;word-break:break-word}.bre-table-cell[data-bre-placeholder]:empty:before{color:var(--bre-color-placeholder);content:attr(data-bre-placeholder);pointer-events:none;user-select:none}.bre-table-controls{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px}.bre-table-btn{background:var(--bre-color-surface);border:1px solid var(--bre-color-border);border-radius:var(--bre-radius);color:var(--bre-color-text);cursor:pointer;font-size:.75rem;line-height:1.6;padding:2px 8px}.bre-table-btn:hover{background:var(--bre-color-border)}.bre-audio-block,.bre-image-block,.bre-video-block{flex:1;margin:0;min-width:0;padding:0}.bre-image{border-radius:var(--bre-radius);cursor:pointer;display:block;max-width:100%}.bre-audio-placeholder,.bre-image-placeholder,.bre-video-placeholder{align-items:center;border:2px dashed var(--bre-color-border);border-radius:var(--bre-radius);color:var(--bre-color-placeholder);cursor:pointer;display:flex;font-size:.9rem;justify-content:center;min-height:80px;padding:16px;text-align:center}.bre-audio-placeholder:hover,.bre-image-placeholder:hover,.bre-video-placeholder:hover{background:var(--bre-color-surface);border-color:var(--bre-color-accent)}.bre-audio{display:block;width:100%}.bre-video{border-radius:var(--bre-radius);display:block;width:100%}.bre-video-embed{aspect-ratio:16/9;border:none;border-radius:var(--bre-radius);display:block;width:100%}.bre-media-caption[data-bre-placeholder]:empty:before{color:var(--bre-color-placeholder);content:attr(data-bre-placeholder);pointer-events:none;user-select:none}.bre-media-caption{color:var(--bre-color-placeholder);display:block;font-size:.85rem;margin-top:4px;outline:none;text-align:center}.bre-media-caption:focus{color:var(--bre-color-text)}.bre-virt-spacer-bottom,.bre-virt-spacer-top{display:block;pointer-events:none;width:100%;will-change:height}.bre-slash-menu{background:var(--bre-color-bg);border:1px solid var(--bre-color-border);border-radius:var(--bre-radius);box-shadow:0 8px 24px rgba(0,0,0,.12),0 2px 8px rgba(0,0,0,.08);max-height:280px;max-width:300px;min-width:240px;overflow:hidden;overflow-y:auto;position:fixed;z-index:9999}.bre-slash-item{align-items:center;cursor:pointer;display:flex;gap:10px;padding:8px 12px;transition:background .1s ease}.bre-slash-item:hover,.bre-slash-item[data-active=true]{background:var(--bre-color-surface)}.bre-slash-item[data-active=true]{background:#eff6ff}.bre-slash-item__icon{background:#eff6ff;border:1px solid #bfdbfe;border-radius:4px;color:var(--bre-color-accent);flex-shrink:0;font-family:var(--bre-font-mono);font-size:.75em;font-weight:600;min-width:28px;padding:2px 5px;text-align:center}.bre-slash-item__text{display:flex;flex-direction:column;gap:1px}.bre-slash-item__label{color:var(--bre-color-text);font-size:.9em;font-weight:500}.bre-slash-item__desc{color:var(--bre-color-text-muted);font-size:.78em}.bre-slash-empty{color:var(--bre-color-text-muted);font-size:.875em;padding:12px;text-align:center}.bre-drop-line{background:var(--bre-color-accent);border-radius:1px;box-shadow:0 0 0 2px rgba(59,130,246,.2);height:2px;left:0;pointer-events:none;position:absolute;right:0;z-index:100}.bre-block--dragging{opacity:.4}.bre-placeholder{color:var(--bre-color-placeholder);font-size:16px;pointer-events:none;user-select:none}.bre-editor--brem{padding:var(--bre-editor-padding);position:relative}.bre-brem-textarea{background:transparent;border:none;color:var(--bre-color-text);font-family:var(--bre-font-mono);font-size:15px;line-height:1.7;min-height:300px;outline:none;overflow:hidden;padding:0;resize:none;width:100%}.bre-brem-preview{cursor:text;min-height:300px}.bre-brem-preview h1,.bre-brem-preview h2,.bre-brem-preview h3,.bre-brem-preview h4,.bre-brem-preview h5,.bre-brem-preview h6{font-weight:700;line-height:1.3;margin:.75em 0 .25em}.bre-brem-preview h1{font-size:2em}.bre-brem-preview h2{font-size:1.5em}.bre-brem-preview h3{font-size:1.25em}.bre-brem-preview p{margin:.5em 0}.bre-brem-preview ol,.bre-brem-preview ul{margin:.5em 0;padding-left:1.5em}.bre-brem-preview li{margin:.2em 0}.bre-brem-preview blockquote{background:var(--bre-color-surface);border-left:3px solid var(--bre-color-quote-border);border-radius:0 var(--bre-radius) var(--bre-radius) 0;color:var(--bre-color-text-muted);font-style:italic;margin:.5em 0;padding:6px 16px}.bre-brem-preview pre{background:var(--bre-color-code-bg);border-radius:var(--bre-radius);color:var(--bre-color-code-text);margin:.5em 0;overflow-x:auto;padding:12px 16px}.bre-brem-preview code,.bre-brem-preview pre code{font-family:var(--bre-font-mono);font-size:.875em}.bre-brem-preview code{background:var(--bre-color-surface);border:1px solid var(--bre-color-border);border-radius:3px;padding:1px 4px}.bre-brem-preview hr{border:none;border-top:2px solid var(--bre-color-border);margin:1em 0}.bre-brem-preview a{color:var(--bre-color-accent);text-decoration:underline}.bre-brem-preview .bre-audio-block,.bre-brem-preview .bre-image-block,.bre-brem-preview .bre-table-wrapper,.bre-brem-preview .bre-video-block{margin:.75em 0}.bre-katex-block{display:block;margin:.75em 0;overflow-x:auto;text-align:center}.bre-brem-toggle{background:#eff6ff;border:1px solid #bfdbfe;border-radius:var(--bre-radius);color:var(--bre-color-accent);cursor:pointer;font-family:var(--bre-font-sans);font-size:13px;font-weight:500;padding:4px 12px;position:absolute;right:16px;top:12px;transition:background .15s ease}.bre-brem-toggle:hover{background:#dbeafe}.bre-editor--brew{display:flex;flex-direction:column;padding:0;position:relative}.bre-brew-toolbar{align-items:center;background:var(--bre-color-surface);border-bottom:1px solid var(--bre-color-border);display:flex;flex-wrap:wrap;gap:4px;padding:8px 12px;position:sticky;top:0;z-index:10}.bre-brew-toolbar-sep{background:var(--bre-color-border);flex-shrink:0;height:20px;margin:0 4px;width:1px}.bre-brew-block-type{background:var(--bre-color-bg);border:1px solid var(--bre-color-border);border-radius:var(--bre-radius);outline:none;padding:4px 8px}.bre-brew-block-type,.bre-brew-btn{color:var(--bre-color-text);cursor:pointer;font-family:var(--bre-font-sans);font-size:13px}.bre-brew-btn{background:none;border:1px solid transparent;border-radius:var(--bre-radius);font-weight:600;line-height:1.4;padding:4px 10px;transition:background .1s ease,border-color .1s ease;white-space:nowrap}.bre-brew-btn:hover{background:var(--bre-color-surface);border-color:var(--bre-color-border)}.bre-brew-btn[data-active=true]{background:#eff6ff;border-color:#bfdbfe;color:var(--bre-color-accent)}.bre-brew-btn:disabled{cursor:not-allowed;opacity:.4}.bre-brew-surface{color:var(--bre-color-text);flex:1;font-family:var(--bre-font-sans);font-size:16px;line-height:1.6;min-height:400px;outline:none;padding:var(--bre-editor-padding)}.bre-brew-surface h1,.bre-brew-surface h2,.bre-brew-surface h3,.bre-brew-surface h4,.bre-brew-surface h5,.bre-brew-surface h6{font-weight:700;line-height:1.3;margin:.5em 0 .2em}.bre-brew-surface h1{font-size:2em}.bre-brew-surface h2{font-size:1.5em}.bre-brew-surface h3{font-size:1.25em}.bre-brew-surface p{margin:.2em 0;min-height:1.5em}.bre-brew-surface ol,.bre-brew-surface ul{margin:.3em 0;padding-left:1.5em}.bre-brew-surface li{margin:.15em 0}.bre-brew-surface blockquote{background:var(--bre-color-surface);border-left:3px solid var(--bre-color-quote-border);border-radius:0 var(--bre-radius) var(--bre-radius) 0;color:var(--bre-color-text-muted);font-style:italic;margin:.5em 0;padding:6px 16px}.bre-brew-surface pre{background:var(--bre-color-code-bg);border-radius:var(--bre-radius);color:var(--bre-color-code-text);font-family:var(--bre-font-mono);font-size:.875em;margin:.5em 0;overflow-x:auto;padding:12px 16px;white-space:pre}.bre-brew-surface pre code{display:block;outline:none}.bre-brew-surface hr{border:none;border-top:2px solid var(--bre-color-border);margin:1em 0}.bre-brew-surface a{color:var(--bre-color-accent);cursor:pointer;text-decoration:underline}.bre-brew-link-dialog{background:#fff;box-shadow:0 4px 16px rgba(0,0,0,.12);display:flex;flex-direction:column;gap:8px;left:12px;min-width:280px;padding:12px;position:absolute;top:44px;z-index:200}.bre-brew-link-dialog,.bre-brew-link-input{border:1px solid var(--bre-color-border);border-radius:var(--bre-radius)}.bre-brew-link-input{box-sizing:border-box;font-family:inherit;font-size:13px;outline:none;padding:6px 8px;width:100%}.bre-brew-link-input:focus{border-color:var(--bre-color-accent)}.bre-brew-link-actions{display:flex;gap:6px;justify-content:flex-end}.bre-brew-link-cancel,.bre-brew-link-remove,.bre-brew-link-save{border:1px solid transparent;border-radius:var(--bre-radius);cursor:pointer;font-family:inherit;font-size:13px;padding:5px 12px}.bre-brew-link-save{background:var(--bre-color-accent);border-color:var(--bre-color-accent);color:#fff}.bre-brew-link-save:hover{opacity:.9}.bre-brew-link-cancel{background:#f3f4f6;border-color:var(--bre-color-border);color:#374151}.bre-brew-link-cancel:hover{background:#e5e7eb}.bre-brew-link-remove{background:#fff;border-color:#fca5a5;color:#ef4444;margin-right:auto}.bre-brew-link-remove:hover{background:#fef2f2}.bre-formula{background:var(--bre-color-surface);border:1px solid var(--bre-color-border);border-radius:var(--bre-radius);cursor:pointer;flex:1;min-width:0;outline:none;overflow-x:auto;padding:12px 16px;text-align:center;transition:border-color .15s ease}.bre-formula:focus,.bre-formula:hover{border-color:var(--bre-color-accent)}.bre-formula-placeholder{color:var(--bre-color-placeholder);font-size:.9em;font-style:italic}.bre-formula-error{color:#dc2626;font-family:var(--bre-font-mono);font-size:.85em}