@quillmark/svelte 0.1.1 → 0.3.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 +16 -12
- package/THEMING.md +48 -25
- package/dist/core/address.d.ts +42 -7
- package/dist/core/address.js +49 -21
- package/dist/core/bloom.d.ts +5 -5
- package/dist/core/bloom.js +8 -8
- package/dist/core/codec/anchor.d.ts +1 -1
- package/dist/core/codec/anchor.js +1 -1
- package/dist/core/codec/atoms.d.ts +10 -0
- package/dist/core/codec/atoms.js +78 -0
- package/dist/core/codec/clipboard.d.ts +59 -0
- package/dist/core/codec/clipboard.js +202 -0
- package/dist/core/codec/code.d.ts +1 -1
- package/dist/core/codec/code.js +4 -4
- package/dist/core/codec/decode.d.ts +1 -1
- package/dist/core/codec/decode.js +8 -8
- package/dist/core/codec/encode.d.ts +4 -4
- package/dist/core/codec/encode.js +14 -11
- package/dist/core/codec/field.d.ts +22 -27
- package/dist/core/codec/field.js +33 -32
- package/dist/core/codec/index.js +5 -8
- package/dist/core/codec/inputrules.js +56 -18
- package/dist/core/codec/islands.d.ts +11 -7
- package/dist/core/codec/islands.js +16 -10
- package/dist/core/codec/keymap.d.ts +5 -6
- package/dist/core/codec/keymap.js +8 -7
- package/dist/core/codec/lists.d.ts +3 -3
- package/dist/core/codec/lists.js +32 -13
- package/dist/core/codec/marks.d.ts +1 -1
- package/dist/core/codec/marks.js +1 -1
- package/dist/core/codec/positions.js +1 -1
- package/dist/core/codec/prose.css +266 -228
- package/dist/core/codec/schema.js +4 -4
- package/dist/core/codec/slash.d.ts +39 -41
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-view.d.ts +12 -16
- package/dist/core/codec/table-view.js +488 -292
- package/dist/core/codec/table.d.ts +42 -15
- package/dist/core/codec/table.js +73 -27
- package/dist/core/errors.d.ts +17 -7
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +6 -5
- package/dist/core/lifecycle.d.ts +21 -5
- package/dist/core/lifecycle.js +34 -8
- package/dist/core/rebind.svelte.d.ts +18 -4
- package/dist/core/rebind.svelte.js +30 -12
- package/dist/core/teardown.d.ts +1 -1
- package/dist/core/teardown.js +1 -1
- package/dist/core/theme.css +173 -58
- package/dist/preset/index.css +1 -1
- package/dist/preset/recipes.css +79 -30
- package/dist/preset/scale.css +40 -28
- package/dist/preview/Preview.svelte +22 -19
- package/dist/preview/Preview.svelte.d.ts +8 -8
- package/dist/preview/bridge.d.ts +6 -4
- package/dist/preview/bridge.js +19 -9
- package/dist/preview/controller.d.ts +13 -6
- package/dist/preview/controller.js +8 -5
- package/dist/preview/geometry.d.ts +17 -2
- package/dist/preview/geometry.js +21 -1
- package/dist/preview/index.d.ts +1 -0
- package/dist/preview/index.js +9 -2
- package/dist/preview/overlay.d.ts +2 -1
- package/dist/preview/overlay.js +26 -11
- package/dist/preview/paint.js +19 -17
- package/dist/visual/ArrayField.svelte +103 -83
- package/dist/visual/ArrayField.svelte.d.ts +5 -4
- package/dist/visual/BooleanField.svelte +8 -6
- package/dist/visual/BooleanField.svelte.d.ts +2 -2
- package/dist/visual/Card.svelte +161 -333
- package/dist/visual/Card.svelte.d.ts +5 -4
- package/dist/visual/CardControls.svelte +1 -1
- package/dist/visual/DateField.svelte +19 -19
- package/dist/visual/DateField.svelte.d.ts +3 -3
- package/dist/visual/DiagnosticList.svelte +2 -2
- package/dist/visual/EnumField.svelte +48 -28
- package/dist/visual/EnumField.svelte.d.ts +5 -4
- package/dist/visual/Field.svelte +207 -132
- package/dist/visual/Field.svelte.d.ts +11 -7
- package/dist/visual/FieldHint.svelte +79 -48
- package/dist/visual/FieldHint.svelte.d.ts +1 -1
- package/dist/visual/FieldLabel.svelte +8 -8
- package/dist/visual/FormatPopover.svelte +82 -35
- package/dist/visual/NumberField.svelte +5 -5
- package/dist/visual/NumberField.svelte.d.ts +1 -1
- package/dist/visual/ObjectField.svelte +28 -9
- package/dist/visual/ObjectField.svelte.d.ts +4 -2
- package/dist/visual/ProseArrayElement.svelte +12 -19
- package/dist/visual/ProseArrayElement.svelte.d.ts +1 -2
- package/dist/visual/ProseField.svelte +62 -85
- package/dist/visual/ProseField.svelte.d.ts +14 -17
- package/dist/visual/SlashMenu.svelte +42 -16
- package/dist/visual/TextField.svelte +5 -5
- package/dist/visual/TextField.svelte.d.ts +1 -1
- package/dist/visual/TipsCard.svelte +43 -28
- package/dist/visual/VisualEditor.svelte +18 -19
- package/dist/visual/VisualEditor.svelte.d.ts +2 -3
- package/dist/visual/VisualEditorInner.svelte +251 -184
- package/dist/visual/VisualEditorInner.svelte.d.ts +2 -3
- package/dist/visual/controls.css +85 -38
- package/dist/visual/diagnostics.d.ts +13 -21
- package/dist/visual/diagnostics.js +7 -19
- package/dist/visual/domid.js +3 -3
- package/dist/visual/ext.d.ts +1 -1
- package/dist/visual/ext.js +1 -1
- package/dist/visual/icons/nodes.d.ts +3 -0
- package/dist/visual/icons/nodes.js +2 -1
- package/dist/visual/index.d.ts +1 -0
- package/dist/visual/index.js +10 -3
- package/dist/visual/leaves.d.ts +47 -0
- package/dist/visual/leaves.js +31 -0
- package/dist/visual/links.d.ts +24 -0
- package/dist/visual/links.js +67 -0
- package/dist/visual/props.d.ts +33 -12
- package/dist/visual/signals.d.ts +6 -6
- package/dist/visual/strings.d.ts +14 -19
- package/dist/visual/strings.js +5 -7
- package/dist/visual/structure.d.ts +24 -17
- package/dist/visual/structure.js +8 -8
- package/dist/visual/synced.svelte.d.ts +1 -1
- package/dist/visual/synced.svelte.js +4 -4
- package/dist/visual/tips.d.ts +4 -2
- package/dist/visual/tips.js +7 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -26,10 +26,12 @@ Each subpath is its own module root; a bundler pulls only what the entry you imp
|
|
|
26
26
|
The **consumer** owns the session and the handles, and drives every edit; the surfaces are views over it. The handles come from the `@quillmark/wasm` peer, never re-exported here.
|
|
27
27
|
|
|
28
28
|
```ts
|
|
29
|
-
import {
|
|
29
|
+
import { Engine } from '@quillmark/wasm';
|
|
30
30
|
import { init } from '@quillmark/svelte/core';
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
// One-time, and the only door to Quill and Document: the artifact exports
|
|
33
|
+
// neither statically.
|
|
34
|
+
const { Quill, Document } = await init();
|
|
33
35
|
|
|
34
36
|
const quill = Quill.fromTree(tree); // tree: Map<string, Uint8Array> of the quill dir
|
|
35
37
|
|
|
@@ -49,7 +51,7 @@ const session = await new Engine().open(quill, doc);
|
|
|
49
51
|
A stored document carries none of its quill's bytes, only a reference: `doc.quillRef` is `name@version`, persisted in the markdown itself. Resolution is **host code**: read the ref, map it to a `Quill`, open. No surface resolves, so one resolution per document holds by construction.
|
|
50
52
|
|
|
51
53
|
```ts
|
|
52
|
-
const doc = Document.fromMarkdown(stored);
|
|
54
|
+
const doc = Document.fromMarkdown(stored); // Document, from the gate above
|
|
53
55
|
const quill = await registry.getQuill(doc.quillRef); // your ref → Quill mapping
|
|
54
56
|
const session = await new Engine().open(quill, doc);
|
|
55
57
|
```
|
|
@@ -67,8 +69,8 @@ import { createPreview } from '@quillmark/svelte/preview';
|
|
|
67
69
|
|
|
68
70
|
const preview = createPreview(session, {
|
|
69
71
|
container: document.querySelector('#preview')!,
|
|
70
|
-
|
|
71
|
-
/* a click resolved to a
|
|
72
|
+
onPick: (at) => {
|
|
73
|
+
/* a click resolved to an address, with a caret where the compile has one */
|
|
72
74
|
},
|
|
73
75
|
onError: (err) => {
|
|
74
76
|
/* a page paint the backend refused; the preview shows its error state either way */
|
|
@@ -79,9 +81,9 @@ const preview = createPreview(session, {
|
|
|
79
81
|
preview.refresh(session.update(doc)); // repaint dirtyPages ∩ visible
|
|
80
82
|
```
|
|
81
83
|
|
|
82
|
-
In Svelte, `<Preview {session}
|
|
84
|
+
In Svelte, `<Preview {session} onPick={…} />` exposes the same verbs (`refresh`, `scrollToField`, `focusPosition`, `setZoom`) via `bind:this`.
|
|
83
85
|
|
|
84
|
-
`<Preview>` binds **once**, at mount: swapping `session`, `margin`, `overlays`, `
|
|
86
|
+
`<Preview>` binds **once**, at mount: swapping `session`, `margin`, `overlays`, `onPick`, `onError` or `strings` in place changes nothing on screen and reports `rebind-ignored` through `onError` at `dev` severity, naming the prop. Swap by remounting (`{#key session}`); drive in-place edits through `refresh(change)`. `class` and `style` are the exceptions, landing on the root element and staying live.
|
|
85
87
|
|
|
86
88
|
## Visual editor
|
|
87
89
|
|
|
@@ -135,7 +137,7 @@ Hand it a different `doc` and the editor **re-keys itself**: every leaf remounts
|
|
|
135
137
|
<button onclick={() => editor?.focusField('main.subject')}>Jump to subject</button>
|
|
136
138
|
```
|
|
137
139
|
|
|
138
|
-
`insertCard` hands back the new card's `cardId`; `removeCard`, `moveCard` and `setKind` take one. A card key or a path the surface does not hold is a no-op that reports `target-unknown` through `onError` at `dev` severity.
|
|
140
|
+
`insertCard` hands back the new card's `cardId`; `removeCard`, `moveCard` and `setKind` take one. `focusField` reaches any mounted field: a prose leaf takes its view's focus, a form control the same handoff a click on its label takes, and either way the group holding it is revealed first. A card key or a path the surface does not hold is a no-op that reports `target-unknown` through `onError` at `dev` severity.
|
|
139
141
|
|
|
140
142
|
### Wording
|
|
141
143
|
|
|
@@ -147,7 +149,7 @@ The package ships English and a seam to replace it. `strings` is keyed and **par
|
|
|
147
149
|
{quill}
|
|
148
150
|
strings={{
|
|
149
151
|
cardDelete: 'Supprimer la carte',
|
|
150
|
-
|
|
152
|
+
addCard: '+ Ajouter une carte'
|
|
151
153
|
}}
|
|
152
154
|
formatDiagnostic={(d) =>
|
|
153
155
|
d.code === 'validation::must_fill' ? `${d.path} est obligatoire` : undefined}
|
|
@@ -191,13 +193,15 @@ This shell layer is deliberately yours: the debounce value, what applies at once
|
|
|
191
193
|
The bridge lives at the **consumer** layer and is opt-in; the editor is unaware of the preview, the preview unaware of the editor. Both hops are pass-throughs: the two surfaces already speak one address grammar, the canonical `DocPath`.
|
|
192
194
|
|
|
193
195
|
```ts
|
|
194
|
-
// preview → editor: a click resolved to
|
|
195
|
-
|
|
196
|
+
// preview → editor: a click resolved to an address lands in the field it names.
|
|
197
|
+
onPick: (at) => visualEditor.setCaret(at);
|
|
196
198
|
|
|
197
199
|
// editor → preview: a caret move scrolls the preview to follow it.
|
|
198
200
|
onCaretMove: preview.focusPosition;
|
|
199
201
|
```
|
|
200
202
|
|
|
203
|
+
A pick carries a caret where the compile tracks the content under the point, and the field alone where it tracks only the placement — a scalar the plate prints without tracking. `setCaret` takes both: an absent `pos` reveals and focuses the field, which is the whole of what a click on plate-placed ink can mean. The address may name an array element (`main.references.0`), which lands on that row.
|
|
204
|
+
|
|
201
205
|
A consumer holding an `Addr` of its own maps it with `fieldPathForAddr` (from `@quillmark/svelte/core`). The playground app's split-pane route is the full reference shell: one session, both bridge directions, the preview following edits, and diagnostics routed inline.
|
|
202
206
|
|
|
203
207
|
## Errors
|
|
@@ -223,7 +227,7 @@ This is not the diagnostics channel. A `Diagnostic` is about the **document** an
|
|
|
223
227
|
el.textContent = doc.toMarkdown();
|
|
224
228
|
```
|
|
225
229
|
|
|
226
|
-
|
|
230
|
+
A worked one holds the scroll across the swap and shows a throw in place; the call under it is still the one line above.
|
|
227
231
|
|
|
228
232
|
## Theming
|
|
229
233
|
|
package/THEMING.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`@quillmark/svelte` ships complex UX over a thin skin: the surfaces carry the behavior (direct manipulation, the caret bridge, per-field state) against a neutral visual baseline a consumer restyles to its brand without fighting baked-in design.
|
|
4
4
|
|
|
5
|
-
There are three depths to that
|
|
5
|
+
There are three depths to that. An app mounting into an unstyled page stops at the first; an app with a plate of its own starts at the second:
|
|
6
6
|
|
|
7
7
|
| | You write | You get |
|
|
8
8
|
| ----------------- | --------------------- | ------------------------------------------------------- |
|
|
@@ -39,20 +39,28 @@ The whole contract is **ten CSS custom properties**. They are dials, not a palet
|
|
|
39
39
|
}
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
### Two of them are not branding
|
|
43
|
+
|
|
44
|
+
`--qm-bg` and `--qm-fg` are the two a themed host sets before any brand decision. Their defaults are a self-consistent neutral for a bare mounting site, not a guess at your page.
|
|
45
|
+
|
|
46
|
+
`--qm-bg` is the **card**, not the column behind it: the plane a document is written on, which is also the sheet the preview paints. Everything else is a step off it, and the column the cards sit in is a step down, so a mounted editor draws its own ground and the cards read as islands on it. Give it the tone you want a card to be — white, in most light themes — and let the column fall out. If you want the column to meet your page instead, set a `background` of your own on `.qm-editor`; your CSS is unlayered and wins.
|
|
47
|
+
|
|
48
|
+
The poles take a colour, not `transparent` or `inherit`: the raised surfaces, the borders and the muted inks are each a `color-mix` against `--qm-bg`, so a transparent pole makes every one of them translucent instead of letting your page through. Hand over the value your page is painted with and the derivation steps off it.
|
|
49
|
+
|
|
42
50
|
### The dials
|
|
43
51
|
|
|
44
|
-
| Token | Default | What it sets
|
|
45
|
-
| ---------------- | -------------------------------------- |
|
|
46
|
-
| `--qm-bg` | `#fff` / `#
|
|
47
|
-
| `--qm-fg` | `#
|
|
48
|
-
| `--qm-accent` | `#2563eb` | Focus rings, active marks, the preview's active field box.
|
|
49
|
-
| `--qm-danger` | `#c5221f` | Error diagnostics, the required marker, the delete glyph.
|
|
50
|
-
| `--qm-warning` | `#b25000` | Warning diagnostics.
|
|
51
|
-
| `--qm-font` | `ui-sans-serif, system-ui, sans-serif` | The editor surface's font family. Controls and buttons take it too, in place of the UA face.
|
|
52
|
-
| `--qm-font-mono` | `ui-monospace, monospace` | The monospace face: the JSON array control, the tips card.
|
|
53
|
-
| `--qm-font-size` | `0.875rem` |
|
|
54
|
-
| `--qm-radius` | `8px` | Card & popover corner. Interior controls derive a tighter tier (half).
|
|
55
|
-
| `--qm-space` | `0.25rem` | Spacing base. Gaps and insets are `half`/`1×`/`2×`/`3×`/`4×` multiples of it.
|
|
52
|
+
| Token | Default | What it sets |
|
|
53
|
+
| ---------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
54
|
+
| `--qm-bg` | `#fff` / `#272729` | Base surface: a card, and the preview's sheet. Fields, the popover and the column behind the cards each step off it. |
|
|
55
|
+
| `--qm-fg` | `#1c1c1c` / `#d7dadc` | Base ink. Body text, labels, and borders step off it. |
|
|
56
|
+
| `--qm-accent` | `#2563eb` | Focus rings, active marks, the preview's active field box. |
|
|
57
|
+
| `--qm-danger` | `#c5221f` | Error diagnostics, the required marker, the delete glyph. |
|
|
58
|
+
| `--qm-warning` | `#b25000` | Warning diagnostics. |
|
|
59
|
+
| `--qm-font` | `ui-sans-serif, system-ui, sans-serif` | The editor surface's font family. Controls and buttons take it too, in place of the UA face. |
|
|
60
|
+
| `--qm-font-mono` | `ui-monospace, monospace` | The monospace face: the JSON array control, the tips card. |
|
|
61
|
+
| `--qm-font-size` | `0.875rem` | Chrome text: every control's size, the anchor the ramp derives up (title) and down (label/meta) from, and the anchor a document body sits one small step over. |
|
|
62
|
+
| `--qm-radius` | `8px` | Card & popover corner. Interior controls derive a tighter tier (half), so a square brand is square throughout. |
|
|
63
|
+
| `--qm-space` | `0.25rem` | Spacing base. Gaps and insets are `half`/`1×`/`2×`/`3×`/`4×` multiples of it. |
|
|
56
64
|
|
|
57
65
|
Give a length dial a length: `--qm-space: 4` is a valid custom property and an invalid length, so it substitutes through every `calc()` that reads it. Where `@property` is supported the surface absorbs that, landing on the package default rather than collapsing the padding to zero.
|
|
58
66
|
|
|
@@ -70,7 +78,9 @@ Declared before our sheet it fixes the order outright; declared after, `qm` is a
|
|
|
70
78
|
|
|
71
79
|
### The surface follows your colour scheme
|
|
72
80
|
|
|
73
|
-
|
|
81
|
+
Ink steps `fg → bg` and the borders and hover fills step `bg → fg`, mixed in **oklab**, so inverting the two poles inverts all of them together.
|
|
82
|
+
|
|
83
|
+
**Planes are the exception**, and deliberately: the raised plane is the brighter one in both schemes, which is what makes a card read as a card rather than as a differently-tinted page. So the column under the cards sinks from `--qm-bg` toward shadow and a floating surface rises from it toward light, and neither reads the ink pole. Each direction takes a step sized to the scheme it is in, which is why pinning a dark palette while your `color-scheme` still says light leaves the column barely off its cards — the one case §"Make it yours" tells you to pin the scheme alongside the poles.
|
|
74
84
|
|
|
75
85
|
Which way the poles default is **your `color-scheme`**, not the operating system's. Declare the scheme your app is in and the surface lands on it, along with native chrome, which reads the same property:
|
|
76
86
|
|
|
@@ -131,28 +141,41 @@ It carries a `--qmh-*` scale for the page, derived from the same ten dials and c
|
|
|
131
141
|
|
|
132
142
|
### The shell, if you are building a tool
|
|
133
143
|
|
|
134
|
-
An app that mounts an editor beside a preview over one session needs a screen to put them in, and the
|
|
144
|
+
An app that mounts an editor beside a preview over one session needs a screen to put them in, and the shape of that screen is the same every time. Five more classes are that shape, so what you write is the wiring rather than the grid:
|
|
135
145
|
|
|
136
146
|
```html
|
|
137
147
|
<div class="qm-workspace">
|
|
138
148
|
<header class="qm-bar site-head">
|
|
139
149
|
<span class="site-mark">acme<span class="site-slash">/</span>studio</span>
|
|
140
150
|
</header>
|
|
141
|
-
<div class="
|
|
142
|
-
<
|
|
143
|
-
|
|
151
|
+
<div class="site-body">
|
|
152
|
+
<div class="qm-switch" role="group" aria-label="Visible pane">
|
|
153
|
+
<button class="qm-control" aria-pressed="{shown" ="" ="" ="1}" onclick="{()" ="">
|
|
154
|
+
(shown = 1)}>Editor
|
|
155
|
+
</button>
|
|
156
|
+
<button class="qm-control" aria-pressed="{shown" ="" ="" ="2}" onclick="{()" ="">
|
|
157
|
+
(shown = 2)}>Preview
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="qm-split" data-qm-show="{shown}">
|
|
161
|
+
<section class="qm-frame"><!-- <VisualEditor class="qm-pane" /> --></section>
|
|
162
|
+
<section class="qm-frame"><!-- <Preview /> --></section>
|
|
163
|
+
</div>
|
|
144
164
|
</div>
|
|
145
165
|
</div>
|
|
146
166
|
```
|
|
147
167
|
|
|
148
|
-
| Class | What it is
|
|
149
|
-
| --------------- |
|
|
150
|
-
| `.qm-workspace` | The screen: pinned to the viewport, three bands (a head, the body, an optional foot), the body taking what is left and the document scrolling nowhere.
|
|
151
|
-
| `.qm-bar` | The row a band puts its parts on, wrapping and centred. The rule under it, the gutter beside it and its depth stay yours: that is your page shell.
|
|
152
|
-
| `.qm-split` | Two even tracks for two surfaces, the page gap between them.
|
|
153
|
-
| `.qm-
|
|
168
|
+
| Class | What it is |
|
|
169
|
+
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
170
|
+
| `.qm-workspace` | The screen: pinned to the viewport, three bands (a head, the body, an optional foot), the body taking what is left and the document scrolling nowhere. |
|
|
171
|
+
| `.qm-bar` | The row a band puts its parts on, wrapping and centred. The rule under it, the gutter beside it and its depth stay yours: that is your page shell. |
|
|
172
|
+
| `.qm-split` | Two even tracks for two surfaces, the page gap between them. Under `50rem` wide or `30rem` tall, one track at a time; `data-qm-show="1"\|"2"` says which. |
|
|
173
|
+
| `.qm-switch` | The band that says which track is showing, and nothing at the widths where both are. Its controls read as even halves and stand at the touch floor (`--qmh-tap`, 44px); depth past that is yours. |
|
|
174
|
+
| `.qm-frame` | The mounting site: an edge, a corner, and the clip that holds a surface to them. What is inside it is the surface's own. |
|
|
175
|
+
|
|
176
|
+
The narrow shape is one mount at a time, not two of them stacked: half a narrow viewport is under the width either surface reads at. The track you are not showing is `display: none`, so it leaves the tab order, hit-testing and the accessibility tree together: you need no `inert` on it, and no breakpoint in your JS, since the only state you hold is which of the two you last asked for. Keep both mounted: the hidden pane keeps its pixels, its caret and its scroll, and repaints itself when it comes back. A click that lands in the hidden pane is the one thing the shape adds to your wiring, so send the caret and reveal the track together, or the hit goes somewhere the reader cannot see.
|
|
154
177
|
|
|
155
|
-
The shape is shared; the
|
|
178
|
+
The shape is shared; the look of a band is not. Your wordmark, your nav and the depth of your head are the most app-specific things on the screen, and a preset that drew them would hand every tool built on this the same face, so they read `--qmh-*` like everything else and you write the rules. `.qm-split`'s gap is a default rather than a fact about splitting: close it to `0` and carry a hairline on one pane if you want the two mounts to meet instead of stand apart.
|
|
156
179
|
|
|
157
180
|
It is **unlayered**, so your own rules beat it by ordinary precedence, exactly as they beat the surfaces. It is also opt-in and side-effect free until imported: nothing that styles your document arrives with a component.
|
|
158
181
|
|
package/dist/core/address.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Addr, HitGranularity } from '@quillmark/wasm';
|
|
2
2
|
/**
|
|
3
3
|
* A canonical field address: `main.<field>` / `main.body` /
|
|
4
|
-
* `cards.<kind>[<i>].<field>`, cards keyed by
|
|
4
|
+
* `cards.<kind>[<i>].<field>`, cards keyed by absolute document index.
|
|
5
5
|
*
|
|
6
6
|
* The grammar `parseDocPath` / `formatDocPath` speak, and the one
|
|
7
7
|
* `Diagnostic.path`, `ContentHit.field`, `FieldRegion.field` and
|
|
@@ -23,10 +23,27 @@ export interface Place {
|
|
|
23
23
|
/** The caret in USV. */
|
|
24
24
|
pos: number;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Where a preview click landed. What the preview surfaces and what the editor's
|
|
28
|
+
* `setCaret` takes.
|
|
29
|
+
*
|
|
30
|
+
* **An absent `pos` is the placement rung**, not a caret at zero: the click resolved
|
|
31
|
+
* a field the plate places without tracking its content (`fieldAt` answers,
|
|
32
|
+
* `positionAt` does not), so there is no offset and the landing is a focus. A `Place`
|
|
33
|
+
* is a landing carrying its caret, and a `ContentHit` is one too.
|
|
34
|
+
*/
|
|
35
|
+
export interface Landing {
|
|
36
|
+
field: DocPath;
|
|
37
|
+
/** The caret in USV, absent on the placement rung. */
|
|
38
|
+
pos?: number;
|
|
39
|
+
/** Whether `pos` is cluster-exact or floored to a segment start; the boundary's
|
|
40
|
+
* own marker, carried through untouched. */
|
|
41
|
+
granularity?: HitGranularity;
|
|
42
|
+
}
|
|
26
43
|
/**
|
|
27
44
|
* A field's canonical `DocPath`, or `undefined` when `addr.card` is outside the live
|
|
28
45
|
* `kinds` array (a stale address: drop it rather than mis-target). A field-less card
|
|
29
|
-
* addresses its
|
|
46
|
+
* addresses its body, which is the leaf `{card: i}` names.
|
|
30
47
|
*
|
|
31
48
|
* - `{}` → `"main.body"`
|
|
32
49
|
* - `{field}` → `"main.<field>"`
|
|
@@ -38,7 +55,7 @@ export interface Place {
|
|
|
38
55
|
*/
|
|
39
56
|
export declare function fieldPathForAddr(addr: Addr, kinds: readonly string[]): DocPath | undefined;
|
|
40
57
|
/**
|
|
41
|
-
* A
|
|
58
|
+
* A card's own path (`cards.<kind>[i]`), not a leaf's: what a structure op names,
|
|
42
59
|
* where the change is the card rather than anything inside it. `undefined` for an
|
|
43
60
|
* index outside `kinds`.
|
|
44
61
|
*/
|
|
@@ -46,11 +63,29 @@ export declare function cardPath(index: number, kinds: readonly string[]): DocPa
|
|
|
46
63
|
/**
|
|
47
64
|
* The inverse: a canonical `DocPath` back to the `Addr` the document verbs take, or
|
|
48
65
|
* `undefined` for a path that names no single commit address — a nested or
|
|
49
|
-
* array-element path (`main.references.0
|
|
50
|
-
*
|
|
51
|
-
* the field-less `{card: i}` the body leaf answers to.
|
|
66
|
+
* array-element path (`main.references.0`, which {@link elementAddrForFieldPath}
|
|
67
|
+
* takes instead), a field-rooted one, or a malformed one. A bare card and a `.body`
|
|
68
|
+
* terminal both land on the field-less `{card: i}` the body leaf answers to.
|
|
52
69
|
*
|
|
53
70
|
* Needs no `kinds`: the path carries the absolute index, and the kind in it is
|
|
54
71
|
* decoration the `Addr` has no room for.
|
|
55
72
|
*/
|
|
56
73
|
export declare function addrForFieldPath(path: DocPath): Addr | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* An array element's address, split. `main.references.0` and `main.references[0]`
|
|
76
|
+
* both land here; anything {@link addrForFieldPath} can name, and anything whose
|
|
77
|
+
* trailing segment is not an index, does not.
|
|
78
|
+
*
|
|
79
|
+
* Both spellings, because the boundary mints one and formats the other: `regions()`
|
|
80
|
+
* and `positionAt` emit `main.references.0`, which `parseDocPath` reads as a field
|
|
81
|
+
* literally named `"0"`, while `formatDocPath` spells the index segment
|
|
82
|
+
* `main.references[0]`. A trailing all-digits field name is therefore an index that
|
|
83
|
+
* lost its brackets — but only under a field the schema declares an array, the
|
|
84
|
+
* caller's guard to apply: this module holds the grammar and no schema.
|
|
85
|
+
*/
|
|
86
|
+
export declare function elementAddrForFieldPath(path: DocPath): ElementAddr | undefined;
|
|
87
|
+
/** An array element's address: the array's `Addr`, and which element of it. */
|
|
88
|
+
export interface ElementAddr {
|
|
89
|
+
field: Addr;
|
|
90
|
+
index: number;
|
|
91
|
+
}
|
package/dist/core/address.js
CHANGED
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
// strings for places, `Addr` for the document verbs. Declared HERE because both
|
|
3
|
-
// `/preview` and `/visual` name a place in their hooks, and a type declared twice
|
|
4
|
-
// structurally is drift with nothing to catch it: `/core` is the module both
|
|
5
|
-
// already import.
|
|
6
|
-
//
|
|
7
|
-
// Slogan: paths for places, indexes for structure ops. `Addr` (`{card?, field?}`,
|
|
8
|
-
// from `@quillmark/wasm`) is the mutator currency; the conversions at the foot of
|
|
9
|
-
// this module are the hop between the two. `fieldPathForAddr` and `addrForFieldPath`
|
|
10
|
-
// are public, because a host given a path by a hook and holding a verb that takes an
|
|
11
|
-
// `Addr` needs them; `cardPath` is the editor's own.
|
|
12
|
-
import { formatDocPath, parseDocPath } from '@quillmark/wasm';
|
|
1
|
+
import { core } from './lifecycle.js';
|
|
13
2
|
// ── Addr ↔ DocPath ──────────────────────────────────────────────────────────
|
|
14
3
|
// The one hop between the two vocabularies, both directions, pure and
|
|
15
4
|
// document-free: `kinds` is the kind of each composable card by content index
|
|
@@ -19,7 +8,7 @@ import { formatDocPath, parseDocPath } from '@quillmark/wasm';
|
|
|
19
8
|
/**
|
|
20
9
|
* A field's canonical `DocPath`, or `undefined` when `addr.card` is outside the live
|
|
21
10
|
* `kinds` array (a stale address: drop it rather than mis-target). A field-less card
|
|
22
|
-
* addresses its
|
|
11
|
+
* addresses its body, which is the leaf `{card: i}` names.
|
|
23
12
|
*
|
|
24
13
|
* - `{}` → `"main.body"`
|
|
25
14
|
* - `{field}` → `"main.<field>"`
|
|
@@ -34,35 +23,74 @@ export function fieldPathForAddr(addr, kinds) {
|
|
|
34
23
|
if (!head)
|
|
35
24
|
return undefined;
|
|
36
25
|
const tail = addr.field != null ? { seg: 'field', name: addr.field } : { seg: 'body' };
|
|
37
|
-
return formatDocPath([head, tail]);
|
|
26
|
+
return core().formatDocPath([head, tail]);
|
|
38
27
|
}
|
|
39
28
|
/**
|
|
40
|
-
* A
|
|
29
|
+
* A card's own path (`cards.<kind>[i]`), not a leaf's: what a structure op names,
|
|
41
30
|
* where the change is the card rather than anything inside it. `undefined` for an
|
|
42
31
|
* index outside `kinds`.
|
|
43
32
|
*/
|
|
44
33
|
export function cardPath(index, kinds) {
|
|
45
34
|
const head = cardHead(index, kinds);
|
|
46
|
-
return head ? formatDocPath([head]) : undefined;
|
|
35
|
+
return head ? core().formatDocPath([head]) : undefined;
|
|
47
36
|
}
|
|
48
37
|
/**
|
|
49
38
|
* The inverse: a canonical `DocPath` back to the `Addr` the document verbs take, or
|
|
50
39
|
* `undefined` for a path that names no single commit address — a nested or
|
|
51
|
-
* array-element path (`main.references.0
|
|
52
|
-
*
|
|
53
|
-
* the field-less `{card: i}` the body leaf answers to.
|
|
40
|
+
* array-element path (`main.references.0`, which {@link elementAddrForFieldPath}
|
|
41
|
+
* takes instead), a field-rooted one, or a malformed one. A bare card and a `.body`
|
|
42
|
+
* terminal both land on the field-less `{card: i}` the body leaf answers to.
|
|
54
43
|
*
|
|
55
44
|
* Needs no `kinds`: the path carries the absolute index, and the kind in it is
|
|
56
45
|
* decoration the `Addr` has no room for.
|
|
57
46
|
*/
|
|
58
47
|
export function addrForFieldPath(path) {
|
|
59
|
-
|
|
48
|
+
const segs = segsOf(path);
|
|
49
|
+
return segs && addrForSegs(segs);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* An array element's address, split. `main.references.0` and `main.references[0]`
|
|
53
|
+
* both land here; anything {@link addrForFieldPath} can name, and anything whose
|
|
54
|
+
* trailing segment is not an index, does not.
|
|
55
|
+
*
|
|
56
|
+
* Both spellings, because the boundary mints one and formats the other: `regions()`
|
|
57
|
+
* and `positionAt` emit `main.references.0`, which `parseDocPath` reads as a field
|
|
58
|
+
* literally named `"0"`, while `formatDocPath` spells the index segment
|
|
59
|
+
* `main.references[0]`. A trailing all-digits field name is therefore an index that
|
|
60
|
+
* lost its brackets — but only under a field the schema declares an array, the
|
|
61
|
+
* caller's guard to apply: this module holds the grammar and no schema.
|
|
62
|
+
*/
|
|
63
|
+
export function elementAddrForFieldPath(path) {
|
|
64
|
+
const segs = segsOf(path);
|
|
65
|
+
if (!segs)
|
|
66
|
+
return undefined;
|
|
67
|
+
const last = segs[segs.length - 1];
|
|
68
|
+
const index = last?.seg === 'index'
|
|
69
|
+
? last.index
|
|
70
|
+
: last?.seg === 'field' && /^\d+$/.test(last.name)
|
|
71
|
+
? Number(last.name)
|
|
72
|
+
: undefined;
|
|
73
|
+
if (index == null)
|
|
74
|
+
return undefined;
|
|
75
|
+
const field = addrForSegs(segs.slice(0, -1));
|
|
76
|
+
// A body has no elements: the parent must name a field.
|
|
77
|
+
return field?.field != null ? { field, index } : undefined;
|
|
78
|
+
}
|
|
79
|
+
/** `parseDocPath`, with a malformed path as `undefined` rather than a throw. The gate
|
|
80
|
+
* is read outside the try: an uninitialized core is not a malformed path, and would
|
|
81
|
+
* otherwise leave here as one. */
|
|
82
|
+
function segsOf(path) {
|
|
83
|
+
const { parseDocPath } = core();
|
|
60
84
|
try {
|
|
61
|
-
|
|
85
|
+
return parseDocPath(path);
|
|
62
86
|
}
|
|
63
87
|
catch {
|
|
64
88
|
return undefined;
|
|
65
89
|
}
|
|
90
|
+
}
|
|
91
|
+
/** {@link addrForFieldPath} over already-parsed segments, so the element walk reuses
|
|
92
|
+
* it for the head of a path rather than re-serializing one. */
|
|
93
|
+
function addrForSegs(segs) {
|
|
66
94
|
const [head, ...rest] = segs;
|
|
67
95
|
if (!head)
|
|
68
96
|
return undefined;
|
package/dist/core/bloom.d.ts
CHANGED
|
@@ -9,12 +9,12 @@ export interface BloomTiming {
|
|
|
9
9
|
/**
|
|
10
10
|
* Resolve the timing off `el`, so the dwell is single-sourced in CSS.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* One property, both motion preferences: `--_qm-bloom-dwell` already shortens under
|
|
13
13
|
* `prefers-reduced-motion` in the derivation, so what is left to `matchMedia` here is
|
|
14
|
-
* the
|
|
14
|
+
* the shape: a hold and a cut carry no ramps, which is a frame list rather than a
|
|
15
15
|
* number and the one half CSS cannot hand over.
|
|
16
16
|
*
|
|
17
|
-
* Read
|
|
17
|
+
* Read once per batch, not per element: `getComputedStyle` forces a style recalc and
|
|
18
18
|
* `animate` re-dirties style, so a read inside a loop flushes once per element (~0.15ms
|
|
19
19
|
* each). The rungs inherit, so every element in a batch resolves the same numbers.
|
|
20
20
|
*/
|
|
@@ -27,10 +27,10 @@ export declare function bloomTiming(el: HTMLElement): BloomTiming;
|
|
|
27
27
|
*/
|
|
28
28
|
export declare function bloom(el: HTMLElement, elapsed?: number, timing?: BloomTiming): Animation | undefined;
|
|
29
29
|
/**
|
|
30
|
-
* Bloom over `host`'s
|
|
30
|
+
* Bloom over `host`'s content: an inset child, so the wash neither tints the host's
|
|
31
31
|
* own background nor fades the text under it. `host` must be positioned.
|
|
32
32
|
*
|
|
33
|
-
*
|
|
33
|
+
* One wash node per host, reused while it lives: two quick landings on the same leaf
|
|
34
34
|
* must not stack two alphas over it. No `elapsed` seam: the surfaces that use this
|
|
35
35
|
* (editor leaves) are not rebuilt under the animation the way the preview's boxes are.
|
|
36
36
|
*/
|
package/dist/core/bloom.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// The correlation bloom: a soft accent wash that rises over a target and decays to
|
|
2
|
-
// nothing. The editor↔preview address is an
|
|
2
|
+
// nothing. The editor↔preview address is an event, not a state: the preview claims
|
|
3
3
|
// to be the rendered output, so it carries no resting ink, and a highlight that
|
|
4
4
|
// outlives the hop that caused it is ink the document did not ask for. Both
|
|
5
5
|
// directions land here: the preview's field boxes bloom themselves (they are already
|
|
6
6
|
// empty rects over the page), an editor leaf blooms a transient child over its
|
|
7
7
|
// surface.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
9
|
+
// Opacity is the animated property, never the colour. The wash's alpha is a peak
|
|
10
10
|
// fixed by `--_qm-accent-wash`, so interpolating 0→1→0 keeps the scale in CSS where
|
|
11
11
|
// `color-mix` resolves, instead of in keyframe values a script would have to
|
|
12
12
|
// pre-resolve and re-resolve on a theme change.
|
|
@@ -23,7 +23,7 @@ const WASH = 'var(--_qm-accent-wash)';
|
|
|
23
23
|
* absence of the scale, not a copy of it. */
|
|
24
24
|
const FALLBACK_MS = 1100;
|
|
25
25
|
// Rise fast, hold, then leave slowly: a highlighter's decay, not a linear fade. A
|
|
26
|
-
// keyframe's easing governs the segment that
|
|
26
|
+
// keyframe's easing governs the segment that starts at it. Under reduced motion the
|
|
27
27
|
// ramps are dropped entirely: the wash holds at full for a beat and cuts, which
|
|
28
28
|
// still answers "here" with nothing to track.
|
|
29
29
|
const FRAMES = [
|
|
@@ -40,12 +40,12 @@ export function primeWash(el) {
|
|
|
40
40
|
/**
|
|
41
41
|
* Resolve the timing off `el`, so the dwell is single-sourced in CSS.
|
|
42
42
|
*
|
|
43
|
-
*
|
|
43
|
+
* One property, both motion preferences: `--_qm-bloom-dwell` already shortens under
|
|
44
44
|
* `prefers-reduced-motion` in the derivation, so what is left to `matchMedia` here is
|
|
45
|
-
* the
|
|
45
|
+
* the shape: a hold and a cut carry no ramps, which is a frame list rather than a
|
|
46
46
|
* number and the one half CSS cannot hand over.
|
|
47
47
|
*
|
|
48
|
-
* Read
|
|
48
|
+
* Read once per batch, not per element: `getComputedStyle` forces a style recalc and
|
|
49
49
|
* `animate` re-dirties style, so a read inside a loop flushes once per element (~0.15ms
|
|
50
50
|
* each). The rungs inherit, so every element in a batch resolves the same numbers.
|
|
51
51
|
*/
|
|
@@ -74,10 +74,10 @@ export function bloom(el, elapsed = 0, timing) {
|
|
|
74
74
|
return anim;
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
77
|
-
* Bloom over `host`'s
|
|
77
|
+
* Bloom over `host`'s content: an inset child, so the wash neither tints the host's
|
|
78
78
|
* own background nor fades the text under it. `host` must be positioned.
|
|
79
79
|
*
|
|
80
|
-
*
|
|
80
|
+
* One wash node per host, reused while it lives: two quick landings on the same leaf
|
|
81
81
|
* must not stack two alphas over it. No `elapsed` seam: the surfaces that use this
|
|
82
82
|
* (editor leaves) are not rebuilt under the animation the way the preview's boxes are.
|
|
83
83
|
*/
|
|
@@ -8,7 +8,7 @@ export interface RangeAnchor {
|
|
|
8
8
|
/**
|
|
9
9
|
* A virtual anchor over one PM range, measured when asked rather than when made.
|
|
10
10
|
*
|
|
11
|
-
* The measure is
|
|
11
|
+
* The measure is total. `coordsAtPos` reads layout, and layout can be gone: a
|
|
12
12
|
* position the document has since invalidated, a leaf inside a collapsed section, a
|
|
13
13
|
* view torn down between a scroll firing and the measure running. `autoUpdate` calls
|
|
14
14
|
* this at moments none of its callers choose, so an unmeasurable anchor returns the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A virtual anchor over one PM range, measured when asked rather than when made.
|
|
3
3
|
*
|
|
4
|
-
* The measure is
|
|
4
|
+
* The measure is total. `coordsAtPos` reads layout, and layout can be gone: a
|
|
5
5
|
* position the document has since invalidated, a leaf inside a collapsed section, a
|
|
6
6
|
* view torn down between a scroll firing and the measure running. `autoUpdate` calls
|
|
7
7
|
* this at moments none of its callers choose, so an unmeasurable anchor returns the
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Command } from 'prosemirror-state';
|
|
2
|
+
import type { Schema } from 'prosemirror-model';
|
|
3
|
+
/**
|
|
4
|
+
* The block-atom bindings for a block-schema leaf: `{}` for the inline/plaintext
|
|
5
|
+
* schemas, which hold no block to be against.
|
|
6
|
+
*
|
|
7
|
+
* Both keys are the same command on opposite sides, because what a delete against an
|
|
8
|
+
* atom means does not depend on which side the caret approached from.
|
|
9
|
+
*/
|
|
10
|
+
export declare function atomKeymap(schema: Schema): Record<string, Command>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// The block-atom link of the body's Backspace/Delete chains: a delete against an
|
|
2
|
+
// island or a divider selects it, and the press after that deletes it.
|
|
3
|
+
//
|
|
4
|
+
// This is the neighbour half of the rule the leaf already holds for a key typed over a
|
|
5
|
+
// selection (CODEC §"The table island"): a selection is the subject of the next
|
|
6
|
+
// command, never a thing armed for replacement. Without it the base keymap's
|
|
7
|
+
// `joinBackward` / `joinForward` reach their "the neighbour is an atom, delete it" arm
|
|
8
|
+
// and a table goes in one keystroke, from the paragraph a table insert parks the caret
|
|
9
|
+
// in — with nothing drawn first to say what is about to go.
|
|
10
|
+
//
|
|
11
|
+
// It runs last of the links, after the list ones: a Backspace at the start of a list's
|
|
12
|
+
// first item lifts the item, and that stays the item's answer whatever sits above the
|
|
13
|
+
// list.
|
|
14
|
+
import { NodeSelection, TextSelection } from 'prosemirror-state';
|
|
15
|
+
/** The caret, when it sits at a textblock edge with an empty selection: the only place
|
|
16
|
+
* a delete is about a neighbour rather than about a character. `null` everywhere else,
|
|
17
|
+
* which is where the key keeps its ordinary meaning. */
|
|
18
|
+
function atEdge(state, side) {
|
|
19
|
+
const { selection } = state;
|
|
20
|
+
if (!(selection instanceof TextSelection) || !selection.empty)
|
|
21
|
+
return null;
|
|
22
|
+
const $head = selection.$head;
|
|
23
|
+
const at = side < 0 ? 0 : $head.parent.content.size;
|
|
24
|
+
return $head.parentOffset === at ? $head : null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The cut the caret's block sits against on `side`: the shallowest boundary with a
|
|
28
|
+
* sibling beyond it. `prosemirror-commands` walks the same one to find the node a join
|
|
29
|
+
* would reach; this reads it to find the node a delete would take.
|
|
30
|
+
*/
|
|
31
|
+
function cutAt($pos, side) {
|
|
32
|
+
if ($pos.parent.type.spec.isolating)
|
|
33
|
+
return null;
|
|
34
|
+
for (let d = $pos.depth - 1; d >= 0; d--) {
|
|
35
|
+
if (side < 0) {
|
|
36
|
+
if ($pos.index(d) > 0)
|
|
37
|
+
return $pos.doc.resolve($pos.before(d + 1));
|
|
38
|
+
}
|
|
39
|
+
else if ($pos.index(d) + 1 < $pos.node(d).childCount) {
|
|
40
|
+
return $pos.doc.resolve($pos.after(d + 1));
|
|
41
|
+
}
|
|
42
|
+
if ($pos.node(d).type.spec.isolating)
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
/** Select the block atom the caret is against, rather than deleting it. Declines when
|
|
48
|
+
* the neighbour is anything a caret can enter, so a paragraph, a heading, a quote and
|
|
49
|
+
* a list all keep the join the base keymap gives them. */
|
|
50
|
+
function selectAtom(side) {
|
|
51
|
+
return (state, dispatch) => {
|
|
52
|
+
const $head = atEdge(state, side);
|
|
53
|
+
const $cut = $head && cutAt($head, side);
|
|
54
|
+
if (!$cut)
|
|
55
|
+
return false;
|
|
56
|
+
const node = side < 0 ? $cut.nodeBefore : $cut.nodeAfter;
|
|
57
|
+
if (!node || !node.isAtom || !node.isBlock || !NodeSelection.isSelectable(node))
|
|
58
|
+
return false;
|
|
59
|
+
const at = side < 0 ? $cut.pos - node.nodeSize : $cut.pos;
|
|
60
|
+
dispatch?.(state.tr.setSelection(NodeSelection.create(state.doc, at)).scrollIntoView());
|
|
61
|
+
return true;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The block-atom bindings for a block-schema leaf: `{}` for the inline/plaintext
|
|
66
|
+
* schemas, which hold no block to be against.
|
|
67
|
+
*
|
|
68
|
+
* Both keys are the same command on opposite sides, because what a delete against an
|
|
69
|
+
* atom means does not depend on which side the caret approached from.
|
|
70
|
+
*/
|
|
71
|
+
export function atomKeymap(schema) {
|
|
72
|
+
if (!schema.nodes.island_block)
|
|
73
|
+
return {};
|
|
74
|
+
return {
|
|
75
|
+
Backspace: selectAtom(-1),
|
|
76
|
+
Delete: selectAtom(1)
|
|
77
|
+
};
|
|
78
|
+
}
|