@quillmark/svelte 0.24.0 → 0.25.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/THEMING.md +1 -1
- package/dist/core/codec/blocks.d.ts +23 -6
- package/dist/core/codec/blocks.js +163 -38
- package/dist/core/codec/code.js +3 -8
- package/dist/core/codec/field.d.ts +4 -2
- package/dist/core/codec/field.js +12 -4
- package/dist/core/codec/index.d.ts +2 -0
- package/dist/core/codec/index.js +4 -0
- package/dist/core/codec/inputrules.js +40 -10
- package/dist/core/codec/keymap.js +19 -1
- package/dist/core/codec/line-paste.d.ts +7 -0
- package/dist/core/codec/line-paste.js +36 -0
- package/dist/core/codec/list-move.d.ts +6 -0
- package/dist/core/codec/list-move.js +30 -0
- package/dist/core/codec/list-paste.d.ts +2 -0
- package/dist/core/codec/list-paste.js +142 -0
- package/dist/core/codec/list-toggle.d.ts +4 -0
- package/dist/core/codec/list-toggle.js +170 -0
- package/dist/core/codec/lists.d.ts +22 -15
- package/dist/core/codec/lists.js +404 -97
- package/dist/core/codec/markdown-paste.d.ts +9 -0
- package/dist/core/codec/markdown-paste.js +63 -2
- package/dist/core/codec/nesting.d.ts +5 -1
- package/dist/core/codec/nesting.js +1 -1
- package/dist/core/codec/prose.css +21 -20
- package/dist/core/codec/slash.d.ts +2 -1
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/touching.d.ts +12 -0
- package/dist/core/codec/touching.js +33 -0
- package/dist/core/codec/undo.d.ts +5 -0
- package/dist/core/codec/undo.js +21 -0
- package/dist/preview/Preview.svelte +88 -13
- package/dist/preview/Preview.svelte.d.ts +2 -2
- package/dist/preview/bridge.js +8 -2
- package/dist/preview/controller.d.ts +8 -4
- package/dist/preview/controller.js +10 -2
- package/dist/preview/index.js +1 -1
- package/dist/preview/paint.d.ts +6 -2
- package/dist/preview/paint.js +67 -56
- package/dist/preview/strings.d.ts +2 -0
- package/dist/preview/strings.js +4 -2
- package/dist/preview/viewport.d.ts +10 -0
- package/dist/preview/viewport.js +271 -0
- package/dist/preview/zoom.d.ts +41 -0
- package/dist/preview/zoom.js +52 -0
- package/dist/visual/ArrayField.svelte +24 -4
- package/dist/visual/DateField.svelte +26 -13
- package/dist/visual/FormatPopover.svelte +41 -2
- package/dist/visual/ProseValue.svelte +35 -3
- package/dist/visual/ProseValue.svelte.d.ts +7 -0
- package/dist/visual/icons/nodes.d.ts +26 -0
- package/dist/visual/icons/nodes.js +16 -0
- package/dist/visual/strings.d.ts +4 -2
- package/dist/visual/strings.js +9 -3
- package/package.json +1 -1
|
@@ -4,20 +4,81 @@
|
|
|
4
4
|
// the paste stores what the source editor stores of the same text. Text holding none stays
|
|
5
5
|
// literal: a sentence with a dash in it, or a `**` with no block around it, keeps every
|
|
6
6
|
// character.
|
|
7
|
+
//
|
|
8
|
+
// A list typed in an email or a memo draft marks its items with what markdown does not
|
|
9
|
+
// spell: a bullet character, a letter, a number in parentheses. Those lines are items too,
|
|
10
|
+
// nested by the outline the memo writes them in (`1.`, `a.`, `(1)`, `(a)`): each kind of
|
|
11
|
+
// marker is a level, the first line of a kind opening one under the level above.
|
|
7
12
|
import { Slice } from 'prosemirror-model';
|
|
8
13
|
import { Plugin } from 'prosemirror-state';
|
|
9
14
|
import { core } from '../lifecycle.js';
|
|
10
15
|
import { decode, fitsLeaf } from './decode.js';
|
|
11
16
|
import { blockSchema } from './schema.js';
|
|
17
|
+
/** A line opening an item: its indent, then the marker as markdown spells one (`-`, `*`,
|
|
18
|
+
* `+`, `1.`, `1)`), or as a plain-text list does (`•` and the bullet characters word
|
|
19
|
+
* processors write as text, a lowercase letter closed by `.` or `)`, and a number or a
|
|
20
|
+
* lowercase letter in parentheses). A capital is a name's initial as often as a marker,
|
|
21
|
+
* so `J. Doe` is not an item. */
|
|
22
|
+
const ITEM = /^([ \t]*)(?:([-*+])|(\d{1,9})[.)]|([•◦▪▫‣⁃●○■□·])|([a-z])[.)]|\((\d{1,9})\)|\(([a-z])\))[ \t]+(?=\S)/;
|
|
23
|
+
/** The markers plain text writes and markdown does not. */
|
|
24
|
+
const LOOSE_ITEM = /^[ \t]*(?:[•◦▪▫‣⁃●○■□·]|[a-z][.)]|\((?:\d{1,9}|[a-z])\))[ \t]+\S/m;
|
|
12
25
|
/** A line opening a block markdown spells and prose does not: a bullet or an ordered item at
|
|
13
26
|
* any indent, and within CommonMark's three spaces an ATX heading, a quote, a fence, or a
|
|
14
|
-
* pipe table's delimiter row. */
|
|
27
|
+
* pipe table's delimiter row; or an item a plain-text list marks. */
|
|
15
28
|
const BLOCK_HEAD = new RegExp([
|
|
16
29
|
String.raw `^[ \t]*(?:[-*+]|\d{1,9}[.)])[ \t]`,
|
|
30
|
+
LOOSE_ITEM.source,
|
|
17
31
|
String.raw `^ {0,3}(?:#{1,6}|>)(?:[ \t]|$)`,
|
|
18
32
|
'^ {0,3}(?:```|~~~)',
|
|
19
33
|
String.raw `^ {0,3}(?=[^\n]*\|)\|?(?:[ \t]*:?-+:?[ \t]*\|)*[ \t]*:?-+:?[ \t]*\|?[ \t]*$`
|
|
20
34
|
].join('|'), 'm');
|
|
35
|
+
/**
|
|
36
|
+
* `text` with every item line spelled as markdown, where some line marks its item as
|
|
37
|
+
* markdown does not; otherwise `text` as it stands. A line's level is the open one of its
|
|
38
|
+
* kind at its indent, give or take a column, the levels below it closing; a marker no open
|
|
39
|
+
* level holds opens one beneath the deepest. Markdown nests by indent alone, so each line
|
|
40
|
+
* is indented past the markers of the levels above it. A letter's ordinal is its place in
|
|
41
|
+
* the alphabet. Lines in a fence and lines opening no item stand as they are.
|
|
42
|
+
*/
|
|
43
|
+
export function plainTextList(text) {
|
|
44
|
+
if (!LOOSE_ITEM.test(text))
|
|
45
|
+
return text;
|
|
46
|
+
const levels = [];
|
|
47
|
+
let fence = false;
|
|
48
|
+
let prev = '';
|
|
49
|
+
return text
|
|
50
|
+
.split(/\r\n?|\n/)
|
|
51
|
+
.map((line) => {
|
|
52
|
+
const above = prev;
|
|
53
|
+
prev = line;
|
|
54
|
+
if (/^ {0,3}(?:```|~~~)/.test(line))
|
|
55
|
+
fence = !fence;
|
|
56
|
+
const m = fence ? null : ITEM.exec(line);
|
|
57
|
+
if (!m)
|
|
58
|
+
return line;
|
|
59
|
+
const [head, lead, dash, num, bullet, alpha, pnum, palpha] = m;
|
|
60
|
+
const indent = lead.replace(/\t/g, ' ').length;
|
|
61
|
+
const kind = dash ?? bullet ?? String(m.slice(2).findIndex(Boolean));
|
|
62
|
+
const letter = alpha ?? palpha;
|
|
63
|
+
const ordinal = letter ? letter.charCodeAt(0) - 96 : Number(num ?? pnum);
|
|
64
|
+
const marker = dash || bullet ? '- ' : `${ordinal}. `;
|
|
65
|
+
let depth = levels.findLastIndex((l) => l.kind === kind && Math.abs(l.indent - indent) <= 1);
|
|
66
|
+
const opens = depth < 0;
|
|
67
|
+
if (opens) {
|
|
68
|
+
while (levels.length && levels.at(-1).indent > indent + 1)
|
|
69
|
+
levels.pop();
|
|
70
|
+
depth = levels.length;
|
|
71
|
+
}
|
|
72
|
+
levels.length = depth;
|
|
73
|
+
levels.push({ kind, indent, width: marker.length });
|
|
74
|
+
const pad = levels.slice(0, depth).reduce((sum, l) => sum + l.width, 0);
|
|
75
|
+
// Only a list counting from 1 interrupts the line above it; a blank line lets
|
|
76
|
+
// any other open where it stands.
|
|
77
|
+
const gap = opens && ordinal !== 1 && marker !== '- ' && above.trim() ? '\n' : '';
|
|
78
|
+
return gap + ' '.repeat(pad) + marker + line.slice(head.length);
|
|
79
|
+
})
|
|
80
|
+
.join('\n');
|
|
81
|
+
}
|
|
21
82
|
/**
|
|
22
83
|
* A block leaf's markdown paste, consulted after the table paste: ProseMirror hands text
|
|
23
84
|
* over only where the clipboard holds no HTML, whose DOM parse reads the source's own
|
|
@@ -37,7 +98,7 @@ export function markdownPastePlugin() {
|
|
|
37
98
|
const { importMarkdown } = core();
|
|
38
99
|
let rt;
|
|
39
100
|
try {
|
|
40
|
-
rt = importMarkdown(text).content;
|
|
101
|
+
rt = importMarkdown(plainTextList(text)).content;
|
|
41
102
|
}
|
|
42
103
|
catch {
|
|
43
104
|
// Nesting past the store's depth is refused; the literal text holds it.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Node as PMNode } from 'prosemirror-model';
|
|
2
|
-
import { Plugin } from 'prosemirror-state';
|
|
2
|
+
import { Plugin, type Transaction } from 'prosemirror-state';
|
|
3
3
|
/** The store's bound on the containers around one line: a commit nesting past it is
|
|
4
4
|
* refused, and `@quillmark/wasm` exports no constant to read it from. */
|
|
5
5
|
export declare const MAX_NESTING_DEPTH = 12;
|
|
@@ -10,6 +10,10 @@ export declare function nestingDepth(doc: PMNode): number;
|
|
|
10
10
|
/** Whether `doc` nests within {@link MAX_NESTING_DEPTH}: what a command that nests asks
|
|
11
11
|
* of the doc it would dispatch. */
|
|
12
12
|
export declare function withinNesting(doc: PMNode): boolean;
|
|
13
|
+
/** Whether a step of `tr` can move a block under another container: any replace but
|
|
14
|
+
* inline content inside one textblock, which is every keystroke's. Mark and attribute
|
|
15
|
+
* steps move no node. */
|
|
16
|
+
export declare function mayNest(tr: Transaction): boolean;
|
|
13
17
|
/**
|
|
14
18
|
* Refuses a transaction leaving `doc` past {@link MAX_NESTING_DEPTH}, which the commit
|
|
15
19
|
* would lose: a paste or a drop into a deep item, whose slice the import bounded alone.
|
|
@@ -28,7 +28,7 @@ export function withinNesting(doc) {
|
|
|
28
28
|
/** Whether a step of `tr` can move a block under another container: any replace but
|
|
29
29
|
* inline content inside one textblock, which is every keystroke's. Mark and attribute
|
|
30
30
|
* steps move no node. */
|
|
31
|
-
function mayNest(tr) {
|
|
31
|
+
export function mayNest(tr) {
|
|
32
32
|
return tr.steps.some((step, i) => {
|
|
33
33
|
if (step instanceof ReplaceAroundStep)
|
|
34
34
|
return true;
|
|
@@ -56,26 +56,30 @@ li.ProseMirror-selectednode::after {
|
|
|
56
56
|
|
|
57
57
|
/* One rhythm between sibling blocks, in place of the UA's collapsed pair. Scoped by
|
|
58
58
|
parent rather than by tag, which is what keeps it true as the schema grows: the
|
|
59
|
-
containers below are every node whose
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
next line of one list, not a new block after a paragraph — so an item's children
|
|
67
|
-
take the gap only up to the point where one of them is a list. Written as its own
|
|
68
|
-
arm rather than as a `:not()` over the shared one, because the exception is the
|
|
69
|
-
item's: a list after a paragraph inside a blockquote is an ordinary sibling pair
|
|
70
|
-
and keeps the gap.
|
|
59
|
+
containers below are every node whose children are blocks (`schema.ts`), whatever
|
|
60
|
+
kinds exist; a task item holds its blocks in a body past its checkbox (`tasks.ts`),
|
|
61
|
+
so the body is the container rather than the item. A list and an item are
|
|
62
|
+
containers like any other: an item stands off its neighbours as a paragraph does, a
|
|
63
|
+
list nested under an item stands off the item's text, and an item's unnumbered line
|
|
64
|
+
has the gap below it that the next item draws above, where a tight run would read it
|
|
65
|
+
as the head of the next item.
|
|
71
66
|
|
|
72
67
|
A line box is ~22px at the paper size and the body leading, so the rung is a
|
|
73
68
|
little over half a line: enough that a break reads as a break, short of the full
|
|
74
69
|
blank line that doubles the leading and makes a short document read as a list of
|
|
75
70
|
fragments. */
|
|
76
71
|
.ProseMirror > * + *,
|
|
77
|
-
.ProseMirror
|
|
78
|
-
|
|
72
|
+
.ProseMirror
|
|
73
|
+
:is(
|
|
74
|
+
blockquote,
|
|
75
|
+
ul,
|
|
76
|
+
ol,
|
|
77
|
+
li:not([data-qm-checked]),
|
|
78
|
+
[data-qm-task-body],
|
|
79
|
+
[data-qm-unknown-container]
|
|
80
|
+
)
|
|
81
|
+
> *
|
|
82
|
+
+ * {
|
|
79
83
|
margin-top: var(--_qm-space-3);
|
|
80
84
|
}
|
|
81
85
|
|
|
@@ -200,9 +204,6 @@ li.ProseMirror-selectednode::after {
|
|
|
200
204
|
|
|
201
205
|
/* The UA's 40px list indent is the one box the reset above does not take (it is
|
|
202
206
|
padding, not margin), and it is a length nothing chose. A rung replaces it.
|
|
203
|
-
Per-item spacing is untouched, which is the point of putting this on the list
|
|
204
|
-
rather than the item: the sibling-gap rule deliberately omits `ul`/`ol`, and
|
|
205
|
-
padding on the container cannot reintroduce what it withholds.
|
|
206
207
|
|
|
207
208
|
The markers are the UA's, reclaimed rather than inherited. `disc` and `decimal`
|
|
208
209
|
are right and this restates neither: a host reset (Tailwind's preflight sets
|
|
@@ -369,9 +370,9 @@ li.ProseMirror-selectednode::after {
|
|
|
369
370
|
absorbs the gap, at both ends. A negative margin would hang the band over the
|
|
370
371
|
neighbouring block, and an `opacity: 0` control is still hit-tested, so it would take
|
|
371
372
|
that block's presses. The `.ProseMirror` qualifier is what makes the override hold
|
|
372
|
-
wherever the island lands:
|
|
373
|
-
|
|
374
|
-
schema admits. */
|
|
373
|
+
wherever the island lands: the gap rule's container arm outweighs a bare class, and
|
|
374
|
+
loses to this one on order alone, for an island inside a list item or a quote, both
|
|
375
|
+
of which the schema admits. */
|
|
375
376
|
.ProseMirror .qm-table-island {
|
|
376
377
|
margin-top: 0;
|
|
377
378
|
margin-left: calc(var(--_qm-space-3) * -1);
|
|
@@ -32,7 +32,8 @@ export declare const DEFAULT_SLASH_STRINGS: SlashStrings;
|
|
|
32
32
|
* by, each asked whether it would fire here. A row that declines when picked is worse
|
|
33
33
|
* than an absent one, and asking the command rather than restating its guards is what
|
|
34
34
|
* keeps the menu from drifting off what a pick does. So the offers narrow as the caret
|
|
35
|
-
* moves: the lists and the island alone mid-paragraph,
|
|
35
|
+
* moves: the lists and the island alone mid-paragraph, and at an item's head only the
|
|
36
|
+
* list of the other kind, which retypes the item's list.
|
|
36
37
|
*
|
|
37
38
|
* `state` is the state a pick leaves behind — the run already consumed — since that is
|
|
38
39
|
* the caret every command answers for (`blocks.ts`).
|
package/dist/core/codec/slash.js
CHANGED
|
Binary file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Node as PMNode } from 'prosemirror-model';
|
|
2
|
+
import type { Transaction } from 'prosemirror-state';
|
|
3
|
+
/** Whether two lists of one kind meet at `pos`. */
|
|
4
|
+
export declare function touching(doc: PMNode, pos: number): boolean;
|
|
5
|
+
/** Join the two lists of one kind meeting at `pos`, the first's `start` counting for
|
|
6
|
+
* both. */
|
|
7
|
+
export declare function joinLists(tr: Transaction, pos: number): boolean;
|
|
8
|
+
/** Join the list at `pos` to a list of its kind touching it on either side. */
|
|
9
|
+
export declare function joinAround(tr: Transaction, pos: number): void;
|
|
10
|
+
/** Join at every seam of `seams` that two lists of one kind meet at, the last first so
|
|
11
|
+
* each join leaves the positions before it standing. */
|
|
12
|
+
export declare function joinAll(tr: Transaction, seams: Iterable<number>): void;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { canJoin } from 'prosemirror-transform';
|
|
2
|
+
function isList(node) {
|
|
3
|
+
return node.firstChild?.type === node.type.schema.nodes.list_item;
|
|
4
|
+
}
|
|
5
|
+
/** Whether two lists of one kind meet at `pos`. */
|
|
6
|
+
export function touching(doc, pos) {
|
|
7
|
+
const $pos = doc.resolve(pos);
|
|
8
|
+
const before = $pos.nodeBefore;
|
|
9
|
+
const after = $pos.nodeAfter;
|
|
10
|
+
return !!before && !!after && before.type === after.type && isList(before);
|
|
11
|
+
}
|
|
12
|
+
/** Join the two lists of one kind meeting at `pos`, the first's `start` counting for
|
|
13
|
+
* both. */
|
|
14
|
+
export function joinLists(tr, pos) {
|
|
15
|
+
if (!touching(tr.doc, pos) || !canJoin(tr.doc, pos))
|
|
16
|
+
return false;
|
|
17
|
+
tr.join(pos);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
/** Join the list at `pos` to a list of its kind touching it on either side. */
|
|
21
|
+
export function joinAround(tr, pos) {
|
|
22
|
+
const node = tr.doc.nodeAt(pos);
|
|
23
|
+
if (!node)
|
|
24
|
+
return;
|
|
25
|
+
joinLists(tr, pos + node.nodeSize);
|
|
26
|
+
joinLists(tr, pos);
|
|
27
|
+
}
|
|
28
|
+
/** Join at every seam of `seams` that two lists of one kind meet at, the last first so
|
|
29
|
+
* each join leaves the positions before it standing. */
|
|
30
|
+
export function joinAll(tr, seams) {
|
|
31
|
+
for (const seam of [...new Set(seams)].sort((a, b) => b - a))
|
|
32
|
+
joinLists(tr, seam);
|
|
33
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Plugin } from 'prosemirror-state';
|
|
2
|
+
/** Marks each block-moving transaction as the start of an undo step. `filterTransaction`
|
|
3
|
+
* is the one hook that sees a transaction before history's `apply` does; it passes
|
|
4
|
+
* every one. A transaction a plugin appended stays in its root's step. */
|
|
5
|
+
export declare function undoStepPlugin(): Plugin;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Undo steps. History groups a run of adjacent edits under its delay, so a burst of
|
|
2
|
+
// typing with a list change in it would undo as one step. A transaction that moves a
|
|
3
|
+
// block (`mayNest`: a split, a join, a wrap, a lift) opens a step of its own, the typing
|
|
4
|
+
// after it joining that step.
|
|
5
|
+
import { closeHistory, isHistoryTransaction } from 'prosemirror-history';
|
|
6
|
+
import { Plugin } from 'prosemirror-state';
|
|
7
|
+
import { mayNest } from './nesting.js';
|
|
8
|
+
/** Marks each block-moving transaction as the start of an undo step. `filterTransaction`
|
|
9
|
+
* is the one hook that sees a transaction before history's `apply` does; it passes
|
|
10
|
+
* every one. A transaction a plugin appended stays in its root's step. */
|
|
11
|
+
export function undoStepPlugin() {
|
|
12
|
+
return new Plugin({
|
|
13
|
+
filterTransaction: (tr) => {
|
|
14
|
+
if (!tr.docChanged || isHistoryTransaction(tr) || tr.getMeta('appendedTransaction'))
|
|
15
|
+
return true;
|
|
16
|
+
if (mayNest(tr))
|
|
17
|
+
closeHistory(tr);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
onPick?: (at: Landing) => void;
|
|
39
39
|
/** A page paint the backend refused; the error message state shows either way. */
|
|
40
40
|
onError?: EditorErrorHandler;
|
|
41
|
-
/** The
|
|
41
|
+
/** The surface's wording, keyed and partial (`PreviewStrings`). */
|
|
42
42
|
strings?: PreviewStringsInput;
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -78,8 +78,9 @@
|
|
|
78
78
|
export function endFollow(): void {
|
|
79
79
|
controller?.endFollow();
|
|
80
80
|
}
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
/** A fraction of actual size; `undefined` is the default fit ({@link PreviewController}). */
|
|
82
|
+
export function setZoom(zoom: number | undefined): void {
|
|
83
|
+
controller?.setZoom(zoom);
|
|
83
84
|
}
|
|
84
85
|
</script>
|
|
85
86
|
|
|
@@ -93,25 +94,99 @@
|
|
|
93
94
|
tone behind it is the sunken rung and the gutter is the margin the sheet floats
|
|
94
95
|
in. Paper is the brighter plane at both poles, exactly as a card is over the
|
|
95
96
|
editor's own column, so the two panes step the same way. Both are
|
|
96
|
-
the surface's own, so a bare `<div>` is a mounting site. The
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
the surface's own, so a bare `<div>` is a mounting site. The viewport fits the
|
|
98
|
+
pages to the content box, padding off.
|
|
99
|
+
|
|
100
|
+
A flex column, so the zoom pill's dock can take the space under pages shorter than
|
|
101
|
+
the pane and stand at its corner (viewport.ts). The gutter is reserved, so a
|
|
102
|
+
scrollbar arriving with a taller zoom narrows nothing the default fits against.
|
|
103
|
+
Scroll anchoring is off because the viewport holds the reader's place itself, on
|
|
104
|
+
every zoom and re-fit, and the browser's correction would land on top of it.
|
|
105
|
+
`touch-action` leaves a pinch to the viewport, which zooms the pages rather than
|
|
106
|
+
the host's page.
|
|
100
107
|
|
|
101
108
|
`:global(:where(…))` puts this promised class at zero rank (ARCHITECTURE §Styling). */
|
|
102
109
|
:global(:where(.qm-preview)) {
|
|
103
110
|
box-sizing: border-box;
|
|
104
111
|
position: relative;
|
|
112
|
+
display: flex;
|
|
113
|
+
flex-direction: column;
|
|
105
114
|
width: 100%;
|
|
106
|
-
/* The width is the host's, never the document's, which is the same boundary
|
|
107
|
-
editor states (`visual/VisualEditorInner.svelte` §`.qm-editor`): a
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
it. */
|
|
115
|
+
/* The pane's width is the host's, never the document's, which is the same boundary
|
|
116
|
+
the editor states (`visual/VisualEditorInner.svelte` §`.qm-editor`): a page zoomed
|
|
117
|
+
wider than the pane scrolls inside it rather than widening it. `inline-size`, not
|
|
118
|
+
`size`: the column is as tall as the pages painted in it. */
|
|
111
119
|
contain: inline-size;
|
|
112
120
|
height: 100%;
|
|
113
|
-
overflow
|
|
121
|
+
overflow: auto;
|
|
122
|
+
scrollbar-gutter: stable;
|
|
123
|
+
overflow-anchor: none;
|
|
124
|
+
touch-action: pan-x pan-y;
|
|
114
125
|
padding: var(--_qm-space-4);
|
|
115
126
|
background: var(--_qm-surface-sunken);
|
|
116
127
|
}
|
|
128
|
+
|
|
129
|
+
/* Sticky to the pane's bottom edge and stretched across it, so the pill's corner is
|
|
130
|
+
the pane's however the pages scroll; zero height, so it displaces nothing. */
|
|
131
|
+
:global(:where(.qm-preview-dock)) {
|
|
132
|
+
order: 1;
|
|
133
|
+
flex: none;
|
|
134
|
+
position: sticky;
|
|
135
|
+
bottom: 0;
|
|
136
|
+
inset-inline-start: 0;
|
|
137
|
+
height: 0;
|
|
138
|
+
margin-top: auto;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* Floats over the desk, so it lifts the way a floating surface does (ARCHITECTURE
|
|
142
|
+
§Styling), and says only the zoom: the label never changes. Tabular figures and a
|
|
143
|
+
floor as wide as `100%`, so a third digit never shifts its edge. It arrives at once and leaves on the reveal, so a glance at
|
|
144
|
+
it after a gesture still finds it. */
|
|
145
|
+
:global(:where(.qm-preview-zoom)) {
|
|
146
|
+
position: absolute;
|
|
147
|
+
inset-inline-end: 0;
|
|
148
|
+
bottom: 0;
|
|
149
|
+
display: inline-flex;
|
|
150
|
+
align-items: center;
|
|
151
|
+
justify-content: center;
|
|
152
|
+
min-width: calc(4ch + 2 * var(--_qm-space-2));
|
|
153
|
+
min-height: var(--_qm-tap-min);
|
|
154
|
+
padding: 0 var(--_qm-space-2);
|
|
155
|
+
border: var(--_qm-border-width) solid var(--_qm-border);
|
|
156
|
+
border-radius: var(--_qm-radius-pill);
|
|
157
|
+
background: var(--_qm-surface-popover);
|
|
158
|
+
backdrop-filter: blur(var(--_qm-blur));
|
|
159
|
+
box-shadow: var(--_qm-lift);
|
|
160
|
+
color: var(--_qm-ink-label);
|
|
161
|
+
font-family: var(--_qm-font);
|
|
162
|
+
font-size: var(--_qm-text-label);
|
|
163
|
+
font-variant-numeric: tabular-nums;
|
|
164
|
+
cursor: pointer;
|
|
165
|
+
user-select: none;
|
|
166
|
+
transition:
|
|
167
|
+
opacity var(--_qm-duration-fast) var(--_qm-ease-reverse),
|
|
168
|
+
color var(--_qm-duration-fast) var(--_qm-ease-reverse);
|
|
169
|
+
}
|
|
170
|
+
:global(:where(.qm-preview-zoom:hover)) {
|
|
171
|
+
color: var(--_qm-ink);
|
|
172
|
+
}
|
|
173
|
+
:global(:where(.qm-preview-zoom:active)) {
|
|
174
|
+
background: var(--_qm-surface-hover);
|
|
175
|
+
}
|
|
176
|
+
:global(:where(.qm-preview-zoom[data-idle])) {
|
|
177
|
+
opacity: 0;
|
|
178
|
+
pointer-events: none;
|
|
179
|
+
transition: opacity var(--_qm-reveal);
|
|
180
|
+
}
|
|
181
|
+
:global(:where(.qm-preview-zoom:focus-visible)) {
|
|
182
|
+
opacity: 1;
|
|
183
|
+
pointer-events: auto;
|
|
184
|
+
outline: var(--_qm-ring);
|
|
185
|
+
outline-offset: var(--_qm-ring-offset);
|
|
186
|
+
}
|
|
187
|
+
@media (prefers-reduced-motion: reduce) {
|
|
188
|
+
:global(:where(.qm-preview-zoom, .qm-preview-zoom[data-idle])) {
|
|
189
|
+
transition: none;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
117
192
|
</style>
|
|
@@ -25,7 +25,7 @@ interface Props {
|
|
|
25
25
|
onPick?: (at: Landing) => void;
|
|
26
26
|
/** A page paint the backend refused; the error message state shows either way. */
|
|
27
27
|
onError?: EditorErrorHandler;
|
|
28
|
-
/** The
|
|
28
|
+
/** The surface's wording, keyed and partial (`PreviewStrings`). */
|
|
29
29
|
strings?: PreviewStringsInput;
|
|
30
30
|
}
|
|
31
31
|
declare const Preview: import("svelte").Component<Props, {
|
|
@@ -33,7 +33,7 @@ declare const Preview: import("svelte").Component<Props, {
|
|
|
33
33
|
scrollToField: (field: DocPath) => boolean;
|
|
34
34
|
focusPosition: (at: Place) => void;
|
|
35
35
|
endFollow: () => void;
|
|
36
|
-
setZoom: (
|
|
36
|
+
setZoom: (zoom: number | undefined) => void;
|
|
37
37
|
}, "">;
|
|
38
38
|
type Preview = ReturnType<typeof Preview>;
|
|
39
39
|
export default Preview;
|
package/dist/preview/bridge.js
CHANGED
|
@@ -86,6 +86,9 @@ export function createBridge(session, container, slots, onPick, held = {}) {
|
|
|
86
86
|
// and this one moves the container's box, and one forced layout serves the whole hop.
|
|
87
87
|
function centre(target, port) {
|
|
88
88
|
container.scrollTop += target.top + target.height / 2 - (port.top + port.height / 2);
|
|
89
|
+
reveal(target, port);
|
|
90
|
+
}
|
|
91
|
+
function reveal(target, port) {
|
|
89
92
|
if (target.left < port.left)
|
|
90
93
|
container.scrollLeft += target.left - port.left;
|
|
91
94
|
else if (target.right > port.right)
|
|
@@ -145,10 +148,13 @@ export function createBridge(session, container, slots, onPick, held = {}) {
|
|
|
145
148
|
// the pane only when the caret has left the fold. Centring unconditionally
|
|
146
149
|
// takes the scrollport back from the user on every one of them, including
|
|
147
150
|
// the ones that changed nothing about where the caret already was.
|
|
151
|
+
// Across, a page zoomed wider than the pane has a caret to keep in view too, and
|
|
152
|
+
// the least trip does it without moving the line the reader is on.
|
|
148
153
|
const target = measure(slot, rectToPercent(region.rect, slot.size));
|
|
149
154
|
if (clearOfTheFold(target, port))
|
|
150
|
-
|
|
151
|
-
|
|
155
|
+
reveal(target, port);
|
|
156
|
+
else
|
|
157
|
+
centre(target, port);
|
|
152
158
|
}
|
|
153
159
|
return {
|
|
154
160
|
scrollToField,
|
|
@@ -14,8 +14,8 @@ export interface PreviewOptions {
|
|
|
14
14
|
/** A page paint the backend refused ({@link EditorErrorHandler}). The preview
|
|
15
15
|
* shows its error message state either way; this routes it to an app's sink. */
|
|
16
16
|
onError?: EditorErrorHandler;
|
|
17
|
-
/** The
|
|
18
|
-
*
|
|
17
|
+
/** The surface's strings, keyed and partial: unset keys take the package's
|
|
18
|
+
* English. */
|
|
19
19
|
strings?: PreviewStringsInput;
|
|
20
20
|
}
|
|
21
21
|
export interface PreviewController {
|
|
@@ -50,8 +50,12 @@ export interface PreviewController {
|
|
|
50
50
|
* own next caret.
|
|
51
51
|
*/
|
|
52
52
|
endFollow(): void;
|
|
53
|
-
/**
|
|
54
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Draw every page at `zoom`, a fraction of actual size (`1` is 100%), held between
|
|
55
|
+
* the pane's floor and 200%; `undefined` is the default, the widest page fitting the
|
|
56
|
+
* pane's width up to 125%. The reader's own gestures move the same zoom.
|
|
57
|
+
*/
|
|
58
|
+
setZoom(zoom: number | undefined): void;
|
|
55
59
|
destroy(): void;
|
|
56
60
|
}
|
|
57
61
|
export declare function createPreview(session: LiveSession, opts: PreviewOptions): PreviewController;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { reportError, errorMessage } from '../core/errors.js';
|
|
2
2
|
import { createPaintLoop } from './paint.js';
|
|
3
3
|
import { createBridge } from './bridge.js';
|
|
4
|
+
import { createViewport } from './viewport.js';
|
|
4
5
|
import { mergePreviewStrings } from './strings.js';
|
|
5
6
|
const CONTAINER_CLASS = 'qm-preview';
|
|
6
7
|
// The message states share one element; each carries `MESSAGE_CLASS` plus a state
|
|
@@ -82,7 +83,11 @@ export function createPreview(session, opts) {
|
|
|
82
83
|
attach();
|
|
83
84
|
}
|
|
84
85
|
}
|
|
86
|
+
// Built before the first bridge, so its ResizeObserver is the earlier one and a pane
|
|
87
|
+
// shown again re-fits before the bridge runs the trip it held at the old scale.
|
|
88
|
+
const viewport = createViewport(container, paintLoop.slots, paintLoop.setScale, t.resetZoom);
|
|
85
89
|
render(session.pageCount, []);
|
|
90
|
+
viewport.update();
|
|
86
91
|
// The last place the editor put the caret, re-located after every recompile and
|
|
87
92
|
// dropped on a focus change or a discrete hop. `session.locate` answers against the
|
|
88
93
|
// last compiled layout and a consumer debounces `update`, so a caret typed past that
|
|
@@ -94,6 +99,8 @@ export function createPreview(session, opts) {
|
|
|
94
99
|
return {
|
|
95
100
|
refresh(change) {
|
|
96
101
|
render(change.pageCount, change.dirtyPages);
|
|
102
|
+
// The default fits the widest page, which a recompile can move.
|
|
103
|
+
viewport.update();
|
|
97
104
|
// Guarded like any other caret hop, so a caret already clear of the fold
|
|
98
105
|
// leaves a pane the user scrolled where they left it.
|
|
99
106
|
if (followed)
|
|
@@ -115,11 +122,12 @@ export function createPreview(session, opts) {
|
|
|
115
122
|
endFollow() {
|
|
116
123
|
followed = undefined;
|
|
117
124
|
},
|
|
118
|
-
setZoom(
|
|
119
|
-
|
|
125
|
+
setZoom(zoom) {
|
|
126
|
+
viewport.setZoom(zoom);
|
|
120
127
|
},
|
|
121
128
|
destroy() {
|
|
122
129
|
detach();
|
|
130
|
+
viewport.destroy();
|
|
123
131
|
paintLoop.destroy();
|
|
124
132
|
hideMessage();
|
|
125
133
|
container.classList.remove(CONTAINER_CLASS);
|
package/dist/preview/index.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import '../core/theme.css';
|
|
14
14
|
export { createPreview } from './controller.js';
|
|
15
15
|
export { default as Preview } from './Preview.svelte';
|
|
16
|
-
// What the preview says
|
|
16
|
+
// What the preview says: its message states and the zoom pill's name. Keyed and partial; the
|
|
17
17
|
// English is exported so a consumer composes against it rather than restating it.
|
|
18
18
|
// The merge is the surface's own: a consumer hands `strings` to a prop, never
|
|
19
19
|
// merges one.
|
package/dist/preview/paint.d.ts
CHANGED
|
@@ -10,8 +10,12 @@ export interface PaintLoop {
|
|
|
10
10
|
readonly slots: readonly PageSlot[];
|
|
11
11
|
/** Reconcile slot count to `pageCount`, re-cache geometry, repaint mounted `dirtyPages`. */
|
|
12
12
|
refresh(dirtyPages: readonly number[], pageCount: number): void;
|
|
13
|
-
/**
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Draw every page at `scale` CSS px per PDF pt and repaint the mounted ones: on the
|
|
15
|
+
* next frame, or once `settle` has passed without another call, which is what a
|
|
16
|
+
* gesture asks for, the stretched raster standing in until it rests.
|
|
17
|
+
*/
|
|
18
|
+
setScale(scale: number, settle?: number): void;
|
|
15
19
|
destroy(): void;
|
|
16
20
|
}
|
|
17
21
|
export declare function createPaintLoop(session: LiveSession, container: HTMLElement, margin: number, onPaintError?: (page: number, err: unknown) => void): PaintLoop;
|