@quillmark/svelte 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -8
- package/dist/core/codec/cap.d.ts +21 -0
- package/dist/core/codec/cap.js +63 -0
- package/dist/core/codec/decode.d.ts +2 -1
- package/dist/core/codec/decode.js +19 -11
- package/dist/core/codec/field.d.ts +9 -1
- package/dist/core/codec/field.js +8 -1
- package/dist/core/codec/index.d.ts +2 -0
- package/dist/core/codec/index.js +1 -0
- package/dist/core/codec/islands.d.ts +4 -1
- package/dist/core/codec/islands.js +43 -16
- package/dist/core/codec/markdown-paste.d.ts +4 -3
- package/dist/core/codec/markdown-paste.js +7 -4
- package/dist/core/codec/prose.css +15 -0
- package/dist/index.js +1 -1
- package/dist/visual/ArrayField.svelte +2 -0
- package/dist/visual/Card.svelte +5 -4
- package/dist/visual/Field.svelte +15 -3
- package/dist/visual/FieldLabel.svelte +15 -1
- package/dist/visual/FieldLabel.svelte.d.ts +4 -0
- package/dist/visual/ObjectField.svelte +16 -9
- package/dist/visual/ObjectField.svelte.d.ts +2 -2
- package/dist/visual/ProseField.svelte +10 -1
- package/dist/visual/ProseField.svelte.d.ts +3 -0
- package/dist/visual/ProseValue.svelte +7 -2
- package/dist/visual/ProseValue.svelte.d.ts +3 -0
- package/dist/visual/VisualEditorInner.svelte +5 -11
- package/dist/visual/strings.d.ts +8 -2
- package/dist/visual/strings.js +1 -0
- package/dist/visual/structure.d.ts +29 -13
- package/dist/visual/structure.js +54 -15
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,12 +14,12 @@ npm install @quillmark/svelte
|
|
|
14
14
|
|
|
15
15
|
Each subpath is its own module root; a bundler pulls only what the entry you import reaches.
|
|
16
16
|
|
|
17
|
-
| Import | Surface
|
|
18
|
-
| --------------------------- |
|
|
19
|
-
| `@quillmark/svelte/core` | What the surfaces share: the `DocPath`/`Place` address vocabulary, the `EditorError` channel, `init`.
|
|
20
|
-
| `@quillmark/svelte/preview` | The live preview: `createPreview` + `<Preview>`. Reaches nothing editor-side.
|
|
21
|
-
| `@quillmark/svelte/visual` | The federated WYSIWYG: `<VisualEditor>`, the codec's `createField` prose leaf.
|
|
22
|
-
| `@quillmark/svelte` | Re-exports `/core`.
|
|
17
|
+
| Import | Surface |
|
|
18
|
+
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
19
|
+
| `@quillmark/svelte/core` | What the surfaces share: the `DocPath`/`Place` address vocabulary, the `EditorError` channel, `init`. No UI framework. |
|
|
20
|
+
| `@quillmark/svelte/preview` | The live preview: `createPreview` + `<Preview>`. Reaches nothing editor-side. |
|
|
21
|
+
| `@quillmark/svelte/visual` | The federated WYSIWYG: `<VisualEditor>`, the codec's `createField` prose leaf. |
|
|
22
|
+
| `@quillmark/svelte` | Re-exports `/core`. |
|
|
23
23
|
|
|
24
24
|
## Open a session
|
|
25
25
|
|
|
@@ -29,8 +29,7 @@ The **consumer** owns the session and the handles, and drives every edit; the su
|
|
|
29
29
|
import { Engine } from '@quillmark/wasm';
|
|
30
30
|
import { init } from '@quillmark/svelte/core';
|
|
31
31
|
|
|
32
|
-
//
|
|
33
|
-
// neither statically.
|
|
32
|
+
// Quill and Document are exported only through init(), which loads the wasm once.
|
|
34
33
|
const { Quill, Document } = await init();
|
|
35
34
|
|
|
36
35
|
const quill = Quill.fromTree(tree); // tree: Map<string, Uint8Array> of the quill dir
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Node as PMNode } from 'prosemirror-model';
|
|
2
|
+
import { Plugin } from 'prosemirror-state';
|
|
3
|
+
/** A leaf's character cap: the schema's `max:`, and whether the count drops trailing
|
|
4
|
+
* whitespace, which a one-line `text` does and nothing else. */
|
|
5
|
+
export interface TextCap {
|
|
6
|
+
max: number;
|
|
7
|
+
trimEnd?: boolean;
|
|
8
|
+
/** The count, at mount and after every change to the document. */
|
|
9
|
+
onCount?: (count: number) => void;
|
|
10
|
+
}
|
|
11
|
+
/** The characters `doc` holds as `FieldSchema::char_count` counts them: Unicode scalar
|
|
12
|
+
* values of the plain projection, islands counting nothing and trailing line breaks
|
|
13
|
+
* none. */
|
|
14
|
+
export declare function capCount(doc: PMNode, trimEnd?: boolean): number;
|
|
15
|
+
/**
|
|
16
|
+
* Refuses a transaction leaving the leaf past `max`, so a keystroke at the cap lands
|
|
17
|
+
* nothing, and cuts a paste to what fits. Past the cap, a transaction passes where it
|
|
18
|
+
* leaves the count no higher, so a value stored over the cap still edits down; undo and
|
|
19
|
+
* redo pass, each restoring a state the leaf already held.
|
|
20
|
+
*/
|
|
21
|
+
export declare function capPlugin(cap: TextCap): Plugin;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { isHistoryTransaction } from 'prosemirror-history';
|
|
2
|
+
import { Plugin } from 'prosemirror-state';
|
|
3
|
+
import { usvLength } from './decode.js';
|
|
4
|
+
import { scanDoc } from './encode.js';
|
|
5
|
+
import { ISLAND_SLOT } from './islands.js';
|
|
6
|
+
/** The characters `doc` holds as `FieldSchema::char_count` counts them: Unicode scalar
|
|
7
|
+
* values of the plain projection, islands counting nothing and trailing line breaks
|
|
8
|
+
* none. */
|
|
9
|
+
export function capCount(doc, trimEnd = false) {
|
|
10
|
+
const text = scanDoc(doc).text.replaceAll(ISLAND_SLOT, '');
|
|
11
|
+
return usvLength(trimEnd ? text.trimEnd() : text.replace(/[\n\r]+$/, ''));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Refuses a transaction leaving the leaf past `max`, so a keystroke at the cap lands
|
|
15
|
+
* nothing, and cuts a paste to what fits. Past the cap, a transaction passes where it
|
|
16
|
+
* leaves the count no higher, so a value stored over the cap still edits down; undo and
|
|
17
|
+
* redo pass, each restoring a state the leaf already held.
|
|
18
|
+
*/
|
|
19
|
+
export function capPlugin(cap) {
|
|
20
|
+
const count = (doc) => capCount(doc, cap.trimEnd);
|
|
21
|
+
return new Plugin({
|
|
22
|
+
filterTransaction: (tr) => {
|
|
23
|
+
if (!tr.docChanged || isHistoryTransaction(tr))
|
|
24
|
+
return true;
|
|
25
|
+
const after = count(tr.doc);
|
|
26
|
+
return after <= cap.max || after <= count(tr.before);
|
|
27
|
+
},
|
|
28
|
+
props: {
|
|
29
|
+
handlePaste: (view, _event, slice) => {
|
|
30
|
+
const { state } = view;
|
|
31
|
+
const pasted = () => state.tr.replaceSelection(slice);
|
|
32
|
+
if (count(pasted().doc) <= cap.max)
|
|
33
|
+
return false;
|
|
34
|
+
const tr = pasted();
|
|
35
|
+
const start = tr.mapping.map(state.selection.from, -1);
|
|
36
|
+
const end = tr.mapping.map(state.selection.to, 1);
|
|
37
|
+
const fits = (cut) => count(pasted().delete(cut, end).doc) <= cap.max;
|
|
38
|
+
if (!fits(start))
|
|
39
|
+
return true;
|
|
40
|
+
let lo = start;
|
|
41
|
+
let hi = end;
|
|
42
|
+
while (hi - lo > 1) {
|
|
43
|
+
const mid = (lo + hi) >> 1;
|
|
44
|
+
if (fits(mid))
|
|
45
|
+
lo = mid;
|
|
46
|
+
else
|
|
47
|
+
hi = mid;
|
|
48
|
+
}
|
|
49
|
+
view.dispatch(tr.delete(lo, end).scrollIntoView().setMeta('uiEvent', 'paste'));
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
view: (v) => {
|
|
54
|
+
cap.onCount?.(count(v.state.doc));
|
|
55
|
+
return {
|
|
56
|
+
update: (next, prev) => {
|
|
57
|
+
if (next.state.doc !== prev.doc)
|
|
58
|
+
cap.onCount?.(count(next.state.doc));
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -29,7 +29,8 @@ export declare function fitsInline(rt: Content): boolean;
|
|
|
29
29
|
export declare function fitsPlain(rt: Content): boolean;
|
|
30
30
|
/** Whether a leaf so declared edits `rt` losing nothing: {@link fitsInline} narrowed
|
|
31
31
|
* and {@link fitsPlain} plain, past which the decode drops what the first commit would
|
|
32
|
-
* store; on the block schema anything but an element
|
|
32
|
+
* store; on the block schema anything but a construct read and not edited — an element,
|
|
33
|
+
* a definition list ({@link editable}), a footnote or an equation (`isEditedIsland`) —
|
|
33
34
|
* and an island where the leaf mounts no island view (`islands`), which draws as its
|
|
34
35
|
* placeholder tag, an atom a keystroke deletes unseen. What a leaf held over the rest
|
|
35
36
|
* reads. */
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// applied here; they are decorations (field.ts). Positions throughout are USV; `Array.from` iterates by
|
|
9
9
|
// code point so an astral char is one unit, never a surrogate half.
|
|
10
10
|
import { DOMSerializer } from 'prosemirror-model';
|
|
11
|
-
import { ISLAND_SLOT, isBlockIsland } from './islands.js';
|
|
11
|
+
import { ISLAND_SLOT, isBlockIsland, isEditedIsland } from './islands.js';
|
|
12
12
|
import { descriptorOf, markKey, pmMarkFromContent } from './marks.js';
|
|
13
13
|
import { core } from '../lifecycle.js';
|
|
14
14
|
import { hasMarks, isBlockSchema, isInlineSchema, takesLineBreak } from './schema.js';
|
|
@@ -96,7 +96,8 @@ export function fitsPlain(rt) {
|
|
|
96
96
|
}
|
|
97
97
|
/** Whether a leaf so declared edits `rt` losing nothing: {@link fitsInline} narrowed
|
|
98
98
|
* and {@link fitsPlain} plain, past which the decode drops what the first commit would
|
|
99
|
-
* store; on the block schema anything but an element
|
|
99
|
+
* store; on the block schema anything but a construct read and not edited — an element,
|
|
100
|
+
* a definition list ({@link editable}), a footnote or an equation (`isEditedIsland`) —
|
|
100
101
|
* and an island where the leaf mounts no island view (`islands`), which draws as its
|
|
101
102
|
* placeholder tag, an atom a keystroke deletes unseen. What a leaf held over the rest
|
|
102
103
|
* reads. */
|
|
@@ -105,11 +106,16 @@ export function fitsLeaf(rt, leaf) {
|
|
|
105
106
|
return fitsInline(rt);
|
|
106
107
|
if (leaf.plaintext)
|
|
107
108
|
return fitsPlain(rt);
|
|
108
|
-
|
|
109
|
+
if (!leaf.islands && rt.islands.length)
|
|
110
|
+
return false;
|
|
111
|
+
return rt.islands.every((isl) => isEditedIsland(isl.type)) && rt.lines.every(editable);
|
|
109
112
|
}
|
|
110
|
-
/** Whether
|
|
111
|
-
*
|
|
113
|
+
/** Whether the block schema edits `line`'s kind and every container on it: a term draws
|
|
114
|
+
* as a paragraph and an element and a description as the blocks they wrap, so the first
|
|
115
|
+
* commit would drop each. */
|
|
112
116
|
function editable(line) {
|
|
117
|
+
if (line.kind === 'term')
|
|
118
|
+
return false;
|
|
113
119
|
return line.containers.every((c) => c.container === 'quote' || c.container === 'list_item');
|
|
114
120
|
}
|
|
115
121
|
/** Inline decode: one paragraph, containers and islands stripped. */
|
|
@@ -192,9 +198,9 @@ function groupBlocks(schema, leaves, depth, marks, cursor) {
|
|
|
192
198
|
i = j;
|
|
193
199
|
continue;
|
|
194
200
|
}
|
|
195
|
-
if (here.container === 'element') {
|
|
196
|
-
// No node holds an element, and a leaf holding one is held
|
|
197
|
-
// what it wraps draws in place, read-only.
|
|
201
|
+
if (here.container === 'element' || here.container === 'description') {
|
|
202
|
+
// No node holds an element or a description, and a leaf holding one is held
|
|
203
|
+
// (`fitsLeaf`), so what it wraps draws in place, read-only.
|
|
198
204
|
const key = containerKey(here);
|
|
199
205
|
let j = i + 1;
|
|
200
206
|
while (j < leaves.length) {
|
|
@@ -249,7 +255,7 @@ function instanceOf(c) {
|
|
|
249
255
|
}
|
|
250
256
|
/** Identity of a container for run gathering: its name, its `instance` and an element's
|
|
251
257
|
* attributes, NUL-joined as `markKey` joins a mark's. `list_item` carries its own run
|
|
252
|
-
* rule, so the
|
|
258
|
+
* rule, so the rest are what key here. */
|
|
253
259
|
function containerKey(c) {
|
|
254
260
|
const attrs = c.container === 'element' ? JSON.stringify(c.attrs) : '';
|
|
255
261
|
return `${c.container}\u0000${instanceOf(c)}\u0000${attrs}`;
|
|
@@ -269,13 +275,15 @@ function makeLeaf(schema, leaf, marks, cursor) {
|
|
|
269
275
|
return schema.nodes.code_block.create({ lang: line.attrs?.lang ?? null }, content);
|
|
270
276
|
}
|
|
271
277
|
case 'para':
|
|
278
|
+
case 'term':
|
|
272
279
|
case 'heading': {
|
|
273
|
-
if (line.kind
|
|
280
|
+
if (line.kind !== 'heading' && blockIslandLine(leaf, cursor))
|
|
274
281
|
return schema.nodes.island_block.create(islandAttrs(cursor));
|
|
275
282
|
// Inline content, the segment boundary a `hard_break` in a paragraph and a space
|
|
276
283
|
// in a heading, which takes none (`schema.ts` §`takesLineBreak`). A stored heading
|
|
277
284
|
// carries no continuation — the store clears one after a block of a single line —
|
|
278
|
-
// so the space is what keeps a hand-built content decodable.
|
|
285
|
+
// so the space is what keeps a hand-built content decodable. A term draws as a
|
|
286
|
+
// paragraph, read-only (`fitsLeaf`).
|
|
279
287
|
const type = line.kind === 'heading' ? schema.nodes.heading : schema.nodes.paragraph;
|
|
280
288
|
const inline = [];
|
|
281
289
|
leaf.segments.forEach((seg, idx) => {
|
|
@@ -3,6 +3,7 @@ import { Plugin } from 'prosemirror-state';
|
|
|
3
3
|
import { EditorView } from 'prosemirror-view';
|
|
4
4
|
import type { Document, Content, Addr, Quill } from '@quillmark/wasm';
|
|
5
5
|
import type { EditorErrorHandler } from '../errors.js';
|
|
6
|
+
import { type TextCap } from './cap.js';
|
|
6
7
|
import { type TableChromeStrings } from './table-view.js';
|
|
7
8
|
import { type SlashState, type SlashStrings } from './slash.js';
|
|
8
9
|
/** Options for {@link createField}. */
|
|
@@ -27,7 +28,8 @@ export interface CreateFieldOpts {
|
|
|
27
28
|
* leaf, or a `text` one, is held over a value its schema cannot
|
|
28
29
|
* hold (`fitsLeaf`, upstream's `isInline` and `isPlain`): the decode would join lines
|
|
29
30
|
* and drop containers and islands for the first commit to store. Any leaf is held
|
|
30
|
-
* over an element,
|
|
31
|
+
* over what is read and not edited: an element, a definition list, a footnote and an
|
|
32
|
+
* equation (CODEC §Decode). A held leaf draws
|
|
31
33
|
* its content on the block schema, read-only, and commits nothing. Judged of the value
|
|
32
34
|
* each mount and re-hydrate reads, so {@link FieldController.applyExternal} is what
|
|
33
35
|
* re-evaluates it.
|
|
@@ -75,6 +77,9 @@ export interface CreateFieldOpts {
|
|
|
75
77
|
* a body's invitation. The initial value; {@link FieldController.setPlaceholder}
|
|
76
78
|
* moves it after mount. Empty/absent shows no ghost. */
|
|
77
79
|
placeholder?: string;
|
|
80
|
+
/** The schema's `max:` on the field: a character count no edit takes the leaf past
|
|
81
|
+
* (`cap.ts`). */
|
|
82
|
+
cap?: TextCap;
|
|
78
83
|
/** The placeholder goes at the leaf's first edit: the edit answers the leaf, and
|
|
79
84
|
* emptied it holds an empty answer, which prints empty. Absent, the placeholder
|
|
80
85
|
* returns whenever the leaf is empty. */
|
|
@@ -224,5 +229,8 @@ export declare function proseLeafPlugins(schema: Schema, opts: {
|
|
|
224
229
|
placeholder?: () => string | undefined;
|
|
225
230
|
/** {@link CreateFieldOpts.placeholderUntilEdit}. */
|
|
226
231
|
placeholderUntilEdit?: boolean;
|
|
232
|
+
/** {@link CreateFieldOpts.cap}. Ahead of every other paste handler, which a paste
|
|
233
|
+
* it cuts never reaches. */
|
|
234
|
+
cap?: TextCap;
|
|
227
235
|
afterHistory?: Plugin[];
|
|
228
236
|
}): Plugin[];
|
package/dist/core/codec/field.js
CHANGED
|
@@ -24,6 +24,7 @@ import { reportError, errorMessage } from '../errors.js';
|
|
|
24
24
|
import { decode, fitsLeaf } from './decode.js';
|
|
25
25
|
import { usvToPM, pmToUsv, buildLineIndex, lineIndexOf } from './positions.js';
|
|
26
26
|
import { lower, pmToContent, scanContent, scanDoc, contentEdit } from './encode.js';
|
|
27
|
+
import { capPlugin } from './cap.js';
|
|
27
28
|
import { islandPastePlugin } from './islands.js';
|
|
28
29
|
import { anchorsFromContent } from './marks.js';
|
|
29
30
|
import { contentEqual, createReconciler } from './reconcile.js';
|
|
@@ -282,6 +283,7 @@ export function createField(opts) {
|
|
|
282
283
|
// given one later; the plugin draws nothing while the text is empty.
|
|
283
284
|
placeholder: () => placeholderText,
|
|
284
285
|
placeholderUntilEdit: opts.placeholderUntilEdit,
|
|
286
|
+
cap: opts.cap,
|
|
285
287
|
afterHistory: [anchorPlugin(seededAnchors)]
|
|
286
288
|
})
|
|
287
289
|
});
|
|
@@ -547,7 +549,12 @@ export function createField(opts) {
|
|
|
547
549
|
* its author typed without a second rule saying so.
|
|
548
550
|
*/
|
|
549
551
|
export function proseLeafPlugins(schema, opts) {
|
|
550
|
-
const list = [
|
|
552
|
+
const list = [
|
|
553
|
+
history(),
|
|
554
|
+
...(opts.cap ? [capPlugin(opts.cap)] : []),
|
|
555
|
+
...(opts.afterHistory ?? []),
|
|
556
|
+
linebreakPlugin(schema)
|
|
557
|
+
];
|
|
551
558
|
if (opts.slash)
|
|
552
559
|
list.push(slashPlugin(opts.slash, opts.slashHint));
|
|
553
560
|
if (!opts.noInputRules)
|
|
@@ -2,6 +2,8 @@ export { rangeAnchor } from './anchor.js';
|
|
|
2
2
|
export type { RangeAnchor } from './anchor.js';
|
|
3
3
|
export { createField, emptyContent, heldAttributes, isEmptyContent, proseAttributes, proseLeafPlugins } from './field.js';
|
|
4
4
|
export type { CreateFieldOpts, FieldController, LeafViews } from './field.js';
|
|
5
|
+
export { capCount } from './cap.js';
|
|
6
|
+
export type { TextCap } from './cap.js';
|
|
5
7
|
export { DEFAULT_TABLE_STRINGS } from './table-view.js';
|
|
6
8
|
export type { TableChromeStrings } from './table-view.js';
|
|
7
9
|
export { DEFAULT_SLASH_STRINGS } from './slash.js';
|
package/dist/core/codec/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import './prose.css';
|
|
|
14
14
|
export { rangeAnchor } from './anchor.js';
|
|
15
15
|
// The prose leaf.
|
|
16
16
|
export { createField, emptyContent, heldAttributes, isEmptyContent, proseAttributes, proseLeafPlugins } from './field.js';
|
|
17
|
+
export { capCount } from './cap.js';
|
|
17
18
|
// The table island's chrome vocabulary: the visual tier's `strings` set extends it,
|
|
18
19
|
// so the island's wording is overridden beside every other key.
|
|
19
20
|
export { DEFAULT_TABLE_STRINGS } from './table-view.js';
|
|
@@ -5,9 +5,12 @@ import type { ContentIsland, TableProps } from '@quillmark/wasm';
|
|
|
5
5
|
export declare const ISLAND_SLOT = "\uFFFC";
|
|
6
6
|
/** Whether an island of `type` is block markup: a `table` stands alone on its line. */
|
|
7
7
|
export declare function isBlockIsland(type: ContentIsland['type']): boolean;
|
|
8
|
+
/** Whether a leaf edits an island of `type`; one holding any other is held (`fitsLeaf`). */
|
|
9
|
+
export declare function isEditedIsland(type: ContentIsland['type']): boolean;
|
|
8
10
|
/** Block island node (a table): a `U+FFFC` slot alone on its `para` line. Atom, unselectable content. */
|
|
9
11
|
export declare const islandBlockSpec: NodeSpec;
|
|
10
|
-
/** Inline island node (an image): a `U+FFFC` slot inside a
|
|
12
|
+
/** Inline island node (an image, a footnote, an equation): a `U+FFFC` slot inside a
|
|
13
|
+
* `para` line. */
|
|
11
14
|
export declare const islandInlineSpec: NodeSpec;
|
|
12
15
|
/** A PM island node's attributes: the content entry, spelled the way a node spec
|
|
13
16
|
* spells it (`islandType`, because `type` on a PM node is the node type). `islandType`
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// Islands: the `U+FFFC` slot + `ContentIsland` entry ↔ a PM leaf node. Every slot sits
|
|
2
2
|
// in a `para` line; the island's type says which node it is, a `table` a block alone on
|
|
3
|
-
// its line and
|
|
3
|
+
// its line and every other type inline. The node carries `id`, `islandType` and `props`
|
|
4
|
+
// verbatim.
|
|
4
5
|
//
|
|
5
6
|
// A props-aware consumer reads the shape straight off the boundary:
|
|
6
|
-
// `ContentIsland.props` is the typed union (`TableProps` for `table`,
|
|
7
|
-
// `
|
|
7
|
+
// `ContentIsland.props` is the typed union (`TableProps` for `table`, `ImageProps` for
|
|
8
|
+
// `image`, …; DOCUMENT_MODEL §Stability seams), so the codec
|
|
8
9
|
// reads it rather than hand-rolling a shape. `type` is a closed set, so a
|
|
9
10
|
// discriminant check narrows it; `props` still crosses the DOM as opaque JSON,
|
|
10
11
|
// which is what §`tablePropsOfNode` guards.
|
|
@@ -38,25 +39,35 @@ function islandDOM(node) {
|
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
41
|
/** 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`
|
|
42
|
-
* so a member added upstream is a missing key here
|
|
43
|
-
* one door with no decode behind it.
|
|
44
|
-
|
|
42
|
+
* which decides the node its slot decodes to, and whether a leaf edits it. A `Record`
|
|
43
|
+
* over the boundary's own union, so a member added upstream is a missing key here
|
|
44
|
+
* rather than a silent refusal at the one door with no decode behind it. A footnote and
|
|
45
|
+
* an equation are read, not edited (CODEC §Decode): a leaf holding one is held. */
|
|
46
|
+
const ISLANDS = {
|
|
47
|
+
table: { block: true, edited: true },
|
|
48
|
+
image: { block: false, edited: true },
|
|
49
|
+
footnote: { block: false, edited: false },
|
|
50
|
+
math: { block: false, edited: false }
|
|
51
|
+
};
|
|
45
52
|
/** Whether an island of `type` is block markup: a `table` stands alone on its line. */
|
|
46
53
|
export function isBlockIsland(type) {
|
|
47
|
-
return
|
|
54
|
+
return ISLANDS[type].block;
|
|
55
|
+
}
|
|
56
|
+
/** Whether a leaf edits an island of `type`; one holding any other is held (`fitsLeaf`). */
|
|
57
|
+
export function isEditedIsland(type) {
|
|
58
|
+
return ISLANDS[type].edited;
|
|
48
59
|
}
|
|
49
60
|
/** An island node's attributes off that DOM, or `false` where the id is absent or the
|
|
50
|
-
* type is one the
|
|
51
|
-
*
|
|
52
|
-
* written above reads as `null`, and one of the wrong shape is
|
|
53
|
-
* reader (§`tablePropsOfNode`): the entry survives as an island of
|
|
54
|
-
* than taking the paste down with it. */
|
|
61
|
+
* type is one no leaf edits — a name the vocabulary does not hold, or a footnote or an
|
|
62
|
+
* equation, which no gesture mints — so the element's text arrives as text. A `props`
|
|
63
|
+
* that is not the JSON written above reads as `null`, and one of the wrong shape is
|
|
64
|
+
* `undefined` at its reader (§`tablePropsOfNode`): the entry survives as an island of
|
|
65
|
+
* its type rather than taking the paste down with it. */
|
|
55
66
|
function islandAttrsFromDOM(el) {
|
|
56
67
|
const id = el.getAttribute('data-qm-island-id');
|
|
57
68
|
// Asserted off an untrusted attribute and checked against the set on the next line.
|
|
58
69
|
const type = el.getAttribute('data-qm-island');
|
|
59
|
-
if (id == null || type == null || !Object.hasOwn(
|
|
70
|
+
if (id == null || type == null || !Object.hasOwn(ISLANDS, type) || !isEditedIsland(type))
|
|
60
71
|
return false;
|
|
61
72
|
const raw = el.getAttribute('data-qm-island-props');
|
|
62
73
|
let props = null;
|
|
@@ -86,7 +97,8 @@ export const islandBlockSpec = {
|
|
|
86
97
|
parseDOM: [{ tag: 'div[data-qm-island]', getAttrs: (el) => islandAttrsFromDOM(el) }],
|
|
87
98
|
toDOM: (node) => ['div', islandDOM(node), `[${node.attrs.islandType || 'island'}]`]
|
|
88
99
|
};
|
|
89
|
-
/** Inline island node (an image): a `U+FFFC` slot inside a
|
|
100
|
+
/** Inline island node (an image, a footnote, an equation): a `U+FFFC` slot inside a
|
|
101
|
+
* `para` line. */
|
|
90
102
|
export const islandInlineSpec = {
|
|
91
103
|
group: 'inline',
|
|
92
104
|
inline: true,
|
|
@@ -94,8 +106,23 @@ export const islandInlineSpec = {
|
|
|
94
106
|
selectable: true,
|
|
95
107
|
attrs: islandAttrs,
|
|
96
108
|
parseDOM: [{ tag: 'span[data-qm-island]', getAttrs: (el) => islandAttrsFromDOM(el) }],
|
|
97
|
-
toDOM:
|
|
109
|
+
toDOM: inlineIslandDOM
|
|
98
110
|
};
|
|
111
|
+
/** An inline island's DOM: a footnote its reference mark, whose number `prose.css` draws
|
|
112
|
+
* in reading order and whose note is its title, an equation its TeX between the dollars
|
|
113
|
+
* that write it, and an image its tag. */
|
|
114
|
+
function inlineIslandDOM(node) {
|
|
115
|
+
const attrs = node.attrs;
|
|
116
|
+
const dom = islandDOM(node);
|
|
117
|
+
const props = attrs.props;
|
|
118
|
+
if (attrs.islandType === 'footnote')
|
|
119
|
+
return ['sup', typeof props?.text === 'string' ? { ...dom, title: props.text } : dom];
|
|
120
|
+
if (attrs.islandType === 'math' && typeof props?.tex === 'string') {
|
|
121
|
+
const fence = props.display ? '$$' : '$';
|
|
122
|
+
return ['span', dom, `${fence}${props.tex}${fence}`];
|
|
123
|
+
}
|
|
124
|
+
return ['span', dom, `[${attrs.islandType || 'island'}]`];
|
|
125
|
+
}
|
|
99
126
|
/** Build a content island entry from a PM island node's attrs (block or inline). The
|
|
100
127
|
* cast pairs a closed `type` with a `props` that crossed the DOM as opaque JSON. The
|
|
101
128
|
* store is lenient about a table's shape — it fills what it can and keeps the rest —
|
|
@@ -2,8 +2,9 @@ import { Plugin } from 'prosemirror-state';
|
|
|
2
2
|
/**
|
|
3
3
|
* A block leaf's markdown paste, consulted after the table paste: ProseMirror hands text
|
|
4
4
|
* over only where the clipboard holds no HTML, whose DOM parse reads the source's own
|
|
5
|
-
* structure, so HTML wins. A paste as plain text keeps the text
|
|
6
|
-
*
|
|
7
|
-
* re-mints where the
|
|
5
|
+
* structure, so HTML wins. A paste as plain text keeps the text, and so does a parse
|
|
6
|
+
* building a construct no leaf edits (`fitsLeaf`). An island the parse builds arrives
|
|
7
|
+
* under the id `importMarkdown` minted, which the paste's island pass re-mints where the
|
|
8
|
+
* field holds it (`islands.ts`).
|
|
8
9
|
*/
|
|
9
10
|
export declare function markdownPastePlugin(): Plugin;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { Slice } from 'prosemirror-model';
|
|
8
8
|
import { Plugin } from 'prosemirror-state';
|
|
9
9
|
import { core } from '../lifecycle.js';
|
|
10
|
-
import { decode } from './decode.js';
|
|
10
|
+
import { decode, fitsLeaf } from './decode.js';
|
|
11
11
|
import { blockSchema } from './schema.js';
|
|
12
12
|
/** A line opening a block markdown spells and prose does not: a bullet or an ordered item at
|
|
13
13
|
* any indent, and within CommonMark's three spaces an ATX heading, a quote, a fence, or a
|
|
@@ -21,9 +21,10 @@ const BLOCK_HEAD = new RegExp([
|
|
|
21
21
|
/**
|
|
22
22
|
* A block leaf's markdown paste, consulted after the table paste: ProseMirror hands text
|
|
23
23
|
* over only where the clipboard holds no HTML, whose DOM parse reads the source's own
|
|
24
|
-
* structure, so HTML wins. A paste as plain text keeps the text
|
|
25
|
-
*
|
|
26
|
-
* re-mints where the
|
|
24
|
+
* structure, so HTML wins. A paste as plain text keeps the text, and so does a parse
|
|
25
|
+
* building a construct no leaf edits (`fitsLeaf`). An island the parse builds arrives
|
|
26
|
+
* under the id `importMarkdown` minted, which the paste's island pass re-mints where the
|
|
27
|
+
* field holds it (`islands.ts`).
|
|
27
28
|
*/
|
|
28
29
|
export function markdownPastePlugin() {
|
|
29
30
|
return new Plugin({
|
|
@@ -42,6 +43,8 @@ export function markdownPastePlugin() {
|
|
|
42
43
|
// Nesting past the store's depth is refused; the literal text holds it.
|
|
43
44
|
return null;
|
|
44
45
|
}
|
|
46
|
+
if (!fitsLeaf(rt, { inline: false, plaintext: false, islands: true }))
|
|
47
|
+
return null;
|
|
45
48
|
return new Slice(decode(rt, blockSchema).content, 0, 0);
|
|
46
49
|
})
|
|
47
50
|
}
|
|
@@ -162,6 +162,21 @@ li.ProseMirror-selectednode::after {
|
|
|
162
162
|
pointer-events: none;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
/* A footnote and an equation are read, not edited (CODEC §Decode), and draw as what
|
|
166
|
+
their markdown writes: a footnote its reference mark, numbered in reading order off a
|
|
167
|
+
counter each leaf resets, its note the marker's title (`islands.ts`); an equation its
|
|
168
|
+
TeX between its dollars, in the code face. */
|
|
169
|
+
.ProseMirror {
|
|
170
|
+
counter-reset: qm-footnote;
|
|
171
|
+
}
|
|
172
|
+
.ProseMirror sup[data-qm-island='footnote']::after {
|
|
173
|
+
counter-increment: qm-footnote;
|
|
174
|
+
content: counter(qm-footnote);
|
|
175
|
+
}
|
|
176
|
+
.ProseMirror [data-qm-island='math'] {
|
|
177
|
+
font-family: var(--_qm-font-mono);
|
|
178
|
+
}
|
|
179
|
+
|
|
165
180
|
/* A link takes the surface's ink (the UA's blue is the `#8cf` defect again) and a
|
|
166
181
|
dotted underline. Dotted rather than solid because `underline` is a mark in the
|
|
167
182
|
same schema, and a solid rule renders an authored `<u>` and a link identically in a
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `@quillmark/svelte` root: the
|
|
1
|
+
// `@quillmark/svelte` root: the substrate, free of any UI framework.
|
|
2
2
|
//
|
|
3
3
|
// The headline surfaces mount from their own subpaths (`@quillmark/svelte/preview`,
|
|
4
4
|
// `/visual`) so a bundler pulls only what an entry reaches; the root
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
literalText,
|
|
75
75
|
rowSummary,
|
|
76
76
|
schemaAt,
|
|
77
|
+
textCap,
|
|
77
78
|
type ArrayLayout
|
|
78
79
|
} from './structure.js';
|
|
79
80
|
import { splitDeep, unrouted, type DeepDiagnostic } from './diagnostics.js';
|
|
@@ -800,6 +801,7 @@
|
|
|
800
801
|
content={() => readAt([k])}
|
|
801
802
|
value={arr[k]}
|
|
802
803
|
plaintext={items != null && literalText(items)}
|
|
804
|
+
cap={textCap(items)}
|
|
803
805
|
label={rowName(k)}
|
|
804
806
|
onChange={(rt) => commitElement(k, rt)}
|
|
805
807
|
onKey={(e) => onElementKey(e, k)}
|
package/dist/visual/Card.svelte
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
One card block, its
|
|
2
|
+
One card block, its top-level objects (or legacy `ui.group`s) drawn as panels and its
|
|
3
|
+
fields packed by `ui.compact`. `main` is headerless
|
|
3
4
|
and takes no controls. Every prose leaf takes a parent-built live address, so a card
|
|
4
5
|
reorder re-targets its commits without a remount.
|
|
5
6
|
-->
|
|
@@ -273,9 +274,9 @@
|
|
|
273
274
|
{@const headIds = section.head ? ops.domIds(section.head.name) : undefined}
|
|
274
275
|
{@const headHint = section.head?.description}
|
|
275
276
|
<div class={['qm-group', { 'qm-open': isOpen }]}>
|
|
276
|
-
<!-- A
|
|
277
|
-
takes its label id and parks its description: the field
|
|
278
|
-
label of its own to say the section's name twice. -->
|
|
277
|
+
<!-- A panel's object, or an object alone in its section, is named by the
|
|
278
|
+
header, which takes its label id and parks its description: the field
|
|
279
|
+
draws no label of its own to say the section's name twice. -->
|
|
279
280
|
<div class="qm-group-head">
|
|
280
281
|
<button
|
|
281
282
|
type="button"
|
package/dist/visual/Field.svelte
CHANGED
|
@@ -33,13 +33,13 @@
|
|
|
33
33
|
VARIANT_DISCRIMINANT,
|
|
34
34
|
arrayLayout,
|
|
35
35
|
baseType,
|
|
36
|
-
flatObject,
|
|
37
36
|
ghostDefault,
|
|
38
37
|
isContainer,
|
|
39
38
|
optionalCell,
|
|
40
39
|
printedText,
|
|
41
40
|
renderedBoolean,
|
|
42
41
|
stringifyGhost,
|
|
42
|
+
textCap,
|
|
43
43
|
titleText
|
|
44
44
|
} from './structure.js';
|
|
45
45
|
import { wording } from './strings.js';
|
|
@@ -163,9 +163,18 @@
|
|
|
163
163
|
// reference must vanish with it: `aria-describedby` pointing at nothing describes
|
|
164
164
|
// nothing, and silently.
|
|
165
165
|
const describedBy = $derived(field.description ? domIds.description : undefined);
|
|
166
|
+
// A capped prose leaf reports its count, and the count describes it too: a keystroke
|
|
167
|
+
// the cap refuses changes nothing else a reader can perceive.
|
|
168
|
+
let chars = $state<number>();
|
|
169
|
+
const cap = $derived(field.control === 'prose' ? textCap(field.schema) : undefined);
|
|
170
|
+
const countId = $derived(`${domIds.control}-count`);
|
|
171
|
+
const proseDescribedBy = $derived(
|
|
172
|
+
cap ? [describedBy, countId].filter(Boolean).join(' ') : describedBy
|
|
173
|
+
);
|
|
166
174
|
// Two controls own their label row, the add chip and the held count sharing it.
|
|
167
175
|
const ownsLabel = $derived(field.control === 'array' || field.control === 'matrix');
|
|
168
|
-
|
|
176
|
+
// A headed object is a panel's, and the panel is its frame.
|
|
177
|
+
const flat = $derived(headed);
|
|
169
178
|
|
|
170
179
|
// A container walks its diagnostics down to the cell each names and draws the rest
|
|
171
180
|
// at its own foot (`diagnostics.ts`); every other control draws them here, under
|
|
@@ -285,6 +294,8 @@
|
|
|
285
294
|
descriptionId={domIds.description}
|
|
286
295
|
{onActivate}
|
|
287
296
|
description={field.description}
|
|
297
|
+
count={cap && chars != null ? t.strings.textCount(chars, cap.max) : undefined}
|
|
298
|
+
{countId}
|
|
288
299
|
/>
|
|
289
300
|
{/if}
|
|
290
301
|
<!-- The control and what hangs under it, in one grid cell: the diagnostics stack
|
|
@@ -305,7 +316,8 @@
|
|
|
305
316
|
placeholder={none}
|
|
306
317
|
emptyUnsets={optional}
|
|
307
318
|
labelledBy={domIds.label}
|
|
308
|
-
{
|
|
319
|
+
describedBy={proseDescribedBy}
|
|
320
|
+
cap={cap && { ...cap, onCount: (n) => (chars = n) }}
|
|
309
321
|
{leafKey}
|
|
310
322
|
{onFocus}
|
|
311
323
|
{onCaretMove}
|
|
@@ -40,8 +40,13 @@
|
|
|
40
40
|
onActivate?: () => void;
|
|
41
41
|
/** Schema `description`, or undefined: the affordance renders only when set. */
|
|
42
42
|
description?: string;
|
|
43
|
+
/** A capped field's character count, drawn at the row's end. */
|
|
44
|
+
count?: string;
|
|
45
|
+
/** The count's own id, for the control's `aria-describedby`. */
|
|
46
|
+
countId?: string;
|
|
43
47
|
}
|
|
44
|
-
let { label, controlId, id, descriptionId, onActivate, description }: Props =
|
|
48
|
+
let { label, controlId, id, descriptionId, onActivate, description, count, countId }: Props =
|
|
49
|
+
$props();
|
|
45
50
|
</script>
|
|
46
51
|
|
|
47
52
|
<div class="qm-field-label-row">
|
|
@@ -68,6 +73,9 @@
|
|
|
68
73
|
<FieldHint {description} {label} describedBy={descriptionId} />
|
|
69
74
|
<span class="qm-visually-hidden" id={descriptionId}>{description}</span>
|
|
70
75
|
{/if}
|
|
76
|
+
{#if count != null}
|
|
77
|
+
<span class="qm-field-count" id={countId}>{count}</span>
|
|
78
|
+
{/if}
|
|
71
79
|
</div>
|
|
72
80
|
|
|
73
81
|
<style>
|
|
@@ -91,6 +99,12 @@
|
|
|
91
99
|
color: var(--_qm-ink-label);
|
|
92
100
|
min-width: 0;
|
|
93
101
|
}
|
|
102
|
+
.qm-field-count {
|
|
103
|
+
margin-inline-start: auto;
|
|
104
|
+
font-size: var(--_qm-text-label);
|
|
105
|
+
color: var(--_qm-ink-label);
|
|
106
|
+
font-variant-numeric: tabular-nums;
|
|
107
|
+
}
|
|
94
108
|
/* A label that lands focus says so. Only when it has somewhere to land: an
|
|
95
109
|
`ObjectField` property's label names a subform, not one control. */
|
|
96
110
|
.qm-field-label.targeted {
|
|
@@ -11,6 +11,10 @@ interface Props {
|
|
|
11
11
|
onActivate?: () => void;
|
|
12
12
|
/** Schema `description`, or undefined: the affordance renders only when set. */
|
|
13
13
|
description?: string;
|
|
14
|
+
/** A capped field's character count, drawn at the row's end. */
|
|
15
|
+
count?: string;
|
|
16
|
+
/** The count's own id, for the control's `aria-describedby`. */
|
|
17
|
+
countId?: string;
|
|
14
18
|
}
|
|
15
19
|
declare const FieldLabel: import("svelte").Component<Props, {}, "">;
|
|
16
20
|
type FieldLabel = ReturnType<typeof FieldLabel>;
|
|
@@ -30,9 +30,8 @@
|
|
|
30
30
|
member's id. So a cell reads at whatever depth it sits, which nothing here knows.
|
|
31
31
|
|
|
32
32
|
`bare` is the table's row: the same cells with no labels and no figure of their own,
|
|
33
|
-
laid onto the tracks the table's header names ({@link ArrayField}). `flat` is
|
|
34
|
-
|
|
35
|
-
depth of whatever holds the subform.
|
|
33
|
+
laid onto the tracks the table's header names ({@link ArrayField}). `flat` is a
|
|
34
|
+
panel's object: the cells keep their labels and lose the figure, the panel being it.
|
|
36
35
|
-->
|
|
37
36
|
<script lang="ts">
|
|
38
37
|
import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
|
|
@@ -42,7 +41,6 @@
|
|
|
42
41
|
controlKind,
|
|
43
42
|
declaredContent,
|
|
44
43
|
fieldModels,
|
|
45
|
-
flatObject,
|
|
46
44
|
humanize,
|
|
47
45
|
isContainer,
|
|
48
46
|
literalText,
|
|
@@ -51,7 +49,8 @@
|
|
|
51
49
|
placeFields,
|
|
52
50
|
printedText,
|
|
53
51
|
renderedBoolean,
|
|
54
|
-
shortCell
|
|
52
|
+
shortCell,
|
|
53
|
+
textCap
|
|
55
54
|
} from './structure.js';
|
|
56
55
|
import { splitDeep, unrouted, type DeepDiagnostic } from './diagnostics.js';
|
|
57
56
|
import type { LandingBox } from './leaves.js';
|
|
@@ -102,8 +101,8 @@
|
|
|
102
101
|
/** A table's row: the cells alone, on the tracks the table's header names, with
|
|
103
102
|
* no labels and no vertical of their own. */
|
|
104
103
|
bare?: boolean;
|
|
105
|
-
/**
|
|
106
|
-
*
|
|
104
|
+
/** A panel's object: no vertical and no rung, the properties at the depth of the
|
|
105
|
+
* panel's field list. */
|
|
107
106
|
flat?: boolean;
|
|
108
107
|
/** A card-level object, or one nested in it: its properties are placed as a
|
|
109
108
|
* card's fields are (`placeFields`), so moving a field into a group leaves its row
|
|
@@ -136,6 +135,8 @@
|
|
|
136
135
|
}: Props = $props();
|
|
137
136
|
|
|
138
137
|
const t = wording();
|
|
138
|
+
/** A capped prose cell's character count, by property. */
|
|
139
|
+
const counts: Record<string, number> = $state({});
|
|
139
140
|
const entries = $derived(Object.entries(properties ?? {}));
|
|
140
141
|
const obj = $derived((value ?? {}) as Record<string, unknown>);
|
|
141
142
|
const has = (key: string): boolean => Object.hasOwn(properties ?? {}, key);
|
|
@@ -298,6 +299,8 @@
|
|
|
298
299
|
(`packable`). A block prose cell spans it too, and keeps its label row, as
|
|
299
300
|
does a placed cell `placeFields` gives the whole row. -->
|
|
300
301
|
{@const block = kind === 'prose' && !shortCell(sub)}
|
|
302
|
+
{@const cap = kind === 'prose' ? textCap(sub) : undefined}
|
|
303
|
+
{@const countId = cap && ids ? `${ids.control}-count` : undefined}
|
|
301
304
|
<div
|
|
302
305
|
class={[
|
|
303
306
|
'qm-object-prop',
|
|
@@ -316,6 +319,10 @@
|
|
|
316
319
|
descriptionId={describes}
|
|
317
320
|
onActivate={labelable ? undefined : () => focusProp(key)}
|
|
318
321
|
description={sub.description}
|
|
322
|
+
count={cap && counts[key] != null
|
|
323
|
+
? t.strings.textCount(counts[key], cap.max)
|
|
324
|
+
: undefined}
|
|
325
|
+
{countId}
|
|
319
326
|
/>
|
|
320
327
|
{/if}
|
|
321
328
|
{#if kind === 'enum'}
|
|
@@ -385,7 +392,8 @@
|
|
|
385
392
|
{block}
|
|
386
393
|
label={named}
|
|
387
394
|
labelledBy={ids?.label}
|
|
388
|
-
describedBy={describes}
|
|
395
|
+
describedBy={countId ? [describes, countId].filter(Boolean).join(' ') : describes}
|
|
396
|
+
cap={cap && { ...cap, onCount: (n) => (counts[key] = n) }}
|
|
389
397
|
onChange={(rt) =>
|
|
390
398
|
commitProp(key, optionalCell(sub) && isEmptyContent(rt) ? undefined : rt)}
|
|
391
399
|
onKey={onCellKey ? (e) => onCellKey(e, key) : undefined}
|
|
@@ -416,7 +424,6 @@
|
|
|
416
424
|
bind:this={nestedEls[key]}
|
|
417
425
|
value={obj[key] as Record<string, unknown> | undefined}
|
|
418
426
|
properties={sub.properties}
|
|
419
|
-
flat={flatObject(sub)}
|
|
420
427
|
{placed}
|
|
421
428
|
label={ids ? undefined : fallbackName(key, sub)}
|
|
422
429
|
idBase={ids?.control}
|
|
@@ -32,8 +32,8 @@ interface Props {
|
|
|
32
32
|
/** A table's row: the cells alone, on the tracks the table's header names, with
|
|
33
33
|
* no labels and no vertical of their own. */
|
|
34
34
|
bare?: boolean;
|
|
35
|
-
/**
|
|
36
|
-
*
|
|
35
|
+
/** A panel's object: no vertical and no rung, the properties at the depth of the
|
|
36
|
+
* panel's field list. */
|
|
37
37
|
flat?: boolean;
|
|
38
38
|
/** A card-level object, or one nested in it: its properties are placed as a
|
|
39
39
|
* card's fields are (`placeFields`), so moving a field into a group leaves its row
|
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
-->
|
|
7
7
|
<script lang="ts">
|
|
8
8
|
import { untrack } from 'svelte';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
createField,
|
|
11
|
+
type FieldController,
|
|
12
|
+
type SlashState,
|
|
13
|
+
type TextCap
|
|
14
|
+
} from '../core/codec/index.js';
|
|
10
15
|
import type { LeafRegistry } from './leaves.js';
|
|
11
16
|
import SlashMenu from './SlashMenu.svelte';
|
|
12
17
|
import { wording } from './strings.js';
|
|
@@ -48,6 +53,8 @@
|
|
|
48
53
|
placeholderUntilEdit?: boolean;
|
|
49
54
|
/** An emptied leaf removes the field (`createField`). */
|
|
50
55
|
emptyUnsets?: boolean;
|
|
56
|
+
/** The declared `max:`, a character count no edit takes the leaf past. */
|
|
57
|
+
cap?: TextCap;
|
|
51
58
|
/** Registry identity, stamped on the DOM node so a remount is visible as one. */
|
|
52
59
|
leafKey: string;
|
|
53
60
|
onFocus?: (addr: Addr) => void;
|
|
@@ -77,6 +84,7 @@
|
|
|
77
84
|
placeholder,
|
|
78
85
|
placeholderUntilEdit,
|
|
79
86
|
emptyUnsets,
|
|
87
|
+
cap,
|
|
80
88
|
leafKey,
|
|
81
89
|
onFocus,
|
|
82
90
|
onCaretMove,
|
|
@@ -127,6 +135,7 @@
|
|
|
127
135
|
placeholder,
|
|
128
136
|
placeholderUntilEdit,
|
|
129
137
|
emptyUnsets,
|
|
138
|
+
cap,
|
|
130
139
|
tableStrings: () => t.strings,
|
|
131
140
|
slashStrings: () => t.strings,
|
|
132
141
|
onSlash: (next) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TextCap } from '../core/codec/index.js';
|
|
1
2
|
import type { LeafRegistry } from './leaves.js';
|
|
2
3
|
import type { Document, Quill, Addr, Content, Diagnostic } from '@quillmark/wasm';
|
|
3
4
|
import type { EditorErrorHandler } from '../core/errors.js';
|
|
@@ -35,6 +36,8 @@ interface Props {
|
|
|
35
36
|
placeholderUntilEdit?: boolean;
|
|
36
37
|
/** An emptied leaf removes the field (`createField`). */
|
|
37
38
|
emptyUnsets?: boolean;
|
|
39
|
+
/** The declared `max:`, a character count no edit takes the leaf past. */
|
|
40
|
+
cap?: TextCap;
|
|
38
41
|
/** Registry identity, stamped on the DOM node so a remount is visible as one. */
|
|
39
42
|
leafKey: string;
|
|
40
43
|
onFocus?: (addr: Addr) => void;
|
|
@@ -60,7 +60,8 @@
|
|
|
60
60
|
pmToUsv,
|
|
61
61
|
usvToPM,
|
|
62
62
|
valueEqual,
|
|
63
|
-
type LeafViews
|
|
63
|
+
type LeafViews,
|
|
64
|
+
type TextCap
|
|
64
65
|
} from '../core/codec/index.js';
|
|
65
66
|
import { valueLeafSeat } from './leaves.js';
|
|
66
67
|
import { wording } from './strings.js';
|
|
@@ -99,6 +100,8 @@
|
|
|
99
100
|
/** The empty leaf's ghost, a subform cell's: the `none` an optional cell prints
|
|
100
101
|
* unset. Read per decoration pass. */
|
|
101
102
|
placeholder?: string;
|
|
103
|
+
/** The declared `max:`, a character count no edit takes the leaf past. */
|
|
104
|
+
cap?: TextCap;
|
|
102
105
|
onChange: (rt: Content) => void;
|
|
103
106
|
/** Raw keydown, for a container whose own keys run through this leaf: the
|
|
104
107
|
* array repeater's Enter/Backspace (`ArrayField`). Fires before the view's own
|
|
@@ -115,6 +118,7 @@
|
|
|
115
118
|
labelledBy,
|
|
116
119
|
describedBy,
|
|
117
120
|
placeholder,
|
|
121
|
+
cap,
|
|
118
122
|
onChange,
|
|
119
123
|
onKey
|
|
120
124
|
}: Props = $props();
|
|
@@ -190,7 +194,8 @@
|
|
|
190
194
|
? []
|
|
191
195
|
: proseLeafPlugins(schema, {
|
|
192
196
|
inline,
|
|
193
|
-
placeholder: () => placeholder
|
|
197
|
+
placeholder: () => placeholder,
|
|
198
|
+
cap
|
|
194
199
|
})
|
|
195
200
|
});
|
|
196
201
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TextCap } from '../core/codec/index.js';
|
|
1
2
|
import type { Content } from '@quillmark/wasm';
|
|
2
3
|
interface Props {
|
|
3
4
|
/** This leaf's content (the parent's boundary read), `undefined` where nothing is
|
|
@@ -31,6 +32,8 @@ interface Props {
|
|
|
31
32
|
/** The empty leaf's ghost, a subform cell's: the `none` an optional cell prints
|
|
32
33
|
* unset. Read per decoration pass. */
|
|
33
34
|
placeholder?: string;
|
|
35
|
+
/** The declared `max:`, a character count no edit takes the leaf past. */
|
|
36
|
+
cap?: TextCap;
|
|
34
37
|
onChange: (rt: Content) => void;
|
|
35
38
|
/** Raw keydown, for a container whose own keys run through this leaf: the
|
|
36
39
|
* array repeater's Enter/Backspace (`ArrayField`). Fires before the view's own
|
|
@@ -54,10 +54,7 @@
|
|
|
54
54
|
isContainer,
|
|
55
55
|
shortCell,
|
|
56
56
|
schemaAt,
|
|
57
|
-
|
|
58
|
-
groupOrder,
|
|
59
|
-
groupSections,
|
|
60
|
-
groupLabel,
|
|
57
|
+
cardSections,
|
|
61
58
|
cardTitle,
|
|
62
59
|
kindTitle,
|
|
63
60
|
fieldValues,
|
|
@@ -325,9 +322,9 @@
|
|
|
325
322
|
try {
|
|
326
323
|
if (value === undefined) {
|
|
327
324
|
// The unset rung (VISUAL_EDITOR §"The commitment ladder"): removing the field
|
|
328
|
-
// leaves
|
|
325
|
+
// leaves Quillmark's authored › `default:` › blank-fill resolve to render the
|
|
329
326
|
// default, where writing one bakes a snapshot the schema cannot track (canon
|
|
330
|
-
// SCHEMAS.md:
|
|
327
|
+
// SCHEMAS.md: Quillmark never writes a default; the editor writes one only
|
|
331
328
|
// as the edit that takes it). Removal writes no value, so there is
|
|
332
329
|
// nothing for a schema to conform and the lane is the quill-free one.
|
|
333
330
|
if (isMain) {
|
|
@@ -632,14 +629,11 @@
|
|
|
632
629
|
payloadItems: readonly PayloadItem[];
|
|
633
630
|
ext?: Record<string, unknown>;
|
|
634
631
|
},
|
|
635
|
-
cardSchema: Parameters<typeof
|
|
632
|
+
cardSchema: Parameters<typeof cardSections>[0] | undefined,
|
|
636
633
|
rows: ResolvedCardRows
|
|
637
634
|
): CardModel {
|
|
638
635
|
const values = fieldValues(card.payloadItems);
|
|
639
|
-
const
|
|
640
|
-
const sections = cardSchema
|
|
641
|
-
? groupSections(fields, groupOrder(cardSchema), (g) => groupLabel(cardSchema, g))
|
|
642
|
-
: [];
|
|
636
|
+
const sections = cardSchema ? cardSections(cardSchema) : [];
|
|
643
637
|
const extEditor = card.ext?.editor as { title?: string } | undefined;
|
|
644
638
|
const hasBody = bodyEnabled(cardSchema);
|
|
645
639
|
return {
|
package/dist/visual/strings.d.ts
CHANGED
|
@@ -35,6 +35,12 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
|
|
|
35
35
|
* spells the separator.
|
|
36
36
|
*/
|
|
37
37
|
arrayCount: (count: number, max: number) => string;
|
|
38
|
+
/**
|
|
39
|
+
* The count beside the label of a `text` or `richtext` field declaring `max:`, the
|
|
40
|
+
* carrier of a keystroke the cap refuses: `12 / 40`. Parametric, so a translator
|
|
41
|
+
* spells the separator.
|
|
42
|
+
*/
|
|
43
|
+
textCount: (count: number, max: number) => string;
|
|
38
44
|
/**
|
|
39
45
|
* A collapsed array element with nothing to summarize itself by: the label and the
|
|
40
46
|
* 1-based index, which is what its accessible name already says. Parametric, so
|
|
@@ -116,7 +122,7 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
|
|
|
116
122
|
*
|
|
117
123
|
* - **`validation::enum_violation`, `type_mismatch`, `format_violation`**: the
|
|
118
124
|
* constraint re-words from the quill's schema at `path`, the offending value from
|
|
119
|
-
* `args` (`value` / `sourceToken` / `actual`,
|
|
125
|
+
* `args` (`value` / `sourceToken` / `actual`, Quillmark testifying to what it
|
|
120
126
|
* saw) and never from the document at `path`: validation runs post-coercion, so
|
|
121
127
|
* the validator read a value the document does not hold and a sentence built from
|
|
122
128
|
* `path` names a spelling the user never typed.
|
|
@@ -124,7 +130,7 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
|
|
|
124
130
|
* refused value being in neither document (unchanged on throw) nor schema.
|
|
125
131
|
* - **`parse::yaml_error_with_location`, `invalid_structure`**: does not re-word. No
|
|
126
132
|
* `path`; `args` names the block (`blockIndex`) and `location` places the failure in
|
|
127
|
-
* the document's own coordinates,
|
|
133
|
+
* the document's own coordinates, Quillmark prose riding under no key: the parser's own
|
|
128
134
|
* text, its column and its caret snippet exist only inside `message`.
|
|
129
135
|
* - **`LiveSession.warnings`**: does not re-word. Backend text, an external feed.
|
|
130
136
|
*
|
package/dist/visual/strings.js
CHANGED
|
@@ -41,6 +41,7 @@ export const DEFAULT_VISUAL_STRINGS = {
|
|
|
41
41
|
arrayDrag: 'Drag to move',
|
|
42
42
|
arrayRowAction: (action, row) => `${action} ${row}`,
|
|
43
43
|
arrayCount: (count, max) => `${count} / ${max}`,
|
|
44
|
+
textCount: (count, max) => `${count} / ${max}`,
|
|
44
45
|
elementUntitled: (label, index) => `${label} ${index}`,
|
|
45
46
|
enumUnsetTag: 'Default',
|
|
46
47
|
enumUnsetTagNone: 'Unset',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Content, type PathStep, type PayloadItem, type QuillCardSchema, type QuillFieldSchema, type QuillFieldType, type ResolvedField, type Resolved } from '@quillmark/wasm';
|
|
2
|
+
import type { TextCap } from '../core/codec/index.js';
|
|
2
3
|
/** The control a field type maps to (VISUAL_EDITOR §"Structure mirrors the schema"). */
|
|
3
4
|
export type ControlKind = 'prose' | 'text' | 'enum' | 'variant' | 'number' | 'boolean' | 'date' | 'array' | 'object' | 'matrix';
|
|
4
5
|
/** The container controls: the ones a nested address walks into and a deep
|
|
@@ -25,18 +26,17 @@ export interface FieldModel {
|
|
|
25
26
|
inline: boolean;
|
|
26
27
|
plaintext: boolean;
|
|
27
28
|
}
|
|
28
|
-
/** A `ui.group`
|
|
29
|
+
/** A panel section, a top-level `object`'s or a legacy `ui.group`'s: its label and the
|
|
30
|
+
* fields (declaration order) inside it. */
|
|
29
31
|
export interface GroupSection {
|
|
30
32
|
group: string | undefined;
|
|
31
33
|
label: string;
|
|
32
34
|
fields: FieldModel[];
|
|
33
|
-
/** The
|
|
34
|
-
*
|
|
35
|
+
/** The object the section's header names in place of a label of its own: the
|
|
36
|
+
* object a panel is, or a group section's one field where that field is an
|
|
37
|
+
* `object`. Its properties draw flat, the panel being their frame. */
|
|
35
38
|
head?: FieldModel;
|
|
36
39
|
}
|
|
37
|
-
/** `ui.layout: flat` on a typed dictionary: no frame of its own, its properties
|
|
38
|
-
* standing at the depth of the fields around it rather than a rung in. */
|
|
39
|
-
export declare function flatObject(schema: QuillFieldSchema): boolean;
|
|
40
40
|
/**
|
|
41
41
|
* One card instance projected for rendering: the schema × payload join result
|
|
42
42
|
* the VisualEditor's `$derived` produces and hands to `<Card>`. Positional
|
|
@@ -111,7 +111,7 @@ export declare function resolvedByCardIndex(resolved: Resolved | undefined): Map
|
|
|
111
111
|
export declare function ghostDefault(row: ResolvedField | undefined): unknown;
|
|
112
112
|
/** A stored `boolean` cell as the render reads it: a boolean, `true` or `false` in any
|
|
113
113
|
* case, or a number by whether it is non-zero; undefined where the render refuses the
|
|
114
|
-
* value.
|
|
114
|
+
* value. Quillmark's coercion, restated, so the suite holds it to `reader.resolve()`. */
|
|
115
115
|
export declare function renderedBoolean(value: unknown): boolean | undefined;
|
|
116
116
|
/** A ghost value's string form, or undefined for null/object (only text ghosts
|
|
117
117
|
* render a placeholder). A scalar's resolved default reads through it; a content
|
|
@@ -161,6 +161,8 @@ export declare function baseType(f: QuillFieldSchema): QuillFieldType;
|
|
|
161
161
|
export declare function oneLine(f: QuillFieldSchema): boolean;
|
|
162
162
|
/** Whether a content field's codec is the literal one, its value text with no marks: `text`. */
|
|
163
163
|
export declare function literalText(f: QuillFieldSchema): boolean;
|
|
164
|
+
/** The character cap a content field's `max:` declares, `undefined` on any other. */
|
|
165
|
+
export declare function textCap(f: QuillFieldSchema | undefined): TextCap | undefined;
|
|
164
166
|
/**
|
|
165
167
|
* A scalar `default:` as the text an unset control holds, where it prints: as
|
|
166
168
|
* declared, `undefined` for none, a blank or a non-scalar. A default of spaces prints
|
|
@@ -175,9 +177,10 @@ export declare function printedText(v: unknown): string | undefined;
|
|
|
175
177
|
* a blank or a non-scalar.
|
|
176
178
|
*/
|
|
177
179
|
export declare function declaredContent(v: unknown, markdown: boolean): Content | undefined;
|
|
178
|
-
/** A field's `{#each}` key: its name and what a mounted leaf reads once, its type
|
|
179
|
-
* {@link oneLine} and its elements'. A retype or a quill swap that changes
|
|
180
|
-
* under a name remounts the field, where a kept leaf would commit on the old
|
|
180
|
+
/** A field's `{#each}` key: its name and what a mounted leaf reads once, its type,
|
|
181
|
+
* {@link oneLine} and `max:`, and its elements'. A retype or a quill swap that changes
|
|
182
|
+
* any of them under a name remounts the field, where a kept leaf would commit on the old
|
|
183
|
+
* schema. */
|
|
181
184
|
export declare function mountKey(name: string, f: QuillFieldSchema): string;
|
|
182
185
|
/** Map a field schema to its control. An array's element control is this over `items`;
|
|
183
186
|
* a type this build does not know takes the text input. */
|
|
@@ -203,9 +206,9 @@ export declare function matrixMembers(roster: Record<string, string> | undefined
|
|
|
203
206
|
export declare function memberValue(map: Record<string, unknown> | undefined, id: string): unknown;
|
|
204
207
|
/**
|
|
205
208
|
* Whether a stored member is held: by being present. A mapping is held whatever it
|
|
206
|
-
* carries, `{}` included, and a bare scalar is the tick itself, read as
|
|
209
|
+
* carries, `{}` included, and a bare scalar is the tick itself, read as Quillmark's
|
|
207
210
|
* coercion reads it: `false`, `0`, `"false"` and `null` unheld, every other present
|
|
208
|
-
* scalar held, so a spelling
|
|
211
|
+
* scalar held, so a spelling Quillmark would refuse still draws the tick it claims.
|
|
209
212
|
*/
|
|
210
213
|
export declare function matrixHeld(stored: unknown): boolean;
|
|
211
214
|
/** A member's columns: its mapping without the reserved keys, `{}` for a bare tick or an
|
|
@@ -241,7 +244,7 @@ export declare function matrixDeclares(matrix: QuillFieldSchema, id: string): bo
|
|
|
241
244
|
/**
|
|
242
245
|
* The added items a stored map holds, sorted by id as the plate orders them: on an
|
|
243
246
|
* open matrix, each key off the roster spelled as an id whose mapping carries a
|
|
244
|
-
* `title`. Any other key off the roster is
|
|
247
|
+
* `title`. Any other key off the roster is Quillmark's refusal, drawn nowhere and
|
|
245
248
|
* ridden through.
|
|
246
249
|
*/
|
|
247
250
|
export declare function addedItems(matrix: QuillFieldSchema, map: Record<string, unknown> | undefined): MatrixMember[];
|
|
@@ -325,6 +328,19 @@ export declare function groupLabel(cardSchema: QuillCardSchema, group: string):
|
|
|
325
328
|
* the card schema, which falls back to {@link humanize} itself.
|
|
326
329
|
*/
|
|
327
330
|
export declare function groupSections(fields: FieldModel[], order: string[], labelFor: (group: string) => string): GroupSection[];
|
|
331
|
+
/**
|
|
332
|
+
* Object-panel sections (VISUAL_EDITOR §"Structure mirrors the schema"): every
|
|
333
|
+
* top-level `object` is a panel of its own, headed by the object, in declaration
|
|
334
|
+
* order; everything else stands in one unframed section above them. The section id
|
|
335
|
+
* is the field name, which a card's keys hold unique.
|
|
336
|
+
*/
|
|
337
|
+
export declare function objectSections(fields: FieldModel[]): GroupSection[];
|
|
338
|
+
/**
|
|
339
|
+
* A card's sections: its ungrouped fields' {@link objectSections}, then each legacy
|
|
340
|
+
* `ui.group` a panel in registry order, the order the loader settles the fields in.
|
|
341
|
+
* A panel named as a group is keyed off the group's id space, which `.` never joins.
|
|
342
|
+
*/
|
|
343
|
+
export declare function cardSections(cardSchema: QuillCardSchema): GroupSection[];
|
|
328
344
|
/**
|
|
329
345
|
* The group section a card's accordion opens on first mount: the first in
|
|
330
346
|
* order, or `null` when the card declares none. Ungrouped fields render outside
|
package/dist/visual/structure.js
CHANGED
|
@@ -11,11 +11,6 @@ import { core } from '../core/lifecycle.js';
|
|
|
11
11
|
export function isContainer(kind) {
|
|
12
12
|
return kind === 'array' || kind === 'object' || kind === 'variant' || kind === 'matrix';
|
|
13
13
|
}
|
|
14
|
-
/** `ui.layout: flat` on a typed dictionary: no frame of its own, its properties
|
|
15
|
-
* standing at the depth of the fields around it rather than a rung in. */
|
|
16
|
-
export function flatObject(schema) {
|
|
17
|
-
return controlKind(schema) === 'object' && schema.ui?.layout === 'flat';
|
|
18
|
-
}
|
|
19
14
|
/**
|
|
20
15
|
* A card's resolved rows keyed by field name: the provenance channel parallel to
|
|
21
16
|
* {@link CardModel.values}. `resolve` returns rows in declaration order; the
|
|
@@ -47,7 +42,7 @@ export function ghostDefault(row) {
|
|
|
47
42
|
}
|
|
48
43
|
/** A stored `boolean` cell as the render reads it: a boolean, `true` or `false` in any
|
|
49
44
|
* case, or a number by whether it is non-zero; undefined where the render refuses the
|
|
50
|
-
* value.
|
|
45
|
+
* value. Quillmark's coercion, restated, so the suite holds it to `reader.resolve()`. */
|
|
51
46
|
export function renderedBoolean(value) {
|
|
52
47
|
if (typeof value === 'boolean')
|
|
53
48
|
return value;
|
|
@@ -93,6 +88,12 @@ export function oneLine(f) {
|
|
|
93
88
|
export function literalText(f) {
|
|
94
89
|
return baseType(f) === 'text';
|
|
95
90
|
}
|
|
91
|
+
/** The character cap a content field's `max:` declares, `undefined` on any other. */
|
|
92
|
+
export function textCap(f) {
|
|
93
|
+
if (f?.max == null || controlKind(f) !== 'prose')
|
|
94
|
+
return undefined;
|
|
95
|
+
return { max: f.max, trimEnd: literalText(f) && oneLine(f) };
|
|
96
|
+
}
|
|
96
97
|
/**
|
|
97
98
|
* A scalar `default:` as the text an unset control holds, where it prints: as
|
|
98
99
|
* declared, `undefined` for none, a blank or a non-scalar. A default of spaces prints
|
|
@@ -126,12 +127,21 @@ export function declaredContent(v, markdown) {
|
|
|
126
127
|
islands: []
|
|
127
128
|
};
|
|
128
129
|
}
|
|
129
|
-
/** A field's `{#each}` key: its name and what a mounted leaf reads once, its type
|
|
130
|
-
* {@link oneLine} and its elements'. A retype or a quill swap that changes
|
|
131
|
-
* under a name remounts the field, where a kept leaf would commit on the old
|
|
130
|
+
/** A field's `{#each}` key: its name and what a mounted leaf reads once, its type,
|
|
131
|
+
* {@link oneLine} and `max:`, and its elements'. A retype or a quill swap that changes
|
|
132
|
+
* any of them under a name remounts the field, where a kept leaf would commit on the old
|
|
133
|
+
* schema. */
|
|
132
134
|
export function mountKey(name, f) {
|
|
133
135
|
const items = f.items;
|
|
134
|
-
return
|
|
136
|
+
return [
|
|
137
|
+
name,
|
|
138
|
+
f.type,
|
|
139
|
+
oneLine(f),
|
|
140
|
+
f.max ?? '',
|
|
141
|
+
items?.type ?? '',
|
|
142
|
+
items ? oneLine(items) : '',
|
|
143
|
+
items?.max ?? ''
|
|
144
|
+
].join('\u0000');
|
|
135
145
|
}
|
|
136
146
|
/** Map a field schema to its control. An array's element control is this over `items`;
|
|
137
147
|
* a type this build does not know takes the text input. */
|
|
@@ -182,16 +192,16 @@ export function memberValue(map, id) {
|
|
|
182
192
|
}
|
|
183
193
|
/**
|
|
184
194
|
* Whether a stored member is held: by being present. A mapping is held whatever it
|
|
185
|
-
* carries, `{}` included, and a bare scalar is the tick itself, read as
|
|
195
|
+
* carries, `{}` included, and a bare scalar is the tick itself, read as Quillmark's
|
|
186
196
|
* coercion reads it: `false`, `0`, `"false"` and `null` unheld, every other present
|
|
187
|
-
* scalar held, so a spelling
|
|
197
|
+
* scalar held, so a spelling Quillmark would refuse still draws the tick it claims.
|
|
188
198
|
*/
|
|
189
199
|
export function matrixHeld(stored) {
|
|
190
200
|
if (typeof stored === 'object' && stored !== null)
|
|
191
201
|
return true;
|
|
192
202
|
return tickOf(stored);
|
|
193
203
|
}
|
|
194
|
-
/** A tick's scalar reading: absent and
|
|
204
|
+
/** A tick's scalar reading: absent and Quillmark's false spellings unheld, the rest held. */
|
|
195
205
|
function tickOf(v) {
|
|
196
206
|
if (v === undefined || v === null)
|
|
197
207
|
return false;
|
|
@@ -265,7 +275,7 @@ export function matrixDeclares(matrix, id) {
|
|
|
265
275
|
/**
|
|
266
276
|
* The added items a stored map holds, sorted by id as the plate orders them: on an
|
|
267
277
|
* open matrix, each key off the roster spelled as an id whose mapping carries a
|
|
268
|
-
* `title`. Any other key off the roster is
|
|
278
|
+
* `title`. Any other key off the roster is Quillmark's refusal, drawn nowhere and
|
|
269
279
|
* ridden through.
|
|
270
280
|
*/
|
|
271
281
|
export function addedItems(matrix, map) {
|
|
@@ -511,7 +521,7 @@ export function groupSections(fields, order, labelFor) {
|
|
|
511
521
|
for (const g of order) {
|
|
512
522
|
const fs = fields.filter((f) => f.group === g);
|
|
513
523
|
if (fs.length) {
|
|
514
|
-
const head = fs.length === 1 &&
|
|
524
|
+
const head = fs.length === 1 && fs[0].control === 'object' ? fs[0] : undefined;
|
|
515
525
|
sections.push({ group: g, label: labelFor(g), fields: fs, head });
|
|
516
526
|
emitted.add(g);
|
|
517
527
|
}
|
|
@@ -528,6 +538,35 @@ export function groupSections(fields, order, labelFor) {
|
|
|
528
538
|
}
|
|
529
539
|
return sections;
|
|
530
540
|
}
|
|
541
|
+
/**
|
|
542
|
+
* Object-panel sections (VISUAL_EDITOR §"Structure mirrors the schema"): every
|
|
543
|
+
* top-level `object` is a panel of its own, headed by the object, in declaration
|
|
544
|
+
* order; everything else stands in one unframed section above them. The section id
|
|
545
|
+
* is the field name, which a card's keys hold unique.
|
|
546
|
+
*/
|
|
547
|
+
export function objectSections(fields) {
|
|
548
|
+
const panel = (f) => f.control === 'object';
|
|
549
|
+
const loose = fields.filter((f) => !panel(f));
|
|
550
|
+
const sections = loose.length
|
|
551
|
+
? [{ group: undefined, label: '', fields: loose }]
|
|
552
|
+
: [];
|
|
553
|
+
for (const f of fields)
|
|
554
|
+
if (panel(f))
|
|
555
|
+
sections.push({ group: f.name, label: f.label, fields: [f], head: f });
|
|
556
|
+
return sections;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* A card's sections: its ungrouped fields' {@link objectSections}, then each legacy
|
|
560
|
+
* `ui.group` a panel in registry order, the order the loader settles the fields in.
|
|
561
|
+
* A panel named as a group is keyed off the group's id space, which `.` never joins.
|
|
562
|
+
*/
|
|
563
|
+
export function cardSections(cardSchema) {
|
|
564
|
+
const fields = fieldModels(cardSchema);
|
|
565
|
+
const order = groupOrder(cardSchema);
|
|
566
|
+
const panels = objectSections(fields.filter((f) => f.group === undefined)).map((s) => s.head && order.includes(s.head.name) ? { ...s, group: `${s.head.name}.` } : s);
|
|
567
|
+
const grouped = fields.filter((f) => f.group !== undefined);
|
|
568
|
+
return [...panels, ...groupSections(grouped, order, (g) => groupLabel(cardSchema, g))];
|
|
569
|
+
}
|
|
531
570
|
/**
|
|
532
571
|
* The group section a card's accordion opens on first mount: the first in
|
|
533
572
|
* order, or `null` when the card declares none. Ungrouped fields render outside
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quillmark/svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Editor, live-preview and source surfaces over a Quillmark WASM session.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"check": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
|
-
"@quillmark/wasm": ">=0.
|
|
72
|
+
"@quillmark/wasm": ">=0.129.0-0",
|
|
73
73
|
"svelte": "^5.33.0"
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|