@quillmark/svelte 0.9.0 → 0.10.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/THEMING.md +2 -0
- package/dist/core/codec/decode.js +3 -4
- package/dist/core/codec/encode.js +9 -11
- package/dist/core/codec/prose.css +10 -0
- package/dist/core/codec/slash.js +0 -0
- package/dist/preview/Preview.svelte +6 -0
- package/dist/visual/ArrayField.svelte +38 -19
- package/dist/visual/ArrayField.svelte.d.ts +1 -1
- package/dist/visual/Field.svelte +8 -5
- package/dist/visual/VisualEditorInner.svelte +26 -11
- package/dist/visual/leaves.d.ts +6 -1
- package/package.json +2 -2
package/THEMING.md
CHANGED
|
@@ -24,6 +24,8 @@ Nothing to import and nothing to set. The package pulls its own stylesheet, and
|
|
|
24
24
|
|
|
25
25
|
**Placing and sizing**: each surface carries a stable root class (`.qm-editor`, `.qm-preview`), so you can place it from your own stylesheet without a wrapper. Each also takes a `class` prop that merges onto the same element, which is the better handle when you have one to give.
|
|
26
26
|
|
|
27
|
+
A surface's width is the one you give it. Each contains its own inline size, so what a document holds — a table is as wide as its columns — scrolls inside the mount rather than widening the column the mount stands in.
|
|
28
|
+
|
|
27
29
|
**Taking the column back**: our selectors compute at specificity zero (below), so `padding: 0` or a `background` of your own on `.qm-editor` wins outright. Nothing is locked.
|
|
28
30
|
|
|
29
31
|
## Make it yours
|
|
@@ -103,8 +103,7 @@ function groupBlocks(schema, leaves, depth, marks, cursor) {
|
|
|
103
103
|
if (isListItemContainer(here)) {
|
|
104
104
|
// Gather the maximal run of sibling `list_item` leaves at this depth
|
|
105
105
|
// (same ordered/start), then split it into items by `ordinal`.
|
|
106
|
-
const { ordered, start } = here;
|
|
107
|
-
const instance = here.instance ?? 0;
|
|
106
|
+
const { ordered, start, instance } = here;
|
|
108
107
|
let j = i + 1;
|
|
109
108
|
while (j < leaves.length) {
|
|
110
109
|
const c = atDepth(leaves[j], depth);
|
|
@@ -112,7 +111,7 @@ function groupBlocks(schema, leaves, depth, marks, cursor) {
|
|
|
112
111
|
break;
|
|
113
112
|
// `instance` is the boundary between two adjacent lists; the normalizer
|
|
114
113
|
// numbers a run's `ordinal`s gaplessly from 0, so a reset carries none.
|
|
115
|
-
if (
|
|
114
|
+
if (c.instance !== instance)
|
|
116
115
|
break;
|
|
117
116
|
j++;
|
|
118
117
|
}
|
|
@@ -166,7 +165,7 @@ function ordinalAt(leaf, depth) {
|
|
|
166
165
|
* shape, which contiguity alone reads as one. Every arm but `list_item` (which has its
|
|
167
166
|
* own run rule) keys here. */
|
|
168
167
|
function containerKey(c) {
|
|
169
|
-
const id = `${c.container}\u0000${c.instance
|
|
168
|
+
const id = `${c.container}\u0000${c.instance}`;
|
|
170
169
|
return 'attrs' in c ? `${id}\u0000${JSON.stringify(c.attrs)}` : id;
|
|
171
170
|
}
|
|
172
171
|
/** A single leaf block node (para/heading/code/rule/island, or an unknown kind
|
|
@@ -69,11 +69,6 @@ function runShape(node) {
|
|
|
69
69
|
return null;
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
/** `instance` where it tells a container from an identical adjacent sibling. The
|
|
73
|
-
* canonical form omits a zero, so emitting one would diff against every store read. */
|
|
74
|
-
function discriminate(container, instance) {
|
|
75
|
-
return instance ? { ...container, instance } : container;
|
|
76
|
-
}
|
|
77
72
|
function scanBlock(acc, node, nodePos, containers, instance) {
|
|
78
73
|
const name = node.type.name;
|
|
79
74
|
const contentStart = nodePos + 1;
|
|
@@ -115,15 +110,12 @@ function scanBlock(acc, node, nodePos, containers, instance) {
|
|
|
115
110
|
acc.lastContentEndPm = nodePos + 1;
|
|
116
111
|
break;
|
|
117
112
|
case 'blockquote':
|
|
118
|
-
scanBlocks(acc, node, contentStart, [
|
|
119
|
-
...containers,
|
|
120
|
-
discriminate({ container: 'quote' }, instance)
|
|
121
|
-
]);
|
|
113
|
+
scanBlocks(acc, node, contentStart, [...containers, { container: 'quote', instance }]);
|
|
122
114
|
break;
|
|
123
115
|
case 'unknown_container':
|
|
124
116
|
scanBlocks(acc, node, contentStart, [
|
|
125
117
|
...containers,
|
|
126
|
-
|
|
118
|
+
{ container: node.attrs.container, attrs: node.attrs.attrs, instance }
|
|
127
119
|
]);
|
|
128
120
|
break;
|
|
129
121
|
case 'bullet_list':
|
|
@@ -132,7 +124,13 @@ function scanBlock(acc, node, nodePos, containers, instance) {
|
|
|
132
124
|
const start = ordered ? node.attrs.start : 1;
|
|
133
125
|
let itemPos = contentStart;
|
|
134
126
|
node.forEach((item, _off, index) => {
|
|
135
|
-
const container =
|
|
127
|
+
const container = {
|
|
128
|
+
container: 'list_item',
|
|
129
|
+
ordered,
|
|
130
|
+
start,
|
|
131
|
+
ordinal: index,
|
|
132
|
+
instance
|
|
133
|
+
};
|
|
136
134
|
scanBlocks(acc, item, itemPos + 1, [...containers, container]);
|
|
137
135
|
itemPos += item.nodeSize;
|
|
138
136
|
});
|
|
@@ -228,6 +228,16 @@ li.ProseMirror-selectednode::after {
|
|
|
228
228
|
.ProseMirror > .qm-table-island + * {
|
|
229
229
|
margin-top: 0;
|
|
230
230
|
}
|
|
231
|
+
/* Width from above, never from the columns. The scroller below caps at `100%` of this
|
|
232
|
+
box, and a percentage is no cap while an ancestor is being sized to what it holds: a
|
|
233
|
+
leaf taking its width from its own contents is asked for the table's max-content width
|
|
234
|
+
and the cap never applies. The surface closes the same question at its edge
|
|
235
|
+
(`visual/VisualEditorInner.svelte` §`.qm-editor`); this end of it is what makes the cap
|
|
236
|
+
below true wherever the island lands. `inline-size`, not `size`: an island is as tall
|
|
237
|
+
as its rows. */
|
|
238
|
+
.qm-table-island {
|
|
239
|
+
contain: inline-size;
|
|
240
|
+
}
|
|
231
241
|
.qm-table-scroller {
|
|
232
242
|
/* The one horizontally scrolling box in a leaf: a table is as wide as its
|
|
233
243
|
columns, and a wide one must not push the card's own layout. `overflow-y` is
|
package/dist/core/codec/slash.js
CHANGED
|
Binary file
|
|
@@ -108,6 +108,12 @@
|
|
|
108
108
|
box-sizing: border-box;
|
|
109
109
|
position: relative;
|
|
110
110
|
width: 100%;
|
|
111
|
+
/* The width is the host's, never the document's, which is the same boundary the
|
|
112
|
+
editor states (`visual/VisualEditorInner.svelte` §`.qm-editor`): a mount standing
|
|
113
|
+
in a column that floors at what it holds is asked for nothing the sheets in it
|
|
114
|
+
are. `inline-size`, not `size`: the column is as tall as the pages painted in
|
|
115
|
+
it. */
|
|
116
|
+
contain: inline-size;
|
|
111
117
|
height: 100%;
|
|
112
118
|
overflow-y: auto;
|
|
113
119
|
padding: var(--_qm-space-4);
|
|
@@ -116,7 +116,10 @@
|
|
|
116
116
|
if (ids.length === n) return;
|
|
117
117
|
if (ids.length < n) ids = [...ids, ...seq.take(n - ids.length)];
|
|
118
118
|
else {
|
|
119
|
-
for (const id of ids.slice(n))
|
|
119
|
+
for (const id of ids.slice(n)) {
|
|
120
|
+
delete els[id];
|
|
121
|
+
delete rowEls[id];
|
|
122
|
+
}
|
|
120
123
|
ids = ids.slice(0, n);
|
|
121
124
|
}
|
|
122
125
|
});
|
|
@@ -138,6 +141,11 @@
|
|
|
138
141
|
// {@link Card} keeps its header/panel refs in.
|
|
139
142
|
const els: Record<string, { focus: () => void; setCaret?: (pos: number) => void } | undefined> =
|
|
140
143
|
$state({});
|
|
144
|
+
// The rows' own boxes, on the same key and dropped on the same paths: what an
|
|
145
|
+
// element landing blooms in ({@link focusElement}), and what an object row's
|
|
146
|
+
// summary is found through. A row is a box whatever its element type, so the two
|
|
147
|
+
// element shapes register alike.
|
|
148
|
+
const rowEls: Record<string, HTMLElement | undefined> = $state({});
|
|
141
149
|
let addEl: HTMLButtonElement | undefined = $state();
|
|
142
150
|
let rootEl: HTMLElement | undefined = $state();
|
|
143
151
|
let rowsEl: HTMLElement | undefined = $state();
|
|
@@ -218,6 +226,7 @@
|
|
|
218
226
|
const next = ids.filter((_, i) => i !== k);
|
|
219
227
|
ids = next;
|
|
220
228
|
delete els[dropped];
|
|
229
|
+
delete rowEls[dropped];
|
|
221
230
|
// The open row can be the one removed; `openId` is cleared with it rather than
|
|
222
231
|
// left naming an element that has gone.
|
|
223
232
|
if (openId === dropped) openId = undefined;
|
|
@@ -241,13 +250,7 @@
|
|
|
241
250
|
* the row's own summary otherwise — a collapsed row's control is its summary. */
|
|
242
251
|
function focusObjectRow(id: string): void {
|
|
243
252
|
if (id === openId && openObjEl) return openObjEl.focus();
|
|
244
|
-
|
|
245
|
-
// which the test DOM does not carry.
|
|
246
|
-
const rows = rowsEl?.querySelectorAll<HTMLElement>('[data-qm-el]') ?? [];
|
|
247
|
-
for (const row of rows) {
|
|
248
|
-
if (row.dataset.qmEl === id)
|
|
249
|
-
return row.querySelector<HTMLElement>('.qm-element-summary')?.focus();
|
|
250
|
-
}
|
|
253
|
+
rowEls[id]?.querySelector<HTMLElement>('.qm-element-summary')?.focus();
|
|
251
254
|
}
|
|
252
255
|
/** The box an arrival wash blooms in (`leaves.ts`, `core/bloom.ts`): the elements,
|
|
253
256
|
* not the header above them. This component owns the field's label, so the wrapper
|
|
@@ -266,21 +269,33 @@
|
|
|
266
269
|
*
|
|
267
270
|
* An absent `pos` is the placement rung, exactly as on `Landing`. A row that takes
|
|
268
271
|
* no offset gets the bare focus: the JSON element, whose textarea has no coordinate
|
|
269
|
-
* to spend one in, and a `string` element, which the compile never addresses.
|
|
270
|
-
|
|
272
|
+
* to spend one in, and a `string` element, which the compile never addresses.
|
|
273
|
+
*
|
|
274
|
+
* The row's box comes back with the landing: the arrival wash is the address's own
|
|
275
|
+
* granularity, and one row is what an element address named. A fallback to the
|
|
276
|
+
* field answers `undefined`, which is the field's box again (`leaves.ts`). */
|
|
277
|
+
export function focusElement(k: number, pos?: number): HTMLElement | undefined {
|
|
278
|
+
const id = ids[k];
|
|
271
279
|
// A collapsed row holds no control for a caret to land in, so opening it is part
|
|
272
280
|
// of the landing rather than something the user does first: a preview click that
|
|
273
281
|
// resolves into an element has to arrive somewhere the caret can sit.
|
|
274
282
|
if (control === 'object') {
|
|
275
|
-
|
|
276
|
-
|
|
283
|
+
if (id === undefined) {
|
|
284
|
+
focus();
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
277
287
|
openId = id;
|
|
278
|
-
|
|
288
|
+
void focusAfterFlush(id);
|
|
289
|
+
return rowEls[id];
|
|
279
290
|
}
|
|
280
|
-
const el = els[
|
|
281
|
-
if (!el)
|
|
282
|
-
|
|
283
|
-
|
|
291
|
+
const el = id === undefined ? undefined : els[id];
|
|
292
|
+
if (!el) {
|
|
293
|
+
focus();
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
if (pos != null && el.setCaret) el.setCaret(pos);
|
|
297
|
+
else el.focus();
|
|
298
|
+
return rowEls[id];
|
|
284
299
|
}
|
|
285
300
|
/** Focus element `id` after the flush, never in the same tick: a mutation commits
|
|
286
301
|
* the array by value, so the parent re-derives and the row does not exist until
|
|
@@ -362,7 +377,7 @@
|
|
|
362
377
|
{#if control === 'object'}
|
|
363
378
|
{@const open = openId === id}
|
|
364
379
|
{@const shown = elementTitle(k)}
|
|
365
|
-
<div class="qm-array-row qm-element" class:open
|
|
380
|
+
<div class="qm-array-row qm-element" class:open bind:this={rowEls[id]}>
|
|
366
381
|
<!-- The head is the row in collapsed form, and it is a box: the element IS
|
|
367
382
|
a value, the way the enum trigger is, and the remove slab's grammar
|
|
368
383
|
(the box's two end-side corners) needs corners to take. So a list of
|
|
@@ -404,7 +419,7 @@
|
|
|
404
419
|
{/if}
|
|
405
420
|
</div>
|
|
406
421
|
{:else}
|
|
407
|
-
<div class="qm-array-row">
|
|
422
|
+
<div class="qm-array-row" bind:this={rowEls[id]}>
|
|
408
423
|
{#if control === 'prose'}
|
|
409
424
|
<ProseValue
|
|
410
425
|
bind:this={els[id]}
|
|
@@ -477,6 +492,10 @@
|
|
|
477
492
|
position: relative;
|
|
478
493
|
display: grid;
|
|
479
494
|
grid-template-columns: minmax(0, 1fr);
|
|
495
|
+
/* The corners the row's wash takes, an element landing blooming here rather than
|
|
496
|
+
over the whole list (`focusElement`): the row draws no box of its own, so the
|
|
497
|
+
radius it lends the wash is the one its element's box draws. */
|
|
498
|
+
border-radius: var(--_qm-radius-inner);
|
|
480
499
|
}
|
|
481
500
|
/* The end inset the slab stands in, taken off whichever box the element drew:
|
|
482
501
|
`.qm-input` is the text element and the JSON textarea, `.qm-control-box` the prose
|
|
@@ -32,7 +32,7 @@ interface Props {
|
|
|
32
32
|
declare const ArrayField: import("svelte").Component<Props, {
|
|
33
33
|
focus: () => void;
|
|
34
34
|
washBox: () => HTMLElement | undefined;
|
|
35
|
-
focusElement: (k: number, pos?: number) =>
|
|
35
|
+
focusElement: (k: number, pos?: number) => HTMLElement | undefined;
|
|
36
36
|
}, "">;
|
|
37
37
|
type ArrayField = ReturnType<typeof ArrayField>;
|
|
38
38
|
export default ArrayField;
|
package/dist/visual/Field.svelte
CHANGED
|
@@ -147,7 +147,7 @@
|
|
|
147
147
|
let arrayEl = $state<
|
|
148
148
|
| {
|
|
149
149
|
focus: () => void;
|
|
150
|
-
focusElement: (k: number, pos?: number) =>
|
|
150
|
+
focusElement: (k: number, pos?: number) => HTMLElement | undefined;
|
|
151
151
|
washBox: () => HTMLElement | undefined;
|
|
152
152
|
}
|
|
153
153
|
| undefined
|
|
@@ -159,9 +159,11 @@
|
|
|
159
159
|
document.getElementById(domIds.control)?.focus();
|
|
160
160
|
}
|
|
161
161
|
/** The array's per-ELEMENT landing, for the addresses the preview mints under an
|
|
162
|
-
* array field (`leaves.ts`); read at the call, so it tracks the mounted repeater.
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
* array field (`leaves.ts`); read at the call, so it tracks the mounted repeater.
|
|
163
|
+
* The row it settled in comes back with it, the wash being the landing's own
|
|
164
|
+
* granularity. */
|
|
165
|
+
function focusElement(k: number, pos?: number): HTMLElement | undefined {
|
|
166
|
+
return arrayEl?.focusElement(k, pos);
|
|
165
167
|
}
|
|
166
168
|
// Only where `for` cannot reach; the labelable four are the browser's own, and a
|
|
167
169
|
// second handler over them would be a focus the label already placed.
|
|
@@ -181,7 +183,8 @@
|
|
|
181
183
|
* that does not remount.
|
|
182
184
|
*
|
|
183
185
|
* An array carries the per-element lane as well; no other control has elements for
|
|
184
|
-
* an address to name.
|
|
186
|
+
* an address to name. `el` is the field-wide box, which an element landing does not
|
|
187
|
+
* wash: that lane hands back its own row (`leaves.ts`).
|
|
185
188
|
*/
|
|
186
189
|
let controlEl = $state<HTMLElement | undefined>();
|
|
187
190
|
$effect(() => {
|
|
@@ -640,12 +640,11 @@
|
|
|
640
640
|
// did not resolve. `'cluster'` (and an absent granularity: the backend did not
|
|
641
641
|
// report it, treat as exact) places the caret. An absent `pos` is the placement
|
|
642
642
|
// rung and reaches the same floor.
|
|
643
|
-
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
//
|
|
647
|
-
|
|
648
|
-
bloomInside(found.control.el);
|
|
643
|
+
// The wash is unconditional, and takes the box the landing settled in: a preview
|
|
644
|
+
// click is one discrete act, and its commonest target is the leaf already focused
|
|
645
|
+
// (where landing a caret changes nothing on screen) or one off-screen, where the
|
|
646
|
+
// browser's focus-scroll moves the page and leaves the caret to be hunted for.
|
|
647
|
+
bloomInside(land(found, at.granularity === 'segment' ? undefined : at.pos));
|
|
649
648
|
}
|
|
650
649
|
/** The active leaf's controller: the formatting popover's observation seam.
|
|
651
650
|
* `undefined` for a focused form control, which holds no marks to toggle. */
|
|
@@ -689,16 +688,25 @@
|
|
|
689
688
|
* selection outright), which is also the whole of what a click on plate-placed ink
|
|
690
689
|
* can mean. An element is no `createField` leaf, so the offset goes down the element
|
|
691
690
|
* lane instead, where what it means is the row control's (`leaves.ts`).
|
|
691
|
+
*
|
|
692
|
+
* Hands back the box the arrival wash blooms in, which is the landing's own
|
|
693
|
+
* granularity rather than the registry's: an element lands in one row and says so
|
|
694
|
+
* over that row, and everything else — the field, and a row the document has since
|
|
695
|
+
* dropped — over the field's box.
|
|
692
696
|
*/
|
|
693
|
-
function land(found: Landed, pos: number | undefined):
|
|
697
|
+
function land(found: Landed, pos: number | undefined): HTMLElement {
|
|
694
698
|
if (found.element != null && found.control.focusElement) {
|
|
695
|
-
return found.control.focusElement(found.element, pos);
|
|
699
|
+
return found.control.focusElement(found.element, pos) ?? found.control.el;
|
|
696
700
|
}
|
|
697
701
|
if (pos != null) {
|
|
698
702
|
const prose = leaves.prose(found.key);
|
|
699
|
-
if (prose)
|
|
703
|
+
if (prose) {
|
|
704
|
+
prose.setCaret(pos);
|
|
705
|
+
return found.control.el;
|
|
706
|
+
}
|
|
700
707
|
}
|
|
701
708
|
found.control.focus();
|
|
709
|
+
return found.control.el;
|
|
702
710
|
}
|
|
703
711
|
|
|
704
712
|
// ── The verbs, as instance exports ──────────────────────────────────────────
|
|
@@ -717,8 +725,7 @@
|
|
|
717
725
|
export async function focusField(field: DocPath): Promise<void> {
|
|
718
726
|
const found = await revealLeaf(field);
|
|
719
727
|
if (!found) return missed(`no mounted field at ${field}`, field);
|
|
720
|
-
land(found, undefined);
|
|
721
|
-
bloomInside(found.control.el);
|
|
728
|
+
bloomInside(land(found, undefined));
|
|
722
729
|
}
|
|
723
730
|
/** Seed a card of `kind` and insert it at `at` (default: the end). Returns the new
|
|
724
731
|
* card's session key, or `undefined` when the quill seeds no card of that kind. */
|
|
@@ -935,6 +942,14 @@
|
|
|
935
942
|
box-sizing: border-box;
|
|
936
943
|
display: flex;
|
|
937
944
|
flex-direction: column;
|
|
945
|
+
/* The width is the host's, never the document's. A mount standing in a column that
|
|
946
|
+
floors at what it holds — a flex or grid track with no minimum of its own — is
|
|
947
|
+
otherwise asked for the widest thing in the document, and a table is as wide as
|
|
948
|
+
its columns. The island holds its own end of that (`codec/prose.css` §"The table
|
|
949
|
+
island"); this is the boundary, so a construct a leaf gains later cannot reach
|
|
950
|
+
past it either. `inline-size`, not `size`: a surface is as tall as its cards, or
|
|
951
|
+
the pane's under `.qm-pane`. */
|
|
952
|
+
contain: inline-size;
|
|
938
953
|
gap: var(--_qm-space-2);
|
|
939
954
|
padding: var(--_qm-space-5);
|
|
940
955
|
background: var(--_qm-surface-sunken);
|
package/dist/visual/leaves.d.ts
CHANGED
|
@@ -19,8 +19,13 @@ export interface FieldControl {
|
|
|
19
19
|
* `Landing`, and what a `pos` means is the row control's, the same way focusing is.
|
|
20
20
|
* The place rides the call because the registry stays parent-keyed: a per-element
|
|
21
21
|
* key is positional, in a registry whose doctrine is dodging positional churn.
|
|
22
|
+
*
|
|
23
|
+
* Returns the element's own box, which is what the wash blooms in: the address
|
|
24
|
+
* named one row, and a wash over the whole repeater says the field where the click
|
|
25
|
+
* said the row. `undefined` is the fallback the index past the live list took, and
|
|
26
|
+
* reads as {@link el}.
|
|
22
27
|
*/
|
|
23
|
-
focusElement?(index: number, pos?: number):
|
|
28
|
+
focusElement?(index: number, pos?: number): HTMLElement | undefined;
|
|
24
29
|
readonly el: HTMLElement;
|
|
25
30
|
}
|
|
26
31
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quillmark/svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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.110.0-0",
|
|
70
70
|
"svelte": "^5.0.0"
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|