@quillmark/svelte 0.1.1 → 0.2.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 +15 -11
- package/THEMING.md +42 -23
- package/dist/core/address.d.ts +39 -4
- package/dist/core/address.js +47 -19
- package/dist/core/codec/encode.js +4 -1
- package/dist/core/codec/field.d.ts +1 -1
- package/dist/core/codec/prose.css +41 -18
- package/dist/core/codec/table.js +3 -1
- package/dist/core/errors.d.ts +15 -5
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +5 -4
- package/dist/core/lifecycle.d.ts +21 -5
- package/dist/core/lifecycle.js +34 -8
- package/dist/core/rebind.svelte.d.ts +16 -2
- package/dist/core/rebind.svelte.js +26 -8
- package/dist/core/theme.css +43 -18
- package/dist/preset/recipes.css +55 -8
- package/dist/preset/scale.css +5 -4
- package/dist/preview/Preview.svelte +10 -9
- package/dist/preview/Preview.svelte.d.ts +5 -5
- package/dist/preview/bridge.d.ts +6 -4
- package/dist/preview/bridge.js +17 -7
- package/dist/preview/controller.d.ts +13 -6
- package/dist/preview/controller.js +5 -2
- package/dist/preview/geometry.d.ts +16 -1
- package/dist/preview/geometry.js +20 -0
- package/dist/preview/index.d.ts +1 -0
- package/dist/preview/index.js +7 -0
- package/dist/preview/overlay.d.ts +2 -1
- package/dist/preview/overlay.js +24 -9
- package/dist/preview/paint.js +4 -0
- package/dist/visual/ArrayField.svelte +26 -20
- package/dist/visual/ArrayField.svelte.d.ts +4 -3
- package/dist/visual/Card.svelte +21 -19
- package/dist/visual/Card.svelte.d.ts +8 -3
- package/dist/visual/DiagnosticList.svelte +1 -1
- package/dist/visual/EnumField.svelte +34 -14
- package/dist/visual/EnumField.svelte.d.ts +4 -3
- package/dist/visual/Field.svelte +87 -25
- package/dist/visual/Field.svelte.d.ts +11 -7
- package/dist/visual/FieldHint.svelte +59 -10
- package/dist/visual/FormatPopover.svelte +3 -1
- package/dist/visual/ObjectField.svelte +21 -2
- package/dist/visual/ObjectField.svelte.d.ts +3 -1
- package/dist/visual/ProseArrayElement.svelte +1 -6
- package/dist/visual/ProseArrayElement.svelte.d.ts +0 -1
- package/dist/visual/ProseField.svelte +20 -12
- package/dist/visual/ProseField.svelte.d.ts +5 -3
- package/dist/visual/TipsCard.svelte +1 -1
- package/dist/visual/VisualEditor.svelte +12 -13
- package/dist/visual/VisualEditor.svelte.d.ts +2 -3
- package/dist/visual/VisualEditorInner.svelte +140 -66
- package/dist/visual/VisualEditorInner.svelte.d.ts +2 -3
- package/dist/visual/controls.css +14 -11
- package/dist/visual/diagnostics.d.ts +12 -20
- package/dist/visual/diagnostics.js +6 -18
- package/dist/visual/index.d.ts +1 -0
- package/dist/visual/index.js +7 -0
- package/dist/visual/leaves.d.ts +47 -0
- package/dist/visual/leaves.js +31 -0
- package/dist/visual/props.d.ts +27 -6
- package/dist/visual/tips.d.ts +3 -1
- package/dist/visual/tips.js +5 -2
- package/package.json +2 -2
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
• commit routing: prose leaves lower to `applyChange` (in the codec); scalars/
|
|
17
17
|
arrays/objects go through the typed `writer`; structure through the mutators;
|
|
18
18
|
• focus + the bridge outputs (`onActiveLeafChange`, `onCaretMove`) and the
|
|
19
|
-
`setCaret(
|
|
19
|
+
`setCaret(at)` entry wired to the preview;
|
|
20
20
|
• the ONE formatting popover (`FormatPopover`, mounted once, observing the
|
|
21
21
|
active leaf via `getActiveLeaf`) and diagnostics routing (`diagnostics.ts`:
|
|
22
22
|
quill.validate + local commit errors + the external `diagnostics` prop,
|
|
@@ -34,24 +34,25 @@
|
|
|
34
34
|
import { onDestroy, tick } from 'svelte';
|
|
35
35
|
import { DropdownMenu } from 'bits-ui';
|
|
36
36
|
import { isQuillmarkError, MAIN_CARD_ADDR } from '@quillmark/wasm';
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
addrForFieldPath,
|
|
39
|
+
cardPath,
|
|
40
|
+
elementAddrForFieldPath,
|
|
41
|
+
fieldPathForAddr,
|
|
42
|
+
type DocPath,
|
|
43
|
+
type Landing
|
|
44
|
+
} from '../core/address.js';
|
|
38
45
|
import { errorMessage, reportError } from '../core/errors.js';
|
|
39
46
|
import { bloomInside } from '../core/bloom.js';
|
|
40
47
|
import { createLifespan } from '../core/teardown.js';
|
|
41
|
-
import type {
|
|
42
|
-
Addr,
|
|
43
|
-
CardAddr,
|
|
44
|
-
ContentHit,
|
|
45
|
-
Diagnostic,
|
|
46
|
-
Resolved,
|
|
47
|
-
ResolvedField
|
|
48
|
-
} from '@quillmark/wasm';
|
|
48
|
+
import type { Addr, CardAddr, Diagnostic, Resolved, ResolvedField } from '@quillmark/wasm';
|
|
49
49
|
import type { VisualEditorProps } from './props.js';
|
|
50
50
|
import { mergeStrings, setWording } from './strings.js';
|
|
51
51
|
import type { CardId, ChangeSource } from './signals.js';
|
|
52
52
|
import type { FieldController } from '../core/codec/index.js';
|
|
53
53
|
import {
|
|
54
54
|
IdSeq,
|
|
55
|
+
controlKind,
|
|
55
56
|
fieldModels,
|
|
56
57
|
groupOrder,
|
|
57
58
|
groupSections,
|
|
@@ -71,7 +72,6 @@
|
|
|
71
72
|
} from './structure.js';
|
|
72
73
|
import {
|
|
73
74
|
fieldKeyToString,
|
|
74
|
-
parsePath,
|
|
75
75
|
resolveCardKey,
|
|
76
76
|
routeAndResolve,
|
|
77
77
|
mergeDiagnostics,
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
type RoutedDiagnostic
|
|
80
80
|
} from './diagnostics.js';
|
|
81
81
|
import { fieldDomIds, groupPanelId } from './domid.js';
|
|
82
|
+
import { createLeafRegistry, type FieldControl } from './leaves.js';
|
|
82
83
|
import { reorder } from './motion.js';
|
|
83
84
|
import { tipsChannel } from './tips.js';
|
|
84
85
|
import { patchEditorExt } from './ext.js';
|
|
@@ -98,6 +99,7 @@
|
|
|
98
99
|
onError,
|
|
99
100
|
diagnostics,
|
|
100
101
|
enumOptionAllowed,
|
|
102
|
+
enumDisallowed = 'disable',
|
|
101
103
|
strings,
|
|
102
104
|
formatDiagnostic,
|
|
103
105
|
class: className,
|
|
@@ -187,8 +189,11 @@
|
|
|
187
189
|
const span = createLifespan();
|
|
188
190
|
onDestroy(() => span.end());
|
|
189
191
|
|
|
190
|
-
// ── Leaf registry (
|
|
191
|
-
|
|
192
|
+
// ── Leaf registry (landing target lookup + the active-leaf seam) ────────────
|
|
193
|
+
// Every mounted field is in it, prose leaf and form control alike (`leaves.ts`):
|
|
194
|
+
// a landing needs a focus and a box to bloom, which every control has, and the
|
|
195
|
+
// codec lane is asked for only where there is an offset to place.
|
|
196
|
+
const leaves = createLeafRegistry();
|
|
192
197
|
// Each leaf unregisters itself on its own teardown; this covers the surface
|
|
193
198
|
// going away as a whole, where a leaf's cleanup order relative to the parent's
|
|
194
199
|
// is Svelte's business and not something to depend on.
|
|
@@ -204,12 +209,6 @@
|
|
|
204
209
|
/** The stack's own element: it carries `data-qm-root`, so it is what the kind
|
|
205
210
|
* menu portals INTO, the way each leaf's surface resolves its nearest root. */
|
|
206
211
|
let rootEl = $state<HTMLElement | undefined>(undefined);
|
|
207
|
-
function register(key: string, controller: FieldController): void {
|
|
208
|
-
leaves.set(key, controller);
|
|
209
|
-
}
|
|
210
|
-
function unregister(key: string): void {
|
|
211
|
-
leaves.delete(key);
|
|
212
|
-
}
|
|
213
212
|
|
|
214
213
|
// ── Focus + bridge outputs ──────────────────────────────────────────────────
|
|
215
214
|
// `activeCardId` is the id-keyed half of `activeAddr` (whose `card` is positional),
|
|
@@ -670,64 +669,93 @@
|
|
|
670
669
|
|
|
671
670
|
// ── Public entry points ─────────────────────────────────────────────────────
|
|
672
671
|
/**
|
|
673
|
-
* Resolve a preview
|
|
672
|
+
* Resolve a preview {@link Landing} to a mounted leaf and land in it.
|
|
674
673
|
*
|
|
675
674
|
* Async because the reveal has to RENDER before the landing: a collapsed group
|
|
676
675
|
* is `inert`, which swallows a focus silently, so a caret placed in the same
|
|
677
676
|
* tick as the reveal would go nowhere and report nothing. The consumer's
|
|
678
|
-
* `
|
|
677
|
+
* `onPick` ignores the promise: awaiting it is for a caller that wants to
|
|
679
678
|
* observe where the caret went.
|
|
680
679
|
*
|
|
681
680
|
* A destroy lands INSIDE this one: `leaf` is looked up before the flush and
|
|
682
681
|
* dispatched into after it, and a PM view destroyed in that window throws on the
|
|
683
|
-
* dispatch. A consumer's `
|
|
684
|
-
*
|
|
682
|
+
* dispatch. A consumer's `onPick` outlives the surface it points at too, so the
|
|
683
|
+
* span is asked on the way in as well as after the await.
|
|
685
684
|
*/
|
|
686
|
-
export async function setCaret(
|
|
687
|
-
const
|
|
688
|
-
if (!
|
|
685
|
+
export async function setCaret(at: Landing): Promise<void> {
|
|
686
|
+
const found = await revealLeaf(at.field);
|
|
687
|
+
if (!found) return missed(`no mounted field at ${at.field}`, at.field);
|
|
689
688
|
// A `'segment'` hit landed on origin-less ink (list markers, a code fence's
|
|
690
689
|
// interior): `pos` is the segment START, not a cluster-exact caret
|
|
691
|
-
// (HitGranularity), so
|
|
692
|
-
//
|
|
693
|
-
//
|
|
694
|
-
|
|
695
|
-
|
|
690
|
+
// (HitGranularity), so it is dropped rather than snapped to a spot the click
|
|
691
|
+
// did not resolve. `'cluster'` (and an absent granularity: the backend did not
|
|
692
|
+
// report it, treat as exact) places the caret. An absent `pos` is the PLACEMENT
|
|
693
|
+
// rung and reaches the same floor.
|
|
694
|
+
land(found, at.granularity === 'segment' ? undefined : at.pos);
|
|
696
695
|
// The arrival cue. Unconditional, unlike the preview side's change-guarded
|
|
697
696
|
// bloom: a preview click is one discrete act, and its commonest target is the
|
|
698
697
|
// leaf ALREADY focused (where landing a caret changes nothing on screen) or
|
|
699
698
|
// one off-screen, where the browser's focus-scroll moves the page and leaves
|
|
700
699
|
// the caret to be hunted for in a long form.
|
|
701
|
-
bloomInside(
|
|
700
|
+
bloomInside(found.control.el);
|
|
702
701
|
}
|
|
703
|
-
/** The active leaf's controller: the formatting popover's observation seam.
|
|
702
|
+
/** The active leaf's controller: the formatting popover's observation seam.
|
|
703
|
+
* `undefined` for a focused form control, which holds no marks to toggle. */
|
|
704
704
|
export function getActiveLeaf(): FieldController | undefined {
|
|
705
705
|
if (!activeAddr) return undefined;
|
|
706
706
|
const card = activeAddr.card != null ? activeCardId : undefined;
|
|
707
|
-
return leaves.
|
|
707
|
+
return leaves.prose(fieldKeyToString({ card, field: activeAddr.field }));
|
|
708
708
|
}
|
|
709
709
|
|
|
710
710
|
/**
|
|
711
|
-
* Reveal the
|
|
712
|
-
* RENDERED. The shared half of the two landing
|
|
713
|
-
* to zero height and sits inside an `inert`
|
|
714
|
-
* so a caret placed in the same tick as the
|
|
715
|
-
* nothing. Exactly one card holds the key; the rest
|
|
711
|
+
* Reveal the field at `field` and hand back its landing handle plus the target it
|
|
712
|
+
* resolved to, once the reveal has RENDERED. The shared half of the two landing
|
|
713
|
+
* verbs: a collapsed group is clipped to zero height and sits inside an `inert`
|
|
714
|
+
* panel, which swallows a focus silently, so a caret placed in the same tick as the
|
|
715
|
+
* reveal goes nowhere and reports nothing. Exactly one card holds the key; the rest
|
|
716
|
+
* do nothing.
|
|
716
717
|
*
|
|
717
|
-
*
|
|
718
|
+
* The target comes back with the handle because the caller needs its key to ask for
|
|
719
|
+
* the codec lane (`leaves.prose`), and minting it twice would be two parses of one
|
|
720
|
+
* path. The reveal itself is the KEY's: a group holds the field, and an element
|
|
721
|
+
* address opens the same group its array sits in.
|
|
722
|
+
*
|
|
723
|
+
* A destroy lands INSIDE this: the field is looked up before the flush and
|
|
718
724
|
* dispatched into after it, and a PM view destroyed in that window throws on the
|
|
719
725
|
* dispatch. A consumer's call outlives the surface it points at too, so the span is
|
|
720
726
|
* asked on the way in as well as after the await.
|
|
721
727
|
*/
|
|
722
|
-
async function revealLeaf(field: DocPath): Promise<
|
|
728
|
+
async function revealLeaf(field: DocPath): Promise<Landed | undefined> {
|
|
723
729
|
if (!span.alive) return undefined;
|
|
724
|
-
const
|
|
725
|
-
const
|
|
726
|
-
if (!
|
|
727
|
-
mainCard?.revealLeaf(key
|
|
728
|
-
for (const card of cardRefs) card?.revealLeaf(key
|
|
730
|
+
const target = leafTargetFor(field);
|
|
731
|
+
const control = target && leaves.control(target.key);
|
|
732
|
+
if (!target || !control) return undefined;
|
|
733
|
+
mainCard?.revealLeaf(target.key);
|
|
734
|
+
for (const card of cardRefs) card?.revealLeaf(target.key);
|
|
729
735
|
if (!(await span.resumes(tick()))) return undefined;
|
|
730
|
-
return
|
|
736
|
+
return { ...target, control };
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Put the caret in a revealed target, at the finest grain it can take.
|
|
741
|
+
*
|
|
742
|
+
* - An ELEMENT address focuses that row, with no caret inside it: an array element
|
|
743
|
+
* is no `createField` leaf and its handle is a bare focus.
|
|
744
|
+
* - A prose leaf with a `pos` takes the caret at that USV offset.
|
|
745
|
+
* - Everything else takes the focus alone: the offset is a position in RENDERED
|
|
746
|
+
* content and a form control has no coordinate to spend it in (an
|
|
747
|
+
* `<input type="number">` refuses a selection outright), which is also the whole
|
|
748
|
+
* of what a click on plate-placed ink can mean.
|
|
749
|
+
*/
|
|
750
|
+
function land(found: Landed, pos: number | undefined): void {
|
|
751
|
+
if (found.element != null && found.control.focusElement) {
|
|
752
|
+
return found.control.focusElement(found.element);
|
|
753
|
+
}
|
|
754
|
+
if (pos != null) {
|
|
755
|
+
const prose = leaves.prose(found.key);
|
|
756
|
+
if (prose) return prose.setCaret(pos);
|
|
757
|
+
}
|
|
758
|
+
found.control.focus();
|
|
731
759
|
}
|
|
732
760
|
|
|
733
761
|
// ── The verbs, as instance exports ──────────────────────────────────────────
|
|
@@ -738,15 +766,24 @@
|
|
|
738
766
|
// card — so a host drives them with what the hooks handed it.
|
|
739
767
|
//
|
|
740
768
|
// A target the surface does not hold is a NO-OP that reports `target-unknown` at
|
|
741
|
-
// `dev`:
|
|
742
|
-
//
|
|
769
|
+
// `dev`: a key from a previous session or a card already removed, or a path naming
|
|
770
|
+
// no declared field — an ELEMENT path (`main.references.0`) whose array is
|
|
771
|
+
// undeclared or unmounted included, that being the only way one misses. `setCaret`
|
|
772
|
+
// reports it too, being the verb a preview click drives: a landing that resolved
|
|
773
|
+
// nothing is indistinguishable from one that landed, and a consumer wiring the
|
|
774
|
+
// bridge reads the difference off nothing else.
|
|
743
775
|
|
|
744
|
-
/**
|
|
776
|
+
/**
|
|
777
|
+
* Reveal and focus the field at `field`, without placing a caret inside it. Any
|
|
778
|
+
* mounted field: a prose leaf takes its view's focus, a form control takes the same
|
|
779
|
+
* handoff a click on its label does (`Field`, `leaves.ts`), and an element address
|
|
780
|
+
* takes its own row.
|
|
781
|
+
*/
|
|
745
782
|
export async function focusField(field: DocPath): Promise<void> {
|
|
746
|
-
const
|
|
747
|
-
if (!
|
|
748
|
-
|
|
749
|
-
bloomInside(
|
|
783
|
+
const found = await revealLeaf(field);
|
|
784
|
+
if (!found) return missed(`no mounted field at ${field}`, field);
|
|
785
|
+
land(found, undefined);
|
|
786
|
+
bloomInside(found.control.el);
|
|
750
787
|
}
|
|
751
788
|
/** Seed a card of `kind` and insert it at `at` (default: the end). Returns the new
|
|
752
789
|
* card's session key, or `undefined` when the quill seeds no card of that kind. */
|
|
@@ -777,14 +814,51 @@
|
|
|
777
814
|
reportError(onError, { code: 'target-unknown', severity: 'dev', message, path });
|
|
778
815
|
}
|
|
779
816
|
|
|
780
|
-
/**
|
|
781
|
-
*
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
817
|
+
/** What a `DocPath` resolves to in the mounted tree: a leaf key, and the array
|
|
818
|
+
* ELEMENT within it when the address names one. */
|
|
819
|
+
interface LeafTarget {
|
|
820
|
+
key: string;
|
|
821
|
+
element?: number;
|
|
822
|
+
}
|
|
823
|
+
/** A resolved target with the handle the registry holds for it. */
|
|
824
|
+
type Landed = LeafTarget & { control: FieldControl };
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Map a landing's `field` (a canonical `DocPath`) to a mounted target: the
|
|
828
|
+
* `addrForFieldPath` route the diagnostics take, the absolute card index resolved
|
|
829
|
+
* to its live stable id, then the shared `fieldKeyToString` form.
|
|
830
|
+
*
|
|
831
|
+
* TWO RUNGS, because the boundary mints addresses at a finer granularity than
|
|
832
|
+
* `Addr` can name: a `richtext[]` element surfaces as `main.references.0`, which
|
|
833
|
+
* the grammar reads as a field literally named `"0"`. The second rung reads that
|
|
834
|
+
* trailing segment as an INDEX, under a field the schema declares an array — which
|
|
835
|
+
* is why the ladder is the editor's (VISUAL_EDITOR.md §Surface): the preview
|
|
836
|
+
* carries no schema, and truncating the address there is worse than guessing, since
|
|
837
|
+
* `pos` is an offset into the ELEMENT's own content.
|
|
838
|
+
*/
|
|
839
|
+
function leafTargetFor(field: DocPath): LeafTarget | undefined {
|
|
840
|
+
const direct = addrForFieldPath(field);
|
|
841
|
+
if (direct) {
|
|
842
|
+
const resolved = resolveCardKey(direct, cardIds);
|
|
843
|
+
return resolved ? { key: fieldKeyToString(resolved) } : undefined;
|
|
844
|
+
}
|
|
845
|
+
const element = elementAddrForFieldPath(field);
|
|
846
|
+
if (!element || !isArrayField(element.field)) return undefined;
|
|
847
|
+
const resolved = resolveCardKey(element.field, cardIds);
|
|
848
|
+
return resolved ? { key: fieldKeyToString(resolved), element: element.index } : undefined;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** Whether `addr` names a field this document renders as an array repeater: the
|
|
852
|
+
* guard the element rung stands on. Asked of `controlKind`, so what the ladder
|
|
853
|
+
* tests is the control the tree mounted — the one holding a `focusElement` — and
|
|
854
|
+
* not a second reading of the schema beside it. */
|
|
855
|
+
function isArrayField(addr: Addr): boolean {
|
|
856
|
+
if (addr.field == null) return false;
|
|
857
|
+
const kind = addr.card == null ? undefined : model.cards[addr.card]?.kind;
|
|
858
|
+
const schema = quill.schema;
|
|
859
|
+
const card = addr.card == null ? schema.main : kind ? schema.card_kinds?.[kind] : undefined;
|
|
860
|
+
const declared = card?.fields?.[addr.field];
|
|
861
|
+
return !!declared && controlKind(declared) === 'array';
|
|
788
862
|
}
|
|
789
863
|
</script>
|
|
790
864
|
|
|
@@ -803,12 +877,12 @@
|
|
|
803
877
|
isLast={true}
|
|
804
878
|
{kinds}
|
|
805
879
|
ops={opsFor('main', true)}
|
|
880
|
+
{enumDisallowed}
|
|
806
881
|
onFocus={handleFocus}
|
|
807
882
|
onCaretMove={handleCaret}
|
|
808
883
|
onChange={proseChanged}
|
|
809
884
|
{onError}
|
|
810
|
-
{
|
|
811
|
-
{unregister}
|
|
885
|
+
{leaves}
|
|
812
886
|
/>
|
|
813
887
|
|
|
814
888
|
<!-- The tips card: a fixed slot after `main`, ahead of the cards, so
|
|
@@ -837,12 +911,12 @@
|
|
|
837
911
|
isLast={i === model.cards.length - 1}
|
|
838
912
|
{kinds}
|
|
839
913
|
ops={opsFor(c.id, false)}
|
|
914
|
+
{enumDisallowed}
|
|
840
915
|
onFocus={handleFocus}
|
|
841
916
|
onCaretMove={handleCaret}
|
|
842
917
|
onChange={proseChanged}
|
|
843
918
|
{onError}
|
|
844
|
-
{
|
|
845
|
-
{unregister}
|
|
919
|
+
{leaves}
|
|
846
920
|
/>
|
|
847
921
|
{@render addAffordance(i + 1)}
|
|
848
922
|
</div>
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { type DocPath } from '../core/address.js';
|
|
2
|
-
import type { ContentHit } from '@quillmark/wasm';
|
|
1
|
+
import { type DocPath, type Landing } from '../core/address.js';
|
|
3
2
|
import type { VisualEditorProps } from './props.js';
|
|
4
3
|
import type { CardId } from './signals.js';
|
|
5
4
|
import type { FieldController } from '../core/codec/index.js';
|
|
6
5
|
import './controls.css';
|
|
7
6
|
declare const VisualEditorInner: import("svelte").Component<VisualEditorProps, {
|
|
8
|
-
setCaret: (
|
|
7
|
+
setCaret: (at: Landing) => Promise<void>;
|
|
9
8
|
getActiveLeaf: () => FieldController | undefined;
|
|
10
9
|
focusField: (field: DocPath) => Promise<void>;
|
|
11
10
|
insertCard: (kind: string, at?: number) => CardId | undefined;
|
package/dist/visual/controls.css
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
@layer qm.chrome {
|
|
19
19
|
/* The themed focus ring in place of the raw UA outline, as a
|
|
20
|
-
RULE rather than a pair of rungs each control assembles. `--_qm-ring
|
|
20
|
+
RULE rather than a pair of rungs each control assembles. `--_qm-ring`
|
|
21
21
|
fixes the VALUE; which properties draw a ring, and on what state, is the other
|
|
22
22
|
half of the decision, and `check:style` sees a minted literal but never a
|
|
23
23
|
divergent rule. A control opts in by carrying the marker class.
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
it. That is the only distinction, and this pair is the whole of it. */
|
|
29
29
|
.qm-focus-ring:focus-visible,
|
|
30
30
|
.qm-focus-ring-within:focus-within {
|
|
31
|
-
outline: var(--_qm-ring
|
|
31
|
+
outline: var(--_qm-ring);
|
|
32
32
|
outline-offset: var(--_qm-ring-offset);
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -46,9 +46,10 @@
|
|
|
46
46
|
DIFFERENT rungs, which no gate can see: `check:style`'s type axis holds that
|
|
47
47
|
each side reads a rung, never that both read the same one.
|
|
48
48
|
|
|
49
|
-
The list is wider than either recipe's chrome:
|
|
49
|
+
The list is wider than either recipe's chrome: a prose leaf withholds the box
|
|
50
50
|
and still reads the ramp, so `.qm-prose` is
|
|
51
|
-
here and not below.
|
|
51
|
+
here and not below. The BODY leaf steps off this rung onto `--_qm-text-paper`
|
|
52
|
+
from its own block (`ProseField`).
|
|
52
53
|
|
|
53
54
|
`font` is a shorthand carrying `line-height`, so each family's leading is
|
|
54
55
|
declared in the rules BELOW and would be dropped in silence above them. */
|
|
@@ -70,9 +71,11 @@
|
|
|
70
71
|
green while a control renders subtly the wrong height).
|
|
71
72
|
|
|
72
73
|
LEADING sits on the wide list rather than on the box's own, because a box's
|
|
73
|
-
height is
|
|
74
|
-
input beside it agree on height exactly as long as they agree on leading
|
|
75
|
-
is the whole of what "by construction" buys.
|
|
74
|
+
height is an inset rung plus a line box plus two hairlines: a leaf and the
|
|
75
|
+
input beside it agree on height exactly as long as they agree on leading and on
|
|
76
|
+
size, which is the whole of what "by construction" buys. The body leaf keeps the
|
|
77
|
+
leading and steps off the size, which costs it nothing: it draws no box and
|
|
78
|
+
stands in no row.
|
|
76
79
|
|
|
77
80
|
COLOUR is the trap one property over: `font` does not carry it either, so an
|
|
78
81
|
input or trigger left off this line falls to the UA's own text colour: a second
|
|
@@ -90,7 +93,7 @@
|
|
|
90
93
|
}
|
|
91
94
|
.qm-control-box,
|
|
92
95
|
.qm-input {
|
|
93
|
-
padding: var(--_qm-
|
|
96
|
+
padding: var(--_qm-inset-control);
|
|
94
97
|
border: var(--_qm-border-width) solid var(--_qm-border);
|
|
95
98
|
border-radius: var(--_qm-radius-inner);
|
|
96
99
|
background: var(--_qm-surface);
|
|
@@ -137,7 +140,7 @@
|
|
|
137
140
|
which is why it is here and not on one family, and it is what keeps a button off
|
|
138
141
|
the host page's leading the way the size rung keeps it off the host page's body
|
|
139
142
|
size. A caller whose button carries a LABEL restates the rung it wants from its
|
|
140
|
-
own block: the tips foot takes the
|
|
143
|
+
own block: the tips foot takes the label rung, which is that surface's decision,
|
|
141
144
|
not the family's. */
|
|
142
145
|
.qm-icon-btn,
|
|
143
146
|
.qm-add-affordance {
|
|
@@ -244,10 +247,10 @@
|
|
|
244
247
|
-webkit-backdrop-filter: blur(var(--_qm-blur));
|
|
245
248
|
border: var(--_qm-border-width) solid var(--_qm-border);
|
|
246
249
|
border-radius: var(--_qm-radius);
|
|
247
|
-
animation: qm-pop-in var(--_qm-duration-fast) var(--_qm-ease-
|
|
250
|
+
animation: qm-pop-in var(--_qm-duration-fast) var(--_qm-ease-reverse);
|
|
248
251
|
}
|
|
249
252
|
.qm-popover-surface[data-ending-style] {
|
|
250
|
-
animation: qm-pop-out var(--_qm-duration-fast) var(--_qm-ease-
|
|
253
|
+
animation: qm-pop-out var(--_qm-duration-fast) var(--_qm-ease-reverse);
|
|
251
254
|
}
|
|
252
255
|
@keyframes qm-pop-in {
|
|
253
256
|
from {
|
|
@@ -1,28 +1,20 @@
|
|
|
1
1
|
import type { Diagnostic } from '@quillmark/wasm';
|
|
2
|
-
/** A field's routing address
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
/** A field's routing address, and `/core`'s `Addr` structurally: an `Addr` IS a
|
|
3
|
+
* positional `FieldKey`, so `addrForFieldPath` is the path→key walk and routing
|
|
4
|
+
* carries no second copy of the grammar. `card` is `undefined` for the main card; a
|
|
5
|
+
* composable card slot is a stable session id (the editor's own bookkeeping) once
|
|
6
|
+
* resolved, or an ABSOLUTE document-array index straight off a parsed `DocPath`
|
|
7
|
+
* (resolve via {@link resolveCardKey} before merging with id-keyed sources). `field`
|
|
8
|
+
* `undefined` addresses the body (the field-less leaf). */
|
|
7
9
|
export interface FieldKey {
|
|
8
10
|
card?: string | number;
|
|
9
11
|
field?: string;
|
|
10
12
|
}
|
|
11
13
|
/** Canonical string key for a `FieldKey`: the routing `Map`'s key space. */
|
|
12
14
|
export declare function fieldKeyToString(k: FieldKey): string;
|
|
13
|
-
/**
|
|
14
|
-
* Route a canonical `DocPath` (a `Diagnostic.path`, or a `ContentHit` /
|
|
15
|
-
* `FieldRegion` field address; one grammar) to a `FieldKey`, `card` the ABSOLUTE
|
|
16
|
-
* document-array index. `/core`'s `addrForFieldPath` IS this walk — an `Addr` is a
|
|
17
|
-
* positional `FieldKey` — so routing reads the one public inverse rather than a
|
|
18
|
-
* second copy of the grammar that drifts from it. `undefined` for a path naming no
|
|
19
|
-
* single commit address (a nested / array-element path, a field-rooted one, a
|
|
20
|
-
* malformed one).
|
|
21
|
-
*/
|
|
22
|
-
export declare function parsePath(path: string): FieldKey | undefined;
|
|
23
15
|
/**
|
|
24
16
|
* Resolve a positional `FieldKey` (an ABSOLUTE document-array index, straight off
|
|
25
|
-
*
|
|
17
|
+
* `addrForFieldPath`) to the editor's stable-id keying using the LIVE `cardIds`
|
|
26
18
|
* array: re-resolved fresh on every call (never cached), the same "resolve only
|
|
27
19
|
* at the point of use" discipline `cardIndexOf` applies to writes (VISUAL_EDITOR
|
|
28
20
|
* §"The address is the spine"). A key already id-keyed (`card` a string) or main
|
|
@@ -37,10 +29,10 @@ export interface RoutedDiagnostic {
|
|
|
37
29
|
}
|
|
38
30
|
/**
|
|
39
31
|
* Route a path-keyed producer's raw `Diagnostic[]` (validate / external) to the
|
|
40
|
-
* editor's stable-id keying: the ONE door producers #1/#3 take, `
|
|
41
|
-
* `resolveCardKey` in a single pass. An entry drops rather than mis-routes when
|
|
42
|
-
* carries no `path`, when
|
|
43
|
-
*
|
|
32
|
+
* editor's stable-id keying: the ONE door producers #1/#3 take, `addrForFieldPath`
|
|
33
|
+
* and `resolveCardKey` in a single pass. An entry drops rather than mis-routes when
|
|
34
|
+
* it carries no `path`, when the path names no single commit address (a nested /
|
|
35
|
+
* array-element one), or when its absolute card index is out of the live `cardIds`.
|
|
44
36
|
*/
|
|
45
37
|
export declare function routeAndResolve(diagnostics: Diagnostic[] | undefined, cardIds: readonly string[]): RoutedDiagnostic[];
|
|
46
38
|
/**
|
|
@@ -3,21 +3,9 @@ import { addrForFieldPath } from '../core/address.js';
|
|
|
3
3
|
export function fieldKeyToString(k) {
|
|
4
4
|
return `${k.card ?? 'main'}:${k.field ?? '$body'}`;
|
|
5
5
|
}
|
|
6
|
-
/**
|
|
7
|
-
* Route a canonical `DocPath` (a `Diagnostic.path`, or a `ContentHit` /
|
|
8
|
-
* `FieldRegion` field address; one grammar) to a `FieldKey`, `card` the ABSOLUTE
|
|
9
|
-
* document-array index. `/core`'s `addrForFieldPath` IS this walk — an `Addr` is a
|
|
10
|
-
* positional `FieldKey` — so routing reads the one public inverse rather than a
|
|
11
|
-
* second copy of the grammar that drifts from it. `undefined` for a path naming no
|
|
12
|
-
* single commit address (a nested / array-element path, a field-rooted one, a
|
|
13
|
-
* malformed one).
|
|
14
|
-
*/
|
|
15
|
-
export function parsePath(path) {
|
|
16
|
-
return addrForFieldPath(path);
|
|
17
|
-
}
|
|
18
6
|
/**
|
|
19
7
|
* Resolve a positional `FieldKey` (an ABSOLUTE document-array index, straight off
|
|
20
|
-
*
|
|
8
|
+
* `addrForFieldPath`) to the editor's stable-id keying using the LIVE `cardIds`
|
|
21
9
|
* array: re-resolved fresh on every call (never cached), the same "resolve only
|
|
22
10
|
* at the point of use" discipline `cardIndexOf` applies to writes (VISUAL_EDITOR
|
|
23
11
|
* §"The address is the spine"). A key already id-keyed (`card` a string) or main
|
|
@@ -32,17 +20,17 @@ export function resolveCardKey(key, cardIds) {
|
|
|
32
20
|
}
|
|
33
21
|
/**
|
|
34
22
|
* Route a path-keyed producer's raw `Diagnostic[]` (validate / external) to the
|
|
35
|
-
* editor's stable-id keying: the ONE door producers #1/#3 take, `
|
|
36
|
-
* `resolveCardKey` in a single pass. An entry drops rather than mis-routes when
|
|
37
|
-
* carries no `path`, when
|
|
38
|
-
*
|
|
23
|
+
* editor's stable-id keying: the ONE door producers #1/#3 take, `addrForFieldPath`
|
|
24
|
+
* and `resolveCardKey` in a single pass. An entry drops rather than mis-routes when
|
|
25
|
+
* it carries no `path`, when the path names no single commit address (a nested /
|
|
26
|
+
* array-element one), or when its absolute card index is out of the live `cardIds`.
|
|
39
27
|
*/
|
|
40
28
|
export function routeAndResolve(diagnostics, cardIds) {
|
|
41
29
|
const out = [];
|
|
42
30
|
for (const d of diagnostics ?? []) {
|
|
43
31
|
if (!d.path)
|
|
44
32
|
continue;
|
|
45
|
-
const key =
|
|
33
|
+
const key = addrForFieldPath(d.path);
|
|
46
34
|
const resolved = key && resolveCardKey(key, cardIds);
|
|
47
35
|
if (resolved)
|
|
48
36
|
out.push({ key: resolved, diagnostic: d });
|
package/dist/visual/index.d.ts
CHANGED
package/dist/visual/index.js
CHANGED
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
// The barrel carries what a consumer story wants, and nothing else: a symbol
|
|
9
9
|
// earns its line when a caller outside the package needs it, and grows one when
|
|
10
10
|
// that caller arrives, not before.
|
|
11
|
+
// The theme derivation, which every control on this surface reads through `var()`.
|
|
12
|
+
// Imported at the BARREL because a subpath is what a consumer gets: this surface
|
|
13
|
+
// reaches modules inside `core/` and never its entry, so a sheet hanging off that
|
|
14
|
+
// entry arrives only for a consumer importing `/core` for some other reason. The
|
|
15
|
+
// sheet rather than the entry, because the barrel needs the derivation and not
|
|
16
|
+
// `init`. `check:deps` (rule 4) walks this file's own graph and holds the reach.
|
|
17
|
+
import '../core/theme.css';
|
|
11
18
|
export { default as VisualEditor } from './VisualEditor.svelte';
|
|
12
19
|
// The prose leaf: the vanilla mount API a non-Svelte consumer (or a custom
|
|
13
20
|
// surface) builds on.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { FieldController } from '../core/codec/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* A mounted field's landing handle: focus it, and the element the arrival wash blooms
|
|
4
|
+
* inside ({@link import('../core/bloom.js').bloomInside}).
|
|
5
|
+
*
|
|
6
|
+
* A `FieldController` is one, which is what lets the two lanes share a lookup; `Field`
|
|
7
|
+
* builds the same shape over a form control off the handoff a label click already
|
|
8
|
+
* takes, so `focusField` and a label cannot land in different places.
|
|
9
|
+
*/
|
|
10
|
+
export interface FieldControl {
|
|
11
|
+
focus(): void;
|
|
12
|
+
/**
|
|
13
|
+
* Focus element `index` of a control that HAS elements, resolving the index to the
|
|
14
|
+
* element's session id at the call; {@link focus}'s own answer for an index past
|
|
15
|
+
* the live list. Absent on every control without elements.
|
|
16
|
+
*
|
|
17
|
+
* The granularity rides the CALL because the registry stays parent-keyed: a
|
|
18
|
+
* per-element key is positional, in a registry whose doctrine is dodging
|
|
19
|
+
* positional churn.
|
|
20
|
+
*/
|
|
21
|
+
focusElement?(index: number): void;
|
|
22
|
+
readonly el: HTMLElement;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The registry the editor holds and the mounted tree writes into.
|
|
26
|
+
*
|
|
27
|
+
* A LANE IS DROPPED BY ITS OWN OWNER, which is what the symmetric pairs are for: a
|
|
28
|
+
* card retyped to a kind that declares the same field name at another type swaps the
|
|
29
|
+
* control under one leaf key without remounting `Field`, so the leaving control and
|
|
30
|
+
* the arriving prose leaf both touch that key with no order between them to rely on.
|
|
31
|
+
*/
|
|
32
|
+
export interface LeafRegistry {
|
|
33
|
+
/** A prose leaf, by its controller: the landing handle and the codec seam at once. */
|
|
34
|
+
registerProse(key: string, controller: FieldController): void;
|
|
35
|
+
unregisterProse(key: string): void;
|
|
36
|
+
/** A form control: the landing lane only, having no codec half to offer. */
|
|
37
|
+
registerControl(key: string, control: FieldControl): void;
|
|
38
|
+
unregisterControl(key: string): void;
|
|
39
|
+
/** The landing handle at `key`, whichever lane holds it. */
|
|
40
|
+
control(key: string): FieldControl | undefined;
|
|
41
|
+
/** The codec seam at `key`, `undefined` for a form control. */
|
|
42
|
+
prose(key: string): FieldController | undefined;
|
|
43
|
+
/** Drop everything: the surface going away as a whole, where a leaf's cleanup
|
|
44
|
+
* order relative to the parent's is Svelte's business. */
|
|
45
|
+
clear(): void;
|
|
46
|
+
}
|
|
47
|
+
export declare function createLeafRegistry(): LeafRegistry;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function createLeafRegistry() {
|
|
2
|
+
const controls = new Map();
|
|
3
|
+
const proses = new Map();
|
|
4
|
+
return {
|
|
5
|
+
registerProse(key, controller) {
|
|
6
|
+
proses.set(key, controller);
|
|
7
|
+
},
|
|
8
|
+
unregisterProse(key) {
|
|
9
|
+
proses.delete(key);
|
|
10
|
+
},
|
|
11
|
+
registerControl(key, control) {
|
|
12
|
+
controls.set(key, control);
|
|
13
|
+
},
|
|
14
|
+
unregisterControl(key) {
|
|
15
|
+
controls.delete(key);
|
|
16
|
+
},
|
|
17
|
+
// The prose map is read SECOND: a controller is a control, so a leaf answers a
|
|
18
|
+
// landing without a second entry, and a key the two lanes overlap on during a
|
|
19
|
+
// retype resolves to the form control the tree is currently rendering.
|
|
20
|
+
control(key) {
|
|
21
|
+
return controls.get(key) ?? proses.get(key);
|
|
22
|
+
},
|
|
23
|
+
prose(key) {
|
|
24
|
+
return proses.get(key);
|
|
25
|
+
},
|
|
26
|
+
clear() {
|
|
27
|
+
controls.clear();
|
|
28
|
+
proses.clear();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|