@quillmark/svelte 0.0.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/LICENSE +201 -0
- package/NOTICE +58 -0
- package/README.md +241 -0
- package/THEMING.md +153 -0
- package/dist/core/address.d.ts +56 -0
- package/dist/core/address.js +95 -0
- package/dist/core/bloom.d.ts +37 -0
- package/dist/core/bloom.js +116 -0
- package/dist/core/codec/anchor.d.ts +19 -0
- package/dist/core/codec/anchor.js +36 -0
- package/dist/core/codec/code.d.ts +12 -0
- package/dist/core/codec/code.js +118 -0
- package/dist/core/codec/decode.d.ts +24 -0
- package/dist/core/codec/decode.js +270 -0
- package/dist/core/codec/encode.d.ts +71 -0
- package/dist/core/codec/encode.js +480 -0
- package/dist/core/codec/field.d.ts +171 -0
- package/dist/core/codec/field.js +493 -0
- package/dist/core/codec/index.d.ts +22 -0
- package/dist/core/codec/index.js +43 -0
- package/dist/core/codec/inputrules.d.ts +7 -0
- package/dist/core/codec/inputrules.js +168 -0
- package/dist/core/codec/islands.d.ts +36 -0
- package/dist/core/codec/islands.js +82 -0
- package/dist/core/codec/keymap.d.ts +13 -0
- package/dist/core/codec/keymap.js +29 -0
- package/dist/core/codec/lists.d.ts +22 -0
- package/dist/core/codec/lists.js +104 -0
- package/dist/core/codec/marks.d.ts +37 -0
- package/dist/core/codec/marks.js +74 -0
- package/dist/core/codec/positions.d.ts +17 -0
- package/dist/core/codec/positions.js +108 -0
- package/dist/core/codec/prose.css +458 -0
- package/dist/core/codec/reconcile.d.ts +26 -0
- package/dist/core/codec/reconcile.js +64 -0
- package/dist/core/codec/schema.d.ts +13 -0
- package/dist/core/codec/schema.js +155 -0
- package/dist/core/codec/slash.d.ts +79 -0
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-view.d.ts +50 -0
- package/dist/core/codec/table-view.js +928 -0
- package/dist/core/codec/table.d.ts +85 -0
- package/dist/core/codec/table.js +224 -0
- package/dist/core/errors.d.ts +73 -0
- package/dist/core/errors.js +24 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +19 -0
- package/dist/core/lifecycle.d.ts +7 -0
- package/dist/core/lifecycle.js +21 -0
- package/dist/core/rebind.svelte.d.ts +18 -0
- package/dist/core/rebind.svelte.js +44 -0
- package/dist/core/teardown.d.ts +20 -0
- package/dist/core/teardown.js +37 -0
- package/dist/core/theme.css +276 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +9 -0
- package/dist/preset/index.css +10 -0
- package/dist/preset/recipes.css +188 -0
- package/dist/preset/scale.css +97 -0
- package/dist/preview/Preview.svelte +118 -0
- package/dist/preview/Preview.svelte.d.ts +39 -0
- package/dist/preview/bridge.d.ts +13 -0
- package/dist/preview/bridge.js +88 -0
- package/dist/preview/controller.d.ts +40 -0
- package/dist/preview/controller.js +141 -0
- package/dist/preview/geometry.d.ts +37 -0
- package/dist/preview/geometry.js +45 -0
- package/dist/preview/index.d.ts +5 -0
- package/dist/preview/index.js +14 -0
- package/dist/preview/overlay.d.ts +10 -0
- package/dist/preview/overlay.js +82 -0
- package/dist/preview/paint.d.ts +17 -0
- package/dist/preview/paint.js +262 -0
- package/dist/preview/strings.d.ts +14 -0
- package/dist/preview/strings.js +22 -0
- package/dist/visual/ArrayField.svelte +362 -0
- package/dist/visual/ArrayField.svelte.d.ts +24 -0
- package/dist/visual/BooleanField.svelte +88 -0
- package/dist/visual/BooleanField.svelte.d.ts +20 -0
- package/dist/visual/Card.svelte +849 -0
- package/dist/visual/Card.svelte.d.ts +43 -0
- package/dist/visual/CardControls.svelte +92 -0
- package/dist/visual/CardControls.svelte.d.ts +11 -0
- package/dist/visual/DateField.svelte +203 -0
- package/dist/visual/DateField.svelte.d.ts +28 -0
- package/dist/visual/DiagnosticList.svelte +50 -0
- package/dist/visual/DiagnosticList.svelte.d.ts +7 -0
- package/dist/visual/EnumField.svelte +169 -0
- package/dist/visual/EnumField.svelte.d.ts +25 -0
- package/dist/visual/Field.svelte +270 -0
- package/dist/visual/Field.svelte.d.ts +37 -0
- package/dist/visual/FieldHint.svelte +185 -0
- package/dist/visual/FieldHint.svelte.d.ts +13 -0
- package/dist/visual/FieldLabel.svelte +141 -0
- package/dist/visual/FieldLabel.svelte.d.ts +19 -0
- package/dist/visual/FormatPopover.svelte +396 -0
- package/dist/visual/FormatPopover.svelte.d.ts +9 -0
- package/dist/visual/NumberField.svelte +72 -0
- package/dist/visual/NumberField.svelte.d.ts +19 -0
- package/dist/visual/ObjectField.svelte +131 -0
- package/dist/visual/ObjectField.svelte.d.ts +17 -0
- package/dist/visual/ProseArrayElement.svelte +98 -0
- package/dist/visual/ProseArrayElement.svelte.d.ts +19 -0
- package/dist/visual/ProseField.svelte +213 -0
- package/dist/visual/ProseField.svelte.d.ts +50 -0
- package/dist/visual/SlashMenu.svelte +126 -0
- package/dist/visual/SlashMenu.svelte.d.ts +14 -0
- package/dist/visual/TextField.svelte +63 -0
- package/dist/visual/TextField.svelte.d.ts +23 -0
- package/dist/visual/TipsCard.svelte +191 -0
- package/dist/visual/TipsCard.svelte.d.ts +13 -0
- package/dist/visual/VisualEditor.svelte +96 -0
- package/dist/visual/VisualEditor.svelte.d.ts +14 -0
- package/dist/visual/VisualEditorInner.svelte +1032 -0
- package/dist/visual/VisualEditorInner.svelte.d.ts +17 -0
- package/dist/visual/controls.css +272 -0
- package/dist/visual/diagnostics.d.ts +52 -0
- package/dist/visual/diagnostics.js +83 -0
- package/dist/visual/domid.d.ts +21 -0
- package/dist/visual/domid.js +36 -0
- package/dist/visual/ext.d.ts +19 -0
- package/dist/visual/ext.js +29 -0
- package/dist/visual/icons/Icon.svelte +39 -0
- package/dist/visual/icons/Icon.svelte.d.ts +12 -0
- package/dist/visual/icons/nodes.d.ts +103 -0
- package/dist/visual/icons/nodes.js +55 -0
- package/dist/visual/index.d.ts +9 -0
- package/dist/visual/index.js +24 -0
- package/dist/visual/motion.d.ts +18 -0
- package/dist/visual/motion.js +53 -0
- package/dist/visual/props.d.ts +77 -0
- package/dist/visual/props.js +1 -0
- package/dist/visual/signals.d.ts +59 -0
- package/dist/visual/signals.js +1 -0
- package/dist/visual/strings.d.ts +123 -0
- package/dist/visual/strings.js +80 -0
- package/dist/visual/structure.d.ts +224 -0
- package/dist/visual/structure.js +270 -0
- package/dist/visual/synced.svelte.d.ts +15 -0
- package/dist/visual/synced.svelte.js +34 -0
- package/dist/visual/tips.d.ts +24 -0
- package/dist/visual/tips.js +50 -0
- package/package.json +102 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
One element of an array-of-`richtext` field (fixture `references`). Array
|
|
3
|
+
elements are NOT `applyChange`-addressable: `Addr.field` is a flat name, so
|
|
4
|
+
`references.0` has no op address. So
|
|
5
|
+
this is NOT a `createField` leaf: it mounts a minimal PM view over the codec's
|
|
6
|
+
decode/encode + inline schema, and on every edit hands the re-encoded
|
|
7
|
+
`Content` UP to the parent {@link ArrayField}, which commits the WHOLE array
|
|
8
|
+
by value (`writer.set(field, arrayWithElementReplaced)`). Anchors within an
|
|
9
|
+
element are dropped on that value write: acceptable for inline refs. Mounts
|
|
10
|
+
ONCE per stable element id (no reset on the parent's re-derive).
|
|
11
|
+
-->
|
|
12
|
+
<script lang="ts">
|
|
13
|
+
import { onMount } from 'svelte';
|
|
14
|
+
import { EditorState } from 'prosemirror-state';
|
|
15
|
+
import { EditorView } from 'prosemirror-view';
|
|
16
|
+
import { decode, pmToContent, inlineSchema, proseLeafPlugins } from '../core/codec/index.js';
|
|
17
|
+
import './controls.css';
|
|
18
|
+
import type { Content } from '@quillmark/wasm';
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
value: Content;
|
|
22
|
+
plaintext?: boolean;
|
|
23
|
+
/** Accessible name for the editable region (the array has no per-element label). */
|
|
24
|
+
label?: string;
|
|
25
|
+
onChange: (rt: Content) => void;
|
|
26
|
+
/** Raw keydown, for a container whose own keys run through this element: the
|
|
27
|
+
* array repeater's Enter/Backspace (`ArrayField`). Fires BEFORE the view's own
|
|
28
|
+
* keymap, so the state it reads is the one this keystroke has yet to change. */
|
|
29
|
+
onKey?: (e: KeyboardEvent) => void;
|
|
30
|
+
onFocusEl?: () => void;
|
|
31
|
+
}
|
|
32
|
+
let { value, plaintext, label, onChange, onKey, onFocusEl }: Props = $props();
|
|
33
|
+
|
|
34
|
+
let containerEl: HTMLDivElement | undefined = $state();
|
|
35
|
+
let view: EditorView | undefined;
|
|
36
|
+
/** Take the caret: what a parent placing focus on this element calls. The VIEW's
|
|
37
|
+
* focus, not the element's: a PM view restores its selection, where a bare DOM
|
|
38
|
+
* focus on a contenteditable leaves the caret unplaced. */
|
|
39
|
+
export function focus(): void {
|
|
40
|
+
view?.focus();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
onMount(() => {
|
|
44
|
+
if (!containerEl) return;
|
|
45
|
+
// An array element is an inline, value-by-value leaf: the SAME keymap and
|
|
46
|
+
// plugin stack a `createField` leaf mounts (shared `proseLeafPlugins`), minus
|
|
47
|
+
// the anchor-position plugin (element anchors are dropped on the array's value
|
|
48
|
+
// write, per the header). Its own `dispatchTransaction` hands `Content` up.
|
|
49
|
+
const pmDoc = decode(value, inlineSchema, { plaintext: !!plaintext });
|
|
50
|
+
const state = EditorState.create({
|
|
51
|
+
doc: pmDoc,
|
|
52
|
+
plugins: proseLeafPlugins(inlineSchema, { inline: true, plaintext: !!plaintext })
|
|
53
|
+
});
|
|
54
|
+
const mounted = new EditorView(containerEl, {
|
|
55
|
+
state,
|
|
56
|
+
// Names the `contenteditable` for assistive tech (the array row has no
|
|
57
|
+
// label element to associate).
|
|
58
|
+
attributes: label ? { 'aria-label': label } : undefined,
|
|
59
|
+
dispatchTransaction(tr) {
|
|
60
|
+
const next = mounted.state.apply(tr);
|
|
61
|
+
mounted.updateState(next);
|
|
62
|
+
if (tr.docChanged) onChange(pmToContent(next.doc));
|
|
63
|
+
},
|
|
64
|
+
handleDOMEvents: {
|
|
65
|
+
focus: () => {
|
|
66
|
+
onFocusEl?.();
|
|
67
|
+
return false;
|
|
68
|
+
},
|
|
69
|
+
keydown: (_v, e) => {
|
|
70
|
+
onKey?.(e);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
view = mounted;
|
|
76
|
+
return () => {
|
|
77
|
+
view = undefined;
|
|
78
|
+
mounted.destroy();
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<!-- `.qm-control-box` (controls.css) is the whole box, so an array of `richtext` and
|
|
84
|
+
an array of `string` render rows of equal height. No floor: the reset in
|
|
85
|
+
`core/codec/prose.css` makes one line of prose measure one line. Width is the
|
|
86
|
+
row's to give: the element fills the track it is placed in (`ArrayField`). -->
|
|
87
|
+
<div bind:this={containerEl} class="qm-array-prose qm-control-box"></div>
|
|
88
|
+
|
|
89
|
+
<style>
|
|
90
|
+
/* Caret-primary, matching ProseField: the contenteditable outline is dropped
|
|
91
|
+
and the active element is cued by the wrapper border tint below, not a ring. */
|
|
92
|
+
.qm-array-prose :global(.ProseMirror) {
|
|
93
|
+
outline: none;
|
|
94
|
+
}
|
|
95
|
+
.qm-array-prose:focus-within {
|
|
96
|
+
border-color: var(--_qm-accent);
|
|
97
|
+
}
|
|
98
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import './controls.css';
|
|
2
|
+
import type { Content } from '@quillmark/wasm';
|
|
3
|
+
interface Props {
|
|
4
|
+
value: Content;
|
|
5
|
+
plaintext?: boolean;
|
|
6
|
+
/** Accessible name for the editable region (the array has no per-element label). */
|
|
7
|
+
label?: string;
|
|
8
|
+
onChange: (rt: Content) => void;
|
|
9
|
+
/** Raw keydown, for a container whose own keys run through this element: the
|
|
10
|
+
* array repeater's Enter/Backspace (`ArrayField`). Fires BEFORE the view's own
|
|
11
|
+
* keymap, so the state it reads is the one this keystroke has yet to change. */
|
|
12
|
+
onKey?: (e: KeyboardEvent) => void;
|
|
13
|
+
onFocusEl?: () => void;
|
|
14
|
+
}
|
|
15
|
+
declare const ProseArrayElement: import("svelte").Component<Props, {
|
|
16
|
+
focus: () => void;
|
|
17
|
+
}, "">;
|
|
18
|
+
type ProseArrayElement = ReturnType<typeof ProseArrayElement>;
|
|
19
|
+
export default ProseArrayElement;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
One content prose leaf: a thin Svelte mount of the codec's `createField`
|
|
3
|
+
(VISUAL_EDITOR §Surface). Mounts ONCE per stable leaf key and tears down on
|
|
4
|
+
unmount; the leaf owns its PM state, history, and per-keystroke `applyChange`
|
|
5
|
+
commit, so this wrapper adds no logic beyond wiring and registration. The
|
|
6
|
+
`addr` is supplied by the parent as a LIVE object (its `card` a getter over the
|
|
7
|
+
stable-id→index map), so a card reorder re-targets this leaf's commits without
|
|
8
|
+
a remount: the caret rides the untouched PM view.
|
|
9
|
+
-->
|
|
10
|
+
<script lang="ts">
|
|
11
|
+
import { onMount } from 'svelte';
|
|
12
|
+
import { createField, type FieldController, type SlashState } from '../core/codec/index.js';
|
|
13
|
+
import SlashMenu from './SlashMenu.svelte';
|
|
14
|
+
import { wording } from './strings.js';
|
|
15
|
+
import './controls.css';
|
|
16
|
+
import type { Document, Quill, Addr } from '@quillmark/wasm';
|
|
17
|
+
import type { EditorErrorHandler } from '../core/errors.js';
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
doc: Document;
|
|
21
|
+
/** The schema this leaf reads its content through: `createField` binds a reader
|
|
22
|
+
* off it, so a stored string decodes by declared type rather than by guess. */
|
|
23
|
+
quill: Quill;
|
|
24
|
+
addr: Addr;
|
|
25
|
+
inline?: boolean;
|
|
26
|
+
plaintext?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Draw no box: the card body's leaf, and nothing else. A property
|
|
29
|
+
* of the SLOT rather than of the leaf: `block` is not the same predicate, since
|
|
30
|
+
* a `richtext` field without `inline` is block prose and still a control in a
|
|
31
|
+
* row of controls. Only the body is paper, and only its caller knows it is.
|
|
32
|
+
*/
|
|
33
|
+
unframed?: boolean;
|
|
34
|
+
/** Accessible name for a leaf NOTHING else names: an array element, or the
|
|
35
|
+
* card body. A leaf with a field label takes `labelledBy` instead. */
|
|
36
|
+
label?: string;
|
|
37
|
+
/** The field label's own id → `aria-labelledby` on the editable region. `for`
|
|
38
|
+
* cannot reach a `contenteditable` (not a labelable element) so the
|
|
39
|
+
* association runs the other way, and the label's click comes back through
|
|
40
|
+
* the controller's `focus`. */
|
|
41
|
+
labelledBy?: string;
|
|
42
|
+
/** The parked `description` (FieldLabel) → `aria-describedby`. */
|
|
43
|
+
describedBy?: string;
|
|
44
|
+
/** Ghost shown on the empty leaf. An inline leaf's is the resolved `default:`
|
|
45
|
+
* or nothing; a body's always has text, falling back to an invitation
|
|
46
|
+
* (`resolveBodyGhost`). Reactive: a change is pushed into the mounted view,
|
|
47
|
+
* never a remount. */
|
|
48
|
+
placeholder?: string;
|
|
49
|
+
/** Stable identity for the registry, stamped on the DOM node so a remount is
|
|
50
|
+
* visible as one. */
|
|
51
|
+
leafKey: string;
|
|
52
|
+
onFocus?: (addr: Addr) => void;
|
|
53
|
+
onCaretMove?: (addr: Addr, pos: number) => void;
|
|
54
|
+
/** A commit landed on this leaf: the prose change lane. */
|
|
55
|
+
onChange?: (addr: Addr) => void;
|
|
56
|
+
onError?: EditorErrorHandler;
|
|
57
|
+
register?: (key: string, controller: FieldController) => void;
|
|
58
|
+
unregister?: (key: string) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let {
|
|
62
|
+
doc,
|
|
63
|
+
quill,
|
|
64
|
+
addr,
|
|
65
|
+
inline,
|
|
66
|
+
plaintext,
|
|
67
|
+
unframed,
|
|
68
|
+
label,
|
|
69
|
+
labelledBy,
|
|
70
|
+
describedBy,
|
|
71
|
+
placeholder,
|
|
72
|
+
leafKey,
|
|
73
|
+
onFocus,
|
|
74
|
+
onCaretMove,
|
|
75
|
+
onChange,
|
|
76
|
+
onError,
|
|
77
|
+
register,
|
|
78
|
+
unregister
|
|
79
|
+
}: Props = $props();
|
|
80
|
+
|
|
81
|
+
let containerEl: HTMLDivElement | undefined = $state();
|
|
82
|
+
|
|
83
|
+
// The island chrome's wording, read where every other surface reads it. Passed as
|
|
84
|
+
// a GETTER: the codec draws that chrome on each render, so a locale swap reaches a
|
|
85
|
+
// mounted table without remounting the leaf and losing the caret.
|
|
86
|
+
const t = wording();
|
|
87
|
+
|
|
88
|
+
/** The insert menu's live state, pushed by the leaf's trigger plugin. Held HERE
|
|
89
|
+
* rather than in the shell: a trigger belongs to one caret in one leaf, so the
|
|
90
|
+
* menu is the leaf's surface, unlike the one format popover that follows whichever
|
|
91
|
+
* leaf is active. A constrained leaf never receives one (`createField` mounts the
|
|
92
|
+
* trigger on the block schema alone). */
|
|
93
|
+
let slash: SlashState | undefined = $state();
|
|
94
|
+
|
|
95
|
+
// Block prose vs a control in a row of controls: the SAME predicate the codec
|
|
96
|
+
// picks its schema by (`createField`: `plaintext` implies `inline`), so the box a
|
|
97
|
+
// leaf draws and the schema it holds cannot disagree. Keyed on `inline` alone, a
|
|
98
|
+
// `plaintext`-only field would take the body's floor while holding one paragraph.
|
|
99
|
+
const block = $derived(!(inline || plaintext));
|
|
100
|
+
|
|
101
|
+
// An absent richtext field (a `default:`-only field like `tag_line`) is handled
|
|
102
|
+
// by the codec itself: `createField` decodes an empty content and overwrites on
|
|
103
|
+
// the first edit; so this wrapper adds no pre-seeding, and an untouched field
|
|
104
|
+
// stays absent (its default rendering intact) until actually edited.
|
|
105
|
+
let controller: FieldController | undefined;
|
|
106
|
+
|
|
107
|
+
/** Take the caret: what the label click calls. The CONTROLLER's focus, not the
|
|
108
|
+
* container's: a PM view restores a selection that an `HTMLElement.focus` on
|
|
109
|
+
* the wrapper leaves unplaced. */
|
|
110
|
+
export function focus(): void {
|
|
111
|
+
controller?.focus();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
onMount(() => {
|
|
115
|
+
if (!containerEl) return;
|
|
116
|
+
controller = createField({
|
|
117
|
+
doc,
|
|
118
|
+
quill,
|
|
119
|
+
addr,
|
|
120
|
+
container: containerEl,
|
|
121
|
+
inline,
|
|
122
|
+
plaintext,
|
|
123
|
+
label,
|
|
124
|
+
labelledBy,
|
|
125
|
+
describedBy,
|
|
126
|
+
placeholder,
|
|
127
|
+
tableStrings: () => t.strings,
|
|
128
|
+
slashStrings: () => t.strings,
|
|
129
|
+
onSlash: (next) => {
|
|
130
|
+
slash = next;
|
|
131
|
+
},
|
|
132
|
+
onFocus,
|
|
133
|
+
onCaretMove,
|
|
134
|
+
onChange,
|
|
135
|
+
onError
|
|
136
|
+
});
|
|
137
|
+
register?.(leafKey, controller);
|
|
138
|
+
return () => {
|
|
139
|
+
unregister?.(leafKey);
|
|
140
|
+
controller?.destroy();
|
|
141
|
+
controller = undefined;
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// The ghost outlives no remount of its own: the leaf is keyed by the card's
|
|
146
|
+
// session id, so a card RETYPED to another kind keeps this view and would keep
|
|
147
|
+
// the old kind's wording. Push it instead: the caret is worth more than the
|
|
148
|
+
// simplicity of rebuilding.
|
|
149
|
+
$effect(() => {
|
|
150
|
+
controller?.setPlaceholder(placeholder);
|
|
151
|
+
});
|
|
152
|
+
</script>
|
|
153
|
+
|
|
154
|
+
<div
|
|
155
|
+
bind:this={containerEl}
|
|
156
|
+
class="qm-prose"
|
|
157
|
+
class:qm-control-box={!unframed}
|
|
158
|
+
class:qm-prose-block={block}
|
|
159
|
+
data-leaf-key={leafKey}
|
|
160
|
+
></div>
|
|
161
|
+
<SlashMenu menu={slash} leaf={() => controller} label={t.strings.slashLabel} />
|
|
162
|
+
|
|
163
|
+
<style>
|
|
164
|
+
/* The box is `.qm-control-box` (controls.css): the same rule the input beside it
|
|
165
|
+
draws, so the two agree on height by construction rather than by two floors
|
|
166
|
+
tuned to match. Nothing here restates it, and there is no
|
|
167
|
+
`min-height`: `core/codec/prose.css` resets the paragraph box, so one line of
|
|
168
|
+
prose in this box measures one line of text in that one. */
|
|
169
|
+
.qm-prose {
|
|
170
|
+
/* Positioned for the arrival wash `setCaret` inserts (`core/bloom.ts`): an
|
|
171
|
+
inset child, so it covers the text without fading it or tinting the leaf's
|
|
172
|
+
own surface. */
|
|
173
|
+
position: relative;
|
|
174
|
+
}
|
|
175
|
+
/* A leaf the inline schema does NOT constrain (the body) is paper rather than a
|
|
176
|
+
cell in a row of controls, so it opens at a few lines and grows. The floor is
|
|
177
|
+
three LINE BOXES, not a length: size times leading is what one line measures, so
|
|
178
|
+
both factors are named or the expression stops meaning the three lines it claims.
|
|
179
|
+
It moves with the type ramp and with nothing else. Three is what an EMPTY body
|
|
180
|
+
costs, which is the only state the floor is ever the height: the ghost takes the
|
|
181
|
+
first line, so the opening reads as an invitation with room under it rather than
|
|
182
|
+
as a drop. */
|
|
183
|
+
.qm-prose-block {
|
|
184
|
+
min-height: calc(var(--_qm-text-body) * var(--_qm-leading-body) * 3);
|
|
185
|
+
}
|
|
186
|
+
/* The caret is the prose leaf's focus indicator, not a ring: a ring around a
|
|
187
|
+
contenteditable reads as form chrome rather than as paper. So the outline is
|
|
188
|
+
dropped; a boxed leaf tints its wrapper border below. */
|
|
189
|
+
.qm-prose :global(.ProseMirror) {
|
|
190
|
+
outline: none;
|
|
191
|
+
}
|
|
192
|
+
/* Active-leaf cue: tint the hairline to the focus hue, shared with the scalar
|
|
193
|
+
ring and the preview active box: no added box (the editor↔preview address).
|
|
194
|
+
Scoped to the leaves that HAVE a hairline: the body has none, and the caret is
|
|
195
|
+
the whole indicator there: one surface per card, and the largest in it, so
|
|
196
|
+
"which leaf am I in" is not a question it raises. A rule drawn to answer it
|
|
197
|
+
would be a stroke bought back to replace the one unframing removed. */
|
|
198
|
+
.qm-prose.qm-control-box:focus-within {
|
|
199
|
+
border-color: var(--_qm-accent);
|
|
200
|
+
}
|
|
201
|
+
/* Empty-leaf ghost: the resolved `default:` as dim/italic ghost,
|
|
202
|
+
matching the scalar ghost rung. Rendered from a node decoration's data attr so
|
|
203
|
+
it stays out of the
|
|
204
|
+
document; `float`/`height:0` keep it from displacing the caret. */
|
|
205
|
+
.qm-prose :global(.ProseMirror .qm-prose-placeholder::before) {
|
|
206
|
+
content: attr(data-placeholder);
|
|
207
|
+
color: var(--_qm-ink-ghost);
|
|
208
|
+
font-style: italic;
|
|
209
|
+
float: left;
|
|
210
|
+
height: 0;
|
|
211
|
+
pointer-events: none;
|
|
212
|
+
}
|
|
213
|
+
</style>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type FieldController } from '../core/codec/index.js';
|
|
2
|
+
import './controls.css';
|
|
3
|
+
import type { Document, Quill, Addr } from '@quillmark/wasm';
|
|
4
|
+
import type { EditorErrorHandler } from '../core/errors.js';
|
|
5
|
+
interface Props {
|
|
6
|
+
doc: Document;
|
|
7
|
+
/** The schema this leaf reads its content through: `createField` binds a reader
|
|
8
|
+
* off it, so a stored string decodes by declared type rather than by guess. */
|
|
9
|
+
quill: Quill;
|
|
10
|
+
addr: Addr;
|
|
11
|
+
inline?: boolean;
|
|
12
|
+
plaintext?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Draw no box: the card body's leaf, and nothing else. A property
|
|
15
|
+
* of the SLOT rather than of the leaf: `block` is not the same predicate, since
|
|
16
|
+
* a `richtext` field without `inline` is block prose and still a control in a
|
|
17
|
+
* row of controls. Only the body is paper, and only its caller knows it is.
|
|
18
|
+
*/
|
|
19
|
+
unframed?: boolean;
|
|
20
|
+
/** Accessible name for a leaf NOTHING else names: an array element, or the
|
|
21
|
+
* card body. A leaf with a field label takes `labelledBy` instead. */
|
|
22
|
+
label?: string;
|
|
23
|
+
/** The field label's own id → `aria-labelledby` on the editable region. `for`
|
|
24
|
+
* cannot reach a `contenteditable` (not a labelable element) so the
|
|
25
|
+
* association runs the other way, and the label's click comes back through
|
|
26
|
+
* the controller's `focus`. */
|
|
27
|
+
labelledBy?: string;
|
|
28
|
+
/** The parked `description` (FieldLabel) → `aria-describedby`. */
|
|
29
|
+
describedBy?: string;
|
|
30
|
+
/** Ghost shown on the empty leaf. An inline leaf's is the resolved `default:`
|
|
31
|
+
* or nothing; a body's always has text, falling back to an invitation
|
|
32
|
+
* (`resolveBodyGhost`). Reactive: a change is pushed into the mounted view,
|
|
33
|
+
* never a remount. */
|
|
34
|
+
placeholder?: string;
|
|
35
|
+
/** Stable identity for the registry, stamped on the DOM node so a remount is
|
|
36
|
+
* visible as one. */
|
|
37
|
+
leafKey: string;
|
|
38
|
+
onFocus?: (addr: Addr) => void;
|
|
39
|
+
onCaretMove?: (addr: Addr, pos: number) => void;
|
|
40
|
+
/** A commit landed on this leaf: the prose change lane. */
|
|
41
|
+
onChange?: (addr: Addr) => void;
|
|
42
|
+
onError?: EditorErrorHandler;
|
|
43
|
+
register?: (key: string, controller: FieldController) => void;
|
|
44
|
+
unregister?: (key: string) => void;
|
|
45
|
+
}
|
|
46
|
+
declare const ProseField: import("svelte").Component<Props, {
|
|
47
|
+
focus: () => void;
|
|
48
|
+
}, "">;
|
|
49
|
+
type ProseField = ReturnType<typeof ProseField>;
|
|
50
|
+
export default ProseField;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
The slash menu. The leaf's own surface, not the
|
|
3
|
+
shell's: the trigger belongs to one caret in one leaf, so it is mounted by
|
|
4
|
+
`ProseField` beside the view it reads from, unlike the ONE shell-owned format
|
|
5
|
+
popover that follows whichever leaf is active.
|
|
6
|
+
|
|
7
|
+
The MODEL is the codec's (`codec/slash.ts`: the trigger run, the query, the
|
|
8
|
+
highlighted index, and what each pick does). This component draws it and nothing
|
|
9
|
+
else. The keys are the leaf's keymap for the same reason: the caret stays in the
|
|
10
|
+
contenteditable while the menu is open, so a focus-taking listbox would move the
|
|
11
|
+
selection the insert is measured against. What is left here is the POINTER's half:
|
|
12
|
+
buttons that pick, and that swallow their own mousedown so the caret stays put.
|
|
13
|
+
|
|
14
|
+
The highlight has ONE lane: `data-highlighted` marks whichever item the codec's
|
|
15
|
+
index names, and a pointer entering an item MOVES that index rather than painting a
|
|
16
|
+
second highlight the keyboard cannot reach (controls.css, the shared menu recipe).
|
|
17
|
+
-->
|
|
18
|
+
<script lang="ts">
|
|
19
|
+
import { Popover } from 'bits-ui';
|
|
20
|
+
import type { FieldController, SlashState } from '../core/codec/index.js';
|
|
21
|
+
import './controls.css';
|
|
22
|
+
|
|
23
|
+
interface Props {
|
|
24
|
+
/** The live menu, or `undefined` when closed: the codec's report channel. Named
|
|
25
|
+
* `menu` rather than `state`, which is the rune's word in this file. */
|
|
26
|
+
menu: SlashState | undefined;
|
|
27
|
+
/** The leaf the trigger is in; `slashFocus` / `slashPick` drive it. */
|
|
28
|
+
leaf: () => FieldController | undefined;
|
|
29
|
+
/** The menu's accessible name. */
|
|
30
|
+
label: string;
|
|
31
|
+
}
|
|
32
|
+
let { menu, leaf, label }: Props = $props();
|
|
33
|
+
|
|
34
|
+
/** Raised whenever the codec reports offers. Mirrored into local state rather than
|
|
35
|
+
* derived, because the primitive OWNS this prop: its own dismissal layer (an
|
|
36
|
+
* outside press) writes it, and a derived it cannot write to is an error rather
|
|
37
|
+
* than a dismissal. The next report re-syncs, so the codec stays the authority on
|
|
38
|
+
* whether a run is live. */
|
|
39
|
+
let open = $state(false);
|
|
40
|
+
$effect(() => {
|
|
41
|
+
open = !!menu && menu.items.length > 0;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/** The root to portal INTO, resolved from the leaf's own DOM so the menu lands
|
|
45
|
+
* inside whichever `[data-qm-root]` raised it and inherits the consumer's dials;
|
|
46
|
+
* `undefined` falls back to bits-ui's `document.body`. */
|
|
47
|
+
const portalTarget = $derived(leaf()?.el.closest<HTMLElement>('[data-qm-root]') ?? undefined);
|
|
48
|
+
|
|
49
|
+
/** Swallow the item's own mousedown: without it the browser focuses the item and
|
|
50
|
+
* blurs the leaf, taking the trigger run's caret with it. */
|
|
51
|
+
function keepFocus(e: MouseEvent): void {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<Popover.Root bind:open>
|
|
57
|
+
<Popover.Portal to={portalTarget}>
|
|
58
|
+
<Popover.Content
|
|
59
|
+
customAnchor={menu?.anchor ?? null}
|
|
60
|
+
side="bottom"
|
|
61
|
+
align="start"
|
|
62
|
+
trapFocus={false}
|
|
63
|
+
sideOffset={6}
|
|
64
|
+
onOpenAutoFocus={(e: Event) => e.preventDefault()}
|
|
65
|
+
onCloseAutoFocus={(e: Event) => e.preventDefault()}
|
|
66
|
+
>
|
|
67
|
+
{#snippet child({ props, wrapperProps, open: raised })}
|
|
68
|
+
<div {...wrapperProps}>
|
|
69
|
+
<!-- `role="group"`, not `listbox`: the caret never leaves the leaf, so no
|
|
70
|
+
item is ever the focused option an `option` promises, and the buttons
|
|
71
|
+
carry no roving tabindex. A labelled group of real buttons is the honest
|
|
72
|
+
description, and it is the shape the format popover already takes.
|
|
73
|
+
`inert` is the dismissal's other half: a surface on its way out is not
|
|
74
|
+
a thing to click, tab into, or read. -->
|
|
75
|
+
<div
|
|
76
|
+
{...props}
|
|
77
|
+
class="qm-slash-menu qm-menu-surface"
|
|
78
|
+
data-qm-root
|
|
79
|
+
role="group"
|
|
80
|
+
aria-label={label}
|
|
81
|
+
inert={!raised}
|
|
82
|
+
>
|
|
83
|
+
{#each menu?.items ?? [] as item, i (item.id)}
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
class="qm-menu-item qm-slash-item"
|
|
87
|
+
data-highlighted={i === menu?.index ? '' : undefined}
|
|
88
|
+
onmousedown={keepFocus}
|
|
89
|
+
onclick={() => leaf()?.slashPick(item.id)}
|
|
90
|
+
onpointerenter={() => leaf()?.slashFocus(item.id)}
|
|
91
|
+
>
|
|
92
|
+
{item.label}
|
|
93
|
+
</button>
|
|
94
|
+
{/each}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
{/snippet}
|
|
98
|
+
</Popover.Content>
|
|
99
|
+
</Popover.Portal>
|
|
100
|
+
</Popover.Root>
|
|
101
|
+
|
|
102
|
+
<style>
|
|
103
|
+
/* The surface, its inset, its item pad and the rung a highlight fills with are all
|
|
104
|
+
`.qm-menu-surface` / `.qm-menu-item` (controls.css): the same recipe the enum
|
|
105
|
+
listbox and the card stack's kind menu draw, because all three are lists picked
|
|
106
|
+
from. What is here is this menu's own: a measure, so a long label does not stretch
|
|
107
|
+
the surface across the card, and a scroll for a filtered list that stays long. */
|
|
108
|
+
.qm-slash-menu {
|
|
109
|
+
min-width: 12rem;
|
|
110
|
+
max-height: 16rem;
|
|
111
|
+
overflow-y: auto;
|
|
112
|
+
}
|
|
113
|
+
/* An item is a row of text, not a glyph: the whole row is the target, and the label
|
|
114
|
+
is the accessible name with nothing beside it. A button is a button, so the UA's
|
|
115
|
+
own box is taken back; what is left is the shared item recipe. */
|
|
116
|
+
.qm-slash-item {
|
|
117
|
+
display: block;
|
|
118
|
+
width: 100%;
|
|
119
|
+
border: none;
|
|
120
|
+
background: none;
|
|
121
|
+
font: inherit;
|
|
122
|
+
color: inherit;
|
|
123
|
+
text-align: left;
|
|
124
|
+
white-space: nowrap;
|
|
125
|
+
}
|
|
126
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FieldController, SlashState } from '../core/codec/index.js';
|
|
2
|
+
import './controls.css';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** The live menu, or `undefined` when closed: the codec's report channel. Named
|
|
5
|
+
* `menu` rather than `state`, which is the rune's word in this file. */
|
|
6
|
+
menu: SlashState | undefined;
|
|
7
|
+
/** The leaf the trigger is in; `slashFocus` / `slashPick` drive it. */
|
|
8
|
+
leaf: () => FieldController | undefined;
|
|
9
|
+
/** The menu's accessible name. */
|
|
10
|
+
label: string;
|
|
11
|
+
}
|
|
12
|
+
declare const SlashMenu: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type SlashMenu = ReturnType<typeof SlashMenu>;
|
|
14
|
+
export default SlashMenu;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
A `string` field → text input. Commits a non-empty edit LIVE (on input) via the
|
|
3
|
+
parent's typed `writer.set`, so the preview tracks typing. A cleared field is the
|
|
4
|
+
UNSET rung of the commitment ladder (VISUAL_EDITOR §"Structure mirrors the schema"): it
|
|
5
|
+
commits `undefined` (the parent removes the field, ghosted `default:` renders):
|
|
6
|
+
but at `change` (blur), NOT per keystroke, so select-all-and-retype doesn't flash
|
|
7
|
+
the field through its default between the delete and the first typed char.
|
|
8
|
+
Trade-off (recorded in VISUAL_EDITOR): an explicit empty string OVER a non-empty
|
|
9
|
+
default is inexpressible from the UI: clear and unset collapse to one gesture.
|
|
10
|
+
-->
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
import { syncedLocal } from './synced.svelte.js';
|
|
13
|
+
import './controls.css';
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
value: string | undefined;
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
/** Accessible name for an input NOTHING else names: an array element, whose
|
|
19
|
+
* name is the field label plus its 1-based index. A field's own input takes
|
|
20
|
+
* `id` instead and is named by the `<label for>` beside it. */
|
|
21
|
+
label?: string;
|
|
22
|
+
/** `<label for>` target. Set → the label names this input, so `aria-label`
|
|
23
|
+
* comes off: two names is where implementations disagree about which wins. */
|
|
24
|
+
id?: string;
|
|
25
|
+
/** The parked `description` (FieldLabel): announced after the name. */
|
|
26
|
+
describedBy?: string;
|
|
27
|
+
onCommit: (v: string | undefined) => void;
|
|
28
|
+
/** Raw keydown, for a container whose own keys run through this control: the
|
|
29
|
+
* array repeater's Enter/Backspace (`ArrayField`). */
|
|
30
|
+
onKey?: (e: KeyboardEvent) => void;
|
|
31
|
+
}
|
|
32
|
+
let { value, placeholder, label, id, describedBy, onCommit, onKey }: Props = $props();
|
|
33
|
+
|
|
34
|
+
// Local input state synced to `value`: own-typing stays local, only an external
|
|
35
|
+
// change reconciles back in (see `syncedLocal`).
|
|
36
|
+
const local = syncedLocal(() => value ?? '');
|
|
37
|
+
|
|
38
|
+
let inputEl: HTMLInputElement | undefined = $state();
|
|
39
|
+
/** Take the caret: what a parent placing focus on this control calls. */
|
|
40
|
+
export function focus(): void {
|
|
41
|
+
inputEl?.focus();
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<input
|
|
46
|
+
bind:this={inputEl}
|
|
47
|
+
class="qm-input qm-focus-ring"
|
|
48
|
+
type="text"
|
|
49
|
+
value={local.value}
|
|
50
|
+
{id}
|
|
51
|
+
{placeholder}
|
|
52
|
+
aria-label={id ? undefined : label}
|
|
53
|
+
aria-describedby={describedBy}
|
|
54
|
+
onkeydown={onKey}
|
|
55
|
+
oninput={(e) => {
|
|
56
|
+
local.value = (e.currentTarget as HTMLInputElement).value;
|
|
57
|
+
// Live-commit a non-empty edit; defer a cleared field to `change` (see header).
|
|
58
|
+
if (local.value !== '') onCommit(local.value);
|
|
59
|
+
}}
|
|
60
|
+
onchange={() => {
|
|
61
|
+
if (local.value === '') onCommit(undefined);
|
|
62
|
+
}}
|
|
63
|
+
/>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import './controls.css';
|
|
2
|
+
interface Props {
|
|
3
|
+
value: string | undefined;
|
|
4
|
+
placeholder?: string;
|
|
5
|
+
/** Accessible name for an input NOTHING else names: an array element, whose
|
|
6
|
+
* name is the field label plus its 1-based index. A field's own input takes
|
|
7
|
+
* `id` instead and is named by the `<label for>` beside it. */
|
|
8
|
+
label?: string;
|
|
9
|
+
/** `<label for>` target. Set → the label names this input, so `aria-label`
|
|
10
|
+
* comes off: two names is where implementations disagree about which wins. */
|
|
11
|
+
id?: string;
|
|
12
|
+
/** The parked `description` (FieldLabel): announced after the name. */
|
|
13
|
+
describedBy?: string;
|
|
14
|
+
onCommit: (v: string | undefined) => void;
|
|
15
|
+
/** Raw keydown, for a container whose own keys run through this control: the
|
|
16
|
+
* array repeater's Enter/Backspace (`ArrayField`). */
|
|
17
|
+
onKey?: (e: KeyboardEvent) => void;
|
|
18
|
+
}
|
|
19
|
+
declare const TextField: import("svelte").Component<Props, {
|
|
20
|
+
focus: () => void;
|
|
21
|
+
}, "">;
|
|
22
|
+
type TextField = ReturnType<typeof TextField>;
|
|
23
|
+
export default TextField;
|