@quillmark/svelte 0.6.0 → 0.7.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 +1 -1
- package/dist/core/codec/field.d.ts +15 -4
- package/dist/core/codec/field.js +13 -10
- package/dist/core/codec/index.d.ts +1 -1
- package/dist/core/codec/index.js +1 -1
- package/dist/core/codec/table-view.d.ts +4 -0
- package/dist/core/codec/table-view.js +10 -1
- package/dist/core/theme.css +14 -9
- package/dist/preset/recipes.css +15 -4
- package/dist/preview/bridge.d.ts +23 -0
- package/dist/preview/bridge.js +92 -35
- package/dist/preview/controller.d.ts +6 -5
- package/dist/preview/controller.js +13 -7
- package/dist/preview/index.js +3 -2
- package/dist/preview/paint.js +9 -1
- package/dist/visual/ArrayField.svelte +30 -23
- package/dist/visual/ArrayField.svelte.d.ts +9 -7
- package/dist/visual/Card.svelte +88 -45
- package/dist/visual/EnumField.svelte +4 -2
- package/dist/visual/Field.svelte +22 -2
- package/dist/visual/ObjectField.svelte +95 -75
- package/dist/visual/ObjectField.svelte.d.ts +7 -3
- package/dist/visual/ProseValue.svelte +165 -0
- package/dist/visual/ProseValue.svelte.d.ts +33 -0
- package/dist/visual/SlashMenu.svelte +7 -0
- package/dist/visual/VariantField.svelte +11 -6
- package/dist/visual/VariantField.svelte.d.ts +5 -1
- package/dist/visual/VisualEditorInner.svelte +29 -11
- package/dist/visual/hold.d.ts +17 -0
- package/dist/visual/hold.js +39 -0
- package/dist/visual/index.js +1 -1
- package/dist/visual/motion.d.ts +9 -0
- package/dist/visual/motion.js +10 -2
- package/dist/visual/strings.d.ts +4 -3
- package/package.json +2 -2
- package/dist/visual/ProseArrayElement.svelte +0 -139
- package/dist/visual/ProseArrayElement.svelte.d.ts +0 -25
|
@@ -3,7 +3,7 @@
|
|
|
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
5
|
by `items.type`: `richtext` / `plaintext` → a prose element
|
|
6
|
-
({@link
|
|
6
|
+
({@link ProseValue}), `object` → a summary row that opens onto a subform,
|
|
7
7
|
everything else → a text input. The
|
|
8
8
|
add affordance sits in the label header row (space-between with the field label);
|
|
9
9
|
{@link Field} skips its own label for array controls and hands this component the
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
An `object` element collapses: the row is its own summary — a box, titled by the
|
|
13
13
|
element's first `string` cell — and opens onto {@link ObjectField}, one at a time.
|
|
14
|
-
Stacking the subforms instead would nest a field one level past the depth the
|
|
15
|
-
draws, once per row. Opening is therefore part of a landing rather than something the
|
|
14
|
+
Stacking the subforms instead would nest a field one level past the depth the
|
|
15
|
+
subform's own vertical draws, once per row. Opening is therefore part of a landing rather than something the
|
|
16
16
|
user does first: `focusElement` opens the row it is aimed at before it focuses.
|
|
17
17
|
|
|
18
18
|
The remove is inside the element: a slab over the end of the element's own box, taking
|
|
@@ -38,14 +38,15 @@
|
|
|
38
38
|
// off-tree, so this component renders standalone too.
|
|
39
39
|
const t = wording();
|
|
40
40
|
import { onDestroy, tick } from 'svelte';
|
|
41
|
-
import type { Content, QuillFieldSchema } from '@quillmark/wasm';
|
|
41
|
+
import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
|
|
42
42
|
import { emptyContent } from '../core/codec/index.js';
|
|
43
43
|
import { createLifespan } from '../core/teardown.js';
|
|
44
44
|
import { IdSeq, controlKind } from './structure.js';
|
|
45
|
+
import { holdInView } from './hold.js';
|
|
45
46
|
import Icon from './icons/Icon.svelte';
|
|
46
47
|
import TextField from './TextField.svelte';
|
|
47
48
|
import ObjectField from './ObjectField.svelte';
|
|
48
|
-
import
|
|
49
|
+
import ProseValue from './ProseValue.svelte';
|
|
49
50
|
import FieldLabel from './FieldLabel.svelte';
|
|
50
51
|
import './controls.css';
|
|
51
52
|
|
|
@@ -72,12 +73,14 @@
|
|
|
72
73
|
* properties' own names from, one `-e-<element id>` segment down, so each open
|
|
73
74
|
* record's cells carry real `<label for>` pairs like every other control. */
|
|
74
75
|
idBase?: string;
|
|
75
|
-
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
|
|
76
|
+
/** The boundary's nested content read, rooted at this field: `path` is a
|
|
77
|
+
* `PathStep[]` from the field to the leaf, so `[i]` is element `i` and
|
|
78
|
+
* `[i, key]` a content cell of an `object` element's subform (`Field`,
|
|
79
|
+
* `reader.getContentAt`). The codec is the leaf's own declared type's. Asked
|
|
80
|
+
* only for a content-typed leaf: on any other the read is not content and
|
|
81
|
+
* throws. `undefined` for a leaf the stored value does not reach — a slot this
|
|
82
|
+
* control has spliced in and whose commit has yet to land, or was refused. */
|
|
83
|
+
contentAt: (path: PathStep[]) => Content | undefined;
|
|
81
84
|
onCommit: (arr: unknown[]) => void;
|
|
82
85
|
}
|
|
83
86
|
let {
|
|
@@ -89,13 +92,13 @@
|
|
|
89
92
|
labelId,
|
|
90
93
|
descriptionId,
|
|
91
94
|
idBase,
|
|
92
|
-
|
|
95
|
+
contentAt,
|
|
93
96
|
onCommit
|
|
94
97
|
}: Props = $props();
|
|
95
98
|
|
|
96
99
|
// The element control is the item schema's own, with no departure: a content-typed
|
|
97
100
|
// element mounts the prose leaf its scalar field mounts, reading through
|
|
98
|
-
// `
|
|
101
|
+
// `contentAt` whatever the element rests as. An array declaring no `items`
|
|
99
102
|
// has text elements.
|
|
100
103
|
const control = $derived(items ? controlKind(items) : 'text');
|
|
101
104
|
const arr = $derived((value ?? []) as unknown[]);
|
|
@@ -121,7 +124,7 @@
|
|
|
121
124
|
// The focus targets, keyed by element ID rather than index: an index goes stale on
|
|
122
125
|
// the splice that focus is chasing. An element control exposes `focus()` because a
|
|
123
126
|
// text element and a prose element disagree on what focusing is: the difference
|
|
124
|
-
// is stated on `
|
|
127
|
+
// is stated on `ProseValue.focus`, which owns it.
|
|
125
128
|
//
|
|
126
129
|
// Every path that drops an id deletes its entry: `bind:this` teardown nulls the
|
|
127
130
|
// value on unmount and leaves the key, so a card that outlives its elements
|
|
@@ -166,8 +169,12 @@
|
|
|
166
169
|
function untitled(k: number): string {
|
|
167
170
|
return label != null ? t.strings.elementUntitled(label, k + 1) : String(k + 1);
|
|
168
171
|
}
|
|
169
|
-
|
|
170
|
-
|
|
172
|
+
/** The summary is the anchor: a row closing above it is what would carry it off the fold,
|
|
173
|
+
* and a row's subform hangs under its own summary (`hold.ts`). */
|
|
174
|
+
function toggleRow(id: string, summary: HTMLElement): void {
|
|
175
|
+
holdInView(summary, () => {
|
|
176
|
+
openId = openId === id ? undefined : id;
|
|
177
|
+
});
|
|
171
178
|
}
|
|
172
179
|
|
|
173
180
|
// The awaited flush below is the only work that outlives a gesture here, so the
|
|
@@ -365,7 +372,7 @@
|
|
|
365
372
|
type="button"
|
|
366
373
|
class="qm-control-box qm-focus-ring qm-element-summary"
|
|
367
374
|
aria-expanded={open}
|
|
368
|
-
onclick={() => toggleRow(id)}
|
|
375
|
+
onclick={(e) => toggleRow(id, e.currentTarget)}
|
|
369
376
|
>
|
|
370
377
|
<!-- Leading, and it rotates: trailing is the figure for pushing a new
|
|
371
378
|
screen, where this unfolds in place. Same glyph, same rotation and
|
|
@@ -391,7 +398,7 @@
|
|
|
391
398
|
properties={items?.properties}
|
|
392
399
|
label={label != null ? `${label} ${k + 1}` : undefined}
|
|
393
400
|
idBase={idBase != null ? `${idBase}-e-${id}` : undefined}
|
|
394
|
-
|
|
401
|
+
contentAt={(path) => contentAt([k, ...path])}
|
|
395
402
|
onCommit={(obj) => commitElement(k, obj)}
|
|
396
403
|
/>
|
|
397
404
|
{/if}
|
|
@@ -399,9 +406,9 @@
|
|
|
399
406
|
{:else}
|
|
400
407
|
<div class="qm-array-row">
|
|
401
408
|
{#if control === 'prose'}
|
|
402
|
-
<
|
|
409
|
+
<ProseValue
|
|
403
410
|
bind:this={els[id]}
|
|
404
|
-
content={() =>
|
|
411
|
+
content={() => contentAt([k]) ?? emptyContent()}
|
|
405
412
|
plaintext={items?.type === 'plaintext'}
|
|
406
413
|
label={label != null ? `${label} ${k + 1}` : undefined}
|
|
407
414
|
onChange={(rt) => commitElement(k, rt)}
|
|
@@ -517,9 +524,9 @@
|
|
|
517
524
|
/* ── An object element ──────────────────────────────────────────────────────
|
|
518
525
|
The row is a summary and the subform hangs under it. `row-gap` rather than a
|
|
519
526
|
margin on the subform, because the distance between a control and what it has
|
|
520
|
-
unfolded belongs to the thing stacking them: the variant field spends its own
|
|
521
|
-
|
|
522
|
-
|
|
527
|
+
unfolded belongs to the thing stacking them: the variant field spends its own `gap` on
|
|
528
|
+
exactly this, and what `ObjectField` adds is the cap on its own stroke, equal at both
|
|
529
|
+
of its ends. */
|
|
523
530
|
.qm-element {
|
|
524
531
|
row-gap: var(--_qm-space-2);
|
|
525
532
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Content, QuillFieldSchema } from '@quillmark/wasm';
|
|
1
|
+
import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
|
|
2
2
|
import './controls.css';
|
|
3
3
|
interface Props {
|
|
4
4
|
value: unknown[] | undefined;
|
|
@@ -19,12 +19,14 @@ interface Props {
|
|
|
19
19
|
* properties' own names from, one `-e-<element id>` segment down, so each open
|
|
20
20
|
* record's cells carry real `<label for>` pairs like every other control. */
|
|
21
21
|
idBase?: string;
|
|
22
|
-
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
22
|
+
/** The boundary's nested content read, rooted at this field: `path` is a
|
|
23
|
+
* `PathStep[]` from the field to the leaf, so `[i]` is element `i` and
|
|
24
|
+
* `[i, key]` a content cell of an `object` element's subform (`Field`,
|
|
25
|
+
* `reader.getContentAt`). The codec is the leaf's own declared type's. Asked
|
|
26
|
+
* only for a content-typed leaf: on any other the read is not content and
|
|
27
|
+
* throws. `undefined` for a leaf the stored value does not reach — a slot this
|
|
28
|
+
* control has spliced in and whose commit has yet to land, or was refused. */
|
|
29
|
+
contentAt: (path: PathStep[]) => Content | undefined;
|
|
28
30
|
onCommit: (arr: unknown[]) => void;
|
|
29
31
|
}
|
|
30
32
|
declare const ArrayField: import("svelte").Component<Props, {
|
package/dist/visual/Card.svelte
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import type { LeafRegistry } from './leaves.js';
|
|
17
17
|
import type { CardModel, FieldModel } from './structure.js';
|
|
18
18
|
import { placeFields, humanize, initialExpandedGroup } from './structure.js';
|
|
19
|
+
import { holdInView } from './hold.js';
|
|
19
20
|
import type { FieldDomIds } from './domid.js';
|
|
20
21
|
import Field from './Field.svelte';
|
|
21
22
|
import ProseField from './ProseField.svelte';
|
|
@@ -127,9 +128,9 @@
|
|
|
127
128
|
const ungrouped = $derived(card.sections.filter((s) => s.group == null));
|
|
128
129
|
const grouped = $derived(card.sections.filter((s) => s.group != null));
|
|
129
130
|
|
|
130
|
-
// A schema that declares no fields still gets a body, and
|
|
131
|
-
// worse than none:
|
|
132
|
-
//
|
|
131
|
+
// A schema that declares no fields still gets a body, and an empty payload block is
|
|
132
|
+
// worse than none: its two rules land on one y and paint as a single stroke of twice
|
|
133
|
+
// the width, and the card's gap counts the block twice besides.
|
|
133
134
|
const hasMeta = $derived(ungrouped.length > 0 || grouped.length > 0);
|
|
134
135
|
|
|
135
136
|
// Seeded once from the card's shape. Card is keyed by stable id, so this survives the
|
|
@@ -157,9 +158,11 @@
|
|
|
157
158
|
headers[closing]?.focus();
|
|
158
159
|
expanded = next;
|
|
159
160
|
}
|
|
161
|
+
/** The header is the anchor: a section closing above it is what would carry it off the
|
|
162
|
+
* fold, and a section opens under its own header (`hold.ts`). */
|
|
160
163
|
function toggleGroup(group: string): void {
|
|
161
164
|
animate = true;
|
|
162
|
-
setExpanded(expanded === group ? null : group);
|
|
165
|
+
holdInView(headers[group], () => setExpanded(expanded === group ? null : group));
|
|
163
166
|
}
|
|
164
167
|
|
|
165
168
|
/**
|
|
@@ -190,6 +193,9 @@
|
|
|
190
193
|
* an insert, `nearest` for a reorder. The reduced-motion guard is JS rather than the
|
|
191
194
|
* CSS the accordion uses: a scroll's behaviour is an argument, not a transition a
|
|
192
195
|
* media query can cancel.
|
|
196
|
+
*
|
|
197
|
+
* The caller is what waits out a reorder's own trip before asking: the run holds the
|
|
198
|
+
* slot this card sits in, and the slot is the stack's (`VisualEditorInner`).
|
|
193
199
|
*/
|
|
194
200
|
export function scrollIntoViewCard(block: ScrollLogicalPosition): void {
|
|
195
201
|
const reduce = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
@@ -244,10 +250,6 @@
|
|
|
244
250
|
{/if}
|
|
245
251
|
|
|
246
252
|
<div class="qm-card-body">
|
|
247
|
-
<!-- One element whose top and bottom edges are the bracket's two horizontals.
|
|
248
|
-
Anchored here rather than on the header and on the accordion so an open section's
|
|
249
|
-
vertical reaches them, and so a card with no groups still terminates above its
|
|
250
|
-
body. -->
|
|
251
253
|
{#if hasMeta}
|
|
252
254
|
<div
|
|
253
255
|
class="qm-card-meta"
|
|
@@ -261,8 +263,7 @@
|
|
|
261
263
|
{/each}
|
|
262
264
|
|
|
263
265
|
<!-- One open at a time. Rendered only when a card has groups, so an empty
|
|
264
|
-
accordion
|
|
265
|
-
to whichever block abuts it. -->
|
|
266
|
+
accordion does not stand a gap under the ungrouped fields. -->
|
|
266
267
|
{#if grouped.length}
|
|
267
268
|
<div class="qm-groups" class:qm-instant={!animate}>
|
|
268
269
|
{#each grouped as section (section.group)}
|
|
@@ -409,11 +410,12 @@
|
|
|
409
410
|
/* An island: the card is the base plane, the column behind it the sunken one, and the
|
|
410
411
|
hairline closes it (ARCHITECTURE §Styling). The inset is the same rung as the gap, so
|
|
411
412
|
what separates the header from the fields is what separates the fields from the
|
|
412
|
-
card's edge.
|
|
413
|
+
card's edge. The inline half reads `--_qm-nest` at that same number: this edge is the
|
|
414
|
+
nesting ladder's first stroke, and its inset the first step. */
|
|
413
415
|
.qm-card {
|
|
414
416
|
border: var(--_qm-border-width) solid var(--_qm-border);
|
|
415
417
|
border-radius: var(--_qm-radius);
|
|
416
|
-
padding: var(--_qm-space-3);
|
|
418
|
+
padding: var(--_qm-space-3) var(--_qm-nest);
|
|
417
419
|
background: var(--_qm-surface);
|
|
418
420
|
display: flex;
|
|
419
421
|
flex-direction: column;
|
|
@@ -497,37 +499,58 @@
|
|
|
497
499
|
.qm-card-title:focus {
|
|
498
500
|
color: var(--_qm-ink);
|
|
499
501
|
}
|
|
500
|
-
.qm-card-body
|
|
501
|
-
.qm-card-meta {
|
|
502
|
+
.qm-card-body {
|
|
502
503
|
display: flex;
|
|
503
504
|
flex-direction: column;
|
|
504
505
|
gap: var(--_qm-space-3);
|
|
505
506
|
}
|
|
506
|
-
/*
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
507
|
+
/* Where two sections meet the distance is three of one rung — the cap closing the one
|
|
508
|
+
above, this gap, the cap opening the one below — the same whether either side is a
|
|
509
|
+
group or the ungrouped block. That total is what the fields inside a section keep
|
|
510
|
+
between their rows (`.qm-fields` row-gap), so a section boundary stands no further
|
|
511
|
+
apart than a field boundary: what ranks them is the stroke, not the air. */
|
|
512
|
+
.qm-card-meta,
|
|
513
|
+
.qm-groups {
|
|
514
|
+
display: flex;
|
|
515
|
+
flex-direction: column;
|
|
516
|
+
gap: var(--_qm-space);
|
|
517
|
+
}
|
|
518
|
+
/* One rung in two places: what a rule insets its own edge by, and what a vertical
|
|
519
|
+
reaches past the box it is drawn on. `--_qm-nest` is the distance a stroke stands off
|
|
520
|
+
the content it holds — the same one the fields keep off the vertical inline — and a
|
|
521
|
+
section's box already spends `--_qm-space` of it capping its own ends, so both spend
|
|
522
|
+
the difference. That is what squares the corner: the field stands the same rung off
|
|
523
|
+
the rule above it as off the vertical beside it. */
|
|
524
|
+
.qm-card-meta {
|
|
525
|
+
--stroke-reach: calc(var(--_qm-nest) - var(--_qm-space));
|
|
526
|
+
}
|
|
527
|
+
/* The card's two horizontals, and ranking is the one thing a horizontal is for: they
|
|
528
|
+
stand the payload against the title above it and the body below it, where saying what
|
|
529
|
+
sits inside what is the verticals' (ARCHITECTURE §"A plane is a tone"). Each is
|
|
530
|
+
conditional on what it ranks against: `main` is headerless, and a card with no body
|
|
531
|
+
has nothing under the payload to divide it from.
|
|
510
532
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
figure, which is what distinguishes it from a second box inside the card. */
|
|
533
|
+
The inset is the rule's own, so it goes with it: a card drawing one horizontal insets
|
|
534
|
+
that end and not the other. The air outside the pair is the `--_qm-space-3` the card
|
|
535
|
+
spends between its blocks. */
|
|
515
536
|
.qm-meta-top {
|
|
516
537
|
border-top: var(--_qm-border-width) solid var(--_qm-border);
|
|
538
|
+
padding-top: var(--stroke-reach);
|
|
517
539
|
}
|
|
518
540
|
.qm-meta-bottom {
|
|
519
541
|
border-bottom: var(--_qm-border-width) solid var(--_qm-border);
|
|
542
|
+
padding-bottom: var(--stroke-reach);
|
|
520
543
|
}
|
|
521
|
-
/*
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
.qm-
|
|
530
|
-
padding-
|
|
544
|
+
/* A section's share of the boundary between sections: one rung at each end, so two of
|
|
545
|
+
them stand three of it apart whichever kind they are (above). The accordion spends the
|
|
546
|
+
same rung from its header and its panel, where it also caps the vertical those two
|
|
547
|
+
edges hold.
|
|
548
|
+
|
|
549
|
+
No stroke and no inset: fields declaring no `ui.group` are the card's own, at the rank
|
|
550
|
+
its body is at, and the card's edge is the only container there is to state. A
|
|
551
|
+
vertical here would mark a section a reader has no name for. */
|
|
552
|
+
.qm-section {
|
|
553
|
+
padding-block: var(--_qm-space);
|
|
531
554
|
}
|
|
532
555
|
/* Two query containers, one per kind of section, so capacity follows the width the
|
|
533
556
|
fields actually get: a panel's inset makes that narrower than the card. */
|
|
@@ -562,16 +585,32 @@
|
|
|
562
585
|
--cols-half: 2;
|
|
563
586
|
}
|
|
564
587
|
}
|
|
565
|
-
|
|
588
|
+
/* The nesting vertical, one `--_qm-nest` in from the card's edge with the panel's
|
|
589
|
+
fields the same rung off it (ARCHITECTURE §"A plane is a tone"). `--_qm-border` and
|
|
590
|
+
not `--_qm-accent`: a card's edge and the verticals inside it are one stroke at three
|
|
591
|
+
depths, and a tone that changed as it went in would read as unrelated lines rather
|
|
592
|
+
than one figure. A named section is what earns it, which is why the ungrouped block
|
|
593
|
+
above draws none.
|
|
594
|
+
|
|
595
|
+
On the group rather than on its panel: what the stroke marks is the section, and a
|
|
596
|
+
section is its header and what hangs under it. Drawn transparent when closed rather
|
|
597
|
+
than absent, so a toggle moves no text — and closed, there is nothing under the header
|
|
598
|
+
for it to gather.
|
|
599
|
+
|
|
600
|
+
The stroke caps at `--_qm-nest`, at both ends and on every section alike. Its box
|
|
601
|
+
already spends `--_qm-space` of that — the header's block padding above, the panel's
|
|
602
|
+
below, the rung the ungrouped section spends on its own two ends — so the rest is
|
|
603
|
+
grown here and handed straight back as margin: the stroke lengthens, and a section
|
|
604
|
+
boundary keeps the three rungs it stood at. What the stroke reaches into is the air of
|
|
605
|
+
that boundary, or, where the section is the payload's first or last, exactly the inset
|
|
606
|
+
its rule took — so it lands on the rule at a corner with nothing drawn longer for it.
|
|
607
|
+
One section open at a time, so no two strokes are ever visible to meet in between. */
|
|
566
608
|
.qm-group {
|
|
567
609
|
display: flex;
|
|
568
610
|
flex-direction: column;
|
|
569
|
-
}
|
|
570
|
-
/* The bracket's third stroke, on the section rather than on its panel: spanning the
|
|
571
|
-
header is what gives it a corner to close into at the block's edge. Drawn transparent
|
|
572
|
-
when closed rather than absent, so a toggle moves no text. */
|
|
573
|
-
.qm-group {
|
|
574
611
|
border-left: var(--_qm-border-width) solid transparent;
|
|
612
|
+
padding-block: var(--stroke-reach);
|
|
613
|
+
margin-block: calc(-1 * var(--stroke-reach));
|
|
575
614
|
transition: border-color var(--_qm-duration-slow) var(--_qm-ease-reverse);
|
|
576
615
|
}
|
|
577
616
|
.qm-group.qm-open {
|
|
@@ -579,10 +618,13 @@
|
|
|
579
618
|
}
|
|
580
619
|
/* Symmetric vertical padding at the tightest rung that still clears WCAG 2.5.8's
|
|
581
620
|
24×24 floor: the header is the whole row, so adjacent labels share one rhythm with no
|
|
582
|
-
dead strip outside the button. Horizontal is
|
|
583
|
-
because
|
|
584
|
-
|
|
585
|
-
stroke; right, because the row is the target and an
|
|
621
|
+
dead strip outside the button. Horizontal is the nesting step left and zero right —
|
|
622
|
+
left, because the chevron opening the heading starts where the fields under it start,
|
|
623
|
+
so everything the section holds stands off its vertical by one rung and the corner the
|
|
624
|
+
stroke closes against a rule reads square; right, because the row is the target and an
|
|
625
|
+
inset there is target given back. What the glyph gives up by taking the step is the
|
|
626
|
+
gutter: at a tighter rung it stands in the clearance itself, with only its own bearing
|
|
627
|
+
between ink and stroke, and the rotation swaps which bearing faces which.
|
|
586
628
|
|
|
587
629
|
`font: inherit` because a UA button inherits no face, then the body rung, one step
|
|
588
630
|
over the field labels beneath it: at the label rung the two read as one register and
|
|
@@ -595,7 +637,7 @@
|
|
|
595
637
|
width: 100%;
|
|
596
638
|
border: none;
|
|
597
639
|
background: transparent;
|
|
598
|
-
padding: var(--_qm-space) 0 var(--_qm-space) var(--_qm-
|
|
640
|
+
padding: var(--_qm-space) 0 var(--_qm-space) var(--_qm-nest);
|
|
599
641
|
font: inherit;
|
|
600
642
|
font-size: var(--_qm-text-body);
|
|
601
643
|
font-weight: var(--_qm-weight-mid);
|
|
@@ -651,12 +693,13 @@
|
|
|
651
693
|
.qm-group-panel-inner {
|
|
652
694
|
min-height: 0;
|
|
653
695
|
overflow: hidden;
|
|
654
|
-
padding-inline-start: var(--_qm-
|
|
696
|
+
padding-inline-start: var(--_qm-nest);
|
|
655
697
|
padding-inline-end: var(--_qm-ring-reach);
|
|
656
698
|
margin-inline-end: calc(-1 * var(--_qm-ring-reach));
|
|
657
699
|
}
|
|
700
|
+
/* The lower of the two caps an open section's vertical takes (`.qm-group`). */
|
|
658
701
|
.qm-group-panel-inner > .qm-fields {
|
|
659
|
-
padding-block: var(--_qm-space
|
|
702
|
+
padding-block: var(--_qm-space);
|
|
660
703
|
}
|
|
661
704
|
.qm-body-leaf {
|
|
662
705
|
display: flex;
|
|
@@ -108,10 +108,12 @@
|
|
|
108
108
|
<Icon name="chevron-down" size={14} />
|
|
109
109
|
</Select.Trigger>
|
|
110
110
|
<Select.Portal to={portalTarget}>
|
|
111
|
-
<Select.Content sideOffset={4}>
|
|
111
|
+
<Select.Content collisionBoundary={portalTarget ?? []} sideOffset={4}>
|
|
112
112
|
<!-- The list portals out of the trigger's DOM but into the nearest
|
|
113
113
|
`[data-qm-root]`, and carries the marker itself, like FormatPopover.
|
|
114
|
-
|
|
114
|
+
That root is the boundary a flip is measured against too, being the box
|
|
115
|
+
that clips the surface where the consumer scrolls one; the viewport
|
|
116
|
+
default knows nothing of that. The pill is this
|
|
115
117
|
element (not the primitive's) because scoped CSS keys off which
|
|
116
118
|
component owns the markup: a `class` passed to a primitive is a
|
|
117
119
|
plain string and never picks up the scoping hash. -->
|
package/dist/visual/Field.svelte
CHANGED
|
@@ -14,7 +14,15 @@
|
|
|
14
14
|
`DiagnosticList`, non-gating.
|
|
15
15
|
-->
|
|
16
16
|
<script lang="ts">
|
|
17
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
Document,
|
|
19
|
+
Quill,
|
|
20
|
+
Addr,
|
|
21
|
+
Content,
|
|
22
|
+
Diagnostic,
|
|
23
|
+
PathStep,
|
|
24
|
+
ResolvedField
|
|
25
|
+
} from '@quillmark/wasm';
|
|
18
26
|
import type { EditorErrorHandler } from '../core/errors.js';
|
|
19
27
|
import type { LeafRegistry } from './leaves.js';
|
|
20
28
|
import type { FieldModel, FieldSpan } from './structure.js';
|
|
@@ -116,6 +124,16 @@
|
|
|
116
124
|
// nothing, and silently.
|
|
117
125
|
const describedBy = $derived(field.description ? domIds.description : undefined);
|
|
118
126
|
|
|
127
|
+
/** The boundary's nested content read with this field's address already bound: an
|
|
128
|
+
* array's elements, an object's properties, a variant's cells, and a subform's cells
|
|
129
|
+
* under an open element. One door rather than one per depth, since `getContentAt`
|
|
130
|
+
* takes the whole path and the walk down is each container prefixing its own step.
|
|
131
|
+
* Read per call and never held: a leaf takes its state at mount, and a control asks
|
|
132
|
+
* once. */
|
|
133
|
+
function contentAt(path: PathStep[]): Content | undefined {
|
|
134
|
+
return quill.reader(doc).getContentAt(addr, path);
|
|
135
|
+
}
|
|
136
|
+
|
|
119
137
|
// ── Focus: one answer, two callers (`leaves.ts`) ─────────────────────────────
|
|
120
138
|
// A label click and the editor's `focusField`/`setCaret` ask the same question, so
|
|
121
139
|
// they read the same function and cannot land in different places. The four
|
|
@@ -247,6 +265,7 @@
|
|
|
247
265
|
id={domIds.control}
|
|
248
266
|
labelledBy={domIds.label}
|
|
249
267
|
{describedBy}
|
|
268
|
+
{contentAt}
|
|
250
269
|
onCommit={onCommitScalar}
|
|
251
270
|
{optionAllowed}
|
|
252
271
|
{enumDisallowed}
|
|
@@ -288,7 +307,7 @@
|
|
|
288
307
|
labelId={domIds.label}
|
|
289
308
|
descriptionId={domIds.description}
|
|
290
309
|
idBase={domIds.control}
|
|
291
|
-
|
|
310
|
+
{contentAt}
|
|
292
311
|
onCommit={onCommitScalar}
|
|
293
312
|
/>
|
|
294
313
|
{:else if field.control === 'object'}
|
|
@@ -300,6 +319,7 @@
|
|
|
300
319
|
idBase={domIds.control}
|
|
301
320
|
labelledBy={domIds.label}
|
|
302
321
|
{describedBy}
|
|
322
|
+
{contentAt}
|
|
303
323
|
onCommit={onCommitScalar}
|
|
304
324
|
/>
|
|
305
325
|
{:else}
|