@atom-forge/brickee 0.0.2 → 0.0.3
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/CHANGELOG.md +40 -0
- package/README-AI.md +10 -7
- package/dist/Editor.svelte +691 -833
- package/dist/LinkPrompt.svelte +1 -14
- package/dist/Preview.svelte +27 -54
- package/dist/Preview.svelte.d.ts +2 -4
- package/dist/PropsBar.svelte +35 -219
- package/dist/code.js +1 -1
- package/dist/dom.d.ts +8 -0
- package/dist/dom.js +9 -0
- package/dist/editor/BlockMenu.svelte +102 -0
- package/dist/editor/BlockMenu.svelte.d.ts +32 -0
- package/dist/editor/FormatBar.svelte +41 -0
- package/dist/editor/FormatBar.svelte.d.ts +16 -0
- package/dist/editor/drag.svelte.d.ts +63 -0
- package/dist/editor/drag.svelte.js +95 -0
- package/dist/editor/history.d.ts +24 -0
- package/dist/editor/history.js +86 -0
- package/dist/editor/sweep.svelte.d.ts +21 -0
- package/dist/editor/sweep.svelte.js +56 -0
- package/dist/html.d.ts +12 -0
- package/dist/html.js +206 -0
- package/dist/i18n.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -2
- package/dist/kit/Empty.svelte +4 -15
- package/dist/kit/Frame.svelte +7 -37
- package/dist/locales/hu.js +4 -1
- package/dist/markdown.d.ts +20 -4
- package/dist/markdown.js +380 -85
- package/dist/model.js +2 -3
- package/dist/plugins/Gap.svelte +5 -8
- package/dist/plugins/GapPreview.svelte +4 -6
- package/dist/plugins/Map.svelte +2 -17
- package/dist/plugins/MapPreview.svelte +8 -10
- package/dist/plugins/Table.svelte +233 -256
- package/dist/plugins/TablePreview.svelte +28 -28
- package/dist/plugins/YouTube.svelte +3 -31
- package/dist/plugins/YouTubePreview.svelte +45 -43
- package/dist/plugins/index.js +3 -1
- package/dist/types.d.ts +21 -3
- package/dist/types.js +8 -1
- package/dist/view.js +1 -1
- package/docs/en/bundled-plugins.md +1 -1
- package/docs/en/documents-and-markdown.md +20 -14
- package/docs/en/editor-and-preview.md +6 -7
- package/docs/en/getting-started.md +8 -0
- package/docs/en/index.md +1 -1
- package/docs/en/recipes-and-pitfalls.md +2 -2
- package/docs/en/server-side.md +0 -17
- package/docs/en/upgrading.md +14 -0
- package/docs/en/writing-plugins.md +36 -10
- package/docs/hu/bundled-plugins.md +1 -1
- package/docs/hu/documents-and-markdown.md +20 -14
- package/docs/hu/editor-and-preview.md +6 -7
- package/docs/hu/getting-started.md +8 -0
- package/docs/hu/index.md +1 -1
- package/docs/hu/recipes-and-pitfalls.md +2 -2
- package/docs/hu/server-side.md +0 -17
- package/docs/hu/upgrading.md +14 -0
- package/docs/hu/writing-plugins.md +36 -10
- package/package.json +9 -1
package/dist/Editor.svelte
CHANGED
|
@@ -1,34 +1,23 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import { Bold, Check, Code, GripVertical, Italic, Link, Plus, RemoveFormatting, Replace, Strikethrough, Subscript, Superscript, Trash2, Underline, Ungroup } from '@lucide/svelte';
|
|
4
|
-
import {
|
|
5
|
-
rich,
|
|
6
|
-
clean,
|
|
7
|
-
textLen,
|
|
8
|
-
splitAtCaret,
|
|
9
|
-
atStart,
|
|
10
|
-
atEnd,
|
|
11
|
-
caretOffset,
|
|
12
|
-
caretRect,
|
|
13
|
-
caretAt,
|
|
14
|
-
setCaret,
|
|
15
|
-
removeBeforeCaret,
|
|
16
|
-
onEdgeLine,
|
|
17
|
-
focusAtEdge,
|
|
18
|
-
toggleCode,
|
|
19
|
-
plainText
|
|
20
|
-
} from './dom';
|
|
2
|
+
import { tick, untrack } from 'svelte';
|
|
3
|
+
import { ArrowDown, ArrowUp, Bold, Check, Code, GripVertical, Italic, Link, Plus, RemoveFormatting, Replace, Strikethrough, Subscript, Superscript, Trash2, Underline, Ungroup } from '@lucide/svelte';
|
|
4
|
+
import { rich, clean, textLen, splitAtCaret, atStart, atEnd, caretOffset, caretRect, caretAt, setCaret, removeBeforeCaret, onEdgeLine, focusAtEdge, toggleCode, plainText, portal } from './dom';
|
|
21
5
|
import { fromMarkdown, fromStored, plainHtml, toMarkdown, toStored } from './markdown';
|
|
6
|
+
import { fromHtml } from './html';
|
|
22
7
|
import { useCodeLanguages } from './code';
|
|
23
8
|
import { provideText, useText, type Messages } from './i18n';
|
|
24
9
|
import { PopupContainer, createPopupManager, getPopupManager } from '@atom-forge/ui';
|
|
25
10
|
import LinkPrompt from './LinkPrompt.svelte';
|
|
26
11
|
import PluginHost from './PluginHost.svelte';
|
|
27
12
|
import PropsBar from './PropsBar.svelte';
|
|
13
|
+
import BlockMenu, { type MenuRow } from './editor/BlockMenu.svelte';
|
|
14
|
+
import FormatBar from './editor/FormatBar.svelte';
|
|
15
|
+
import { History } from './editor/history';
|
|
16
|
+
import { createSweep } from './editor/sweep.svelte';
|
|
17
|
+
import { createDrag, useHub } from './editor/drag.svelte';
|
|
28
18
|
import { acceptsId, BUILT_IN_IDS, PALETTE_ITEMS, type Icon, type PaletteItem } from './palette';
|
|
29
19
|
import { MAX_INDENT, newBlock, numbering, uid, type Block, type BlockPlugin, type BlockType, type StoredBlock, type PropField, type Content, BUILT_IN_CONTENT, isMd, MD } from './types';
|
|
30
20
|
|
|
31
|
-
const HUB = 'block-editor:hub';
|
|
32
21
|
|
|
33
22
|
interface Props {
|
|
34
23
|
/** The document: built-in blocks as markdown snippets, plugin blocks with their props. */
|
|
@@ -72,21 +61,50 @@
|
|
|
72
61
|
const pluginOf = (type: string | undefined) => plugins.find((p) => p.type === type);
|
|
73
62
|
/** Blocks the caret cannot enter from outside: dividers and plugin blocks. */
|
|
74
63
|
const atomic = (b: Block) => b.type === 'hr' || b.type === 'plugin';
|
|
64
|
+
/** Rendering on the server: the blocks' text goes into the markup (in the browser the `rich` action puts it in). */
|
|
65
|
+
const ssr = typeof document === 'undefined';
|
|
75
66
|
|
|
76
67
|
/* ------------------------------------------------------------ document io */
|
|
77
68
|
|
|
78
|
-
// The editor works on html blocks; `doc` is their markdown form, written back on every change
|
|
69
|
+
// The editor works on html blocks; `doc` is their markdown form, written back on every change (not when read-only:
|
|
70
|
+
// showing a document must not change it).
|
|
79
71
|
const load = (d: StoredBlock[]) => (d.length ? d.map(fromStored) : [newBlock()]);
|
|
80
72
|
let value = $state<Block[]>(load(doc));
|
|
81
|
-
|
|
73
|
+
|
|
74
|
+
// The stored form of a built-in block, kept while the block does not change: typing in one block converts only that
|
|
75
|
+
// block (its inline html goes through the DOM), and the others keep their objects, so a preview re-renders only it.
|
|
76
|
+
// Plugin blocks are cheap to convert and keep their `props` and `blocks` objects anyway.
|
|
77
|
+
const stored = new Map<string, { key: string; out: StoredBlock }>();
|
|
78
|
+
function storedOf(b: Block): StoredBlock {
|
|
79
|
+
if (b.type === 'plugin') return toStored(b);
|
|
80
|
+
const key = `${b.type}|${b.indent}|${b.checked ?? ''}|${b.props?.lang ?? ''}|${b.props?.start ?? ''}|${b.html}`;
|
|
81
|
+
const hit = stored.get(b.id);
|
|
82
|
+
if (hit?.key === key) return hit.out;
|
|
83
|
+
const out = toStored(b);
|
|
84
|
+
stored.set(b.id, { key, out });
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// What we wrote last, and `doc` as read back after writing it (the host's state may wrap it in its own proxy).
|
|
89
|
+
let written: StoredBlock[] | null = null;
|
|
90
|
+
let readBack: unknown = untrack(() => doc);
|
|
82
91
|
$effect(() => {
|
|
83
|
-
const out = value.map(
|
|
84
|
-
|
|
92
|
+
const out = value.map(storedOf);
|
|
93
|
+
if (stored.size > out.length * 2) for (const id of [...stored.keys()]) if (!value.some((b) => b.id === id)) stored.delete(id);
|
|
94
|
+
if (untrack(() => readonly)) return;
|
|
95
|
+
written = out;
|
|
85
96
|
doc = out;
|
|
97
|
+
readBack = untrack(() => doc);
|
|
86
98
|
});
|
|
87
|
-
// A document replaced from outside (not by our own write) is loaded again.
|
|
99
|
+
// A document replaced from outside (not by our own write) is loaded again; an equal copy is not.
|
|
88
100
|
$effect(() => {
|
|
89
|
-
|
|
101
|
+
const d = doc;
|
|
102
|
+
if (d === readBack) return;
|
|
103
|
+
readBack = d;
|
|
104
|
+
if (JSON.stringify(d) === JSON.stringify(written ?? untrack(() => value.map(storedOf)))) return;
|
|
105
|
+
value = load(d);
|
|
106
|
+
// Another document (or this one reverted by the editor around a nested one): the old steps no longer apply.
|
|
107
|
+
untrack(() => forget());
|
|
90
108
|
});
|
|
91
109
|
|
|
92
110
|
/* ------------------------------------------------------------------ state */
|
|
@@ -100,17 +118,9 @@
|
|
|
100
118
|
|
|
101
119
|
// State shared by an editor and every editor nested in its plugin blocks: blocks can be dragged between them,
|
|
102
120
|
// and only the editor last worked in keeps a block selection.
|
|
103
|
-
type DragSource = { editor: string; ids: string[]; payload: StoredBlock[]; rows: HTMLElement[]; remove: () => void };
|
|
104
|
-
type Hub = {
|
|
105
|
-
src: DragSource | null;
|
|
106
|
-
target: { editor: string; over: string; after: boolean } | null;
|
|
107
|
-
active: string | null;
|
|
108
|
-
};
|
|
109
121
|
// A string, not an object: $state would proxy an object and break identity checks.
|
|
110
122
|
const token = uid();
|
|
111
|
-
const
|
|
112
|
-
const hub: Hub = getContext<Hub>(HUB) ?? ownHub;
|
|
113
|
-
setContext(HUB, hub);
|
|
123
|
+
const hub = useHub();
|
|
114
124
|
|
|
115
125
|
$effect(() => {
|
|
116
126
|
if (selA) hub.active = token;
|
|
@@ -120,6 +130,8 @@
|
|
|
120
130
|
});
|
|
121
131
|
|
|
122
132
|
const idx = (id: string) => value.findIndex((b) => b.id === id);
|
|
133
|
+
/** Puts blocks into the document before position `at`. */
|
|
134
|
+
const insertAt = (at: number, ...blocks: Block[]) => (value = [...value.slice(0, at), ...blocks, ...value.slice(at)]);
|
|
123
135
|
// Direct children only: a plugin row may hold a nested editor with its own blocks.
|
|
124
136
|
const el = (id: string) => root?.querySelector<HTMLElement>(`[data-block="${id}"] > .body > .ce`) ?? null;
|
|
125
137
|
const ownRow = (n: Element | null) => {
|
|
@@ -147,66 +159,39 @@
|
|
|
147
159
|
|
|
148
160
|
/* ---------------------------------------------------------------- history */
|
|
149
161
|
|
|
150
|
-
|
|
162
|
+
// A step holds the document as frozen copies of its blocks, and an unchanged block's copy is shared by every step
|
|
163
|
+
// (a step costs only the blocks that changed, not the whole document).
|
|
164
|
+
type Snap = { blocks: readonly Block[]; id: string | null; off: number };
|
|
165
|
+
const copies = new Map<string, { key: string; copy: Block }>();
|
|
166
|
+
function copyOf(b: Block): Block {
|
|
167
|
+
const key = `${b.type}|${b.indent}|${b.checked ?? ''}|${b.plugin ?? ''}|${JSON.stringify(b.props ?? null)}|${JSON.stringify(b.blocks ?? null)}|${b.html}`;
|
|
168
|
+
const hit = copies.get(b.id);
|
|
169
|
+
if (hit?.key === key) return hit.copy;
|
|
170
|
+
const copy = Object.freeze(JSON.parse(JSON.stringify($state.snapshot(b))) as Block);
|
|
171
|
+
copies.set(b.id, { key, copy });
|
|
172
|
+
return copy;
|
|
173
|
+
}
|
|
151
174
|
const take = (): Snap => {
|
|
152
175
|
const e = focusedId ? el(focusedId) : null;
|
|
153
|
-
|
|
176
|
+
const blocks = value.map(copyOf);
|
|
177
|
+
// copies of deleted blocks: the steps that hold them keep them; the map does not need them
|
|
178
|
+
if (copies.size > blocks.length * 2) for (const id of [...copies.keys()]) if (!value.some((b) => b.id === id)) copies.delete(id);
|
|
179
|
+
return { blocks, id: focusedId, off: e ? caretOffset(e) : 0 };
|
|
154
180
|
};
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
/** Call on every text edit: consecutive typing collapses into one undo step. */
|
|
162
|
-
function typed() {
|
|
163
|
-
if (!pending) {
|
|
164
|
-
past.push(last);
|
|
165
|
-
if (past.length > 300) past.shift();
|
|
166
|
-
future = [];
|
|
167
|
-
pending = true;
|
|
168
|
-
}
|
|
169
|
-
clearTimeout(timer);
|
|
170
|
-
timer = setTimeout(() => {
|
|
171
|
-
pending = false;
|
|
172
|
-
last = take();
|
|
173
|
-
}, 600);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/** Wrap every structural change: pushes an undo step. */
|
|
177
|
-
function mutate(fn: () => void) {
|
|
178
|
-
clearTimeout(timer);
|
|
179
|
-
pending = false;
|
|
180
|
-
past.push(take());
|
|
181
|
-
if (past.length > 300) past.shift();
|
|
182
|
-
future = [];
|
|
183
|
-
fn();
|
|
184
|
-
last = take();
|
|
185
|
-
}
|
|
181
|
+
const history = new History<Snap>(take, restore);
|
|
182
|
+
const typed = () => history.typed();
|
|
183
|
+
const mutate = (fn: () => void) => history.mutate(fn);
|
|
184
|
+
const forget = () => history.forget();
|
|
185
|
+
const undo = () => history.undo();
|
|
186
|
+
const redo = () => history.redo();
|
|
186
187
|
|
|
187
188
|
function restore(s: Snap) {
|
|
188
|
-
|
|
189
|
+
// fresh objects: the editor changes its blocks in place, the step's copies stay as they are
|
|
190
|
+
value = s.blocks.map((b) => JSON.parse(JSON.stringify(b)) as Block);
|
|
189
191
|
selA = selH = null;
|
|
190
192
|
slash = null;
|
|
191
|
-
last = s;
|
|
192
193
|
if (s.id && idx(s.id) >= 0) focusBlock(s.id, s.off);
|
|
193
194
|
}
|
|
194
|
-
function undo() {
|
|
195
|
-
clearTimeout(timer);
|
|
196
|
-
pending = false;
|
|
197
|
-
const s = past.pop();
|
|
198
|
-
if (!s) return;
|
|
199
|
-
future.push(take());
|
|
200
|
-
restore(s);
|
|
201
|
-
}
|
|
202
|
-
function redo() {
|
|
203
|
-
clearTimeout(timer);
|
|
204
|
-
pending = false;
|
|
205
|
-
const s = future.pop();
|
|
206
|
-
if (!s) return;
|
|
207
|
-
past.push(take());
|
|
208
|
-
restore(s);
|
|
209
|
-
}
|
|
210
195
|
|
|
211
196
|
/* ---------------------------------------------------------- block catalog */
|
|
212
197
|
|
|
@@ -234,7 +219,7 @@
|
|
|
234
219
|
blocks
|
|
235
220
|
.filter((b) => b.type !== 'hr' || allowed.has('hr'))
|
|
236
221
|
.map((b) => {
|
|
237
|
-
if (!allowed.has(b.type)) convert(b, 'p');
|
|
222
|
+
if (b.type !== 'plugin' && !allowed.has(b.type)) convert(b, 'p');
|
|
238
223
|
return b;
|
|
239
224
|
});
|
|
240
225
|
|
|
@@ -312,12 +297,6 @@
|
|
|
312
297
|
};
|
|
313
298
|
}
|
|
314
299
|
|
|
315
|
-
// Keep the highlighted item visible while moving through the menu with the arrow keys.
|
|
316
|
-
$effect(() => {
|
|
317
|
-
if (slash?.index === undefined && turn?.index === undefined) return;
|
|
318
|
-
root.querySelector(':scope > .menu .item.active')?.scrollIntoView({ block: 'nearest' });
|
|
319
|
-
});
|
|
320
|
-
|
|
321
300
|
function applySlash(item: PaletteItem) {
|
|
322
301
|
if (!slash) return;
|
|
323
302
|
const b = value[idx(slash.id)];
|
|
@@ -336,7 +315,7 @@
|
|
|
336
315
|
if (type === 'hr') {
|
|
337
316
|
const i = idx(b.id);
|
|
338
317
|
const nb = newBlock();
|
|
339
|
-
|
|
318
|
+
insertAt(i + 1, nb);
|
|
340
319
|
focusBlock(nb.id, 'start');
|
|
341
320
|
} else focusBlock(b.id, off);
|
|
342
321
|
});
|
|
@@ -390,7 +369,7 @@
|
|
|
390
369
|
b.type = 'hr';
|
|
391
370
|
b.html = '';
|
|
392
371
|
const nb = newBlock();
|
|
393
|
-
|
|
372
|
+
insertAt(i + 1, nb);
|
|
394
373
|
focusBlock(nb.id, 'start');
|
|
395
374
|
});
|
|
396
375
|
return;
|
|
@@ -431,7 +410,7 @@
|
|
|
431
410
|
const nb = newBlock(type, post, { indent: b.indent, ...(type === 'todo' ? { checked: false } : {}) });
|
|
432
411
|
mutate(() => {
|
|
433
412
|
b.html = pre;
|
|
434
|
-
|
|
413
|
+
insertAt(i + 1, nb);
|
|
435
414
|
});
|
|
436
415
|
focusBlock(nb.id, 'start');
|
|
437
416
|
}
|
|
@@ -468,6 +447,11 @@
|
|
|
468
447
|
else if (k === 'k') fmt('link');
|
|
469
448
|
else if (k === '\\') fmt('plain');
|
|
470
449
|
else if (k === 'x' && e.shiftKey) fmt('strikeThrough');
|
|
450
|
+
else if (k === '/') {
|
|
451
|
+
// The turn-into menu of this block, from the text.
|
|
452
|
+
selectOnly(b.id);
|
|
453
|
+
void openTurnByKey();
|
|
454
|
+
}
|
|
471
455
|
else if (k === 'a' && (!ce.textContent || getSelection()?.toString() === ce.textContent)) {
|
|
472
456
|
selA = value[0].id;
|
|
473
457
|
selH = value[value.length - 1].id;
|
|
@@ -476,7 +460,7 @@
|
|
|
476
460
|
} else if (e.shiftKey && (key === 'ArrowUp' || key === 'ArrowDown')) nudge([b.id], key === 'ArrowUp' ? -1 : 1);
|
|
477
461
|
else if (key === 'Enter' && b.type === 'code') {
|
|
478
462
|
const nb = newBlock();
|
|
479
|
-
mutate(() => (
|
|
463
|
+
mutate(() => insertAt(i + 1, nb));
|
|
480
464
|
focusBlock(nb.id, 'start');
|
|
481
465
|
} else handled = false;
|
|
482
466
|
if (handled) return e.preventDefault();
|
|
@@ -492,7 +476,7 @@
|
|
|
492
476
|
if (b.type === 'code') {
|
|
493
477
|
if (e.shiftKey) {
|
|
494
478
|
const nb = newBlock();
|
|
495
|
-
mutate(() => (
|
|
479
|
+
mutate(() => insertAt(i + 1, nb));
|
|
496
480
|
focusBlock(nb.id, 'start');
|
|
497
481
|
} else document.execCommand('insertLineBreak');
|
|
498
482
|
} else if (e.shiftKey) document.execCommand('insertLineBreak');
|
|
@@ -616,21 +600,94 @@
|
|
|
616
600
|
detectSlash(ce, b);
|
|
617
601
|
}
|
|
618
602
|
|
|
603
|
+
/** The editor's own clipboard format: the copied blocks in their stored form, beside their markdown. */
|
|
604
|
+
const CLIP = 'application/x-brickee+json';
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Blocks from the editor's clipboard format, or null when it is missing, of another version or malformed. Each block
|
|
608
|
+
* carries its markdown form as the copying editor wrote it, for an editor that cannot hold the block (`md`).
|
|
609
|
+
*/
|
|
610
|
+
function fromClip(data: string | undefined): (Block & { md?: string })[] | null {
|
|
611
|
+
if (!data) return null;
|
|
612
|
+
try {
|
|
613
|
+
const clip = JSON.parse(data);
|
|
614
|
+
const ok = (s: unknown, depth: number): s is StoredBlock => {
|
|
615
|
+
const b = s as StoredBlock;
|
|
616
|
+
return (
|
|
617
|
+
!!b && typeof b === 'object' && typeof b.type === 'string' && Number.isInteger(b.indent) && b.indent >= 0 && b.indent <= MAX_INDENT &&
|
|
618
|
+
(b.text === undefined || typeof b.text === 'string') &&
|
|
619
|
+
(b.props === undefined || (typeof b.props === 'object' && !Array.isArray(b.props))) &&
|
|
620
|
+
(b.blocks === undefined || (depth === 0 && Array.isArray(b.blocks) && b.blocks.every((x) => ok(x, 1))))
|
|
621
|
+
);
|
|
622
|
+
};
|
|
623
|
+
if (clip?.v !== 1 || !Array.isArray(clip.blocks) || clip.blocks.length > 2000 || !clip.blocks.every((x: unknown) => ok(x, 0))) return null;
|
|
624
|
+
const md: unknown[] = Array.isArray(clip.md) ? clip.md : [];
|
|
625
|
+
return (clip.blocks as StoredBlock[]).map((s, n) => ({ ...fromStored(s), md: typeof md[n] === 'string' ? (md[n] as string) : undefined }));
|
|
626
|
+
} catch {
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Pasted blocks this editor can hold: a plugin block it does not offer (or a unique one it already has) falls back
|
|
633
|
+
* to its markdown form; built-in types outside the palette become text (fit).
|
|
634
|
+
*/
|
|
635
|
+
function admit(blocks: (Block & { md?: string })[]): Block[] {
|
|
636
|
+
const has = new Set(value.filter((b) => b.type === 'plugin').map((b) => b.plugin));
|
|
637
|
+
const offered = plugins.filter((p) => allowed.has(p.type));
|
|
638
|
+
return fit(
|
|
639
|
+
blocks.flatMap(({ md, ...b }): Block[] => {
|
|
640
|
+
if (b.type !== 'plugin') return [b];
|
|
641
|
+
const p = pluginOf(b.plugin);
|
|
642
|
+
if (p && allowed.has(p.type) && !(p.unique && has.has(p.type))) {
|
|
643
|
+
if (p.unique) has.add(p.type);
|
|
644
|
+
return [b];
|
|
645
|
+
}
|
|
646
|
+
return fromMarkdown(md ?? toMarkdown([b], { plugins }), { plugins: offered }).map((x) => ({ ...x, indent: Math.min(MAX_INDENT, x.indent + b.indent) }));
|
|
647
|
+
})
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* What a paste brings, as blocks: our own blocks as they were, otherwise formatted HTML (a web page, Google Docs,
|
|
653
|
+
* Word), otherwise markdown text; a GFM table goes to a plugin that takes tables, if this editor offers one.
|
|
654
|
+
*/
|
|
655
|
+
function pasted(data: DataTransfer | null): Block[] | null {
|
|
656
|
+
const own = fromClip(data?.getData(CLIP));
|
|
657
|
+
if (own) return admit(own);
|
|
658
|
+
const offered = { plugins: plugins.filter((p) => allowed.has(p.type)) };
|
|
659
|
+
const html = data?.getData('text/html');
|
|
660
|
+
if (html) return fit(fromHtml(html, offered));
|
|
661
|
+
const text = data?.getData('text/plain');
|
|
662
|
+
return text == null ? null : fit(fromMarkdown(text.replace(/\r/g, ''), offered));
|
|
663
|
+
}
|
|
664
|
+
|
|
619
665
|
function onPaste(e: ClipboardEvent, b: Block) {
|
|
620
|
-
const
|
|
621
|
-
|
|
666
|
+
const data = e.clipboardData;
|
|
667
|
+
// Files (a copied image, a screenshot) to a plugin that takes them; an empty block makes room for it.
|
|
668
|
+
const files = [...(data?.files ?? [])];
|
|
669
|
+
if (files.length && b.type !== 'code' && fileTakers().length) {
|
|
670
|
+
e.preventDefault();
|
|
671
|
+
const ce = e.currentTarget as HTMLElement;
|
|
672
|
+
void takeFiles(files, idx(b.id) + 1, clean(ce.innerHTML) ? undefined : b);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
const text = data?.getData('text/plain');
|
|
676
|
+
const rich = b.type !== 'code' && (data?.types.includes(CLIP) || data?.types.includes('text/html'));
|
|
677
|
+
if (!rich && text == null) return;
|
|
622
678
|
e.preventDefault();
|
|
623
679
|
const ce = e.currentTarget as HTMLElement;
|
|
624
|
-
const norm = text.replace(/\r/g, '');
|
|
680
|
+
const norm = (text ?? '').replace(/\r/g, '');
|
|
625
681
|
|
|
626
|
-
|
|
682
|
+
// Plain text into a code block, or a single line of plain text: typed in as it is.
|
|
683
|
+
if (!rich && (b.type === 'code' || !norm.includes('\n'))) {
|
|
627
684
|
norm.split('\n').forEach((line, n) => {
|
|
628
685
|
if (n) document.execCommand('insertLineBreak');
|
|
629
686
|
if (line) document.execCommand('insertText', false, line);
|
|
630
687
|
});
|
|
631
688
|
return;
|
|
632
689
|
}
|
|
633
|
-
const parsed =
|
|
690
|
+
const parsed = pasted(data) ?? [];
|
|
634
691
|
if (!parsed.length) return;
|
|
635
692
|
const parts = splitAtCaret(ce);
|
|
636
693
|
if (!parts) return;
|
|
@@ -638,28 +695,59 @@
|
|
|
638
695
|
const post = clean(parts[1]);
|
|
639
696
|
const i = idx(b.id);
|
|
640
697
|
|
|
698
|
+
// An empty block takes pasted plugin blocks or dividers in its place.
|
|
699
|
+
if (!pre && !post && atomic(parsed[0])) {
|
|
700
|
+
mutate(() => (value = [...value.slice(0, i), ...parsed, ...value.slice(i + 1)]));
|
|
701
|
+
const last = parsed[parsed.length - 1];
|
|
702
|
+
if (atomic(last)) selectOnly(last.id);
|
|
703
|
+
else focusBlock(last.id, 'end');
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
641
706
|
let rest = parsed;
|
|
642
707
|
const first = parsed[0];
|
|
643
708
|
let curHtml = pre;
|
|
644
|
-
if (!pre && first.type !== 'hr') {
|
|
709
|
+
if (!pre && first.type !== 'hr' && first.type !== 'plugin') {
|
|
645
710
|
b.type = first.type;
|
|
646
711
|
b.indent = first.indent;
|
|
647
712
|
if (first.type === 'todo') b.checked = first.checked;
|
|
713
|
+
if (first.props) b.props = first.props;
|
|
714
|
+
else delete b.props;
|
|
648
715
|
curHtml = first.html;
|
|
649
716
|
rest = parsed.slice(1);
|
|
650
717
|
} else if (first.type === 'p') {
|
|
651
718
|
curHtml = pre + first.html;
|
|
652
719
|
rest = parsed.slice(1);
|
|
653
720
|
}
|
|
721
|
+
// Text after the caret goes into the last pasted block, or a new paragraph after a pasted plugin block or divider.
|
|
722
|
+
if (post && rest.length && atomic(rest[rest.length - 1])) rest = [...rest, newBlock('p', '')];
|
|
654
723
|
const target = rest.length ? rest[rest.length - 1] : b;
|
|
655
724
|
const off = textLen(target === b ? curHtml : target.html);
|
|
656
725
|
mutate(() => {
|
|
657
726
|
if (target === b) curHtml += post;
|
|
658
727
|
else target.html += post;
|
|
659
728
|
b.html = curHtml;
|
|
660
|
-
|
|
729
|
+
insertAt(i + 1, ...rest);
|
|
661
730
|
});
|
|
662
|
-
|
|
731
|
+
if (atomic(target)) selectOnly(target.id);
|
|
732
|
+
else focusBlock(target.id, off);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/** Pasting while whole blocks are selected: the blocks go in after the selection. */
|
|
736
|
+
function onRootPaste(e: ClipboardEvent) {
|
|
737
|
+
if (e.target !== root || readonly || !selH) return;
|
|
738
|
+
const at = Math.max(...value.map((b, n) => (selSet.has(b.id) ? n : -1))) + 1;
|
|
739
|
+
const files = [...(e.clipboardData?.files ?? [])];
|
|
740
|
+
if (files.length && fileTakers().length) {
|
|
741
|
+
e.preventDefault();
|
|
742
|
+
void takeFiles(files, at);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const parsed = pasted(e.clipboardData) ?? [];
|
|
746
|
+
e.preventDefault();
|
|
747
|
+
if (!parsed.length) return;
|
|
748
|
+
mutate(() => insertAt(at, ...parsed));
|
|
749
|
+
selA = parsed[0].id;
|
|
750
|
+
selH = parsed[parsed.length - 1].id;
|
|
663
751
|
}
|
|
664
752
|
|
|
665
753
|
/* ---------------------------------------------------------- inline format */
|
|
@@ -766,8 +854,23 @@
|
|
|
766
854
|
}
|
|
767
855
|
|
|
768
856
|
function onRootKey(e: KeyboardEvent) {
|
|
857
|
+
// Undo and redo pressed inside a plugin's own field (a table cell): this editor's history, not the field's
|
|
858
|
+
// native one (which the next re-render would lose anyway). A nested editor handles its own and prevents this.
|
|
859
|
+
const mod0 = e.metaKey || e.ctrlKey;
|
|
860
|
+
const k0 = e.key.toLowerCase();
|
|
861
|
+
if (mod0 && (k0 === 'z' || k0 === 'y') && !e.defaultPrevented && e.target !== root && !(e.target as Element).closest?.('.ce')) {
|
|
862
|
+
e.preventDefault();
|
|
863
|
+
if (k0 === 'y' || e.shiftKey) redo();
|
|
864
|
+
else undo();
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
769
867
|
if (e.target !== root || !selSet.size || !selH) return;
|
|
770
868
|
if (onTurnKey(e)) return;
|
|
869
|
+
// The turn-into menu from the keyboard: '/' or Ctrl+/ (also the menu key, Shift+F10) on selected blocks.
|
|
870
|
+
if (!readonly && (e.key === '/' || e.key === 'ContextMenu' || (e.shiftKey && e.key === 'F10'))) {
|
|
871
|
+
e.preventDefault();
|
|
872
|
+
return void openTurnByKey();
|
|
873
|
+
}
|
|
771
874
|
const mod = e.metaKey || e.ctrlKey;
|
|
772
875
|
const key = e.key;
|
|
773
876
|
const h = idx(selH);
|
|
@@ -789,7 +892,7 @@
|
|
|
789
892
|
if (el(b.id)) focusBlock(b.id, 'end');
|
|
790
893
|
else {
|
|
791
894
|
const nb = newBlock();
|
|
792
|
-
mutate(() => (
|
|
895
|
+
mutate(() => insertAt(h + 1, nb));
|
|
793
896
|
focusBlock(nb.id, 'start');
|
|
794
897
|
}
|
|
795
898
|
} else if (key === 'Escape') {
|
|
@@ -813,7 +916,13 @@
|
|
|
813
916
|
function onCopy(e: ClipboardEvent, cut = false) {
|
|
814
917
|
if (!selSet.size) return;
|
|
815
918
|
e.preventDefault();
|
|
816
|
-
|
|
919
|
+
const blocks = value.filter((b) => selSet.has(b.id));
|
|
920
|
+
e.clipboardData?.setData('text/plain', toMarkdown(blocks, { plugins }));
|
|
921
|
+
// The blocks as they are, for pasting into this or another editor (the markdown is for everything else).
|
|
922
|
+
e.clipboardData?.setData(
|
|
923
|
+
CLIP,
|
|
924
|
+
JSON.stringify({ v: 1, blocks: blocks.map((b) => $state.snapshot(toStored(b))), md: blocks.map((b) => toMarkdown([b], { plugins })) })
|
|
925
|
+
);
|
|
817
926
|
if (cut) deleteSelected();
|
|
818
927
|
}
|
|
819
928
|
|
|
@@ -830,12 +939,23 @@
|
|
|
830
939
|
blurActive();
|
|
831
940
|
root.focus({ preventScroll: true });
|
|
832
941
|
} else selectOnly(b.id);
|
|
833
|
-
if (
|
|
834
|
-
|
|
942
|
+
if (menu) openTurn(e.currentTarget as HTMLElement);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/** The turn-into menu for the selected blocks, beside `anchor` (a handle, a bubble button). */
|
|
946
|
+
function openTurn(anchor: Element) {
|
|
947
|
+
const r = anchor.getBoundingClientRect();
|
|
835
948
|
const up = r.bottom + 340 > innerHeight && r.top > 340;
|
|
836
949
|
turn = { x: Math.min(r.right + 6, innerWidth - 250), y: up ? r.bottom : r.top, up, index: 0 };
|
|
837
950
|
}
|
|
838
951
|
|
|
952
|
+
/** From the keyboard: the turn-into menu of the selected blocks, beside the handle of the selection's head. */
|
|
953
|
+
async function openTurnByKey() {
|
|
954
|
+
await tick();
|
|
955
|
+
const handle = selH ? root.querySelector(`[data-block="${selH}"] > .gutter .handle`) : null;
|
|
956
|
+
if (handle) openTurn(handle);
|
|
957
|
+
}
|
|
958
|
+
|
|
839
959
|
/* ------------------------------------------------------ properties panel */
|
|
840
960
|
|
|
841
961
|
// Plugin blocks and code blocks show their tag and fields on their top (PropsBar); read-only, a code block shows its
|
|
@@ -864,6 +984,17 @@
|
|
|
864
984
|
return f.format ? f.format(v) : v == null ? '' : String(v);
|
|
865
985
|
};
|
|
866
986
|
|
|
987
|
+
/**
|
|
988
|
+
* A change from a plugin's view (a table cell, a nested editor): an undo step like typing, but only when the value
|
|
989
|
+
* really differs (a nested editor writes its document back when it mounts).
|
|
990
|
+
*/
|
|
991
|
+
function pluginChange(b: Block, key: 'props' | 'blocks', v: Block['props'] | StoredBlock[]) {
|
|
992
|
+
if (JSON.stringify(v) === JSON.stringify(b[key])) return;
|
|
993
|
+
typed();
|
|
994
|
+
if (key === 'props') b.props = v as Block['props'];
|
|
995
|
+
else b.blocks = v as StoredBlock[];
|
|
996
|
+
}
|
|
997
|
+
|
|
867
998
|
/** Sets one property to a value as it is ('' or undefined removes it). */
|
|
868
999
|
function setValue(b: Block, key: string, v: unknown) {
|
|
869
1000
|
typed();
|
|
@@ -894,6 +1025,8 @@
|
|
|
894
1025
|
| { kind: 'type'; item: PaletteItem; type: BlockType; current: boolean; lossy: boolean }
|
|
895
1026
|
| { kind: 'wrap'; item: PaletteItem; plugin: BlockPlugin; current: boolean }
|
|
896
1027
|
| { kind: 'retype'; item: PaletteItem; plugin: BlockPlugin; current: boolean }
|
|
1028
|
+
| { kind: 'up' }
|
|
1029
|
+
| { kind: 'down' }
|
|
897
1030
|
| { kind: 'unwrap' }
|
|
898
1031
|
| { kind: 'delete' };
|
|
899
1032
|
|
|
@@ -938,6 +1071,11 @@
|
|
|
938
1071
|
const lossy = to === 'plain' && sel.some((b) => contentOf(b) === 'text' && formatted(b.html));
|
|
939
1072
|
out.push({ kind: 'type', item, type, current: single?.type === type, lossy });
|
|
940
1073
|
}
|
|
1074
|
+
// Moving without dragging (touch, keyboard).
|
|
1075
|
+
const at = value.findIndex((b) => selSet.has(b.id));
|
|
1076
|
+
const end = value.length - 1 - [...value].reverse().findIndex((b) => selSet.has(b.id));
|
|
1077
|
+
if (at > 0) out.push({ kind: 'up' });
|
|
1078
|
+
if (end < value.length - 1) out.push({ kind: 'down' });
|
|
941
1079
|
if (known && kinds.includes('blocks')) out.push({ kind: 'unwrap' });
|
|
942
1080
|
out.push({ kind: 'delete' });
|
|
943
1081
|
return out;
|
|
@@ -953,6 +1091,7 @@
|
|
|
953
1091
|
if (!opt) return;
|
|
954
1092
|
const sel = selected;
|
|
955
1093
|
if (opt.kind === 'delete') return deleteSelected();
|
|
1094
|
+
if (opt.kind === 'up' || opt.kind === 'down') return nudge([...selSet], opt.kind === 'up' ? -1 : 1);
|
|
956
1095
|
if (opt.kind === 'unwrap') return unwrap(sel);
|
|
957
1096
|
if (opt.kind === 'wrap') return wrap(sel, opt.plugin);
|
|
958
1097
|
if (opt.kind === 'retype') return mutate(() => sel.forEach((b) => toPlugin(b, opt.plugin)));
|
|
@@ -1016,10 +1155,35 @@
|
|
|
1016
1155
|
selA = selH = null;
|
|
1017
1156
|
}
|
|
1018
1157
|
|
|
1158
|
+
// Looks shared by several elements (Tailwind; whole class names, so Tailwind finds them).
|
|
1159
|
+
/** The block actions in the bubble: inverted, the theme's text colour as the background. */
|
|
1160
|
+
const BUBBLE =
|
|
1161
|
+
'flex h-[26px] w-7 cursor-pointer items-center justify-center border-0 bg-surface-contrast p-0 text-surface shadow-[0_4px_12px_rgb(0_0_0/0.2)] first:rounded-l-md first:pl-0.5 last:rounded-r-md last:pr-0.5 hover:bg-[color-mix(in_srgb,var(--color-surface-contrast,#1d1d1f)_82%,var(--color-surface,#fff))]';
|
|
1162
|
+
const BUBBLE_DELETE =
|
|
1163
|
+
'flex h-[26px] w-7 cursor-pointer items-center justify-center border-0 bg-surface-contrast p-0 text-surface shadow-[0_4px_12px_rgb(0_0_0/0.2)] first:rounded-l-md first:pl-0.5 last:rounded-r-md last:pr-0.5 hover:bg-[#c62828]';
|
|
1164
|
+
/** A block's small label (its type, a code block's language). */
|
|
1165
|
+
const TAG =
|
|
1166
|
+
'max-w-[18em] overflow-hidden rounded-[5px] px-1.5 py-px font-mono text-[11px] leading-[1.5] font-medium text-ellipsis whitespace-nowrap text-muted-contrast select-none';
|
|
1167
|
+
|
|
1019
1168
|
const optionKey = (o: TurnOption) => ('item' in o ? o.kind + ':' + o.item.id : o.kind);
|
|
1169
|
+
/** The actions after the types: a line above the first of them. */
|
|
1170
|
+
const ACTIONS = new Set<TurnOption['kind']>(['up', 'down', 'unwrap', 'delete']);
|
|
1171
|
+
/** The turn-into options as menu rows. */
|
|
1172
|
+
const turnRows = $derived(
|
|
1173
|
+
turnOptions.map((o, n): MenuRow => ({
|
|
1174
|
+
key: optionKey(o),
|
|
1175
|
+
...optionView(o),
|
|
1176
|
+
warn: 'lossy' in o && o.lossy,
|
|
1177
|
+
danger: o.kind === 'delete',
|
|
1178
|
+
current: 'current' in o && o.current,
|
|
1179
|
+
gap: ACTIONS.has(o.kind) && n > 0 && !ACTIONS.has(turnOptions[n - 1].kind)
|
|
1180
|
+
}))
|
|
1181
|
+
);
|
|
1020
1182
|
function optionView(o: TurnOption): { icon: Icon; label: string; hint: string } {
|
|
1021
1183
|
const n = selected.length;
|
|
1022
1184
|
if (o.kind === 'delete') return { icon: Trash2, label: tr('Delete'), hint: n > 1 ? tr('{n} blocks', { n }) : tr('Remove this block') };
|
|
1185
|
+
if (o.kind === 'up') return { icon: ArrowUp, label: tr('Move up'), hint: 'Ctrl+Shift+↑' };
|
|
1186
|
+
if (o.kind === 'down') return { icon: ArrowDown, label: tr('Move down'), hint: 'Ctrl+Shift+↓' };
|
|
1023
1187
|
if (o.kind === 'unwrap') return { icon: Ungroup, label: tr('Unwrap'), hint: tr('Move the inner blocks out') };
|
|
1024
1188
|
if (o.kind === 'wrap') return { icon: o.item.icon, label: o.item.label, hint: n > 1 ? tr('Put {n} blocks inside', { n }) : tr('Put inside') };
|
|
1025
1189
|
if (o.kind === 'type' && o.lossy) return { icon: o.item.icon, label: o.item.label, hint: tr('Loses formatting') };
|
|
@@ -1040,117 +1204,76 @@
|
|
|
1040
1204
|
|
|
1041
1205
|
/* ------------------------------------------------- selection across blocks */
|
|
1042
1206
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
sweepFrom = t;
|
|
1054
|
-
addEventListener('mousemove', sweep);
|
|
1055
|
-
addEventListener('mouseup', endSweep, { once: true });
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
function sweep(e: MouseEvent) {
|
|
1059
|
-
const from = ownRow(sweepFrom);
|
|
1060
|
-
const here = document.elementFromPoint(e.clientX, e.clientY);
|
|
1061
|
-
if (!from || !here) return;
|
|
1062
|
-
const row = root.contains(here) ? ownRow(here) : null;
|
|
1063
|
-
// Left this editor, or came back to the starting block: back to plain text selection.
|
|
1064
|
-
if (!root.contains(here) || row === from) {
|
|
1065
|
-
if (sweeping) {
|
|
1066
|
-
sweeping = false;
|
|
1067
|
-
selA = selH = null;
|
|
1068
|
-
}
|
|
1069
|
-
return;
|
|
1070
|
-
}
|
|
1071
|
-
if (!row) return;
|
|
1072
|
-
if (!sweeping) {
|
|
1073
|
-
sweeping = true;
|
|
1207
|
+
const sweep = createSweep({
|
|
1208
|
+
root: () => root,
|
|
1209
|
+
ownRow,
|
|
1210
|
+
readonly: () => readonly,
|
|
1211
|
+
select: (a, h) => {
|
|
1212
|
+
selA = a;
|
|
1213
|
+
selH = h;
|
|
1214
|
+
},
|
|
1215
|
+
clear: () => (selA = selH = null),
|
|
1216
|
+
focusRoot: () => {
|
|
1074
1217
|
blurActive();
|
|
1075
1218
|
root.focus({ preventScroll: true });
|
|
1076
1219
|
}
|
|
1077
|
-
|
|
1078
|
-
selA = from.dataset.block!;
|
|
1079
|
-
selH = row.dataset.block!;
|
|
1080
|
-
}
|
|
1220
|
+
});
|
|
1081
1221
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1222
|
+
// Removed in the middle of a sweep or of typing: no listener or timer is left behind. (An effect's cleanup, not
|
|
1223
|
+
// onDestroy: that also runs after server-side rendering, where there is no window.)
|
|
1224
|
+
$effect(() => () => {
|
|
1225
|
+
sweep.end();
|
|
1226
|
+
history.dispose();
|
|
1227
|
+
});
|
|
1087
1228
|
|
|
1088
1229
|
/* ---------------------------------------------------------------- dragging */
|
|
1089
1230
|
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
hub.target = null;
|
|
1101
|
-
if (!e.dataTransfer) return;
|
|
1102
|
-
e.dataTransfer.effectAllowed = 'move';
|
|
1103
|
-
e.dataTransfer.setData('text/plain', ' ');
|
|
1104
|
-
const row = (e.currentTarget as HTMLElement).closest('.row');
|
|
1105
|
-
if (row) e.dataTransfer.setDragImage(row, 0, 0);
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
/** Can the dragged blocks land in this editor? Not inside themselves, and only plugins this editor offers. */
|
|
1109
|
-
function accepts(src: DragSource) {
|
|
1110
|
-
if (src.editor === token) return true;
|
|
1111
|
-
if (src.rows.some((r) => r.contains(root))) return false;
|
|
1112
|
-
return src.payload.every((s) => isMd(s) || (allowed.has(s.type) && !!pluginOf(s.type)));
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
function onDragOver(e: DragEvent) {
|
|
1116
|
-
// A nested editor deeper in the tree already took this event.
|
|
1117
|
-
if (!hub.src || e.defaultPrevented) return;
|
|
1118
|
-
const row = ownRow(e.target as Element);
|
|
1119
|
-
if (!row || !accepts(hub.src)) return;
|
|
1120
|
-
e.preventDefault();
|
|
1121
|
-
const r = row.getBoundingClientRect();
|
|
1122
|
-
hub.target = { editor: token, over: row.dataset.block!, after: e.clientY > r.top + r.height / 2 };
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
function onDrop(e: DragEvent) {
|
|
1126
|
-
const src = hub.src;
|
|
1127
|
-
const at = hub.target;
|
|
1128
|
-
if (!src || at?.editor !== token) return;
|
|
1129
|
-
e.preventDefault();
|
|
1130
|
-
hub.src = hub.target = null;
|
|
1131
|
-
const { over, after } = at;
|
|
1132
|
-
if (src.ids.includes(over)) return;
|
|
1133
|
-
if (src.editor === token) {
|
|
1231
|
+
const drag = createDrag({
|
|
1232
|
+
token,
|
|
1233
|
+
hub,
|
|
1234
|
+
root: () => root,
|
|
1235
|
+
ownRow,
|
|
1236
|
+
moving: (id) => (selSet.has(id) ? value.filter((x) => selSet.has(x.id)).map((x) => x.id) : [id]),
|
|
1237
|
+
payload: (ids) => value.filter((x) => ids.includes(x.id)).map((x) => $state.snapshot(toStored(x)) as StoredBlock),
|
|
1238
|
+
remove: (ids) => mutate(() => (value = value.filter((x) => !ids.includes(x.id)).concat(value.length === ids.length ? [newBlock()] : []))),
|
|
1239
|
+
holds: (payload) => payload.every((s) => isMd(s) || (allowed.has(s.type) && !!pluginOf(s.type))),
|
|
1240
|
+
move: (ids, over, after) =>
|
|
1134
1241
|
mutate(() => {
|
|
1135
|
-
const moving = value.filter((b) =>
|
|
1136
|
-
const rest = value.filter((b) => !
|
|
1242
|
+
const moving = value.filter((b) => ids.includes(b.id));
|
|
1243
|
+
const rest = value.filter((b) => !ids.includes(b.id));
|
|
1137
1244
|
const t = rest.findIndex((b) => b.id === over) + (after ? 1 : 0);
|
|
1138
1245
|
value = [...rest.slice(0, t), ...moving, ...rest.slice(t)];
|
|
1139
|
-
})
|
|
1140
|
-
|
|
1246
|
+
}),
|
|
1247
|
+
insert: (payload, over, after) => {
|
|
1248
|
+
const incoming = fit(payload.map(fromStored));
|
|
1249
|
+
mutate(() => insertAt(idx(over) + (after ? 1 : 0), ...incoming));
|
|
1250
|
+
},
|
|
1251
|
+
takesFiles: () => fileTakers().length > 0,
|
|
1252
|
+
files: (files, over, after) => void takeFiles(files, idx(over) + (after ? 1 : 0))
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
/** The plugins this editor offers that take files. */
|
|
1256
|
+
const fileTakers = () => plugins.filter((p) => p.fromFiles && allowed.has(p.type));
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* Files pasted or dropped: the first plugin that takes them gets one block, at `at` (or in place of the empty block
|
|
1260
|
+
* `empty`). False when none takes them.
|
|
1261
|
+
*/
|
|
1262
|
+
async function takeFiles(files: File[], at: number, empty?: Block): Promise<boolean> {
|
|
1263
|
+
for (const p of fileTakers()) {
|
|
1264
|
+
const props = await p.fromFiles!(files);
|
|
1265
|
+
if (!props) continue;
|
|
1266
|
+
const pb = newBlock('plugin', '', { plugin: p.type, indent: empty?.indent ?? 0, props });
|
|
1267
|
+
fresh.add(pb.id);
|
|
1268
|
+
const i = empty ? idx(empty.id) : -1;
|
|
1269
|
+
mutate(() => (i >= 0 ? (value = [...value.slice(0, i), pb, ...value.slice(i + 1)]) : insertAt(Math.min(at, value.length), pb)));
|
|
1270
|
+
await tick();
|
|
1271
|
+
selectOnly(pb.id);
|
|
1272
|
+
return true;
|
|
1141
1273
|
}
|
|
1142
|
-
|
|
1143
|
-
const incoming = fit(src.payload.map(fromStored));
|
|
1144
|
-
mutate(() => {
|
|
1145
|
-
const t = idx(over) + (after ? 1 : 0);
|
|
1146
|
-
value = [...value.slice(0, t), ...incoming, ...value.slice(t)];
|
|
1147
|
-
});
|
|
1148
|
-
src.remove();
|
|
1274
|
+
return false;
|
|
1149
1275
|
}
|
|
1150
1276
|
|
|
1151
|
-
const dropMark = (id: string, after: boolean) =>
|
|
1152
|
-
hub.target?.editor === token && hub.target.over === id && hub.target.after === after && !hub.src?.ids.includes(id);
|
|
1153
|
-
|
|
1154
1277
|
/* ------------------------------------------------------------------- misc */
|
|
1155
1278
|
|
|
1156
1279
|
/** The + button: a new paragraph below (or this one, if it is an empty paragraph) with the block menu open. */
|
|
@@ -1159,7 +1282,7 @@
|
|
|
1159
1282
|
if (!(b.type === 'p' && !b.html)) {
|
|
1160
1283
|
target = newBlock('p', '', { indent: b.indent });
|
|
1161
1284
|
const i = idx(b.id);
|
|
1162
|
-
mutate(() => (
|
|
1285
|
+
mutate(() => insertAt(i + 1, target));
|
|
1163
1286
|
}
|
|
1164
1287
|
await tick();
|
|
1165
1288
|
const ce = el(target.id);
|
|
@@ -1192,28 +1315,30 @@
|
|
|
1192
1315
|
<div
|
|
1193
1316
|
class="editor"
|
|
1194
1317
|
class:nested
|
|
1195
|
-
class:sweeping
|
|
1318
|
+
class:sweeping={sweep.sweeping}
|
|
1196
1319
|
role="presentation"
|
|
1197
1320
|
bind:this={root}
|
|
1198
1321
|
tabindex="-1"
|
|
1199
1322
|
onkeydown={onRootKey}
|
|
1200
1323
|
oncopy={(e) => onCopy(e)}
|
|
1201
1324
|
oncut={(e) => onCopy(e, true)}
|
|
1325
|
+
onpaste={onRootPaste}
|
|
1202
1326
|
onmousedown={(e) => {
|
|
1203
1327
|
if (selA && !(e.target as HTMLElement).closest('.grab')) selA = selH = null;
|
|
1204
|
-
|
|
1328
|
+
sweep.start(e);
|
|
1205
1329
|
}}
|
|
1206
|
-
ondragover={
|
|
1207
|
-
ondrop={
|
|
1208
|
-
ondragend={
|
|
1330
|
+
ondragover={drag.over}
|
|
1331
|
+
ondrop={drag.drop}
|
|
1332
|
+
ondragend={drag.end}
|
|
1333
|
+
ondragleave={drag.leave}
|
|
1209
1334
|
>
|
|
1210
1335
|
{#each value as b, i (b.id)}
|
|
1211
1336
|
<div
|
|
1212
1337
|
class="row"
|
|
1213
1338
|
class:selected={selSet.has(b.id)}
|
|
1214
|
-
class:dragging={
|
|
1215
|
-
class:drop-before={
|
|
1216
|
-
class:drop-after={
|
|
1339
|
+
class:dragging={drag.moves(b.id)}
|
|
1340
|
+
class:drop-before={drag.mark(b.id, false)}
|
|
1341
|
+
class:drop-after={drag.mark(b.id, true)}
|
|
1217
1342
|
data-block={b.id}
|
|
1218
1343
|
data-type={b.type}
|
|
1219
1344
|
style:--indent={b.indent}
|
|
@@ -1224,17 +1349,20 @@
|
|
|
1224
1349
|
middle. Drag to move, click for the turn-into menu. Hovering it shows a small dark bubble on its right
|
|
1225
1350
|
with the block actions (add a block below, turn into, delete). -->
|
|
1226
1351
|
<div class="grab">
|
|
1227
|
-
|
|
1228
|
-
|
|
1352
|
+
<button
|
|
1353
|
+
type="button"
|
|
1229
1354
|
class="handle"
|
|
1230
1355
|
draggable="true"
|
|
1356
|
+
tabindex="-1"
|
|
1231
1357
|
title={tr('Drag to move, click to select')}
|
|
1232
|
-
|
|
1233
|
-
|
|
1358
|
+
aria-label={tr('Select the block')}
|
|
1359
|
+
ondragstart={(e) => drag.start(e, b.id)}
|
|
1360
|
+
onclick={(e) => onHandleClick(e, b)}><span class="grip"><GripVertical size={16} /></span></button
|
|
1234
1361
|
>
|
|
1235
1362
|
<div class="bubble" role="toolbar" aria-label={tr('Block actions')}>
|
|
1236
1363
|
<button
|
|
1237
1364
|
type="button"
|
|
1365
|
+
class={BUBBLE}
|
|
1238
1366
|
title={tr('Add a block below')}
|
|
1239
1367
|
aria-label={tr('Add a block below')}
|
|
1240
1368
|
onmousedown={(e) => e.preventDefault()}
|
|
@@ -1242,6 +1370,7 @@
|
|
|
1242
1370
|
>
|
|
1243
1371
|
<button
|
|
1244
1372
|
type="button"
|
|
1373
|
+
class={BUBBLE}
|
|
1245
1374
|
title={tr('Turn into')}
|
|
1246
1375
|
aria-label={tr('Turn into')}
|
|
1247
1376
|
onmousedown={(e) => e.preventDefault()}
|
|
@@ -1249,7 +1378,7 @@
|
|
|
1249
1378
|
>
|
|
1250
1379
|
<button
|
|
1251
1380
|
type="button"
|
|
1252
|
-
class=
|
|
1381
|
+
class={BUBBLE_DELETE}
|
|
1253
1382
|
title={tr('Delete')}
|
|
1254
1383
|
aria-label={tr('Delete')}
|
|
1255
1384
|
onmousedown={(e) => e.preventDefault()}
|
|
@@ -1261,18 +1390,18 @@
|
|
|
1261
1390
|
{/if}
|
|
1262
1391
|
<div class="body t-{b.type}">
|
|
1263
1392
|
{#if b.type === 'hr'}
|
|
1264
|
-
<hr />
|
|
1393
|
+
<hr class="m-0 flex-1 border-0 border-t border-frame" />
|
|
1265
1394
|
{:else if b.type === 'plugin'}
|
|
1266
1395
|
{@const plugin = pluginOf(b.plugin)}
|
|
1267
1396
|
{#if plugin}
|
|
1268
|
-
<div class="
|
|
1397
|
+
<div class="my-[0.35em] min-w-0 flex-1">
|
|
1269
1398
|
{#snippet propsBar()}
|
|
1270
1399
|
<PropsBar block={b} tag={tagOf(b)} fields={plugin.fields ?? []} {tr} {setProp} {setValue} open={fresh.has(b.id)} />
|
|
1271
1400
|
{/snippet}
|
|
1272
1401
|
<PluginHost
|
|
1273
1402
|
{plugin}
|
|
1274
|
-
bind:props={() => b.props ?? {}, (p) => (b
|
|
1275
|
-
bind:blocks={() => b.blocks ?? [], (v) => (b
|
|
1403
|
+
bind:props={() => b.props ?? {}, (p) => pluginChange(b, 'props', p)}
|
|
1404
|
+
bind:blocks={() => b.blocks ?? [], (v) => pluginChange(b, 'blocks', v)}
|
|
1276
1405
|
{readonly}
|
|
1277
1406
|
{palette}
|
|
1278
1407
|
{plugins}
|
|
@@ -1280,18 +1409,19 @@
|
|
|
1280
1409
|
/>
|
|
1281
1410
|
</div>
|
|
1282
1411
|
{:else}
|
|
1283
|
-
<div class="
|
|
1412
|
+
<div class="flex-1 rounded-lg border border-dashed border-frame px-[0.9em] py-[0.6em] text-[0.85em] text-muted-contrast">{tr('Unknown block: {type}', { type: b.plugin ?? '' })}</div>
|
|
1284
1413
|
{/if}
|
|
1285
1414
|
{:else}
|
|
1286
1415
|
{#if b.type === 'ul'}
|
|
1287
|
-
<span class="
|
|
1416
|
+
<span class="w-[1.3em] flex-none text-center text-surface-contrast select-none" aria-hidden="true">{['•', '◦', '▪'][b.indent % 3]}</span>
|
|
1288
1417
|
{:else if b.type === 'ol'}
|
|
1289
|
-
<span class="
|
|
1418
|
+
<span class="w-[1.6em] flex-none text-right text-surface-contrast tabular-nums select-none" aria-hidden="true">{nums[b.id]}.</span>
|
|
1290
1419
|
{:else if b.type === 'todo'}
|
|
1291
1420
|
<button
|
|
1292
1421
|
type="button"
|
|
1293
|
-
class="
|
|
1294
|
-
|
|
1422
|
+
class="mt-[0.3em] mr-[0.2em] ml-[0.1em] grid size-[1.1em] flex-none cursor-pointer place-items-center rounded border-[1.6px] p-0 text-white {b.checked
|
|
1423
|
+
? 'border-(--accent) bg-(--accent)'
|
|
1424
|
+
: 'border-muted-contrast bg-transparent'}"
|
|
1295
1425
|
role="checkbox"
|
|
1296
1426
|
aria-checked={!!b.checked}
|
|
1297
1427
|
aria-label={tr('Done')}
|
|
@@ -1301,7 +1431,7 @@
|
|
|
1301
1431
|
>{#if b.checked}<Check size={12} strokeWidth={3} />{/if}</button
|
|
1302
1432
|
>
|
|
1303
1433
|
{:else if b.type === 'code' && !readonly}
|
|
1304
|
-
<div class="
|
|
1434
|
+
<div class="basis-full">
|
|
1305
1435
|
<PropsBar block={b} tag={codeTag(b)} fields={codeFields(b)} {tr} {setProp} {setValue} />
|
|
1306
1436
|
</div>
|
|
1307
1437
|
{/if}
|
|
@@ -1329,628 +1459,356 @@
|
|
|
1329
1459
|
if (focusedId === b.id) focusedId = null;
|
|
1330
1460
|
slash = null;
|
|
1331
1461
|
}}
|
|
1332
|
-
|
|
1462
|
+
>{#if ssr}{@html b.html}{/if}</div>
|
|
1333
1463
|
{/if}
|
|
1334
1464
|
{#if b.type === 'code' && readonly}
|
|
1335
1465
|
<!-- read-only: the language of a code block, in its corner -->
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1466
|
+
<!-- `badge`: a hook for the code's room beside it -->
|
|
1467
|
+
<div class="badge absolute top-[calc(0.15em+5px)] right-1.5 z-[2] flex items-center gap-0.5">
|
|
1468
|
+
<span class={TAG}>{tr('code')}</span>
|
|
1469
|
+
{#if b.props?.lang}<span class={TAG}>{b.props.lang}</span>{/if}
|
|
1339
1470
|
</div>
|
|
1340
1471
|
{:else if b.type === 'plugin' && (readonly || !pluginOf(b.plugin)?.fields?.length)}
|
|
1341
|
-
|
|
1472
|
+
<!-- readable over a map or an image -->
|
|
1473
|
+
<div class="absolute top-[calc(0.35em+5px)] right-1.5 z-[2] flex items-center gap-0.5 rounded-md bg-[color-mix(in_srgb,var(--color-surface,#fff)_92%,transparent)]">
|
|
1474
|
+
<span class={TAG}>{tagOf(b)}</span>
|
|
1475
|
+
</div>
|
|
1342
1476
|
{/if}
|
|
1343
1477
|
</div>
|
|
1344
1478
|
</div>
|
|
1345
1479
|
{/each}
|
|
1346
1480
|
|
|
1347
1481
|
{#if !readonly}
|
|
1348
|
-
<button type="button" class="tail" aria-label={tr('New block at the end')} onclick={tailClick}></button>
|
|
1482
|
+
<button type="button" class="tail block w-full cursor-text border-0 bg-transparent {nested ? 'min-h-3' : 'min-h-20'}" aria-label={tr('New block at the end')} onclick={tailClick}></button>
|
|
1349
1483
|
{/if}
|
|
1350
1484
|
|
|
1351
1485
|
{#if toolbar}
|
|
1352
|
-
<
|
|
1353
|
-
{#each tools as t (t.cmd)}
|
|
1354
|
-
<button
|
|
1355
|
-
type="button"
|
|
1356
|
-
title={tr(t.title)}
|
|
1357
|
-
aria-label={tr(t.title)}
|
|
1358
|
-
onmousedown={(e) => e.preventDefault()}
|
|
1359
|
-
onclick={() => fmt(t.cmd)}><t.icon size={16} /></button
|
|
1360
|
-
>
|
|
1361
|
-
{/each}
|
|
1362
|
-
</div>
|
|
1486
|
+
<FormatBar x={toolbar.x} y={toolbar.y} tools={tools.map((t) => ({ ...t, title: tr(t.title) }))} label={tr('Formatting')} onpick={(cmd) => fmt(cmd as Cmd)} />
|
|
1363
1487
|
{/if}
|
|
1364
1488
|
|
|
1365
1489
|
{#if turn}
|
|
1366
|
-
<
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
{
|
|
1375
|
-
|
|
1376
|
-
{
|
|
1377
|
-
|
|
1378
|
-
{@const v = optionView(o)}
|
|
1379
|
-
{#if (o.kind === 'unwrap' || o.kind === 'delete') && n > 0 && turnOptions[n - 1].kind !== 'unwrap'}
|
|
1380
|
-
<div class="sep"></div>
|
|
1381
|
-
{/if}
|
|
1382
|
-
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
1383
|
-
<div
|
|
1384
|
-
class="item"
|
|
1385
|
-
class:danger={o.kind === 'delete'}
|
|
1386
|
-
class:active={n === turn.index}
|
|
1387
|
-
role="option"
|
|
1388
|
-
tabindex="-1"
|
|
1389
|
-
aria-selected={n === turn.index}
|
|
1390
|
-
onmousedown={(e) => {
|
|
1391
|
-
e.preventDefault();
|
|
1392
|
-
applyTurn(n);
|
|
1393
|
-
}}
|
|
1394
|
-
onmouseenter={() => turn && (turn.index = n)}
|
|
1395
|
-
>
|
|
1396
|
-
<span class="icon"><v.icon size={14} /></span>
|
|
1397
|
-
<span class="txt"><b>{v.label}</b><small class:warn={'lossy' in o && o.lossy}>{v.hint}</small></span>
|
|
1398
|
-
{#if 'current' in o && o.current}<span class="current"><Check size={16} /></span>{/if}
|
|
1399
|
-
</div>
|
|
1400
|
-
{/each}
|
|
1401
|
-
</div>
|
|
1490
|
+
<BlockMenu
|
|
1491
|
+
turn
|
|
1492
|
+
rows={turnRows}
|
|
1493
|
+
index={turn.index}
|
|
1494
|
+
x={turn.x}
|
|
1495
|
+
y={turn.y}
|
|
1496
|
+
up={turn.up}
|
|
1497
|
+
caption={turnOptions[0] && !ACTIONS.has(turnOptions[0].kind) ? tr('Turn into') : undefined}
|
|
1498
|
+
label={tr('Turn into')}
|
|
1499
|
+
onpick={applyTurn}
|
|
1500
|
+
onhover={(n) => turn && (turn.index = n)}
|
|
1501
|
+
/>
|
|
1402
1502
|
{/if}
|
|
1403
1503
|
|
|
1404
1504
|
{#if slash && filtered.length}
|
|
1405
|
-
<
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
{
|
|
1414
|
-
|
|
1415
|
-
<div
|
|
1416
|
-
class="item"
|
|
1417
|
-
class:active={n === slash.index}
|
|
1418
|
-
role="option"
|
|
1419
|
-
tabindex="-1"
|
|
1420
|
-
aria-selected={n === slash.index}
|
|
1421
|
-
onmousedown={(e) => {
|
|
1422
|
-
e.preventDefault();
|
|
1423
|
-
applySlash(it);
|
|
1424
|
-
}}
|
|
1425
|
-
onmouseenter={() => slash && (slash.index = n)}
|
|
1426
|
-
>
|
|
1427
|
-
<span class="icon"><it.icon size={14} /></span>
|
|
1428
|
-
<span class="txt"><b>{it.label}</b><small>{it.hint}</small></span>
|
|
1429
|
-
</div>
|
|
1430
|
-
{/each}
|
|
1431
|
-
</div>
|
|
1505
|
+
<BlockMenu
|
|
1506
|
+
rows={filtered.map((it) => ({ key: it.id, icon: it.icon, label: it.label, hint: it.hint }))}
|
|
1507
|
+
index={slash.index}
|
|
1508
|
+
x={slash.x}
|
|
1509
|
+
y={slash.y}
|
|
1510
|
+
up={slash.up}
|
|
1511
|
+
label={tr('Block type')}
|
|
1512
|
+
onpick={(n) => applySlash(filtered[n])}
|
|
1513
|
+
onhover={(n) => slash && (slash.index = n)}
|
|
1514
|
+
/>
|
|
1432
1515
|
{/if}
|
|
1433
1516
|
</div>
|
|
1434
1517
|
|
|
1435
1518
|
<!-- the popups of this editor and the ones nested in it -->
|
|
1436
|
-
{#if !nested}<PopupContainer
|
|
1437
|
-
|
|
1438
|
-
<style
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
font-weight: 600;
|
|
1733
|
-
line-height: 1.5;
|
|
1734
|
-
letter-spacing: 0.04em;
|
|
1735
|
-
text-transform: uppercase;
|
|
1736
|
-
}
|
|
1737
|
-
.t-h5,
|
|
1738
|
-
.t-h6 {
|
|
1739
|
-
padding-top: 0.2em;
|
|
1740
|
-
}
|
|
1741
|
-
.t-quote .ce {
|
|
1742
|
-
border-left: 3px solid var(--fg);
|
|
1743
|
-
padding-left: 0.9em;
|
|
1744
|
-
font-style: italic;
|
|
1745
|
-
}
|
|
1746
|
-
.t-code,
|
|
1747
|
-
.t-plugin {
|
|
1748
|
-
position: relative;
|
|
1749
|
-
}
|
|
1750
|
-
.badge {
|
|
1751
|
-
position: absolute;
|
|
1752
|
-
top: calc(0.15em + 5px);
|
|
1753
|
-
right: 6px;
|
|
1754
|
-
display: flex;
|
|
1755
|
-
align-items: center;
|
|
1756
|
-
gap: 2px;
|
|
1757
|
-
z-index: 2;
|
|
1758
|
-
}
|
|
1759
|
-
.t-plugin > .badge {
|
|
1760
|
-
top: calc(0.35em + 5px);
|
|
1761
|
-
/* Readable over a map or an image. */
|
|
1762
|
-
background: color-mix(in srgb, var(--color-surface, #fff) 92%, transparent);
|
|
1763
|
-
border-radius: 6px;
|
|
1764
|
-
}
|
|
1765
|
-
.tag {
|
|
1766
|
-
max-width: 18em;
|
|
1767
|
-
overflow: hidden;
|
|
1768
|
-
text-overflow: ellipsis;
|
|
1769
|
-
white-space: nowrap;
|
|
1770
|
-
padding: 1px 6px;
|
|
1771
|
-
border-radius: 5px;
|
|
1772
|
-
color: var(--muted);
|
|
1773
|
-
font: 500 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
1774
|
-
user-select: none;
|
|
1775
|
-
}
|
|
1776
|
-
/* the fields bar on its own row above the code */
|
|
1777
|
-
.t-code {
|
|
1778
|
-
flex-wrap: wrap;
|
|
1779
|
-
row-gap: 0;
|
|
1780
|
-
}
|
|
1781
|
-
.code-fields,
|
|
1782
|
-
.t-code .ce {
|
|
1783
|
-
flex-basis: 100%;
|
|
1784
|
-
}
|
|
1785
|
-
.t-code:has(> .badge) .ce {
|
|
1786
|
-
padding-right: 7.5em;
|
|
1787
|
-
}
|
|
1788
|
-
.t-code .ce {
|
|
1789
|
-
background: var(--code-bg);
|
|
1790
|
-
border-radius: 8px;
|
|
1791
|
-
padding: 0.7em 0.9em;
|
|
1792
|
-
font: 0.88em/1.6 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
1793
|
-
white-space: pre-wrap;
|
|
1794
|
-
}
|
|
1795
|
-
.t-hr {
|
|
1796
|
-
padding: 0.6em 0;
|
|
1797
|
-
}
|
|
1798
|
-
hr {
|
|
1799
|
-
flex: 1;
|
|
1800
|
-
border: 0;
|
|
1801
|
-
border-top: 1px solid var(--border);
|
|
1802
|
-
margin: 0;
|
|
1803
|
-
}
|
|
1804
|
-
|
|
1805
|
-
.marker {
|
|
1806
|
-
flex: none;
|
|
1807
|
-
width: 1.3em;
|
|
1808
|
-
text-align: center;
|
|
1809
|
-
color: var(--fg);
|
|
1810
|
-
user-select: none;
|
|
1811
|
-
}
|
|
1812
|
-
.marker.num {
|
|
1813
|
-
width: 1.6em;
|
|
1814
|
-
text-align: right;
|
|
1815
|
-
font-variant-numeric: tabular-nums;
|
|
1816
|
-
}
|
|
1817
|
-
.check {
|
|
1818
|
-
flex: none;
|
|
1819
|
-
width: 1.1em;
|
|
1820
|
-
height: 1.1em;
|
|
1821
|
-
margin: 0.3em 0.2em 0 0.1em;
|
|
1822
|
-
border: 1.6px solid var(--muted);
|
|
1823
|
-
border-radius: 4px;
|
|
1824
|
-
background: transparent;
|
|
1825
|
-
color: #fff;
|
|
1826
|
-
display: grid;
|
|
1827
|
-
place-items: center;
|
|
1828
|
-
cursor: pointer;
|
|
1829
|
-
padding: 0;
|
|
1830
|
-
}
|
|
1831
|
-
.check.done {
|
|
1832
|
-
background: var(--accent);
|
|
1833
|
-
border-color: var(--accent);
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
.tail {
|
|
1837
|
-
display: block;
|
|
1838
|
-
width: 100%;
|
|
1839
|
-
min-height: 5rem;
|
|
1840
|
-
border: 0;
|
|
1841
|
-
background: none;
|
|
1842
|
-
cursor: text;
|
|
1843
|
-
}
|
|
1844
|
-
|
|
1845
|
-
.toolbar {
|
|
1846
|
-
position: fixed;
|
|
1847
|
-
transform: translate(-50%, calc(-100% - 8px));
|
|
1848
|
-
display: flex;
|
|
1849
|
-
gap: 2px;
|
|
1850
|
-
padding: 4px;
|
|
1851
|
-
background: var(--surface);
|
|
1852
|
-
border: 1px solid var(--border);
|
|
1853
|
-
border-radius: 10px;
|
|
1854
|
-
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
|
|
1855
|
-
z-index: 20;
|
|
1856
|
-
}
|
|
1857
|
-
.toolbar button {
|
|
1858
|
-
min-width: 30px;
|
|
1859
|
-
height: 30px;
|
|
1860
|
-
border: 0;
|
|
1861
|
-
border-radius: 6px;
|
|
1862
|
-
background: none;
|
|
1863
|
-
color: var(--fg);
|
|
1864
|
-
display: grid;
|
|
1865
|
-
place-items: center;
|
|
1866
|
-
cursor: pointer;
|
|
1867
|
-
}
|
|
1868
|
-
.toolbar button:hover {
|
|
1869
|
-
background: var(--code-bg);
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
.menu {
|
|
1873
|
-
position: fixed;
|
|
1874
|
-
width: 260px;
|
|
1875
|
-
max-height: 300px;
|
|
1876
|
-
overflow: auto;
|
|
1877
|
-
padding: 6px;
|
|
1878
|
-
background: var(--surface);
|
|
1879
|
-
border: 1px solid var(--border);
|
|
1880
|
-
border-radius: 12px;
|
|
1881
|
-
box-shadow: 0 10px 36px rgba(0, 0, 0, 0.2);
|
|
1882
|
-
z-index: 30;
|
|
1883
|
-
}
|
|
1884
|
-
.menu.up {
|
|
1885
|
-
transform: translateY(-100%);
|
|
1886
|
-
}
|
|
1887
|
-
.menu.turn {
|
|
1888
|
-
width: 240px;
|
|
1889
|
-
max-height: 340px;
|
|
1890
|
-
}
|
|
1891
|
-
.caption {
|
|
1892
|
-
padding: 4px 8px 6px;
|
|
1893
|
-
font: 600 11px system-ui;
|
|
1894
|
-
letter-spacing: 0.06em;
|
|
1895
|
-
text-transform: uppercase;
|
|
1896
|
-
color: var(--muted);
|
|
1897
|
-
}
|
|
1898
|
-
.sep {
|
|
1899
|
-
height: 1px;
|
|
1900
|
-
margin: 6px 4px;
|
|
1901
|
-
background: var(--border);
|
|
1902
|
-
}
|
|
1903
|
-
.current {
|
|
1904
|
-
margin-left: auto;
|
|
1905
|
-
display: flex;
|
|
1906
|
-
color: var(--accent);
|
|
1907
|
-
}
|
|
1908
|
-
.txt small.warn {
|
|
1909
|
-
color: #b45309;
|
|
1910
|
-
}
|
|
1911
|
-
.item.danger {
|
|
1912
|
-
color: #dc2626;
|
|
1913
|
-
}
|
|
1914
|
-
.item.danger .icon {
|
|
1915
|
-
color: #dc2626;
|
|
1916
|
-
}
|
|
1917
|
-
/* Compact one-line rows: icon, label, and the hint after it in grey. */
|
|
1918
|
-
.item {
|
|
1919
|
-
display: flex;
|
|
1920
|
-
align-items: center;
|
|
1921
|
-
gap: 8px;
|
|
1922
|
-
padding: 3px 6px;
|
|
1923
|
-
border-radius: 6px;
|
|
1924
|
-
cursor: pointer;
|
|
1925
|
-
}
|
|
1926
|
-
.item.active {
|
|
1927
|
-
background: var(--sel);
|
|
1928
|
-
}
|
|
1929
|
-
.icon {
|
|
1930
|
-
flex: none;
|
|
1931
|
-
width: 24px;
|
|
1932
|
-
height: 24px;
|
|
1933
|
-
display: grid;
|
|
1934
|
-
place-items: center;
|
|
1935
|
-
border: 1px solid var(--border);
|
|
1936
|
-
border-radius: 6px;
|
|
1937
|
-
color: var(--fg);
|
|
1938
|
-
}
|
|
1939
|
-
.txt {
|
|
1940
|
-
min-width: 0;
|
|
1941
|
-
display: flex;
|
|
1942
|
-
align-items: baseline;
|
|
1943
|
-
gap: 6px;
|
|
1944
|
-
white-space: nowrap;
|
|
1945
|
-
overflow: hidden;
|
|
1946
|
-
}
|
|
1947
|
-
.txt b {
|
|
1948
|
-
font: 500 13px system-ui;
|
|
1949
|
-
}
|
|
1950
|
-
.txt small {
|
|
1951
|
-
overflow: hidden;
|
|
1952
|
-
text-overflow: ellipsis;
|
|
1953
|
-
color: var(--muted);
|
|
1954
|
-
font-size: 11px;
|
|
1955
|
-
}
|
|
1956
|
-
</style>
|
|
1519
|
+
{#if !nested}<div class="contents" use:portal><PopupContainer /></div>{/if}
|
|
1520
|
+
|
|
1521
|
+
<style>.editor {
|
|
1522
|
+
--fg: var(--color-surface-contrast, #1d1d1f);
|
|
1523
|
+
--muted: var(--color-muted-contrast, #a0a0a8);
|
|
1524
|
+
--accent: var(--color-accent, #3b82f6);
|
|
1525
|
+
--sel: color-mix(in srgb, var(--accent) 14%, transparent);
|
|
1526
|
+
--surface: var(--color-surface, #fff);
|
|
1527
|
+
--border: var(--color-frame, #e5e5ea);
|
|
1528
|
+
--code-bg: rgba(120, 120, 140, 0.14);
|
|
1529
|
+
position: relative;
|
|
1530
|
+
outline: none;
|
|
1531
|
+
color: var(--fg);
|
|
1532
|
+
font-size: 17px;
|
|
1533
|
+
line-height: 1.65;
|
|
1534
|
+
padding-bottom: 6rem;
|
|
1535
|
+
}
|
|
1536
|
+
.editor.sweeping, .editor.sweeping :global(*) {
|
|
1537
|
+
user-select: none;
|
|
1538
|
+
}
|
|
1539
|
+
.editor.nested {
|
|
1540
|
+
font-size: 1em;
|
|
1541
|
+
padding-bottom: 0;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
.row {
|
|
1545
|
+
position: relative;
|
|
1546
|
+
display: flex;
|
|
1547
|
+
border-radius: 6px;
|
|
1548
|
+
margin: 0 -8px;
|
|
1549
|
+
padding: 0 8px;
|
|
1550
|
+
}
|
|
1551
|
+
.row.selected {
|
|
1552
|
+
background: var(--sel);
|
|
1553
|
+
}
|
|
1554
|
+
.row.dragging {
|
|
1555
|
+
opacity: 0.35;
|
|
1556
|
+
}
|
|
1557
|
+
.row.drop-before::before, .row.drop-after::after {
|
|
1558
|
+
content: "";
|
|
1559
|
+
position: absolute;
|
|
1560
|
+
left: 8px;
|
|
1561
|
+
right: 8px;
|
|
1562
|
+
height: 3px;
|
|
1563
|
+
border-radius: 2px;
|
|
1564
|
+
background: var(--accent);
|
|
1565
|
+
z-index: 2;
|
|
1566
|
+
}
|
|
1567
|
+
.row.drop-before::before {
|
|
1568
|
+
top: -2px;
|
|
1569
|
+
}
|
|
1570
|
+
.row.drop-after::after {
|
|
1571
|
+
bottom: -2px;
|
|
1572
|
+
}
|
|
1573
|
+
.row:hover > .gutter .handle {
|
|
1574
|
+
background: rgba(120, 120, 140, 0.06);
|
|
1575
|
+
}
|
|
1576
|
+
.row:focus-within > .gutter .handle, .row.selected > .gutter .handle {
|
|
1577
|
+
box-shadow: inset 0 0 0 1px rgba(120, 120, 140, 0.22);
|
|
1578
|
+
}
|
|
1579
|
+
.row:focus-within:not(:has(.row:focus-within)) > .gutter .handle {
|
|
1580
|
+
animation: caret-pulse 1.1s ease-in-out infinite alternate;
|
|
1581
|
+
}
|
|
1582
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1583
|
+
.row:focus-within:not(:has(.row:focus-within)) > .gutter .handle {
|
|
1584
|
+
animation: none;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
.row:hover > .gutter .grip, .row:focus-within > .gutter .grip, .row.selected > .gutter .grip {
|
|
1588
|
+
opacity: 1;
|
|
1589
|
+
}
|
|
1590
|
+
.row > .gutter .handle:hover, .row.dragging > .gutter .handle {
|
|
1591
|
+
background: var(--code-bg);
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
@keyframes caret-pulse {
|
|
1595
|
+
from {
|
|
1596
|
+
box-shadow: inset 0 0 0 1px rgba(120, 120, 140, 0.06);
|
|
1597
|
+
}
|
|
1598
|
+
to {
|
|
1599
|
+
box-shadow: inset 0 0 0 1px rgba(120, 120, 140, 0.3);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
.gutter {
|
|
1603
|
+
position: absolute;
|
|
1604
|
+
left: -22px;
|
|
1605
|
+
top: 1px;
|
|
1606
|
+
bottom: 1px;
|
|
1607
|
+
width: 20px;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
.handle {
|
|
1611
|
+
width: 100%;
|
|
1612
|
+
height: 100%;
|
|
1613
|
+
padding: 0;
|
|
1614
|
+
border: 0;
|
|
1615
|
+
background: transparent;
|
|
1616
|
+
font: inherit;
|
|
1617
|
+
display: flex;
|
|
1618
|
+
align-items: center;
|
|
1619
|
+
justify-content: center;
|
|
1620
|
+
user-select: none;
|
|
1621
|
+
color: var(--muted);
|
|
1622
|
+
border-radius: 4px;
|
|
1623
|
+
cursor: grab;
|
|
1624
|
+
transition: background 0.12s, box-shadow 0.12s;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
.grip {
|
|
1628
|
+
width: 20px;
|
|
1629
|
+
height: 20px;
|
|
1630
|
+
display: flex;
|
|
1631
|
+
align-items: center;
|
|
1632
|
+
justify-content: center;
|
|
1633
|
+
opacity: 0;
|
|
1634
|
+
transition: opacity 0.12s;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
.grab {
|
|
1638
|
+
position: relative;
|
|
1639
|
+
height: 100%;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
.bubble {
|
|
1643
|
+
position: absolute;
|
|
1644
|
+
left: 100%;
|
|
1645
|
+
top: 50%;
|
|
1646
|
+
z-index: 30;
|
|
1647
|
+
display: flex;
|
|
1648
|
+
padding-left: 6px;
|
|
1649
|
+
translate: 0 -50%;
|
|
1650
|
+
opacity: 0;
|
|
1651
|
+
visibility: hidden;
|
|
1652
|
+
transition: opacity 0.12s, visibility 0s 0.12s;
|
|
1653
|
+
}
|
|
1654
|
+
.bubble::before {
|
|
1655
|
+
content: "";
|
|
1656
|
+
position: absolute;
|
|
1657
|
+
left: 2px;
|
|
1658
|
+
top: 50%;
|
|
1659
|
+
translate: 0 -50%;
|
|
1660
|
+
border: 4px solid transparent;
|
|
1661
|
+
border-right-color: var(--color-surface-contrast, #1d1d1f);
|
|
1662
|
+
border-left: 0;
|
|
1663
|
+
}
|
|
1664
|
+
.grab:has(.grip:hover) .bubble, .grab:focus-within .bubble, .bubble:hover {
|
|
1665
|
+
opacity: 1;
|
|
1666
|
+
visibility: visible;
|
|
1667
|
+
transition-delay: 0.15s, 0s;
|
|
1668
|
+
}
|
|
1669
|
+
@media (hover: none) {
|
|
1670
|
+
.row.selected > .gutter .bubble {
|
|
1671
|
+
opacity: 1;
|
|
1672
|
+
visibility: visible;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
.handle:active ~ .bubble, .editor.sweeping .bubble {
|
|
1676
|
+
opacity: 0;
|
|
1677
|
+
visibility: hidden;
|
|
1678
|
+
transition: none;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
.body {
|
|
1682
|
+
flex: 1;
|
|
1683
|
+
min-width: 0;
|
|
1684
|
+
display: flex;
|
|
1685
|
+
align-items: flex-start;
|
|
1686
|
+
gap: 0.5em;
|
|
1687
|
+
padding: 0.15em 0;
|
|
1688
|
+
margin-left: calc(var(--indent, 0) * 1.7em);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
.ce {
|
|
1692
|
+
flex: 1;
|
|
1693
|
+
min-width: 0;
|
|
1694
|
+
min-height: 1.65em;
|
|
1695
|
+
outline: none;
|
|
1696
|
+
white-space: pre-wrap;
|
|
1697
|
+
word-break: break-word;
|
|
1698
|
+
caret-color: var(--accent);
|
|
1699
|
+
}
|
|
1700
|
+
.ce.empty::before {
|
|
1701
|
+
content: attr(data-placeholder);
|
|
1702
|
+
color: var(--muted);
|
|
1703
|
+
pointer-events: none;
|
|
1704
|
+
}
|
|
1705
|
+
.ce.done {
|
|
1706
|
+
color: var(--muted);
|
|
1707
|
+
text-decoration: line-through;
|
|
1708
|
+
}
|
|
1709
|
+
.ce :global(code) {
|
|
1710
|
+
background: var(--code-bg);
|
|
1711
|
+
padding: 0.1em 0.35em;
|
|
1712
|
+
border-radius: 4px;
|
|
1713
|
+
font: 0.88em ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
1714
|
+
}
|
|
1715
|
+
.ce :global(a) {
|
|
1716
|
+
color: var(--accent);
|
|
1717
|
+
text-decoration: underline;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
.t-p .ce.empty:not(:focus)::before,
|
|
1721
|
+
.t-ul .ce.empty:not(:focus)::before,
|
|
1722
|
+
.t-ol .ce.empty:not(:focus)::before {
|
|
1723
|
+
content: "";
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
.t-h1 {
|
|
1727
|
+
padding-top: 0.6em;
|
|
1728
|
+
}
|
|
1729
|
+
.t-h1 .ce {
|
|
1730
|
+
font-size: 2em;
|
|
1731
|
+
font-weight: 700;
|
|
1732
|
+
line-height: 1.25;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
.t-h2 {
|
|
1736
|
+
padding-top: 0.45em;
|
|
1737
|
+
}
|
|
1738
|
+
.t-h2 .ce {
|
|
1739
|
+
font-size: 1.5em;
|
|
1740
|
+
font-weight: 650;
|
|
1741
|
+
line-height: 1.3;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
.t-h3 {
|
|
1745
|
+
padding-top: 0.3em;
|
|
1746
|
+
}
|
|
1747
|
+
.t-h3 .ce {
|
|
1748
|
+
font-size: 1.2em;
|
|
1749
|
+
font-weight: 600;
|
|
1750
|
+
line-height: 1.4;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
.t-h4 {
|
|
1754
|
+
padding-top: 0.25em;
|
|
1755
|
+
}
|
|
1756
|
+
.t-h4 .ce {
|
|
1757
|
+
font-size: 1.05em;
|
|
1758
|
+
font-weight: 600;
|
|
1759
|
+
line-height: 1.45;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
.t-h5 {
|
|
1763
|
+
padding-top: 0.2em;
|
|
1764
|
+
}
|
|
1765
|
+
.t-h5 .ce {
|
|
1766
|
+
font-size: 0.95em;
|
|
1767
|
+
font-weight: 600;
|
|
1768
|
+
line-height: 1.5;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
.t-h6 {
|
|
1772
|
+
padding-top: 0.2em;
|
|
1773
|
+
}
|
|
1774
|
+
.t-h6 .ce {
|
|
1775
|
+
font-size: 0.85em;
|
|
1776
|
+
font-weight: 600;
|
|
1777
|
+
line-height: 1.5;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
.t-h6 .ce {
|
|
1781
|
+
letter-spacing: 0.04em;
|
|
1782
|
+
text-transform: uppercase;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
.t-quote .ce {
|
|
1786
|
+
border-left: 3px solid var(--fg);
|
|
1787
|
+
padding-left: 0.9em;
|
|
1788
|
+
font-style: italic;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
.t-plugin {
|
|
1792
|
+
position: relative;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
.t-code {
|
|
1796
|
+
position: relative;
|
|
1797
|
+
flex-wrap: wrap;
|
|
1798
|
+
row-gap: 0;
|
|
1799
|
+
}
|
|
1800
|
+
.t-code .ce {
|
|
1801
|
+
flex-basis: 100%;
|
|
1802
|
+
background: var(--code-bg);
|
|
1803
|
+
border-radius: 8px;
|
|
1804
|
+
padding: 0.7em 0.9em;
|
|
1805
|
+
font: 0.88em/1.6 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
1806
|
+
white-space: pre-wrap;
|
|
1807
|
+
}
|
|
1808
|
+
.t-code:has(> .badge) .ce {
|
|
1809
|
+
padding-right: 7.5em;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
.t-hr {
|
|
1813
|
+
padding: 0.6em 0;
|
|
1814
|
+
}</style>
|