@quillmark/svelte 0.4.3 → 0.5.1
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 +8 -4
- package/THEMING.md +11 -12
- package/dist/core/codec/field.d.ts +1 -1
- package/dist/core/codec/field.js +26 -2
- package/dist/core/theme.css +9 -10
- package/dist/preset/recipes.css +0 -4
- package/dist/preset/scale.css +7 -8
- package/dist/preview/Preview.svelte +7 -2
- package/dist/preview/Preview.svelte.d.ts +1 -0
- package/dist/preview/controller.d.ts +11 -0
- package/dist/preview/controller.js +10 -7
- package/dist/visual/ArrayField.svelte +29 -18
- package/dist/visual/ArrayField.svelte.d.ts +7 -1
- package/dist/visual/DiagnosticList.svelte +6 -11
- package/dist/visual/EnumField.svelte +2 -2
- package/dist/visual/Field.svelte +2 -1
- package/dist/visual/FieldLabel.svelte +2 -2
- package/dist/visual/ProseArrayElement.svelte +38 -18
- package/dist/visual/ProseArrayElement.svelte.d.ts +8 -1
- package/dist/visual/VisualEditorInner.svelte +12 -3
- package/dist/visual/diagnostics.d.ts +8 -5
- package/dist/visual/diagnostics.js +9 -11
- package/dist/visual/props.d.ts +6 -1
- package/dist/visual/strings.d.ts +6 -6
- package/dist/visual/structure.d.ts +5 -6
- package/dist/visual/structure.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ const preview = createPreview(session, {
|
|
|
81
81
|
preview.refresh(session.update(doc)); // repaint dirtyPages ∩ visible
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
In Svelte, `<Preview {session} onPick={…} />` exposes the same verbs (`refresh`, `scrollToField`, `focusPosition`, `setZoom`) via `bind:this`.
|
|
84
|
+
In Svelte, `<Preview {session} onPick={…} />` exposes the same verbs (`refresh`, `scrollToField`, `focusPosition`, `endFollow`, `setZoom`) via `bind:this`.
|
|
85
85
|
|
|
86
86
|
`<Preview>` binds **once**, at mount: swapping `session`, `margin`, `onPick`, `onError` or `strings` in place changes nothing on screen and reports `rebind-ignored` through `onError` at `dev` severity, naming the prop. Swap by remounting (`{#key session}`); drive in-place edits through `refresh(change)`. `class` and `style` are the exceptions, landing on the root element and staying live.
|
|
87
87
|
|
|
@@ -152,7 +152,7 @@ The package ships English and a seam to replace it. `strings` is keyed and **par
|
|
|
152
152
|
addCard: '+ Ajouter une carte'
|
|
153
153
|
}}
|
|
154
154
|
formatDiagnostic={(d) =>
|
|
155
|
-
d.code === 'validation::
|
|
155
|
+
d.code === 'validation::type_mismatch' ? `${d.path} : type incorrect` : undefined}
|
|
156
156
|
/>
|
|
157
157
|
```
|
|
158
158
|
|
|
@@ -184,7 +184,7 @@ function recompileNow() {
|
|
|
184
184
|
}
|
|
185
185
|
```
|
|
186
186
|
|
|
187
|
-
`session.warnings` is a **getter on a handle Svelte does not track**: pull it per apply, or the editor shows the previous compile's
|
|
187
|
+
`session.warnings` is a **getter on a handle Svelte does not track**: pull it per apply, or the editor shows the previous compile's errors.
|
|
188
188
|
|
|
189
189
|
This shell layer is deliberately yours: the debounce value, what applies at once, and which surfaces refresh are host policy, so the package ships no scheduler over them. Three obligations ride with that: an edit of your own (an import, an undo, a direct `doc` write) recompiles by the same calls, since nothing polls the document; a compile owes the diagnostics re-read above, since nothing polls the session; and teardown clears the timer **before** freeing the handles, so a pending recompile never touches a freed session.
|
|
190
190
|
|
|
@@ -196,10 +196,14 @@ The bridge lives at the **consumer** layer and is opt-in; the editor is unaware
|
|
|
196
196
|
// preview → editor: a click resolved to an address lands in the field it names.
|
|
197
197
|
onPick: (at) => visualEditor.setCaret(at);
|
|
198
198
|
|
|
199
|
-
// editor → preview: a caret move scrolls the preview to follow it
|
|
199
|
+
// editor → preview: a caret move scrolls the preview to follow it,
|
|
200
|
+
// and a focus into a leaf with no caret to report ends the follow.
|
|
200
201
|
onCaretMove: preview.focusPosition;
|
|
202
|
+
onActiveLeafChange: preview.endFollow;
|
|
201
203
|
```
|
|
202
204
|
|
|
205
|
+
Both editor→preview hops or neither. A control reports its focus and no caret — there is no offset a date field could name — so without the second the preview keeps following the leaf the focus left, and every recompile pulls the pane back to it. A prose leaf restarts the follow with its next caret. A host that reads the active leaf for its own chrome calls `endFollow` from its handler.
|
|
206
|
+
|
|
203
207
|
A pick carries a caret where the compile tracks the content under the point, and the field alone where it tracks only the placement — a scalar the plate prints without tracking. `setCaret` takes both: an absent `pos` reveals and focuses the field, which is the whole of what a click on plate-placed ink can mean. The address may name an array element (`main.keywords[0]`), which lands on that row.
|
|
204
208
|
|
|
205
209
|
A consumer holding an `Addr` of its own maps it with `fieldPathForAddr` (from `@quillmark/svelte/core`). The playground app's split-pane route is the full reference shell: one session, both bridge directions, the preview following edits, and diagnostics routed inline.
|
package/THEMING.md
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
There are three depths to that. An app mounting into an unstyled page stops at the first; an app with a plate of its own starts at the second:
|
|
6
6
|
|
|
7
|
-
| | You write
|
|
8
|
-
| ----------------- |
|
|
9
|
-
| **Drop it in** | nothing
|
|
10
|
-
| **Make it yours** |
|
|
11
|
-
| **Match ours** | one CSS import
|
|
7
|
+
| | You write | You get |
|
|
8
|
+
| ----------------- | ---------------------- | ------------------------------------------------------- |
|
|
9
|
+
| **Drop it in** | nothing | a mounted surface that looks right in a bare `<div>` |
|
|
10
|
+
| **Make it yours** | nine custom properties | the whole surface rescaled and recolored to your brand |
|
|
11
|
+
| **Match ours** | one CSS import | the page around the surface, drawn the way we draw ours |
|
|
12
12
|
|
|
13
13
|
## Drop it in
|
|
14
14
|
|
|
@@ -28,7 +28,7 @@ Nothing to import and nothing to set. The package pulls its own stylesheet, and
|
|
|
28
28
|
|
|
29
29
|
## Make it yours
|
|
30
30
|
|
|
31
|
-
The whole contract is **
|
|
31
|
+
The whole contract is **nine CSS custom properties**. They are dials, not a palette: each derives a closed private scale that every component reads, so one override rescales or recolors the whole surface. Set them on any ancestor of a mounted surface (the app, or one pane):
|
|
32
32
|
|
|
33
33
|
```css
|
|
34
34
|
.my-editor {
|
|
@@ -54,8 +54,7 @@ The poles take a colour, not `transparent` or `inherit`: the raised surfaces, th
|
|
|
54
54
|
| `--qm-bg` | `#fff` / `#272729` | Base surface: a card, and the preview's sheet. Fields, the popover and the column behind the cards each step off it. |
|
|
55
55
|
| `--qm-fg` | `#1c1c1c` / `#d7dadc` | Base ink. Body text, labels, and borders step off it. |
|
|
56
56
|
| `--qm-accent` | `#71717a` / `#a1a1aa` | Focus rings, active marks, the preview's active field box. The surface spends no hue of its own, so this is the one it wears. |
|
|
57
|
-
| `--qm-danger` | `#
|
|
58
|
-
| `--qm-warning` | `#b25000` / `#e0b44c` | Warning diagnostics. |
|
|
57
|
+
| `--qm-danger` | `#9e1518` / `#f9786c` | Error diagnostics, the required marker, the delete glyph. |
|
|
59
58
|
| `--qm-font` | `ui-sans-serif, system-ui, sans-serif` | The editor surface's font family. Controls and buttons take it too, in place of the UA face. |
|
|
60
59
|
| `--qm-font-mono` | `ui-monospace, monospace` | The monospace face: the JSON array control, the tips card. |
|
|
61
60
|
| `--qm-font-size` | `0.875rem` | Chrome text: every control's size, the anchor the ramp derives up (title) and down (label/meta) from, and the anchor a document body sits one small step over. |
|
|
@@ -106,9 +105,9 @@ Setting the poles yourself wins over the default in both schemes, so a fixed pal
|
|
|
106
105
|
}
|
|
107
106
|
```
|
|
108
107
|
|
|
109
|
-
That cuts both ways for the status
|
|
108
|
+
That cuts both ways for the status hue: `--qm-danger` defaults to a pair calibrated against the card at both poles, and one pinned value replaces both arms, so a red measured against white carries into the dark scheme unmeasured. Pin it under your own scheme rule (`.dark { --qm-danger: … }`) where the colour your app means by error is itself scheme-aware.
|
|
110
109
|
|
|
111
|
-
A palette **pinned against** your own scheme still wants a second line, for a smaller reason than it used to be: the planes read your card's lightness, so the surface itself lands right either way, but the poles' own defaults, the status
|
|
110
|
+
A palette **pinned against** your own scheme still wants a second line, for a smaller reason than it used to be: the planes read your card's lightness, so the surface itself lands right either way, but the poles' own defaults, the status hue and native chrome all still read `color-scheme`. Pin it with them (`.my-editor { color-scheme: dark }`). Skip that line and what the browser paints — the selection highlight, the scrollbar, the spinner — stays on whichever scheme the host inherits, and a status hue you left to us is measured against the wrong card.
|
|
112
111
|
|
|
113
112
|
Pinning **one mount** rather than a page is the `style` prop, which lands on the same root: `<Preview style="color-scheme: dark" />`. There is no `scheme` prop, and deliberately — an inline declaration of ours would be the one thing your own CSS could not beat.
|
|
114
113
|
|
|
@@ -131,7 +130,7 @@ import '@quillmark/svelte/preset';
|
|
|
131
130
|
|
|
132
131
|
This is the stylesheet our own apps import: the playground and studio draw with these rules and nothing else, so what you get is what we ship, not a reduction of it.
|
|
133
132
|
|
|
134
|
-
It carries a `--qmh-*` scale for the page, derived from the same
|
|
133
|
+
It carries a `--qmh-*` scale for the page, derived from the same nine dials and calibrated against the surfaces' own steps, so a plate of yours and a card of ours agree about which way "raised" goes. Turning a dial moves both.
|
|
135
134
|
|
|
136
135
|
| Class | What it is |
|
|
137
136
|
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
@@ -139,7 +138,7 @@ It carries a `--qmh-*` scale for the page, derived from the same ten dials and c
|
|
|
139
138
|
| `.qm-panel` | The plate your controls and readouts sit on: one rung off the page, a hairline, a corner. |
|
|
140
139
|
| `.qm-label` | A run of chrome type: a section label, the name over a value. |
|
|
141
140
|
| `.qm-readout` | A value read back out of the session, monospace and tabular. `<pre class="qm-readout">` is the block form. |
|
|
142
|
-
| `.qm-status` | A phase line. `.qm-status-error`
|
|
141
|
+
| `.qm-status` | A phase line. `.qm-status-error` is the one it fails to. |
|
|
143
142
|
| `.qm-control` | A boxed button or select, which is what a host's controls are and the package's are not. |
|
|
144
143
|
| `.qm-measure` | A reading column at the width a passage wants. |
|
|
145
144
|
|
|
@@ -132,7 +132,7 @@ export interface LeafViews {
|
|
|
132
132
|
/** Every view nested inside this leaf, in mount order: one per table cell. */
|
|
133
133
|
nestedViews(): EditorView[];
|
|
134
134
|
}
|
|
135
|
-
/** The canonical empty `Content`: one empty `para` line. The
|
|
135
|
+
/** The canonical empty `Content`: one empty `para` line. The empty a prose
|
|
136
136
|
* leaf (or an array's prose element) seeds from. */
|
|
137
137
|
export declare function emptyContent(): Content;
|
|
138
138
|
export declare function createField(opts: CreateFieldOpts): FieldController;
|
package/dist/core/codec/field.js
CHANGED
|
@@ -70,7 +70,7 @@ function anchorPlugin(seed) {
|
|
|
70
70
|
function readLeaf(reader, addr) {
|
|
71
71
|
return reader.getContent(addr) ?? emptyContent();
|
|
72
72
|
}
|
|
73
|
-
/** The canonical empty `Content`: one empty `para` line. The
|
|
73
|
+
/** The canonical empty `Content`: one empty `para` line. The empty a prose
|
|
74
74
|
* leaf (or an array's prose element) seeds from. */
|
|
75
75
|
export function emptyContent() {
|
|
76
76
|
return { text: '', lines: [{ containers: [], kind: 'para' }], marks: [], islands: [] };
|
|
@@ -133,9 +133,22 @@ export function createField(opts) {
|
|
|
133
133
|
const slash = inline ? undefined : opts.onSlash;
|
|
134
134
|
const state = buildState(reconciler.last);
|
|
135
135
|
index = buildLineIndex(state.doc);
|
|
136
|
+
// The clearance PM's reveal keeps around the caret, in this leaf's own line box.
|
|
137
|
+
// Its default is 5px, which is a caret visible and unusable: the next line typed
|
|
138
|
+
// lands past the fold, the reason the preview's own scroll clears a target by the
|
|
139
|
+
// target's height (PREVIEW.md §"Follow-the-caret scroll"). Measured off the
|
|
140
|
+
// mounted box rather than restated from the rung, and dropped where the
|
|
141
|
+
// derivation is out of reach (jsdom), a fallback there being the scale written
|
|
142
|
+
// where `check:style` cannot read it.
|
|
143
|
+
const lineBox = Number.parseFloat(getComputedStyle(container).lineHeight);
|
|
144
|
+
const clearance = Number.isFinite(lineBox) ? lineBox : undefined;
|
|
136
145
|
view = new EditorView(container, {
|
|
137
146
|
state,
|
|
138
147
|
attributes: proseAttributes(opts),
|
|
148
|
+
// Both terms, one line each: when a flagged dispatch moves the scrollport
|
|
149
|
+
// (`scrollThreshold`) and what it leaves at the edge (`scrollMargin`).
|
|
150
|
+
scrollThreshold: clearance,
|
|
151
|
+
scrollMargin: clearance,
|
|
139
152
|
// Islands are block-schema only, so an inline leaf mounts no node view at all.
|
|
140
153
|
nodeViews: inline
|
|
141
154
|
? undefined
|
|
@@ -270,8 +283,15 @@ export function createField(opts) {
|
|
|
270
283
|
// be non-inline (before an island/rule block, or doc-level in an empty
|
|
271
284
|
// nested textblock), where a raw TextSelection is invalid.
|
|
272
285
|
const sel = Selection.near(view.state.doc.resolve(pm));
|
|
273
|
-
|
|
286
|
+
// Focus first, then the caret: the order a click's own pair arrives in, and the
|
|
287
|
+
// one a consumer that ends its caret follow on the arrival needs — the other
|
|
288
|
+
// way round, the arrival ends the follow this call just started.
|
|
289
|
+
//
|
|
290
|
+
// Flagged, because PM focuses with `preventScroll` and a landing is a caret
|
|
291
|
+
// move like any other: unflagged, the caret an arrow key would have revealed
|
|
292
|
+
// is left off screen by the click that placed it.
|
|
274
293
|
view.focus();
|
|
294
|
+
view.dispatch(view.state.tr.setSelection(sel).scrollIntoView());
|
|
275
295
|
},
|
|
276
296
|
applyExternal() {
|
|
277
297
|
const current = readLeaf(reader, addr);
|
|
@@ -296,6 +316,10 @@ export function createField(opts) {
|
|
|
296
316
|
},
|
|
297
317
|
focus() {
|
|
298
318
|
view.focus();
|
|
319
|
+
// The placement rung's reveal: no caret to flag a transaction with, so the
|
|
320
|
+
// trip is the element's, `nearest` and no clearance — the one the control
|
|
321
|
+
// lane's own focus takes, for the landing that means the same thing there.
|
|
322
|
+
view.dom.scrollIntoView({ block: 'nearest' });
|
|
299
323
|
},
|
|
300
324
|
getContent() {
|
|
301
325
|
return readLeaf(reader, addr);
|
package/dist/core/theme.css
CHANGED
|
@@ -89,16 +89,15 @@
|
|
|
89
89
|
/* Neutral: the surface spends no hue on a state it can draw in grey. The pair
|
|
90
90
|
clears WCAG 2.4.11's 3:1 against the card at both poles, at 4.8:1 and 5.8:1. */
|
|
91
91
|
--_qm-accent: var(--qm-accent, light-dark(#71717a, #a1a1aa));
|
|
92
|
-
/* The
|
|
93
|
-
|
|
94
|
-
white reads as a stain on a dark card.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
(`preset/scale.css`), so a page's status line
|
|
99
|
-
|
|
100
|
-
--_qm-danger: var(--qm-danger, light-dark(#
|
|
101
|
-
--_qm-warning: var(--qm-warning, light-dark(#b25000, #e0b44c));
|
|
92
|
+
/* The one hue the surface spends, and it spends it on a state. Each pole is
|
|
93
|
+
its own figure rather than one ink shown twice: a hue calibrated against
|
|
94
|
+
white reads as a stain on a dark card. Both clear WCAG 1.4.3's 4.5:1 against
|
|
95
|
+
the card, which is the bar the label rung asks for and the one the glyphs
|
|
96
|
+
drawn in this hue clear on the way past 1.4.11's 3:1. Dark enough at its own
|
|
97
|
+
pole to carry a label at 4.5:1, so a filled chip needs no third figure. The
|
|
98
|
+
figures are the host scale's (`preset/scale.css`), so a page's status line
|
|
99
|
+
and a card's diagnostic carry one colour meaning error. */
|
|
100
|
+
--_qm-danger: var(--qm-danger, light-dark(#9e1518, #f9786c));
|
|
102
101
|
--_qm-font: var(--qm-font, ui-sans-serif, system-ui, sans-serif);
|
|
103
102
|
--_qm-font-mono: var(--qm-font-mono, ui-monospace, monospace);
|
|
104
103
|
|
package/dist/preset/recipes.css
CHANGED
|
@@ -247,10 +247,6 @@ pre.qm-readout {
|
|
|
247
247
|
overflow-wrap: anywhere;
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
.qm-status-warn {
|
|
251
|
-
color: var(--qmh-warn);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
250
|
/* ── Surfaces ──────────────────────────────────────────────────────────────── */
|
|
255
251
|
|
|
256
252
|
/* The instrument plate: what a host puts its controls and readouts on. The same plane a
|
package/dist/preset/scale.css
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
page around the surfaces out. `core/theme.css` is the same shape one level down
|
|
3
3
|
(dials in, `--_qm-*` out), and the two are calibrated against each other: the
|
|
4
4
|
figures here are the figures there (the ground step's ramp, 8% hover, 17%
|
|
5
|
-
border, 45% ink, the ring's width, the status
|
|
5
|
+
border, 45% ink, the ring's width, the status hue at both poles) and the
|
|
6
6
|
dial means the same plane in both, so a host plate and a card on it land on the same
|
|
7
7
|
tone at both poles and a host page meets an editor's own column exactly. The well
|
|
8
8
|
under the ground has no counterpart here: a host's own controls are boxed and carry a
|
|
@@ -45,13 +45,12 @@
|
|
|
45
45
|
the underline under a link. */
|
|
46
46
|
--qmh-border-strong: color-mix(in oklab, var(--qmh-surface), var(--qmh-ink) 40%);
|
|
47
47
|
|
|
48
|
-
/* The
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
--qmh-alert: var(--qm-danger, light-dark(#
|
|
54
|
-
--qmh-warn: var(--qm-warning, light-dark(#b25000, #e0b44c));
|
|
48
|
+
/* The hue a host spends on a failure. The figures are the surfaces' own, so a
|
|
49
|
+
page's status line and a card's diagnostic carry one colour meaning error.
|
|
50
|
+
Each pole is its own figure: a hue calibrated against white reads as a stain
|
|
51
|
+
on a dark page. The dial sits in front of the pair, as every pole's does, so
|
|
52
|
+
a host that turns one owns both schemes. */
|
|
53
|
+
--qmh-alert: var(--qm-danger, light-dark(#9e1518, #f9786c));
|
|
55
54
|
|
|
56
55
|
/* ── Rhythm ──────────────────────────────────────────────────────────────── */
|
|
57
56
|
/* One base and its multiples, the same four steps the surfaces derive. A host
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
container div on mount and tears it down on unmount. No logic beyond wiring;
|
|
4
4
|
paint.ts/bridge.ts/controller.ts own the behavior. Exposes the
|
|
5
5
|
`PreviewController` verbs as instance methods (`bind:this`) for a consumer
|
|
6
|
-
that drives `refresh`/`scrollToField`/`focusPosition`/`setZoom`
|
|
7
|
-
(e.g. after `session.apply` elsewhere).
|
|
6
|
+
that drives `refresh`/`scrollToField`/`focusPosition`/`endFollow`/`setZoom`
|
|
7
|
+
imperatively (e.g. after `session.apply` elsewhere).
|
|
8
8
|
-->
|
|
9
9
|
<script lang="ts">
|
|
10
10
|
import { onMount } from 'svelte';
|
|
@@ -78,6 +78,11 @@
|
|
|
78
78
|
export function focusPosition(at: Place): void {
|
|
79
79
|
controller?.focusPosition(at);
|
|
80
80
|
}
|
|
81
|
+
/** The follow ends on a focus change ({@link PreviewController}), and the editor's
|
|
82
|
+
* own signal is one: `onActiveLeafChange={preview.endFollow}`. */
|
|
83
|
+
export function endFollow(): void {
|
|
84
|
+
controller?.endFollow();
|
|
85
|
+
}
|
|
81
86
|
export function setZoom(scale: number): void {
|
|
82
87
|
controller?.setZoom(scale);
|
|
83
88
|
}
|
|
@@ -32,6 +32,7 @@ declare const Preview: import("svelte").Component<Props, {
|
|
|
32
32
|
refresh: (change: ChangeSet) => void;
|
|
33
33
|
scrollToField: (field: DocPath) => boolean;
|
|
34
34
|
focusPosition: (at: Place) => void;
|
|
35
|
+
endFollow: () => void;
|
|
35
36
|
setZoom: (scale: number) => void;
|
|
36
37
|
}, "">;
|
|
37
38
|
type Preview = ReturnType<typeof Preview>;
|
|
@@ -38,6 +38,17 @@ export interface PreviewController {
|
|
|
38
38
|
* `ContentHit` carries both members and fits here too.
|
|
39
39
|
*/
|
|
40
40
|
focusPosition(at: Place): void;
|
|
41
|
+
/**
|
|
42
|
+
* End the follow: the pane stays where the user left it until the next
|
|
43
|
+
* `focusPosition`.
|
|
44
|
+
*
|
|
45
|
+
* The editor's `onActiveLeafChange` is the signal, firing for a form control as for
|
|
46
|
+
* a prose leaf: `onActiveLeafChange={preview.endFollow}`. A control has no caret
|
|
47
|
+
* coordinate to report, so unwired the follow keeps naming the leaf the focus left
|
|
48
|
+
* and every recompile pulls the pane back there. A prose leaf restarts it with its
|
|
49
|
+
* own next caret.
|
|
50
|
+
*/
|
|
51
|
+
endFollow(): void;
|
|
41
52
|
/** Fold a density multiplier into every future paint (crispness, not layout). */
|
|
42
53
|
setZoom(scale: number): void;
|
|
43
54
|
destroy(): void;
|
|
@@ -83,13 +83,13 @@ export function createPreview(session, opts) {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
render(session.pageCount, []);
|
|
86
|
-
// The last place the editor put the caret, re-located after every recompile
|
|
87
|
-
// `session.locate` answers against the last compiled
|
|
88
|
-
// debounces `update`, so a caret typed past that layout's
|
|
89
|
-
// for the whole burst: `focusPosition` no-ops and the pane
|
|
90
|
-
// caret event is otherwise the only thing that asks again. The
|
|
91
|
-
// preview's, not the consumer's: the staleness sits between two
|
|
92
|
-
// this module owns both ends of.
|
|
86
|
+
// The last place the editor put the caret, re-located after every recompile and
|
|
87
|
+
// dropped on a focus change. `session.locate` answers against the last compiled
|
|
88
|
+
// layout and a consumer debounces `update`, so a caret typed past that layout's
|
|
89
|
+
// content is off-content for the whole burst: `focusPosition` no-ops and the pane
|
|
90
|
+
// sits still, and a caret event is otherwise the only thing that asks again. The
|
|
91
|
+
// re-locate is the preview's, not the consumer's: the staleness sits between two
|
|
92
|
+
// session queries this module owns both ends of.
|
|
93
93
|
let followed;
|
|
94
94
|
return {
|
|
95
95
|
refresh(change) {
|
|
@@ -106,6 +106,9 @@ export function createPreview(session, opts) {
|
|
|
106
106
|
followed = at;
|
|
107
107
|
bridge?.focusPosition(at.field, at.pos);
|
|
108
108
|
},
|
|
109
|
+
endFollow() {
|
|
110
|
+
followed = undefined;
|
|
111
|
+
},
|
|
109
112
|
setZoom(scale) {
|
|
110
113
|
paintLoop.setDensityZoom(scale);
|
|
111
114
|
},
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
An `array` field → an add/remove repeater. Elements commit by value: every
|
|
3
3
|
edit / add / remove rebuilds the whole array and hands it to the parent's typed
|
|
4
4
|
`writer.set(field, wholeArray)` (arrays are not op-addressed). Element control
|
|
5
|
-
by `items.type`: `
|
|
5
|
+
by `items.type`: `richtext` / `plaintext` → a prose element
|
|
6
6
|
({@link ProseArrayElement}), `object` → a minimal JSON editor, keeping the prior
|
|
7
|
-
element value on an entry that does not parse
|
|
8
|
-
label header row (space-between with the field label);
|
|
9
|
-
own label for array controls and hands this component the
|
|
7
|
+
element value on an entry that does not parse, everything else → a text input. The
|
|
8
|
+
add affordance sits in the label header row (space-between with the field label);
|
|
9
|
+
{@link Field} skips its own label for array controls and hands this component the
|
|
10
|
+
label track with it.
|
|
10
11
|
|
|
11
12
|
The remove is inside the element: a slab over the end of the element's own box, taking
|
|
12
13
|
its two end-side corners. So a row's box is the element's box, and an array's rows end
|
|
@@ -54,22 +55,31 @@
|
|
|
54
55
|
labelId?: string;
|
|
55
56
|
/** Where the description parks, for the group's `aria-describedby`. */
|
|
56
57
|
descriptionId?: string;
|
|
58
|
+
/** Element `i`'s content, decoded by the boundary through the codec `items`
|
|
59
|
+
* names (`Field`, `reader.getContentAt`). Asked only for a content-typed
|
|
60
|
+
* element: on any other the read is not content and throws. `undefined` for a
|
|
61
|
+
* row the stored value does not reach — a slot this control has spliced in and
|
|
62
|
+
* whose commit has yet to land, or was refused. */
|
|
63
|
+
elementContent: (i: number) => Content | undefined;
|
|
57
64
|
onCommit: (arr: unknown[]) => void;
|
|
58
65
|
}
|
|
59
|
-
let {
|
|
60
|
-
|
|
66
|
+
let {
|
|
67
|
+
value,
|
|
68
|
+
items,
|
|
69
|
+
label,
|
|
70
|
+
required,
|
|
71
|
+
description,
|
|
72
|
+
labelId,
|
|
73
|
+
descriptionId,
|
|
74
|
+
elementContent,
|
|
75
|
+
onCommit
|
|
76
|
+
}: Props = $props();
|
|
61
77
|
|
|
62
|
-
// The element control is the item schema's own, with
|
|
63
|
-
// element
|
|
64
|
-
// `
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
// what the row is handed and a string is what it commits. The prose element stays
|
|
68
|
-
// the `richtext` arm: `Content` after a bound load or a commit, the authored
|
|
69
|
-
// string through the transport door, lowered in the row for the same reason.
|
|
70
|
-
//
|
|
71
|
-
// An array declaring no `items` has text elements.
|
|
72
|
-
const control = $derived(items && items.type !== 'plaintext' ? controlKind(items) : 'text');
|
|
78
|
+
// The element control is the item schema's own, with no departure: a content-typed
|
|
79
|
+
// element mounts the prose leaf its scalar field mounts, reading through
|
|
80
|
+
// `elementContent` whatever the element rests as. An array declaring no `items`
|
|
81
|
+
// has text elements.
|
|
82
|
+
const control = $derived(items ? controlKind(items) : 'text');
|
|
73
83
|
const arr = $derived((value ?? []) as unknown[]);
|
|
74
84
|
|
|
75
85
|
// Parallel stable ids, one per element, kept in lockstep with the data below.
|
|
@@ -264,7 +274,8 @@
|
|
|
264
274
|
{#if control === 'prose'}
|
|
265
275
|
<ProseArrayElement
|
|
266
276
|
bind:this={els[id]}
|
|
267
|
-
|
|
277
|
+
content={() => elementContent(k) ?? emptyContent()}
|
|
278
|
+
plaintext={items?.type === 'plaintext'}
|
|
268
279
|
label={label != null ? `${label} ${k + 1}` : undefined}
|
|
269
280
|
onChange={(rt) => commitElement(k, rt)}
|
|
270
281
|
onKey={(e) => onElementKey(e, k)}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { QuillFieldSchema } from '@quillmark/wasm';
|
|
1
|
+
import type { Content, QuillFieldSchema } from '@quillmark/wasm';
|
|
2
2
|
import './controls.css';
|
|
3
3
|
interface Props {
|
|
4
4
|
value: unknown[] | undefined;
|
|
@@ -15,6 +15,12 @@ interface Props {
|
|
|
15
15
|
labelId?: string;
|
|
16
16
|
/** Where the description parks, for the group's `aria-describedby`. */
|
|
17
17
|
descriptionId?: string;
|
|
18
|
+
/** Element `i`'s content, decoded by the boundary through the codec `items`
|
|
19
|
+
* names (`Field`, `reader.getContentAt`). Asked only for a content-typed
|
|
20
|
+
* element: on any other the read is not content and throws. `undefined` for a
|
|
21
|
+
* row the stored value does not reach — a slot this control has spliced in and
|
|
22
|
+
* whose commit has yet to land, or was refused. */
|
|
23
|
+
elementContent: (i: number) => Content | undefined;
|
|
18
24
|
onCommit: (arr: unknown[]) => void;
|
|
19
25
|
}
|
|
20
26
|
declare const ArrayField: import("svelte").Component<Props, {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
Inline diagnostic display (VISUAL_EDITOR §Diagnostics),
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
Inline diagnostic display (VISUAL_EDITOR §Diagnostics). Errors only, non-gating:
|
|
3
|
+
purely additive, never disables the field it sits under. Shared by `Field.svelte`
|
|
4
|
+
(a scalar/prose field) and `Card.svelte` (the body leaf, which has no
|
|
5
|
+
`Field.svelte` wrapper), so a card's body diagnostics render the same way a
|
|
6
|
+
field's do.
|
|
7
7
|
-->
|
|
8
8
|
<script lang="ts">
|
|
9
9
|
import type { Diagnostic } from '@quillmark/wasm';
|
|
@@ -27,9 +27,7 @@
|
|
|
27
27
|
<!-- role=status: commit errors appear mid-typing; announce without stealing focus. -->
|
|
28
28
|
<div class="qm-diag-list" role="status">
|
|
29
29
|
{#each diagnostics as d, i (i)}
|
|
30
|
-
<span class="qm-diag-line"
|
|
31
|
-
>{diagnosticText(d, t.formatDiagnostic)}</span
|
|
32
|
-
>
|
|
30
|
+
<span class="qm-diag-line">{diagnosticText(d, t.formatDiagnostic)}</span>
|
|
33
31
|
{/each}
|
|
34
32
|
</div>
|
|
35
33
|
{/if}
|
|
@@ -42,9 +40,6 @@
|
|
|
42
40
|
}
|
|
43
41
|
.qm-diag-line {
|
|
44
42
|
font-size: var(--_qm-text-label);
|
|
45
|
-
color: var(--_qm-warning);
|
|
46
|
-
}
|
|
47
|
-
.qm-diag-line[data-severity='error'] {
|
|
48
43
|
color: var(--_qm-danger);
|
|
49
44
|
}
|
|
50
45
|
</style>
|
|
@@ -80,8 +80,8 @@
|
|
|
80
80
|
<span class="qm-select-wrap" bind:this={wrapEl}>
|
|
81
81
|
<!-- `allowDeselect={false}`: the unset sentinel is the clear-to-default
|
|
82
82
|
affordance, so the primitive's own deselect must stay off. It reports a
|
|
83
|
-
deselect as `''
|
|
84
|
-
|
|
83
|
+
deselect as `''`, the boundary's blank: a written value, where the sentinel's
|
|
84
|
+
clear is an absence. -->
|
|
85
85
|
<Select.Root
|
|
86
86
|
type="single"
|
|
87
87
|
allowDeselect={false}
|
package/dist/visual/Field.svelte
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
`diagnostics` is the routed `Diagnostic[]` for this field (VisualEditor's
|
|
12
12
|
`diagByKey`, merging `quill.validate`, local commit errors, and the external
|
|
13
13
|
`diagnostics` prop (VISUAL_EDITOR §Diagnostics)) rendered via the shared
|
|
14
|
-
`DiagnosticList`,
|
|
14
|
+
`DiagnosticList`, non-gating.
|
|
15
15
|
-->
|
|
16
16
|
<script lang="ts">
|
|
17
17
|
import type { Document, Quill, Addr, Diagnostic, ResolvedField } from '@quillmark/wasm';
|
|
@@ -265,6 +265,7 @@
|
|
|
265
265
|
description={field.description}
|
|
266
266
|
labelId={domIds.label}
|
|
267
267
|
descriptionId={domIds.description}
|
|
268
|
+
elementContent={(i) => quill.reader(doc).getContentAt(addr, [i])}
|
|
268
269
|
onCommit={onCommitScalar}
|
|
269
270
|
/>
|
|
270
271
|
{:else if field.control === 'object'}
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
element is inert and invalid.
|
|
12
12
|
|
|
13
13
|
Two non-gating label decorations:
|
|
14
|
-
• a persistent required `*`
|
|
15
|
-
|
|
14
|
+
• a persistent required `*` for a field the schema obliges (`must_fill`, which
|
|
15
|
+
derives from a missing `default:`; DOCUMENT_MODEL). Its
|
|
16
16
|
accessible name is "required", so a screen reader announces the word, not the
|
|
17
17
|
glyph; inside the `<label>`, so it names the control along with the text
|
|
18
18
|
("Subject required") rather than announcing as a node adjacent to it;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
One element of an array
|
|
3
|
-
elements are not `applyChange`-addressable: `Addr.field` is a
|
|
4
|
-
`keywords[0]` has no op address. So
|
|
2
|
+
One element of an array of `richtext` (fixture `keywords`) or `plaintext`
|
|
3
|
+
(`errata`). Array elements are not `applyChange`-addressable: `Addr.field` is a
|
|
4
|
+
flat name, so `keywords[0]` has no op address. So
|
|
5
5
|
this is not a `createField` leaf: it mounts a minimal PM view over the codec's
|
|
6
6
|
decode/encode + inline schema, and on every edit hands the re-encoded
|
|
7
7
|
`Content` up to the parent {@link ArrayField}, which commits the whole array
|
|
@@ -9,24 +9,35 @@
|
|
|
9
9
|
element are dropped on that value write: acceptable for inline refs. Mounts
|
|
10
10
|
once per stable element id (no reset on the parent's re-derive).
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
not a codec failure.
|
|
12
|
+
The content is read, not passed: `reader.getContentAt(addr, [k])` decodes an
|
|
13
|
+
element through the codec its `items` type declares, so what the element rests
|
|
14
|
+
as — the content object, a `plaintext` literal, the authored string a transport
|
|
15
|
+
door left — stops being this row's business. Read once, at mount, since that is
|
|
16
|
+
when this leaf takes its state.
|
|
18
17
|
-->
|
|
19
18
|
<script lang="ts">
|
|
20
19
|
import { onMount } from 'svelte';
|
|
21
20
|
import { EditorState } from 'prosemirror-state';
|
|
22
21
|
import { EditorView } from 'prosemirror-view';
|
|
23
|
-
import {
|
|
24
|
-
|
|
22
|
+
import {
|
|
23
|
+
decode,
|
|
24
|
+
pmToContent,
|
|
25
|
+
inlineSchema,
|
|
26
|
+
plaintextSchema,
|
|
27
|
+
proseLeafPlugins
|
|
28
|
+
} from '../core/codec/index.js';
|
|
25
29
|
import './controls.css';
|
|
26
30
|
import type { Content } from '@quillmark/wasm';
|
|
27
31
|
|
|
28
32
|
interface Props {
|
|
29
|
-
|
|
33
|
+
/** This element's content, read at mount (`ArrayField`, through the boundary's
|
|
34
|
+
* element read). A thunk rather than a value: the parent re-derives per
|
|
35
|
+
* revision and this leaf takes its state once, so a value prop would be a
|
|
36
|
+
* boundary read per row per render, all but one of them discarded. */
|
|
37
|
+
content: () => Content;
|
|
38
|
+
/** The mark-free schema (a `plaintext` item): literal text, no formatting,
|
|
39
|
+
* exactly as the scalar field of that type mounts. */
|
|
40
|
+
plaintext?: boolean;
|
|
30
41
|
/** Accessible name for the editable region (the array has no per-element label). */
|
|
31
42
|
label?: string;
|
|
32
43
|
onChange: (rt: Content) => void;
|
|
@@ -35,15 +46,20 @@
|
|
|
35
46
|
* keymap, so the state it reads is the one this keystroke has yet to change. */
|
|
36
47
|
onKey?: (e: KeyboardEvent) => void;
|
|
37
48
|
}
|
|
38
|
-
let {
|
|
49
|
+
let { content, plaintext = false, label, onChange, onKey }: Props = $props();
|
|
39
50
|
|
|
40
51
|
let containerEl: HTMLDivElement | undefined = $state();
|
|
41
52
|
let view: EditorView | undefined;
|
|
42
53
|
/** Take the caret: what a parent placing focus on this element calls. The view's
|
|
43
54
|
* focus, not the element's: a PM view restores its selection, where a bare DOM
|
|
44
|
-
* focus on a contenteditable leaves the caret unplaced.
|
|
55
|
+
* focus on a contenteditable leaves the caret unplaced. And the reveal beside it,
|
|
56
|
+
* since PM's focus prevents the scroll a `string` element's input takes by
|
|
57
|
+
* default — an array whose two item types revealed differently is one row of it
|
|
58
|
+
* landing off screen. */
|
|
45
59
|
export function focus(): void {
|
|
46
|
-
view
|
|
60
|
+
if (!view) return;
|
|
61
|
+
view.focus();
|
|
62
|
+
view.dom.scrollIntoView({ block: 'nearest' });
|
|
47
63
|
}
|
|
48
64
|
|
|
49
65
|
onMount(() => {
|
|
@@ -52,11 +68,15 @@
|
|
|
52
68
|
// plugin stack a `createField` leaf mounts (shared `proseLeafPlugins`), minus
|
|
53
69
|
// the anchor-position plugin (element anchors are dropped on the array's value
|
|
54
70
|
// write, per the header). Its own `dispatchTransaction` hands `Content` up.
|
|
55
|
-
|
|
56
|
-
|
|
71
|
+
//
|
|
72
|
+
// The schema is the declared type's, as `createField` picks it: `plaintext`
|
|
73
|
+
// declares no mark types, so there is nothing to toggle, to paste in, or to
|
|
74
|
+
// mint a shorthand with.
|
|
75
|
+
const schema = plaintext ? plaintextSchema : inlineSchema;
|
|
76
|
+
const pmDoc = decode(content(), schema);
|
|
57
77
|
const state = EditorState.create({
|
|
58
78
|
doc: pmDoc,
|
|
59
|
-
plugins: proseLeafPlugins(
|
|
79
|
+
plugins: proseLeafPlugins(schema, { inline: true })
|
|
60
80
|
});
|
|
61
81
|
const mounted = new EditorView(containerEl, {
|
|
62
82
|
state,
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import './controls.css';
|
|
2
2
|
import type { Content } from '@quillmark/wasm';
|
|
3
3
|
interface Props {
|
|
4
|
-
|
|
4
|
+
/** This element's content, read at mount (`ArrayField`, through the boundary's
|
|
5
|
+
* element read). A thunk rather than a value: the parent re-derives per
|
|
6
|
+
* revision and this leaf takes its state once, so a value prop would be a
|
|
7
|
+
* boundary read per row per render, all but one of them discarded. */
|
|
8
|
+
content: () => Content;
|
|
9
|
+
/** The mark-free schema (a `plaintext` item): literal text, no formatting,
|
|
10
|
+
* exactly as the scalar field of that type mounts. */
|
|
11
|
+
plaintext?: boolean;
|
|
5
12
|
/** Accessible name for the editable region (the array has no per-element label). */
|
|
6
13
|
label?: string;
|
|
7
14
|
onChange: (rt: Content) => void;
|
|
@@ -206,6 +206,10 @@
|
|
|
206
206
|
const plain = normalize(addr);
|
|
207
207
|
activeAddr = plain;
|
|
208
208
|
activeCardId = cardIdOf(plain);
|
|
209
|
+
// The arrival ends the caret memo's run (`lastCaretField` below): a leaf returned
|
|
210
|
+
// to at the offset it still names has to report again.
|
|
211
|
+
lastCaretField = undefined;
|
|
212
|
+
lastCaretPos = undefined;
|
|
209
213
|
const field = pathFor(plain);
|
|
210
214
|
if (field != null) onActiveLeafChange?.({ field, cardId: activeCardId });
|
|
211
215
|
}
|
|
@@ -236,6 +240,10 @@
|
|
|
236
240
|
// that reflows the page under a caret holding its offset is the case this drops,
|
|
237
241
|
// and a preview does not need the signal to answer it: the place is unchanged, so
|
|
238
242
|
// the one it already followed is the one its recompile re-locates.
|
|
243
|
+
//
|
|
244
|
+
// A focus clears it, and must: a focus into a leaf with no caret to report ends a
|
|
245
|
+
// consumer's follow (PREVIEW §"Follow-the-caret scroll"), and the return to the
|
|
246
|
+
// offset the memo still names is the only thing that would restart it.
|
|
239
247
|
let lastCaretField: DocPath | undefined;
|
|
240
248
|
let lastCaretPos: number | undefined;
|
|
241
249
|
function handleCaret(addr: Addr, pos: number): void {
|
|
@@ -272,7 +280,7 @@
|
|
|
272
280
|
if (value === undefined) {
|
|
273
281
|
// The unset rung of the commitment ladder (VISUAL_EDITOR §"Structure mirrors
|
|
274
282
|
// the schema"): removing the field leaves the engine's authored › `default:`
|
|
275
|
-
// ›
|
|
283
|
+
// › blank-fill resolve to render the default, where writing one bakes a
|
|
276
284
|
// snapshot the schema cannot track (canon SCHEMAS.md: the engine never
|
|
277
285
|
// persists a default; nor do we). Removal writes no value, so there is
|
|
278
286
|
// nothing for a schema to conform and the lane is the quill-free one.
|
|
@@ -477,8 +485,9 @@
|
|
|
477
485
|
});
|
|
478
486
|
|
|
479
487
|
// All three producers. The two positional ones resolve their `.path` to the live
|
|
480
|
-
// stable-id keying; commit errors are id-keyed already. Nothing is dropped
|
|
481
|
-
// diagnostics never gate
|
|
488
|
+
// stable-id keying; commit errors are id-keyed already. Nothing is dropped for
|
|
489
|
+
// being unwelcome — diagnostics never gate — but warnings do not draw: only
|
|
490
|
+
// errors reach a control (`routeAndResolve`).
|
|
482
491
|
const diagByKey = $derived.by(() => {
|
|
483
492
|
const fromValidate = routeAndResolve(validation, cardIds);
|
|
484
493
|
const fromExternal = routeAndResolve(diagnostics, cardIds);
|
|
@@ -30,15 +30,18 @@ export interface RoutedDiagnostic {
|
|
|
30
30
|
/**
|
|
31
31
|
* Route a path-keyed producer's raw `Diagnostic[]` (validate / external) to the
|
|
32
32
|
* editor's stable-id keying: the one door producers #1/#3 take, `addrForFieldPath`
|
|
33
|
-
* and `resolveCardKey` in a single pass.
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* and `resolveCardKey` in a single pass. Warnings do not draw (VISUAL_EDITOR
|
|
34
|
+
* §Diagnostics): obligation, completeness, a render note — none of them is a value
|
|
35
|
+
* the field cannot hold. An entry also drops rather than mis-routes when it carries
|
|
36
|
+
* no `path`, when the path names no single commit address (a nested / array-element
|
|
37
|
+
* one), or when its absolute card index is out of the live `cardIds`. Completeness
|
|
38
|
+
* stays a read the host makes on `quill.validate(doc)`.
|
|
36
39
|
*/
|
|
37
40
|
export declare function routeAndResolve(diagnostics: Diagnostic[] | undefined, cardIds: readonly string[]): RoutedDiagnostic[];
|
|
38
41
|
/**
|
|
39
42
|
* Merge N routed groups into `Map<fieldKeyToString(key), Diagnostic[]>`.
|
|
40
43
|
* Dedupes an identical `(severity, message)` pair landing on the same key from
|
|
41
|
-
* more than one group (e.g. the same
|
|
42
|
-
* an external feed)
|
|
44
|
+
* more than one group (e.g. the same error present in both `validate()` and
|
|
45
|
+
* an external feed). Producer order within a key.
|
|
43
46
|
*/
|
|
44
47
|
export declare function mergeDiagnostics(...groups: RoutedDiagnostic[][]): Map<string, Diagnostic[]>;
|
|
@@ -21,14 +21,17 @@ export function resolveCardKey(key, cardIds) {
|
|
|
21
21
|
/**
|
|
22
22
|
* Route a path-keyed producer's raw `Diagnostic[]` (validate / external) to the
|
|
23
23
|
* editor's stable-id keying: the one door producers #1/#3 take, `addrForFieldPath`
|
|
24
|
-
* and `resolveCardKey` in a single pass.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* and `resolveCardKey` in a single pass. Warnings do not draw (VISUAL_EDITOR
|
|
25
|
+
* §Diagnostics): obligation, completeness, a render note — none of them is a value
|
|
26
|
+
* the field cannot hold. An entry also drops rather than mis-routes when it carries
|
|
27
|
+
* no `path`, when the path names no single commit address (a nested / array-element
|
|
28
|
+
* one), or when its absolute card index is out of the live `cardIds`. Completeness
|
|
29
|
+
* stays a read the host makes on `quill.validate(doc)`.
|
|
27
30
|
*/
|
|
28
31
|
export function routeAndResolve(diagnostics, cardIds) {
|
|
29
32
|
const out = [];
|
|
30
33
|
for (const d of diagnostics ?? []) {
|
|
31
|
-
if (!d.path)
|
|
34
|
+
if (d.severity !== 'error' || !d.path)
|
|
32
35
|
continue;
|
|
33
36
|
const key = addrForFieldPath(d.path);
|
|
34
37
|
const resolved = key && resolveCardKey(key, cardIds);
|
|
@@ -37,13 +40,11 @@ export function routeAndResolve(diagnostics, cardIds) {
|
|
|
37
40
|
}
|
|
38
41
|
return out;
|
|
39
42
|
}
|
|
40
|
-
/** Severity ordering: errors sort before warnings within a field's list (never dropped, just ranked: VISUAL_EDITOR §Diagnostics "nothing gates" extends to nothing hides). */
|
|
41
|
-
const SEVERITY_RANK = { error: 0, warning: 1 };
|
|
42
43
|
/**
|
|
43
44
|
* Merge N routed groups into `Map<fieldKeyToString(key), Diagnostic[]>`.
|
|
44
45
|
* Dedupes an identical `(severity, message)` pair landing on the same key from
|
|
45
|
-
* more than one group (e.g. the same
|
|
46
|
-
* an external feed)
|
|
46
|
+
* more than one group (e.g. the same error present in both `validate()` and
|
|
47
|
+
* an external feed). Producer order within a key.
|
|
47
48
|
*/
|
|
48
49
|
export function mergeDiagnostics(...groups) {
|
|
49
50
|
const byKey = new Map();
|
|
@@ -64,8 +65,5 @@ export function mergeDiagnostics(...groups) {
|
|
|
64
65
|
byKey.set(k, [diagnostic]);
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
|
-
for (const arr of byKey.values()) {
|
|
68
|
-
arr.sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]);
|
|
69
|
-
}
|
|
70
68
|
return byKey;
|
|
71
69
|
}
|
package/dist/visual/props.d.ts
CHANGED
|
@@ -17,6 +17,11 @@ export interface VisualEditorProps {
|
|
|
17
17
|
* The active leaf: its canonical `DocPath` and the session key of the card holding
|
|
18
18
|
* it, which is what a host tracking the active card keeps across a reorder
|
|
19
19
|
* (`CardId`).
|
|
20
|
+
*
|
|
21
|
+
* It fires for a form control as for a prose leaf, which makes it the other half of
|
|
22
|
+
* the preview bridge: a control has no caret coordinate to report, so
|
|
23
|
+
* `onActiveLeafChange={preview.endFollow}` is what stops the pane following the leaf
|
|
24
|
+
* the focus left.
|
|
20
25
|
*/
|
|
21
26
|
onActiveLeafChange?: (active: ActiveLeaf) => void;
|
|
22
27
|
/**
|
|
@@ -56,7 +61,7 @@ export interface VisualEditorProps {
|
|
|
56
61
|
onError?: EditorErrorHandler;
|
|
57
62
|
/**
|
|
58
63
|
* External diagnostics, routed by `.path` and merged with `quill.validate`
|
|
59
|
-
* and local commit errors (VISUAL_EDITOR §Diagnostics).
|
|
64
|
+
* and local commit errors (VISUAL_EDITOR §Diagnostics). Warnings do not draw.
|
|
60
65
|
*/
|
|
61
66
|
diagnostics?: Diagnostic[];
|
|
62
67
|
/**
|
package/dist/visual/strings.d.ts
CHANGED
|
@@ -66,12 +66,12 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
|
|
|
66
66
|
* interpolates, keyed by name. What it can re-word tracks what that type carries per
|
|
67
67
|
* lane (VISUAL_EDITOR §Diagnostics for why each lands where it does):
|
|
68
68
|
*
|
|
69
|
-
* - **`validation::enum_violation`, `type_mismatch`, `format_violation
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
69
|
+
* - **`validation::enum_violation`, `type_mismatch`, `format_violation`**: the
|
|
70
|
+
* constraint re-words from the quill's schema at `path`, the offending value from
|
|
71
|
+
* `args` (`value` / `sourceToken` / `actual`, the engine testifying to what it
|
|
72
|
+
* saw) and never from the document at `path`: validation runs post-coercion, so
|
|
73
|
+
* the validator read a value the document does not hold and a sentence built from
|
|
74
|
+
* `path` names a spelling the user never typed.
|
|
75
75
|
* - **`edit::field_coercion_failed`**: re-words from the app's own control state, the
|
|
76
76
|
* refused value being in neither document (unchanged on throw) nor schema.
|
|
77
77
|
* - **`parse::yaml_error_with_location`, `invalid_structure`**: does not re-word. No
|
|
@@ -17,12 +17,11 @@ export interface FieldModel {
|
|
|
17
17
|
* undefined when the field declares none. Chrome-only; never gates. */
|
|
18
18
|
description: string | undefined;
|
|
19
19
|
/**
|
|
20
|
-
* Required-ness:
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* remains the unmet-ness signal. Label chrome only; never gates.
|
|
20
|
+
* Required-ness: the schema's `must_fill` obligation, which derives from
|
|
21
|
+
* `default:`'s absence when the field declares none (DOCUMENT_MODEL). Drives a
|
|
22
|
+
* persistent label `*`; a field can carry both it and a ghosted `default:`,
|
|
23
|
+
* since the two axes are independent. Persistent (schema-derived, survives
|
|
24
|
+
* filling). Label chrome only; never gates.
|
|
26
25
|
*/
|
|
27
26
|
required: boolean;
|
|
28
27
|
/** Prose-leaf flags (only meaningful when `control === 'prose'` / array items). */
|
package/dist/visual/structure.js
CHANGED
|
@@ -89,7 +89,7 @@ export function fieldModels(cardSchema) {
|
|
|
89
89
|
compact: !!schema.ui?.compact,
|
|
90
90
|
label: schema.ui?.title ?? humanize(name),
|
|
91
91
|
description: schema.description,
|
|
92
|
-
required: schema.default === undefined,
|
|
92
|
+
required: schema.must_fill ?? schema.default === undefined,
|
|
93
93
|
inline: !!schema.inline,
|
|
94
94
|
plaintext: schema.type === 'plaintext'
|
|
95
95
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quillmark/svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Editor, live-preview and source surfaces over a Quillmark WASM session.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"check": "svelte-check --tsconfig ./tsconfig.json"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
|
-
"@quillmark/wasm": ">=0.
|
|
69
|
+
"@quillmark/wasm": ">=0.105.0-0",
|
|
70
70
|
"svelte": "^5.0.0"
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|