@quillmark/svelte 0.11.0 → 0.13.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/dist/core/address.d.ts +17 -10
- package/dist/core/address.js +26 -12
- package/dist/core/bloom.js +4 -3
- package/dist/core/codec/decode.d.ts +17 -0
- package/dist/core/codec/decode.js +78 -19
- package/dist/core/codec/encode.js +1 -1
- package/dist/core/codec/field.d.ts +64 -15
- package/dist/core/codec/field.js +201 -65
- package/dist/core/codec/index.d.ts +3 -3
- package/dist/core/codec/index.js +3 -3
- package/dist/core/codec/islands.d.ts +5 -4
- package/dist/core/codec/islands.js +23 -38
- package/dist/core/codec/keymap.d.ts +5 -4
- package/dist/core/codec/keymap.js +5 -4
- package/dist/core/codec/prose.css +42 -0
- package/dist/core/codec/schema.d.ts +24 -5
- package/dist/core/codec/schema.js +40 -9
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/errors.d.ts +3 -2
- package/dist/core/rungs.d.ts +8 -0
- package/dist/core/rungs.js +24 -0
- package/dist/core/theme.css +19 -6
- package/dist/preview/paint.js +21 -17
- package/dist/visual/ArrayField.svelte +649 -176
- package/dist/visual/ArrayField.svelte.d.ts +16 -1
- package/dist/visual/BooleanField.svelte +36 -11
- package/dist/visual/BooleanField.svelte.d.ts +2 -0
- package/dist/visual/Card.svelte +6 -25
- package/dist/visual/DateField.svelte +66 -31
- package/dist/visual/EnumField.svelte +27 -9
- package/dist/visual/EnumField.svelte.d.ts +5 -0
- package/dist/visual/Field.svelte +113 -38
- package/dist/visual/FormatPopover.svelte +4 -3
- package/dist/visual/MatrixField.svelte +353 -0
- package/dist/visual/MatrixField.svelte.d.ts +36 -0
- package/dist/visual/NumberField.svelte +31 -14
- package/dist/visual/NumberField.svelte.d.ts +5 -1
- package/dist/visual/ObjectField.svelte +404 -139
- package/dist/visual/ObjectField.svelte.d.ts +17 -2
- package/dist/visual/ProseField.svelte +53 -23
- package/dist/visual/ProseField.svelte.d.ts +15 -3
- package/dist/visual/ProseValue.svelte +102 -46
- package/dist/visual/ProseValue.svelte.d.ts +17 -5
- package/dist/visual/TextField.svelte +35 -18
- package/dist/visual/TextField.svelte.d.ts +7 -0
- package/dist/visual/VariantField.svelte +56 -2
- package/dist/visual/VariantField.svelte.d.ts +10 -1
- package/dist/visual/VisualEditorInner.svelte +77 -73
- package/dist/visual/controls.css +47 -5
- package/dist/visual/diagnostics.d.ts +26 -2
- package/dist/visual/diagnostics.js +52 -5
- package/dist/visual/hold.js +2 -2
- package/dist/visual/icons/nodes.d.ts +3 -0
- package/dist/visual/icons/nodes.js +2 -1
- package/dist/visual/leaves.d.ts +25 -12
- package/dist/visual/motion.d.ts +14 -5
- package/dist/visual/motion.js +13 -11
- package/dist/visual/strings.d.ts +27 -10
- package/dist/visual/strings.js +8 -2
- package/dist/visual/structure.d.ts +159 -40
- package/dist/visual/structure.js +337 -45
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// Islands: the `U+FFFC` slot + `ContentIsland` entry ↔ a PM leaf node.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Islands: the `U+FFFC` slot + `ContentIsland` entry ↔ a PM leaf node. Every slot sits
|
|
2
|
+
// in a `para` line; the island's type says which node it is, a `table` a block alone on
|
|
3
|
+
// its line and an `image` inline. The node carries `id`, `islandType` and `props` verbatim.
|
|
4
4
|
//
|
|
5
5
|
// A props-aware consumer reads the shape straight off the boundary:
|
|
6
6
|
// `ContentIsland.props` is the typed union (`TableProps` for `table`,
|
|
@@ -13,22 +13,17 @@ import { Plugin } from 'prosemirror-state';
|
|
|
13
13
|
import { storableUrl } from './urls.js';
|
|
14
14
|
/** The `U+FFFC` object-replacement char that occupies one island slot in `text`. */
|
|
15
15
|
export const ISLAND_SLOT = '';
|
|
16
|
-
// The
|
|
17
|
-
// stable identity (rebases like an anchor), `islandType` the discriminator, `props`
|
|
18
|
-
// the payload the codec preserves byte-for-byte across a round-trip
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// `loss` is authored, not derived: `applyChange` stores the class an island op gives
|
|
22
|
-
// it and re-derives nothing, so an entry that did not carry its own would promote a
|
|
23
|
-
// degraded table to lossless on the first cell edit. Its default is the weakest
|
|
24
|
-
// claim; decode always supplies the stored value.
|
|
16
|
+
// The three island node attributes carry the content entry verbatim: `id` is the
|
|
17
|
+
// stable identity (rebases like an anchor), `islandType` the discriminator, and `props`
|
|
18
|
+
// the payload the codec preserves byte-for-byte across a round-trip. `islandType` and
|
|
19
|
+
// `props` declare no default: a node standing for an island holds one, so the pair is
|
|
20
|
+
// supplied at every `create`.
|
|
25
21
|
const islandAttrs = {
|
|
26
22
|
id: { default: '' },
|
|
27
23
|
islandType: {},
|
|
28
|
-
props: {}
|
|
29
|
-
loss: { default: 'unrepresentable' }
|
|
24
|
+
props: {}
|
|
30
25
|
};
|
|
31
|
-
// All
|
|
26
|
+
// All three attributes cross the DOM: a copy and a paste inside one body run the document
|
|
32
27
|
// through `toDOM`/`parseDOM` (CODEC §"Markdown at the edges"), and an island is a leaf
|
|
33
28
|
// whose tags alone say only `[table]`. `props` crosses as JSON, the form the payload has
|
|
34
29
|
// at the boundary. The names are `data-qm-*` like every other carrier's, and the id is
|
|
@@ -39,21 +34,18 @@ function islandDOM(node) {
|
|
|
39
34
|
return {
|
|
40
35
|
'data-qm-island': node.attrs.islandType,
|
|
41
36
|
'data-qm-island-id': node.attrs.id,
|
|
42
|
-
'data-qm-island-loss': node.attrs.loss,
|
|
43
37
|
'data-qm-island-props': JSON.stringify(node.attrs.props ?? null)
|
|
44
38
|
};
|
|
45
39
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
unrepresentable: true
|
|
56
|
-
};
|
|
40
|
+
/** The island types the content model names, each with whether its markup is a block,
|
|
41
|
+
* which decides the node its slot decodes to. A `Record` over the boundary's own union,
|
|
42
|
+
* so a member added upstream is a missing key here rather than a silent refusal at the
|
|
43
|
+
* one door with no decode behind it. */
|
|
44
|
+
const BLOCK_ISLAND = { table: true, image: false };
|
|
45
|
+
/** Whether an island of `type` is block markup: a `table` stands alone on its line. */
|
|
46
|
+
export function isBlockIsland(type) {
|
|
47
|
+
return BLOCK_ISLAND[type];
|
|
48
|
+
}
|
|
57
49
|
/** An island node's attributes off that DOM, or `false` where the id is absent or the
|
|
58
50
|
* type is one the content vocabulary does not name — a name no read produces and no
|
|
59
51
|
* write takes, so the element's text arrives as text. A `props` that is not the JSON
|
|
@@ -64,7 +56,7 @@ function islandAttrsFromDOM(el) {
|
|
|
64
56
|
const id = el.getAttribute('data-qm-island-id');
|
|
65
57
|
// Asserted off an untrusted attribute and checked against the set on the next line.
|
|
66
58
|
const type = el.getAttribute('data-qm-island');
|
|
67
|
-
if (id == null || type == null || !
|
|
59
|
+
if (id == null || type == null || !Object.hasOwn(BLOCK_ISLAND, type))
|
|
68
60
|
return false;
|
|
69
61
|
const raw = el.getAttribute('data-qm-island-props');
|
|
70
62
|
let props = null;
|
|
@@ -74,13 +66,7 @@ function islandAttrsFromDOM(el) {
|
|
|
74
66
|
catch {
|
|
75
67
|
props = null;
|
|
76
68
|
}
|
|
77
|
-
return { id, islandType: type, props: storableProps(type, props)
|
|
78
|
-
}
|
|
79
|
-
/** The `loss` an element claims, or the weakest class for a claim outside the set:
|
|
80
|
-
* an under-claim keeps a degraded island from re-crossing as a lossless one. */
|
|
81
|
-
function lossFromDOM(el) {
|
|
82
|
-
const raw = el.getAttribute('data-qm-island-loss');
|
|
83
|
-
return raw != null && LOSS_CLASSES[raw] ? raw : 'unrepresentable';
|
|
69
|
+
return { id, islandType: type, props: storableProps(type, props) };
|
|
84
70
|
}
|
|
85
71
|
/** `props` with the one key the authored lane refuses made storable: an `image`'s `url`
|
|
86
72
|
* carrying a line ending, which `IslandOp` and `overwrite` throw on. The rest crosses
|
|
@@ -91,7 +77,7 @@ function storableProps(type, props) {
|
|
|
91
77
|
const url = props.url;
|
|
92
78
|
return typeof url === 'string' ? { ...props, url: storableUrl(url) } : props;
|
|
93
79
|
}
|
|
94
|
-
/** Block island node (a table):
|
|
80
|
+
/** Block island node (a table): a `U+FFFC` slot alone on its `para` line. Atom, unselectable content. */
|
|
95
81
|
export const islandBlockSpec = {
|
|
96
82
|
group: 'block',
|
|
97
83
|
atom: true,
|
|
@@ -119,8 +105,7 @@ export function islandEntryFromNode(attrs) {
|
|
|
119
105
|
return {
|
|
120
106
|
id: attrs.id,
|
|
121
107
|
type: attrs.islandType,
|
|
122
|
-
props: attrs.props
|
|
123
|
-
loss: attrs.loss
|
|
108
|
+
props: attrs.props
|
|
124
109
|
};
|
|
125
110
|
}
|
|
126
111
|
/** The `TableProps` shape, over a value that reached the node off the DOM rather than
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { Schema } from 'prosemirror-model';
|
|
2
2
|
import type { Command } from 'prosemirror-state';
|
|
3
|
-
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
/** A multi-block leaf's structural keys, each link binding only what its schema
|
|
4
|
+
* declares: over `plainSchema`, which holds neither lists nor code blocks nor islands,
|
|
5
|
+
* the block link and the break link alone. The argument list is the precedence order
|
|
6
|
+
* VISUAL_EDITOR §Chrome states, so a new link is one more argument at its place in that
|
|
7
|
+
* order rather than a nesting to read inside out.
|
|
7
8
|
*
|
|
8
9
|
* The slash menu is innermost of the three, and it is conditional rather than
|
|
9
10
|
* schema-gated: the keys exist only where a menu can be drawn (`slash`), because Enter
|
|
@@ -17,10 +17,11 @@ function chainKeymaps(...maps) {
|
|
|
17
17
|
}
|
|
18
18
|
return out;
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
/** A multi-block leaf's structural keys, each link binding only what its schema
|
|
21
|
+
* declares: over `plainSchema`, which holds neither lists nor code blocks nor islands,
|
|
22
|
+
* the block link and the break link alone. The argument list is the precedence order
|
|
23
|
+
* VISUAL_EDITOR §Chrome states, so a new link is one more argument at its place in that
|
|
24
|
+
* order rather than a nesting to read inside out.
|
|
24
25
|
*
|
|
25
26
|
* The slash menu is innermost of the three, and it is conditional rather than
|
|
26
27
|
* schema-gated: the keys exist only where a menu can be drawn (`slash`), because Enter
|
|
@@ -71,6 +71,14 @@ li.ProseMirror-selectednode::after {
|
|
|
71
71
|
margin-top: var(--_qm-space-3);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/* A plain leaf's paragraph is a line of literal text (`schema.ts` §`plainSchema`), and
|
|
75
|
+
a boundary between two stores the one `\n` a hard break does. So no rhythm: a gap
|
|
76
|
+
would draw a blank line the value does not hold, and the two spellings of one string
|
|
77
|
+
would read apart. */
|
|
78
|
+
.ProseMirror > [data-qm-line] + [data-qm-line] {
|
|
79
|
+
margin-top: 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
74
82
|
/* A blockquote's UA indent was its only mark, and the reset above takes it. The rule
|
|
75
83
|
and the inset carry it instead: the same hairline every surface reads, so a quote
|
|
76
84
|
reads as set apart without a fill or a second box. */
|
|
@@ -187,6 +195,40 @@ li.ProseMirror-selectednode::after {
|
|
|
187
195
|
font-weight: var(--_qm-weight-strong);
|
|
188
196
|
}
|
|
189
197
|
|
|
198
|
+
/* The empty leaf's ghost (`field.ts`, `ghostPlugin`): a node decoration's data
|
|
199
|
+
attributes, so the text stays out of the document. At the rung every control's
|
|
200
|
+
placeholder takes; the italic is what a leaf with no written neighbour has instead
|
|
201
|
+
of the step to `ink`.
|
|
202
|
+
|
|
203
|
+
After the empty line rather than before it, and pulled up one line box onto it: the
|
|
204
|
+
caret keeps the start of that line, and a ghost's lines past the first grow the
|
|
205
|
+
leaf rather than hang over what follows it. A block leaf's ghost wraps and keeps the
|
|
206
|
+
breaks it was written with, a `body.example` being a document's worth; an inline
|
|
207
|
+
leaf's keeps to the one line the leaf is. The example is drawn at rest where there
|
|
208
|
+
is no placeholder, and stands in for the placeholder while the leaf holds the focus.
|
|
209
|
+
|
|
210
|
+
Drawn only where there is text to draw: an empty box pulled up a line would take
|
|
211
|
+
the empty line with it. */
|
|
212
|
+
.ProseMirror .qm-prose-placeholder[data-placeholder]::after,
|
|
213
|
+
.ProseMirror .qm-prose-placeholder[data-example]::after {
|
|
214
|
+
content: attr(data-placeholder);
|
|
215
|
+
display: block;
|
|
216
|
+
margin-top: -1lh;
|
|
217
|
+
white-space: pre-line;
|
|
218
|
+
color: var(--_qm-ink-label);
|
|
219
|
+
font-style: italic;
|
|
220
|
+
pointer-events: none;
|
|
221
|
+
}
|
|
222
|
+
.ProseMirror .qm-prose-placeholder[data-example]:not([data-placeholder])::after,
|
|
223
|
+
.ProseMirror-focused .qm-prose-placeholder[data-example]::after {
|
|
224
|
+
content: attr(data-example);
|
|
225
|
+
}
|
|
226
|
+
.ProseMirror .qm-prose-placeholder.qm-prose-placeholder-line::after {
|
|
227
|
+
white-space: nowrap;
|
|
228
|
+
overflow: hidden;
|
|
229
|
+
text-overflow: ellipsis;
|
|
230
|
+
}
|
|
231
|
+
|
|
190
232
|
/* ── The table island ────────────────────────────────────────────────────────
|
|
191
233
|
|
|
192
234
|
The one construct in this file with chrome in it. The rest of the stylesheet
|
|
@@ -7,8 +7,8 @@ export declare const blockSchema: Schema<string, string>;
|
|
|
7
7
|
* a table cell, which is the same content unit). */
|
|
8
8
|
export declare const inlineSchema: Schema<string, string>;
|
|
9
9
|
/**
|
|
10
|
-
* The inline schema with no mark types at all: a `plaintext` field, whose
|
|
11
|
-
* literal text the boundary refuses to coerce back once it carries a mark.
|
|
10
|
+
* The inline schema with no mark types at all: a `plaintext(inline)` field, whose
|
|
11
|
+
* value is literal text the boundary refuses to coerce back once it carries a mark.
|
|
12
12
|
*
|
|
13
13
|
* Declaring none is what makes that structural rather than a rule each path
|
|
14
14
|
* restates: `toggleMark` has no type to apply, the mark input rules build nothing,
|
|
@@ -16,11 +16,30 @@ export declare const inlineSchema: Schema<string, string>;
|
|
|
16
16
|
* carrying (CODEC §Inline mode).
|
|
17
17
|
*/
|
|
18
18
|
export declare const plaintextSchema: Schema<string, never>;
|
|
19
|
-
/**
|
|
20
|
-
*
|
|
19
|
+
/**
|
|
20
|
+
* A `plaintext` field without `inline`: paragraphs and hard breaks, no marks, no
|
|
21
|
+
* islands, no containers, which is the shape `Content::is_plain` admits. Mark-free
|
|
22
|
+
* for the reason {@link plaintextSchema} is.
|
|
23
|
+
*
|
|
24
|
+
* A paragraph and a break both store one `\n`, so a paragraph here is a line and
|
|
25
|
+
* says so (`data-qm-line`): `prose.css` draws a run of them at the leading, and a
|
|
26
|
+
* boundary reads as the one line down that a break does.
|
|
27
|
+
*/
|
|
28
|
+
export declare const plainSchema: Schema<"text" | "doc" | "paragraph" | "hard_break", never>;
|
|
29
|
+
/** The schema a leaf of the declared type mounts: `inline` narrows either type to one
|
|
30
|
+
* textblock, and `plaintext` takes its marks away. */
|
|
31
|
+
export declare function leafSchema(opts: {
|
|
32
|
+
plaintext?: boolean;
|
|
33
|
+
inline?: boolean;
|
|
34
|
+
}): Schema;
|
|
35
|
+
/** Whether `schema` can carry formatting at all: false for the two plaintext schemas.
|
|
36
|
+
* What a mark command asks before it offers itself. */
|
|
21
37
|
export declare function hasMarks(schema: Schema): boolean;
|
|
22
|
-
/** True for the
|
|
38
|
+
/** True for the one-line schemas, which declare no `hard_break`; decode branches on it. */
|
|
23
39
|
export declare function isInlineSchema(schema: Schema): boolean;
|
|
40
|
+
/** True for {@link blockSchema}, the one holding blocks past the paragraph: what a
|
|
41
|
+
* leaf's island, gap-cursor and slash machinery mounts on. */
|
|
42
|
+
export declare function isBlockSchema(schema: Schema): boolean;
|
|
24
43
|
/** Whether `type` can hold a within-block line break, which a heading cannot (§`heading`).
|
|
25
44
|
* What a `\n` landing in a textblock is rewritten to turns on it (`breaks.ts`), and so
|
|
26
45
|
* does what decode puts between a block's continued segments. */
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
// Each set is closed upstream, so the schema names its whole vocabulary and carries
|
|
5
5
|
// nothing inert. `blockSchema` is the full field; `inlineSchema` is the constrained
|
|
6
6
|
// single-textblock form for `richtext(inline)` (one paragraph, no block split, no
|
|
7
|
-
// containers, no islands) and `plaintextSchema` is that one without marks
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// containers, no islands) and `plaintextSchema` is that one without marks, for
|
|
8
|
+
// `plaintext(inline)`; `plainSchema` is a `plaintext` field without `inline`,
|
|
9
|
+
// paragraphs and hard breaks and nothing else. Same decode/lower/position machinery,
|
|
10
|
+
// narrower shapes. Anchors are not marks here (decorations).
|
|
10
11
|
//
|
|
11
12
|
// `toDOM` and `parseDOM` are one tier, not two halves of a rendering: a copy and a
|
|
12
13
|
// paste inside one body run the whole document through them (CODEC §"Markdown at the
|
|
@@ -188,8 +189,8 @@ const inlineNodes = {
|
|
|
188
189
|
* a table cell, which is the same content unit). */
|
|
189
190
|
export const inlineSchema = new Schema({ nodes: inlineNodes, marks });
|
|
190
191
|
/**
|
|
191
|
-
* The inline schema with no mark types at all: a `plaintext` field, whose
|
|
192
|
-
* literal text the boundary refuses to coerce back once it carries a mark.
|
|
192
|
+
* The inline schema with no mark types at all: a `plaintext(inline)` field, whose
|
|
193
|
+
* value is literal text the boundary refuses to coerce back once it carries a mark.
|
|
193
194
|
*
|
|
194
195
|
* Declaring none is what makes that structural rather than a rule each path
|
|
195
196
|
* restates: `toggleMark` has no type to apply, the mark input rules build nothing,
|
|
@@ -197,14 +198,44 @@ export const inlineSchema = new Schema({ nodes: inlineNodes, marks });
|
|
|
197
198
|
* carrying (CODEC §Inline mode).
|
|
198
199
|
*/
|
|
199
200
|
export const plaintextSchema = new Schema({ nodes: inlineNodes, marks: {} });
|
|
200
|
-
/**
|
|
201
|
-
*
|
|
201
|
+
/**
|
|
202
|
+
* A `plaintext` field without `inline`: paragraphs and hard breaks, no marks, no
|
|
203
|
+
* islands, no containers, which is the shape `Content::is_plain` admits. Mark-free
|
|
204
|
+
* for the reason {@link plaintextSchema} is.
|
|
205
|
+
*
|
|
206
|
+
* A paragraph and a break both store one `\n`, so a paragraph here is a line and
|
|
207
|
+
* says so (`data-qm-line`): `prose.css` draws a run of them at the leading, and a
|
|
208
|
+
* boundary reads as the one line down that a break does.
|
|
209
|
+
*/
|
|
210
|
+
export const plainSchema = new Schema({
|
|
211
|
+
nodes: {
|
|
212
|
+
doc: { content: 'paragraph+' },
|
|
213
|
+
paragraph: { ...blockNodes.paragraph, toDOM: () => ['p', { 'data-qm-line': '' }, 0] },
|
|
214
|
+
text: inlineLeafNodes.text,
|
|
215
|
+
hard_break: inlineLeafNodes.hard_break
|
|
216
|
+
},
|
|
217
|
+
marks: {}
|
|
218
|
+
});
|
|
219
|
+
/** The schema a leaf of the declared type mounts: `inline` narrows either type to one
|
|
220
|
+
* textblock, and `plaintext` takes its marks away. */
|
|
221
|
+
export function leafSchema(opts) {
|
|
222
|
+
if (opts.plaintext)
|
|
223
|
+
return opts.inline ? plaintextSchema : plainSchema;
|
|
224
|
+
return opts.inline ? inlineSchema : blockSchema;
|
|
225
|
+
}
|
|
226
|
+
/** Whether `schema` can carry formatting at all: false for the two plaintext schemas.
|
|
227
|
+
* What a mark command asks before it offers itself. */
|
|
202
228
|
export function hasMarks(schema) {
|
|
203
229
|
return Object.keys(schema.marks).length > 0;
|
|
204
230
|
}
|
|
205
|
-
/** True for the
|
|
231
|
+
/** True for the one-line schemas, which declare no `hard_break`; decode branches on it. */
|
|
206
232
|
export function isInlineSchema(schema) {
|
|
207
|
-
return !schema.nodes.
|
|
233
|
+
return !schema.nodes.hard_break;
|
|
234
|
+
}
|
|
235
|
+
/** True for {@link blockSchema}, the one holding blocks past the paragraph: what a
|
|
236
|
+
* leaf's island, gap-cursor and slash machinery mounts on. */
|
|
237
|
+
export function isBlockSchema(schema) {
|
|
238
|
+
return !!schema.nodes.blockquote;
|
|
208
239
|
}
|
|
209
240
|
/** Whether `type` can hold a within-block line break, which a heading cannot (§`heading`).
|
|
210
241
|
* What a `\n` landing in a textblock is rewritten to turns on it (`breaks.ts`), and so
|
package/dist/core/codec/slash.js
CHANGED
|
Binary file
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -11,8 +11,9 @@ export type EditorErrorCode =
|
|
|
11
11
|
* the whole projection instead, so the store is correct and this field's
|
|
12
12
|
* identity anchors were paid. */
|
|
13
13
|
| 'commit-fallback'
|
|
14
|
-
/** The overwrite fallback also failed
|
|
15
|
-
*
|
|
14
|
+
/** The overwrite fallback also failed, or the typed writer refused a `plaintext`
|
|
15
|
+
* leaf's text: the optimistic PM state stands and the store is stale for this
|
|
16
|
+
* field. The one code here that leaves damage. */
|
|
16
17
|
| 'commit-lost'
|
|
17
18
|
/** A card operation (add, move, remove, retype) that threw. The document is
|
|
18
19
|
* unchanged; the boundary's mutators are transactional. */
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The element the scale is declared on, for `node`: its root, or `node` where it stands
|
|
2
|
+
* outside one (a detached surface, a consumer's own mount). */
|
|
3
|
+
export declare function rungStyle(node: Element): CSSStyleDeclaration;
|
|
4
|
+
/** A duration rung off `style`, in ms; `undefined` where the derivation is out of reach
|
|
5
|
+
* (an unstyled root, or jsdom, where `getComputedStyle` reports custom properties as
|
|
6
|
+
* empty). No rung, no motion: a fallback here would be the scale restated in the one
|
|
7
|
+
* place `check:style` cannot read. */
|
|
8
|
+
export declare function rungMs(style: CSSStyleDeclaration, rung: string): number | undefined;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Where a script reads the scale. The derivation declares every `--_qm-*` rung on
|
|
2
|
+
// `[data-qm-root]` and nowhere else (`core/theme.css`), so a descendant's value is that
|
|
3
|
+
// one inherited and the root is the element the question is about: asking a leaf three
|
|
4
|
+
// containers deep is asking an arbitrary element what the surface is set to.
|
|
5
|
+
//
|
|
6
|
+
// The read is also the cost. `getComputedStyle` compounds with nesting depth under jsdom,
|
|
7
|
+
// so a suite that resolves rungs off landed elements pays the depth of whatever it landed
|
|
8
|
+
// on; resolving them off the root pays the root's.
|
|
9
|
+
/** The element the scale is declared on, for `node`: its root, or `node` where it stands
|
|
10
|
+
* outside one (a detached surface, a consumer's own mount). */
|
|
11
|
+
export function rungStyle(node) {
|
|
12
|
+
return getComputedStyle(node.closest('[data-qm-root]') ?? node);
|
|
13
|
+
}
|
|
14
|
+
/** A duration rung off `style`, in ms; `undefined` where the derivation is out of reach
|
|
15
|
+
* (an unstyled root, or jsdom, where `getComputedStyle` reports custom properties as
|
|
16
|
+
* empty). No rung, no motion: a fallback here would be the scale restated in the one
|
|
17
|
+
* place `check:style` cannot read. */
|
|
18
|
+
export function rungMs(style, rung) {
|
|
19
|
+
const raw = style.getPropertyValue(rung).trim();
|
|
20
|
+
const n = Number.parseFloat(raw);
|
|
21
|
+
if (!Number.isFinite(n))
|
|
22
|
+
return undefined;
|
|
23
|
+
return raw.endsWith('ms') ? n : n * 1000;
|
|
24
|
+
}
|
package/dist/core/theme.css
CHANGED
|
@@ -163,13 +163,16 @@
|
|
|
163
163
|
relationship at any mix: a stroke against the surface behind it, not a fill
|
|
164
164
|
stacked on one. */
|
|
165
165
|
--_qm-border-faint: color-mix(in oklab, var(--_qm-bg), var(--_qm-border) 50%);
|
|
166
|
-
/*
|
|
167
|
-
well a field draws it against, and what would take it —
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
166
|
+
/* Three rungs, and nothing under the last: a step below `label` lands under 3:1 on
|
|
167
|
+
the well a field draws it against, and what would take it — an example, an unset
|
|
168
|
+
value's word — is content a reader has to read (WCAG 1.4.3), not decoration.
|
|
169
|
+
`ink` is written. `default` is the resolved `default:` drawn as the value it
|
|
170
|
+
prints: near enough to `ink` to read as an answer, a step off it so it never
|
|
171
|
+
reads as a written one. `label` is words about a value rather than the value — an
|
|
172
|
+
`example:`, `None`, a format hint — and the chrome that names fields; in italic
|
|
173
|
+
where it is a leaf's ghost, which has no written neighbour to be read against. */
|
|
172
174
|
--_qm-ink: var(--_qm-fg);
|
|
175
|
+
--_qm-ink-default: color-mix(in oklab, var(--_qm-fg), var(--_qm-bg) 20%);
|
|
173
176
|
--_qm-ink-label: color-mix(in oklab, var(--_qm-fg), var(--_qm-bg) 35%);
|
|
174
177
|
/* The arrival wash (`core/bloom.ts`). Its alpha is a peak, not a tint: the
|
|
175
178
|
wash animates from and back to zero, never held, so it reads over the leaf it
|
|
@@ -336,6 +339,16 @@
|
|
|
336
339
|
through the icon's `size`, so a stylesheet beats the SVG presentation attribute
|
|
337
340
|
and one rung is the whole of what a control draws. */
|
|
338
341
|
--_qm-glyph-control: 14px;
|
|
342
|
+
/* The narrowest track a control that fills its width stays usable in: a table's
|
|
343
|
+
column floors at it, rises no further than its content, and a row of them scrolls
|
|
344
|
+
sideways rather than crushing a cell. A column drawing a mark takes no floor — a
|
|
345
|
+
switch is one width at every width, so there is no width for it to be usable in
|
|
346
|
+
(`ArrayField`). What the field has spare stands after the last column, an inner
|
|
347
|
+
grid carrying its own rhythm rather than the card's. Under the field minimum the
|
|
348
|
+
capacity ladder steps at (`.qm-tracks`), since a cell in a row has the header to
|
|
349
|
+
name it and no label of its own to fit. A threshold rather than a rhythm choice,
|
|
350
|
+
so it does not move with `--qm-space`. */
|
|
351
|
+
--_qm-track-min: 7rem;
|
|
339
352
|
|
|
340
353
|
/* ── Type ────────────────────────────────────────────────────────────────
|
|
341
354
|
`--qm-font-size` anchors the chrome rung; 1.125 is the ratio between adjacent
|
package/dist/preview/paint.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
// Vertical breathing room between stacked pages; cosmetic only, and a rung like
|
|
2
2
|
// any other gap: the stack breathes on the same rhythm the cards do.
|
|
3
3
|
const PAGE_GAP = 'var(--_qm-space-4)';
|
|
4
|
+
// The least raster density, in device pixels per CSS pixel. Glyphs rasterize
|
|
5
|
+
// unhinted, so a raster at a 1× or fractional ratio (1.25, 1.5; browser zoom)
|
|
6
|
+
// is resampled nearly 1:1 and small text smears; a raster at least twice the
|
|
7
|
+
// box is downsampled instead, which keeps body text legible.
|
|
8
|
+
const MIN_DENSITY = 2;
|
|
4
9
|
export function createPaintLoop(session, container, margin,
|
|
5
10
|
// Notified when a slot's `session.paint` throws: the loop swallows the throw
|
|
6
11
|
// (see `paintSlot`) so one bad page never aborts the band sweep; the caller
|
|
@@ -65,16 +70,19 @@ onPaintError) {
|
|
|
65
70
|
return { page, size, el };
|
|
66
71
|
}
|
|
67
72
|
// Rasterize `slot` into its canvas (creating the canvas if this is its first
|
|
68
|
-
// paint since mounting)
|
|
69
|
-
// CSS width, so layout width tracks the container per the zoom settled
|
|
70
|
-
// decision; `canvas.style.*` is then set from the authoritative PaintResult,
|
|
71
|
-
// never guessed.
|
|
73
|
+
// paint since mounting), at the slot's own current CSS width.
|
|
72
74
|
function paintSlot(slot) {
|
|
73
75
|
let canvas = canvases.get(slot.page);
|
|
74
76
|
if (!canvas) {
|
|
75
77
|
canvas = document.createElement('canvas');
|
|
76
78
|
canvas.className = 'qm-page-canvas';
|
|
77
|
-
Object.assign(canvas.style, {
|
|
79
|
+
Object.assign(canvas.style, {
|
|
80
|
+
position: 'absolute',
|
|
81
|
+
inset: '0',
|
|
82
|
+
display: 'block',
|
|
83
|
+
width: '100%',
|
|
84
|
+
height: '100%'
|
|
85
|
+
});
|
|
78
86
|
}
|
|
79
87
|
// Contextless canvas: bail before registering, so `updateBand` still sees
|
|
80
88
|
// the page as unmounted and retries instead of keeping a blank page.
|
|
@@ -86,11 +94,9 @@ onPaintError) {
|
|
|
86
94
|
canvases.set(slot.page, canvas);
|
|
87
95
|
}
|
|
88
96
|
const layoutScale = (slot.el.clientWidth || slot.size.widthPt) / slot.size.widthPt;
|
|
89
|
-
const densityScale = (window.devicePixelRatio || 1) * zoom;
|
|
97
|
+
const densityScale = Math.max(window.devicePixelRatio || 1, MIN_DENSITY) * zoom;
|
|
90
98
|
try {
|
|
91
|
-
|
|
92
|
-
canvas.style.width = `${result.layoutWidth}px`;
|
|
93
|
-
canvas.style.height = `${result.layoutHeight}px`;
|
|
99
|
+
session.paint(ctx, slot.page, layoutScale * densityScale);
|
|
94
100
|
}
|
|
95
101
|
catch (err) {
|
|
96
102
|
// A paint that throws must not abort the band loop; `updateBand` runs in
|
|
@@ -162,14 +168,12 @@ onPaintError) {
|
|
|
162
168
|
// Slots are built on the first `refresh` (the controller calls it immediately),
|
|
163
169
|
// not here: reconcile → `session.pageSize` must run after the controller's
|
|
164
170
|
// `pageCount` gate, since `pageSize` throws for a page the count excludes.
|
|
165
|
-
// ── Keep mounted rasters
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
// their current box; canvases are `position:absolute`, so a repaint can't feed
|
|
172
|
-
// back into layout (and thus can't re-trigger the resize observer).
|
|
171
|
+
// ── Keep mounted rasters sharp ──────────────────────────────────────────────
|
|
172
|
+
// A canvas stretches with its slot, so a page that stays mounted while the
|
|
173
|
+
// container's CSS width or `devicePixelRatio` shifts keeps its place and goes
|
|
174
|
+
// soft. Two observers repaint mounted pages from their current box; canvases
|
|
175
|
+
// are `position:absolute`, so a repaint can't feed back into layout (and thus
|
|
176
|
+
// can't re-trigger the resize observer).
|
|
173
177
|
// Repaint mounted pages (all, or a given subset) from their live geometry: the
|
|
174
178
|
// one path resize, DPR, and zoom share; each `paintSlot` re-reads the slot's
|
|
175
179
|
// current `clientWidth`. A no-op for an unmounted page.
|