@quillmark/svelte 0.10.1 → 0.11.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 +1 -1
- package/THEMING.md +2 -2
- package/dist/core/codec/blocks.js +21 -1
- package/dist/core/codec/breaks.d.ts +10 -2
- package/dist/core/codec/breaks.js +65 -9
- package/dist/core/codec/code.d.ts +6 -4
- package/dist/core/codec/code.js +7 -4
- package/dist/core/codec/decode.js +91 -62
- package/dist/core/codec/encode.d.ts +5 -8
- package/dist/core/codec/encode.js +55 -85
- package/dist/core/codec/field.js +43 -24
- package/dist/core/codec/index.d.ts +2 -1
- package/dist/core/codec/index.js +6 -3
- package/dist/core/codec/islands.d.ts +13 -9
- package/dist/core/codec/islands.js +75 -49
- package/dist/core/codec/keymap.js +2 -1
- package/dist/core/codec/marks.d.ts +17 -13
- package/dist/core/codec/marks.js +39 -42
- package/dist/core/codec/positions.d.ts +4 -1
- package/dist/core/codec/positions.js +5 -1
- package/dist/core/codec/reconcile.d.ts +6 -0
- package/dist/core/codec/reconcile.js +19 -0
- package/dist/core/codec/schema.d.ts +5 -8
- package/dist/core/codec/schema.js +30 -156
- package/dist/core/codec/table-view.js +77 -26
- package/dist/core/codec/table.d.ts +6 -5
- package/dist/core/codec/table.js +14 -31
- package/dist/core/codec/urls.d.ts +20 -0
- package/dist/core/codec/urls.js +45 -0
- package/dist/preset/recipes.css +1 -1
- package/dist/preset/scale.css +7 -4
- package/dist/preview/bridge.d.ts +8 -1
- package/dist/preview/bridge.js +31 -25
- package/dist/preview/controller.js +13 -13
- package/dist/preview/geometry.d.ts +12 -1
- package/dist/preview/geometry.js +14 -1
- package/dist/preview/paint.js +6 -4
- package/dist/preview/strings.d.ts +0 -2
- package/dist/preview/strings.js +5 -6
- package/dist/visual/ArrayField.svelte +11 -13
- package/dist/visual/Card.svelte +5 -8
- package/dist/visual/DateField.svelte +2 -2
- package/dist/visual/Field.svelte +7 -3
- package/dist/visual/FieldHint.svelte +1 -1
- package/dist/visual/ObjectField.svelte +6 -1
- package/dist/visual/VisualEditor.svelte +1 -1
- package/dist/visual/VisualEditorInner.svelte +65 -18
- package/dist/visual/controls.css +8 -9
- package/dist/visual/ext.d.ts +12 -13
- package/dist/visual/ext.js +22 -19
- package/dist/visual/index.js +2 -3
- package/dist/visual/links.d.ts +4 -0
- package/dist/visual/links.js +15 -4
- package/dist/visual/strings.d.ts +7 -4
- package/dist/visual/strings.js +1 -0
- package/dist/visual/structure.d.ts +9 -1
- package/dist/visual/structure.js +26 -4
- package/package.json +2 -2
package/dist/preview/bridge.d.ts
CHANGED
|
@@ -35,4 +35,11 @@ export interface VerticalSpan {
|
|
|
35
35
|
* the port: no scroll shows more of it.
|
|
36
36
|
*/
|
|
37
37
|
export declare function clearOfTheFold(target: VerticalSpan, port: VerticalSpan): boolean;
|
|
38
|
-
|
|
38
|
+
/** The trip a pane with no box could not run, held for the box coming back. One, and the
|
|
39
|
+
* last one asked for: a caret hop supersedes the hop before it on a showing pane too.
|
|
40
|
+
* A cell rather than closure state, so a bridge rebuilt under a changed page count
|
|
41
|
+
* inherits what the one it replaced was holding (`controller.ts`). */
|
|
42
|
+
export interface HeldTrip {
|
|
43
|
+
run?: () => void;
|
|
44
|
+
}
|
|
45
|
+
export declare function createBridge(session: LiveSession, container: HTMLElement, slots: readonly PageSlot[], onPick: ((at: Landing) => void) | undefined, held?: HeldTrip): BridgeController;
|
package/dist/preview/bridge.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { boxesForField, rectToPercent, clickToPdfPt, applyPercentRect } from './geometry.js';
|
|
1
|
+
import { boxesForField, rectToPercent, clickToPdfPt, pxToPt, applyPercentRect } from './geometry.js';
|
|
2
2
|
/** A pane hidden by the shell's narrow switch (`preset/recipes.css`) measures 0×0, and so
|
|
3
3
|
* does every box on it: the clearance arithmetic reads that as a caret clear of both
|
|
4
4
|
* edges, and a centring computes its trip out of zeros. Neither is an answer about where
|
|
@@ -9,6 +9,11 @@ function hasBox(port) {
|
|
|
9
9
|
/** What a rect reporting no height clears an edge by, in CSS px: a caret's own at the
|
|
10
10
|
* smallest zoom a page still reads at. */
|
|
11
11
|
const MIN_CLEARANCE = 8;
|
|
12
|
+
/** What a click may miss a field's ink by and still land on it, in CSS px of the page as
|
|
13
|
+
* drawn, so it converts per click at the scale the slot is showing. Sized as the
|
|
14
|
+
* pointer's imprecision and not as the leading it partly closes, which is a trade
|
|
15
|
+
* PREVIEW.md §"Click bridge" carries. */
|
|
16
|
+
const CLICK_SLACK_PX = 2;
|
|
12
17
|
/**
|
|
13
18
|
* Whether `target` sits clear of `port`'s edges by the fold's clearance, which is the
|
|
14
19
|
* target's own height: a caret rect flush against an edge is visible and unusable, and
|
|
@@ -29,14 +34,15 @@ export function clearOfTheFold(target, port) {
|
|
|
29
34
|
const clearance = Math.min(Math.max(target.height, MIN_CLEARANCE), (port.height - target.height) / 3);
|
|
30
35
|
return target.top - clearance >= port.top && target.bottom + clearance <= port.bottom;
|
|
31
36
|
}
|
|
32
|
-
export function createBridge(session, container, slots, onPick) {
|
|
37
|
+
export function createBridge(session, container, slots, onPick, held = {}) {
|
|
33
38
|
const unlisten = [];
|
|
34
39
|
// Two rungs: `positionAt` over span-tracked content, `fieldAt` over every placement
|
|
35
40
|
// the compile tracks, a strict superset of it (PREVIEW.md §"Click bridge" carries
|
|
36
41
|
// the measurement). The second fires where a field is placed without its content
|
|
37
42
|
// being tracked, and the pick carries no `pos` there: a fabricated `0` would be an
|
|
38
43
|
// invented offset wearing a real one's type. No third rung hit-tests `regions()`,
|
|
39
|
-
// whose rects bound ink the field does not fill
|
|
44
|
+
// whose rects bound ink the field does not fill; the slack both rungs carry is what
|
|
45
|
+
// that rung would have been reached for.
|
|
40
46
|
if (onPick) {
|
|
41
47
|
for (const { page, el } of slots) {
|
|
42
48
|
const handleClick = (ev) => {
|
|
@@ -50,10 +56,11 @@ export function createBridge(session, container, slots, onPick) {
|
|
|
50
56
|
const px = ev.clientX - box.left;
|
|
51
57
|
const py = ev.clientY - box.top;
|
|
52
58
|
const pt = clickToPdfPt(px, py, box.width, box.height, slot.size);
|
|
53
|
-
const
|
|
59
|
+
const tol = pxToPt(CLICK_SLACK_PX, box.width, slot.size);
|
|
60
|
+
const hit = session.positionAt(slot.page, pt.x, pt.y, tol);
|
|
54
61
|
if (hit)
|
|
55
62
|
return void onPick(hit);
|
|
56
|
-
const field = session.fieldAt(slot.page, pt.x, pt.y);
|
|
63
|
+
const field = session.fieldAt(slot.page, pt.x, pt.y, tol);
|
|
57
64
|
if (field)
|
|
58
65
|
onPick({ field });
|
|
59
66
|
};
|
|
@@ -72,52 +79,50 @@ export function createBridge(session, container, slots, onPick) {
|
|
|
72
79
|
marker.remove();
|
|
73
80
|
return rect;
|
|
74
81
|
}
|
|
75
|
-
// Centre `target` vertically in the scrollport; the cross axis takes the
|
|
76
|
-
// trip instead, since the container is a vertical scroller and only a page
|
|
77
|
-
// than the port has anything to move there.
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
// Centre `target` vertically in `port`, the scrollport's box; the cross axis takes the
|
|
83
|
+
// minimum trip instead, since the container is a vertical scroller and only a page
|
|
84
|
+
// wider than the port has anything to move there. `port` is the caller's own read: the
|
|
85
|
+
// marker `measure` places is absolute inside the slot, so nothing between that read
|
|
86
|
+
// and this one moves the container's box, and one forced layout serves the whole hop.
|
|
87
|
+
function centre(target, port) {
|
|
80
88
|
container.scrollTop += target.top + target.height / 2 - (port.top + port.height / 2);
|
|
81
89
|
if (target.left < port.left)
|
|
82
90
|
container.scrollLeft += target.left - port.left;
|
|
83
91
|
else if (target.right > port.right)
|
|
84
92
|
container.scrollLeft += target.right - port.right;
|
|
85
93
|
}
|
|
86
|
-
// The trip a pane with no box could not run, held for the box coming back. One, and
|
|
87
|
-
// the last one asked for: a caret hop supersedes the hop before it on a showing pane
|
|
88
|
-
// too.
|
|
89
|
-
let held;
|
|
90
94
|
// The re-assert. Nothing else asks on the way back: `refresh` re-asks only on a
|
|
91
95
|
// recompile, and the reader who has just tapped Preview is not typing, so the pane
|
|
92
96
|
// would open where the caret was several edits ago. Guarded for jsdom, which ships no
|
|
93
97
|
// ResizeObserver and lays out nothing for one to report.
|
|
94
98
|
if (typeof ResizeObserver !== 'undefined') {
|
|
95
99
|
const observer = new ResizeObserver(() => {
|
|
96
|
-
if (!held || !hasBox(container.getBoundingClientRect()))
|
|
100
|
+
if (!held.run || !hasBox(container.getBoundingClientRect()))
|
|
97
101
|
return;
|
|
98
|
-
const trip = held;
|
|
99
|
-
held = undefined;
|
|
102
|
+
const trip = held.run;
|
|
103
|
+
held.run = undefined;
|
|
100
104
|
trip();
|
|
101
105
|
});
|
|
102
106
|
observer.observe(container);
|
|
103
107
|
unlisten.push(() => observer.disconnect());
|
|
104
108
|
}
|
|
105
109
|
function scrollToField(field) {
|
|
106
|
-
const box = boxesForField(field, session.fieldBoxes(field), session.regions())[0];
|
|
110
|
+
const box = boxesForField(field, session.fieldBoxes(field), () => session.regions())[0];
|
|
107
111
|
if (!box)
|
|
108
112
|
return false;
|
|
109
113
|
const slot = slots[box.page];
|
|
110
114
|
if (!slot)
|
|
111
115
|
return false;
|
|
116
|
+
const port = container.getBoundingClientRect();
|
|
112
117
|
// The address is placed, which is the whole of what the `boolean` answers; a pane
|
|
113
118
|
// with no box yet is not a second no.
|
|
114
|
-
if (!hasBox(
|
|
115
|
-
held = () => void scrollToField(field);
|
|
119
|
+
if (!hasBox(port)) {
|
|
120
|
+
held.run = () => void scrollToField(field);
|
|
116
121
|
return true;
|
|
117
122
|
}
|
|
118
123
|
// A discrete act ("show me this field"), so it centres every time, unlike
|
|
119
124
|
// the continuous hop below.
|
|
120
|
-
centre(measure(slot, rectToPercent(box.rect, slot.size)));
|
|
125
|
+
centre(measure(slot, rectToPercent(box.rect, slot.size)), port);
|
|
121
126
|
return true;
|
|
122
127
|
}
|
|
123
128
|
function focusPosition(field, pos) {
|
|
@@ -131,8 +136,9 @@ export function createBridge(session, container, slots, onPick) {
|
|
|
131
136
|
// for every keystroke of (`hasBox`): the follow would die silently for as long as
|
|
132
137
|
// a track is hidden. What is held is the request, not the trip, so it re-locates
|
|
133
138
|
// against whatever compile is current when the pane comes back.
|
|
134
|
-
|
|
135
|
-
|
|
139
|
+
const port = container.getBoundingClientRect();
|
|
140
|
+
if (!hasBox(port)) {
|
|
141
|
+
held.run = () => focusPosition(field, pos);
|
|
136
142
|
return;
|
|
137
143
|
}
|
|
138
144
|
// The continuous hop: one call per keystroke and per arrow key, so it moves
|
|
@@ -140,9 +146,9 @@ export function createBridge(session, container, slots, onPick) {
|
|
|
140
146
|
// takes the scrollport back from the user on every one of them, including
|
|
141
147
|
// the ones that changed nothing about where the caret already was.
|
|
142
148
|
const target = measure(slot, rectToPercent(region.rect, slot.size));
|
|
143
|
-
if (clearOfTheFold(target,
|
|
149
|
+
if (clearOfTheFold(target, port))
|
|
144
150
|
return;
|
|
145
|
-
centre(target);
|
|
151
|
+
centre(target, port);
|
|
146
152
|
}
|
|
147
153
|
return {
|
|
148
154
|
scrollToField,
|
|
@@ -7,14 +7,13 @@ const CONTAINER_CLASS = 'qm-preview';
|
|
|
7
7
|
// class, the hooks a consumer and the tests target.
|
|
8
8
|
const MESSAGE_CLASS = 'qm-preview-message';
|
|
9
9
|
const EMPTY_CLASS = 'qm-preview-empty';
|
|
10
|
-
const UNSUPPORTED_CLASS = 'qm-preview-unsupported';
|
|
11
10
|
const ERROR_CLASS = 'qm-preview-error';
|
|
12
11
|
export function createPreview(session, opts) {
|
|
13
12
|
const container = opts.container;
|
|
14
13
|
const margin = opts.margin ?? 1;
|
|
15
14
|
const t = mergePreviewStrings(opts.strings);
|
|
16
15
|
container.classList.add(CONTAINER_CLASS);
|
|
17
|
-
// One element, restamped: the non-paint states (empty,
|
|
16
|
+
// One element, restamped: the non-paint states (empty, a paint that
|
|
18
17
|
// threw) are mutually exclusive.
|
|
19
18
|
let message;
|
|
20
19
|
function showMessage(text, state) {
|
|
@@ -33,8 +32,8 @@ export function createPreview(session, opts) {
|
|
|
33
32
|
// slots and never calls the `paint`/`pageSize` verbs the boundary refuses
|
|
34
33
|
// there. A paint that unexpectedly throws is caught per-slot and surfaced
|
|
35
34
|
// through the shared message rather than aborting the observer callback
|
|
36
|
-
// mid-sweep (
|
|
37
|
-
//
|
|
35
|
+
// mid-sweep (a paint is the one surface that can still refuse a counted page;
|
|
36
|
+
// brittle to leave uncaught).
|
|
38
37
|
const paintLoop = createPaintLoop(session, container, margin, (page, err) => {
|
|
39
38
|
reportError(opts.onError, {
|
|
40
39
|
code: 'paint-failed',
|
|
@@ -50,24 +49,25 @@ export function createPreview(session, opts) {
|
|
|
50
49
|
// slots array is a stable identity but its members are swapped on reconcile, and
|
|
51
50
|
// the listeners are per slot.
|
|
52
51
|
let bridge;
|
|
52
|
+
// Outlives that rebuild, so a trip held for a pane with no box crosses it. Only the
|
|
53
|
+
// follow is re-asserted after a recompile, and a `scrollToField` the pane could not
|
|
54
|
+
// run has nothing else that would ask again for it.
|
|
55
|
+
const held = {};
|
|
53
56
|
function attach() {
|
|
54
|
-
bridge = createBridge(session, container, paintLoop.slots, opts.onPick);
|
|
57
|
+
bridge = createBridge(session, container, paintLoop.slots, opts.onPick, held);
|
|
55
58
|
}
|
|
56
59
|
function detach() {
|
|
57
60
|
bridge?.destroy();
|
|
58
61
|
bridge = undefined;
|
|
59
62
|
}
|
|
60
|
-
// Called at construction and after every `apply`, so
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// capability generally, not just the page count.
|
|
63
|
+
// Called at construction and after every `apply`, so a 0-page compile that later
|
|
64
|
+
// gains pages recovers. A counted page is the whole condition: every backend
|
|
65
|
+
// paints, and `paint`/`pageSize` refuse only a page the count excludes.
|
|
64
66
|
function render(pageCount, dirtyPages) {
|
|
65
|
-
if (
|
|
66
|
-
// A 0-page compile is a recoverable empty; pages the boundary cannot
|
|
67
|
-
// raster are a genuine unsupported.
|
|
67
|
+
if (pageCount === 0) {
|
|
68
68
|
paintLoop.refresh([], 0);
|
|
69
69
|
detach();
|
|
70
|
-
showMessage(
|
|
70
|
+
showMessage(t.noPages, EMPTY_CLASS);
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
73
|
// Read the count against the current slots before reconcile moves it, and
|
|
@@ -27,8 +27,11 @@ export interface PdfPoint {
|
|
|
27
27
|
* boundary character (`.` for a nested field, `[` for an array element) keeps the
|
|
28
28
|
* prefix a path boundary: `main.keywords` matches `main.keywords[0]`, not
|
|
29
29
|
* `main.keywords_note`.
|
|
30
|
+
*
|
|
31
|
+
* `regions` is a thunk because the first rung answers without it, and taking it
|
|
32
|
+
* marshals every placement in the compile across the WASM boundary.
|
|
30
33
|
*/
|
|
31
|
-
export declare function boxesForField(field: string, boxes: readonly FieldRegion[], regions: readonly FieldRegion[]): readonly FieldRegion[];
|
|
34
|
+
export declare function boxesForField(field: string, boxes: readonly FieldRegion[], regions: () => readonly FieldRegion[]): readonly FieldRegion[];
|
|
32
35
|
/**
|
|
33
36
|
* Forward: a PDF-pt rect -> CSS % of the page box. Percent positioning tracks
|
|
34
37
|
* the page's displayed size across DPR and container resize for free, so the
|
|
@@ -41,6 +44,14 @@ export declare function rectToPercent(rect: PdfRect, pageSize: PageSize): Percen
|
|
|
41
44
|
* the transform that produces it rather than at the caller placing a marker.
|
|
42
45
|
*/
|
|
43
46
|
export declare function applyPercentRect(el: HTMLElement, pct: PercentRect): void;
|
|
47
|
+
/**
|
|
48
|
+
* A CSS-px length on a page element of CSS width `cssW` -> the PDF-pt length it spans.
|
|
49
|
+
* Width alone, because that is the scale the slot is drawn at: `paint.ts` sizes a slot
|
|
50
|
+
* by width under a locked aspect ratio, so {@link clickToPdfPt}'s two ratios are one
|
|
51
|
+
* number and a length has one to convert through. What the click bridge reads its
|
|
52
|
+
* pointer slack in points through.
|
|
53
|
+
*/
|
|
54
|
+
export declare function pxToPt(px: number, cssW: number, pageSize: PageSize): number;
|
|
44
55
|
/**
|
|
45
56
|
* Inverse: a click at CSS px `(px, py)` within a page element of CSS size
|
|
46
57
|
* `(cssW, cssH)` -> PDF-pt. The exact inverse of {@link rectToPercent}: the
|
package/dist/preview/geometry.js
CHANGED
|
@@ -12,11 +12,14 @@
|
|
|
12
12
|
* boundary character (`.` for a nested field, `[` for an array element) keeps the
|
|
13
13
|
* prefix a path boundary: `main.keywords` matches `main.keywords[0]`, not
|
|
14
14
|
* `main.keywords_note`.
|
|
15
|
+
*
|
|
16
|
+
* `regions` is a thunk because the first rung answers without it, and taking it
|
|
17
|
+
* marshals every placement in the compile across the WASM boundary.
|
|
15
18
|
*/
|
|
16
19
|
export function boxesForField(field, boxes, regions) {
|
|
17
20
|
if (boxes.length)
|
|
18
21
|
return boxes;
|
|
19
|
-
return regions.filter((r) => r.field === field || r.field.startsWith(`${field}.`) || r.field.startsWith(`${field}[`));
|
|
22
|
+
return regions().filter((r) => r.field === field || r.field.startsWith(`${field}.`) || r.field.startsWith(`${field}[`));
|
|
20
23
|
}
|
|
21
24
|
/**
|
|
22
25
|
* Forward: a PDF-pt rect -> CSS % of the page box. Percent positioning tracks
|
|
@@ -47,6 +50,16 @@ export function applyPercentRect(el, pct) {
|
|
|
47
50
|
height: `${pct.height}%`
|
|
48
51
|
});
|
|
49
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* A CSS-px length on a page element of CSS width `cssW` -> the PDF-pt length it spans.
|
|
55
|
+
* Width alone, because that is the scale the slot is drawn at: `paint.ts` sizes a slot
|
|
56
|
+
* by width under a locked aspect ratio, so {@link clickToPdfPt}'s two ratios are one
|
|
57
|
+
* number and a length has one to convert through. What the click bridge reads its
|
|
58
|
+
* pointer slack in points through.
|
|
59
|
+
*/
|
|
60
|
+
export function pxToPt(px, cssW, pageSize) {
|
|
61
|
+
return (px / cssW) * pageSize.widthPt;
|
|
62
|
+
}
|
|
50
63
|
/**
|
|
51
64
|
* Inverse: a click at CSS px `(px, py)` within a page element of CSS size
|
|
52
65
|
* `(cssW, cssH)` -> PDF-pt. The exact inverse of {@link rectToPercent}: the
|
package/dist/preview/paint.js
CHANGED
|
@@ -41,7 +41,11 @@ onPaintError) {
|
|
|
41
41
|
// responsibilities"). `border-box` costs nothing here and keeps a slot a
|
|
42
42
|
// consumer insets from measuring wider than its paper.
|
|
43
43
|
const el = document.createElement('div');
|
|
44
|
-
|
|
44
|
+
// Not `.qm-page`: that name is the preset's page baseline, an unlayered rule at
|
|
45
|
+
// ordinary specificity a host puts on `<body>`, and a slot wearing it takes that
|
|
46
|
+
// rule's `color-scheme` inside a mounted root, which the derivation leaves to the
|
|
47
|
+
// host (`core/theme.css`).
|
|
48
|
+
el.className = 'qm-page-slot';
|
|
45
49
|
// The page's index, in the DOM. `pageByEl` is this module's and a consumer
|
|
46
50
|
// drawing an overlay of its own needs the number: position among siblings is
|
|
47
51
|
// the only handle without it, which is right today and is not a contract.
|
|
@@ -157,9 +161,7 @@ onPaintError) {
|
|
|
157
161
|
}
|
|
158
162
|
// Slots are built on the first `refresh` (the controller calls it immediately),
|
|
159
163
|
// not here: reconcile → `session.pageSize` must run after the controller's
|
|
160
|
-
// `
|
|
161
|
-
// pages would throw on `pageSize` (runtime.d.ts: it succeeds iff `supportsCanvas`)
|
|
162
|
-
// before the controller can surface its "unsupported" message.
|
|
164
|
+
// `pageCount` gate, since `pageSize` throws for a page the count excludes.
|
|
163
165
|
// ── Keep mounted rasters in step with the display ───────────────────────────
|
|
164
166
|
// Every paint freezes `canvas.style.width/height` to the box width at that
|
|
165
167
|
// paint (paintSlot). A page that stays mounted while the container's CSS width
|
|
@@ -3,8 +3,6 @@ export interface PreviewStrings {
|
|
|
3
3
|
renderFailed: string;
|
|
4
4
|
/** A compile with zero pages: recoverable, and the commonest state of a fresh seed. */
|
|
5
5
|
noPages: string;
|
|
6
|
-
/** A compile whose pages this backend cannot raster at all. */
|
|
7
|
-
unsupported: string;
|
|
8
6
|
}
|
|
9
7
|
/** The package's English. Every key, so the merge below is total. */
|
|
10
8
|
export declare const DEFAULT_PREVIEW_STRINGS: PreviewStrings;
|
package/dist/preview/strings.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Every word the preview says
|
|
2
|
-
//
|
|
1
|
+
// Every word the preview says: the surface is a canvas, and its only prose is the
|
|
2
|
+
// message states that stand in when there is nothing to paint.
|
|
3
3
|
//
|
|
4
4
|
// Separate from the visual surface's set rather than pooled with it: `/preview`
|
|
5
5
|
// reaches `/core` and nothing editor-side, which is what lets it promote to its own
|
|
@@ -8,13 +8,12 @@
|
|
|
8
8
|
// twice rather than learning two idioms.
|
|
9
9
|
//
|
|
10
10
|
// A plain options field rather than a context channel, unlike the visual surface's:
|
|
11
|
-
//
|
|
12
|
-
//
|
|
11
|
+
// they are read in one module by one function, so there is no tree to make them
|
|
12
|
+
// ambient for.
|
|
13
13
|
/** The package's English. Every key, so the merge below is total. */
|
|
14
14
|
export const DEFAULT_PREVIEW_STRINGS = {
|
|
15
15
|
renderFailed: 'Preview failed to render.',
|
|
16
|
-
noPages: 'No pages to preview.'
|
|
17
|
-
unsupported: 'Preview is not available for this document.'
|
|
16
|
+
noPages: 'No pages to preview.'
|
|
18
17
|
};
|
|
19
18
|
/** Fill a partial override to the whole key set. */
|
|
20
19
|
export function mergePreviewStrings(custom) {
|
|
@@ -147,7 +147,6 @@
|
|
|
147
147
|
// element shapes register alike.
|
|
148
148
|
const rowEls: Record<string, HTMLElement | undefined> = $state({});
|
|
149
149
|
let addEl: HTMLButtonElement | undefined = $state();
|
|
150
|
-
let rootEl: HTMLElement | undefined = $state();
|
|
151
150
|
let rowsEl: HTMLElement | undefined = $state();
|
|
152
151
|
|
|
153
152
|
// ── Object elements: one open at a time ──────────────────────────────────────
|
|
@@ -268,8 +267,8 @@
|
|
|
268
267
|
* compile the document has moved past.
|
|
269
268
|
*
|
|
270
269
|
* An absent `pos` is the placement rung, exactly as on `Landing`. A row that takes
|
|
271
|
-
* no offset gets the bare focus:
|
|
272
|
-
* to spend one in
|
|
270
|
+
* no offset gets the bare focus: a `string` element, whose input has no coordinate
|
|
271
|
+
* to spend one in and which the compile never addresses.
|
|
273
272
|
*
|
|
274
273
|
* The row's box comes back with the landing: the arrival wash is the address's own
|
|
275
274
|
* granularity, and one row is what an element address named. A fallback to the
|
|
@@ -325,8 +324,8 @@
|
|
|
325
324
|
/**
|
|
326
325
|
* The element keyboard contract. Both keys ride the element control's own keydown
|
|
327
326
|
* (the input's, or the PM view's through `handleDOMEvents`) since neither
|
|
328
|
-
* surface is a place a keymap of this component's could sit.
|
|
329
|
-
*
|
|
327
|
+
* surface is a place a keymap of this component's could sit. An `object` element
|
|
328
|
+
* wires neither: its row is a summary button, whose Enter is the disclosure's.
|
|
330
329
|
*/
|
|
331
330
|
function onElementKey(e: KeyboardEvent, k: number): void {
|
|
332
331
|
if (control === 'object' || e.isComposing) return;
|
|
@@ -345,7 +344,6 @@
|
|
|
345
344
|
</script>
|
|
346
345
|
|
|
347
346
|
<div
|
|
348
|
-
bind:this={rootEl}
|
|
349
347
|
class="qm-array"
|
|
350
348
|
class:empty={ids.length === 0}
|
|
351
349
|
role="group"
|
|
@@ -484,10 +482,10 @@
|
|
|
484
482
|
.qm-array-rows.empty {
|
|
485
483
|
display: none;
|
|
486
484
|
}
|
|
487
|
-
/* A grid rather than a block: an `<input>`
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
485
|
+
/* A grid rather than a block: an `<input>` is inline-level and would sit on a
|
|
486
|
+
baseline, standing the row a descender taller than the box the slab measures itself
|
|
487
|
+
against. `minmax(0, …)` because a long unbroken value grows an `auto` track, and the
|
|
488
|
+
edge with it. */
|
|
491
489
|
.qm-array-row {
|
|
492
490
|
position: relative;
|
|
493
491
|
display: grid;
|
|
@@ -498,9 +496,9 @@
|
|
|
498
496
|
border-radius: var(--_qm-radius-inner);
|
|
499
497
|
}
|
|
500
498
|
/* The end inset the slab stands in, taken off whichever box the element drew:
|
|
501
|
-
`.qm-input` is the text element
|
|
502
|
-
|
|
503
|
-
class stops at this component's. The longhand beats the family's `padding` shorthand
|
|
499
|
+
`.qm-input` is the text element, `.qm-control-box` the prose one and an object row's
|
|
500
|
+
summary. `:global`, because the box belongs to the child component's markup and the
|
|
501
|
+
scope class stops at this component's. The longhand beats the family's `padding` shorthand
|
|
504
502
|
without a specificity fight: this block is unlayered and `controls.css` is not. */
|
|
505
503
|
.qm-array-row :global(.qm-input),
|
|
506
504
|
.qm-array-row :global(.qm-control-box) {
|
package/dist/visual/Card.svelte
CHANGED
|
@@ -315,7 +315,7 @@
|
|
|
315
315
|
{quill}
|
|
316
316
|
addr={ops.makeAddr(undefined)}
|
|
317
317
|
unframed
|
|
318
|
-
label=
|
|
318
|
+
label={t.strings.bodyLabel}
|
|
319
319
|
placeholder={card.bodyGhost}
|
|
320
320
|
leafKey={ops.leafKey(undefined)}
|
|
321
321
|
{onFocus}
|
|
@@ -359,9 +359,9 @@
|
|
|
359
359
|
{/snippet}
|
|
360
360
|
|
|
361
361
|
<!-- A card whose `kind` has no schema stays visible and removable, its fields, body and
|
|
362
|
-
`$ext` intact in the Document, so it is never a data trap.
|
|
363
|
-
|
|
364
|
-
|
|
362
|
+
`$ext` intact in the Document, so it is never a data trap. A retype swaps the kind and
|
|
363
|
+
leaves payload and body untouched, so it drops no field; with no kinds to offer, delete
|
|
364
|
+
is the only exit. -->
|
|
365
365
|
{#snippet recoveryShell()}
|
|
366
366
|
<header class="qm-card-header">
|
|
367
367
|
<span class="qm-card-title-static">{humanize(card.kind)}</span>
|
|
@@ -393,7 +393,7 @@
|
|
|
393
393
|
</select>
|
|
394
394
|
</label>
|
|
395
395
|
{:else}
|
|
396
|
-
<p class="qm-recovery-note
|
|
396
|
+
<p class="qm-recovery-note">
|
|
397
397
|
{t.strings.noCardKinds}
|
|
398
398
|
</p>
|
|
399
399
|
{/if}
|
|
@@ -780,9 +780,6 @@
|
|
|
780
780
|
font-size: var(--_qm-text-label);
|
|
781
781
|
color: var(--_qm-ink-label);
|
|
782
782
|
}
|
|
783
|
-
.qm-recovery-muted {
|
|
784
|
-
color: var(--_qm-ink-label);
|
|
785
|
-
}
|
|
786
783
|
.qm-recovery-retype {
|
|
787
784
|
display: inline-flex;
|
|
788
785
|
align-items: center;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
calendar) rather than `DatePicker`: the segments are the entry affordance, and a
|
|
12
12
|
calendar is a second surface this field does not need.
|
|
13
13
|
|
|
14
|
-
The boundary is
|
|
14
|
+
The boundary is a string, and the local is too. The primitive speaks
|
|
15
15
|
`CalendarDate`; the document speaks `YYYY-MM-DD`. `CalendarDate` carries no time
|
|
16
16
|
and no zone, so the round-trip is lossless and no local-midnight shift can occur:
|
|
17
17
|
the hazard that makes `new Date('2026-07-25')` the wrong tool here. Authored
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
parsed value: a fresh `CalendarDate` is never `===` the last one, which would
|
|
23
23
|
make every reconcile fire and re-render all seven segments on each commit.
|
|
24
24
|
|
|
25
|
-
The ghost is the default'
|
|
25
|
+
The ghost is the default's digits, not a format hint. An unset field
|
|
26
26
|
carrying a `default:` prints the default's digits in the segments, ghost-toned,
|
|
27
27
|
instead of the primitive's `mm`/`dd`/`yyyy` hints, which say "empty" where the
|
|
28
28
|
rung says "will render 2026-01-01". The ghost is painted in the segment snippet,
|
package/dist/visual/Field.svelte
CHANGED
|
@@ -128,10 +128,13 @@
|
|
|
128
128
|
* array's elements, an object's properties, a variant's cells, and a subform's cells
|
|
129
129
|
* under an open element. One door rather than one per depth, since `getContentAt`
|
|
130
130
|
* takes the whole path and the walk down is each container prefixing its own step.
|
|
131
|
-
*
|
|
132
|
-
*
|
|
131
|
+
*
|
|
132
|
+
* The reader is held, as the codec's is (`field.ts`): a `{quill, doc}` pair reads
|
|
133
|
+
* live, so every call through it sees the commit before it, and an array of prose
|
|
134
|
+
* elements is one handle across the boundary rather than one per element. */
|
|
135
|
+
const reader = $derived(quill.reader(doc));
|
|
133
136
|
function contentAt(path: PathStep[]): Content | undefined {
|
|
134
|
-
return
|
|
137
|
+
return reader.getContentAt(addr, path);
|
|
135
138
|
}
|
|
136
139
|
|
|
137
140
|
// ── Focus: one answer, two callers (`leaves.ts`) ─────────────────────────────
|
|
@@ -239,6 +242,7 @@
|
|
|
239
242
|
{addr}
|
|
240
243
|
inline={field.inline}
|
|
241
244
|
plaintext={field.plaintext}
|
|
245
|
+
placeholder={defaultStr}
|
|
242
246
|
labelledBy={domIds.label}
|
|
243
247
|
{describedBy}
|
|
244
248
|
{leafKey}
|
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
}}><Icon name="info" size={13} /></button
|
|
132
132
|
>
|
|
133
133
|
|
|
134
|
-
<!-- Placement is the description'
|
|
134
|
+
<!-- Placement is the description's height. One line rides the label rung beside its own
|
|
135
135
|
glyph, where the overhang past the rung's line box falls in the label-to-control gaps.
|
|
136
136
|
Anything taller stands over the field: beside the glyph it would hang onto the control
|
|
137
137
|
it describes, the one overlap a reader cannot clear by reaching for what is hidden,
|
|
@@ -122,7 +122,11 @@
|
|
|
122
122
|
prose.focus();
|
|
123
123
|
return true;
|
|
124
124
|
}
|
|
125
|
-
|
|
125
|
+
// Past the label row: a property carrying a `description` opens with the hint
|
|
126
|
+
// trigger, which is focusable and stands before the control it describes.
|
|
127
|
+
const el = [...cell.querySelectorAll<HTMLElement>(FOCUSABLE)].find(
|
|
128
|
+
(n) => !n.closest('.qm-field-label-row')
|
|
129
|
+
);
|
|
126
130
|
el?.focus();
|
|
127
131
|
return !!el;
|
|
128
132
|
}
|
|
@@ -205,6 +209,7 @@
|
|
|
205
209
|
labelledBy={ids?.label}
|
|
206
210
|
describedBy={describes}
|
|
207
211
|
value={obj[key] as string | undefined}
|
|
212
|
+
fallback={sub.default != null ? String(sub.default) : undefined}
|
|
208
213
|
onCommit={(v) => commitProp(key, v)}
|
|
209
214
|
/>
|
|
210
215
|
{:else if kind === 'text'}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
`doc` prop remounts the editor under it, so a consumer holding one mount across
|
|
4
4
|
two documents gets a surface that names the live handle.
|
|
5
5
|
|
|
6
|
-
It is
|
|
6
|
+
It is a `{#key}`, not a reseed. The state a swap invalidates is not one field:
|
|
7
7
|
composable cards key on session id, the main card is keyed on nothing, and each
|
|
8
8
|
prose leaf mounts once per stable leaf key with `createField` closing over the
|
|
9
9
|
`doc` it mounted against — so after an in-place swap the main card's leaves
|