@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.
Files changed (143) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +58 -0
  3. package/README.md +241 -0
  4. package/THEMING.md +153 -0
  5. package/dist/core/address.d.ts +56 -0
  6. package/dist/core/address.js +95 -0
  7. package/dist/core/bloom.d.ts +37 -0
  8. package/dist/core/bloom.js +116 -0
  9. package/dist/core/codec/anchor.d.ts +19 -0
  10. package/dist/core/codec/anchor.js +36 -0
  11. package/dist/core/codec/code.d.ts +12 -0
  12. package/dist/core/codec/code.js +118 -0
  13. package/dist/core/codec/decode.d.ts +24 -0
  14. package/dist/core/codec/decode.js +270 -0
  15. package/dist/core/codec/encode.d.ts +71 -0
  16. package/dist/core/codec/encode.js +480 -0
  17. package/dist/core/codec/field.d.ts +171 -0
  18. package/dist/core/codec/field.js +493 -0
  19. package/dist/core/codec/index.d.ts +22 -0
  20. package/dist/core/codec/index.js +43 -0
  21. package/dist/core/codec/inputrules.d.ts +7 -0
  22. package/dist/core/codec/inputrules.js +168 -0
  23. package/dist/core/codec/islands.d.ts +36 -0
  24. package/dist/core/codec/islands.js +82 -0
  25. package/dist/core/codec/keymap.d.ts +13 -0
  26. package/dist/core/codec/keymap.js +29 -0
  27. package/dist/core/codec/lists.d.ts +22 -0
  28. package/dist/core/codec/lists.js +104 -0
  29. package/dist/core/codec/marks.d.ts +37 -0
  30. package/dist/core/codec/marks.js +74 -0
  31. package/dist/core/codec/positions.d.ts +17 -0
  32. package/dist/core/codec/positions.js +108 -0
  33. package/dist/core/codec/prose.css +458 -0
  34. package/dist/core/codec/reconcile.d.ts +26 -0
  35. package/dist/core/codec/reconcile.js +64 -0
  36. package/dist/core/codec/schema.d.ts +13 -0
  37. package/dist/core/codec/schema.js +155 -0
  38. package/dist/core/codec/slash.d.ts +79 -0
  39. package/dist/core/codec/slash.js +0 -0
  40. package/dist/core/codec/table-view.d.ts +50 -0
  41. package/dist/core/codec/table-view.js +928 -0
  42. package/dist/core/codec/table.d.ts +85 -0
  43. package/dist/core/codec/table.js +224 -0
  44. package/dist/core/errors.d.ts +73 -0
  45. package/dist/core/errors.js +24 -0
  46. package/dist/core/index.d.ts +5 -0
  47. package/dist/core/index.js +19 -0
  48. package/dist/core/lifecycle.d.ts +7 -0
  49. package/dist/core/lifecycle.js +21 -0
  50. package/dist/core/rebind.svelte.d.ts +18 -0
  51. package/dist/core/rebind.svelte.js +44 -0
  52. package/dist/core/teardown.d.ts +20 -0
  53. package/dist/core/teardown.js +37 -0
  54. package/dist/core/theme.css +276 -0
  55. package/dist/index.d.ts +1 -0
  56. package/dist/index.js +9 -0
  57. package/dist/preset/index.css +10 -0
  58. package/dist/preset/recipes.css +188 -0
  59. package/dist/preset/scale.css +97 -0
  60. package/dist/preview/Preview.svelte +118 -0
  61. package/dist/preview/Preview.svelte.d.ts +39 -0
  62. package/dist/preview/bridge.d.ts +13 -0
  63. package/dist/preview/bridge.js +88 -0
  64. package/dist/preview/controller.d.ts +40 -0
  65. package/dist/preview/controller.js +141 -0
  66. package/dist/preview/geometry.d.ts +37 -0
  67. package/dist/preview/geometry.js +45 -0
  68. package/dist/preview/index.d.ts +5 -0
  69. package/dist/preview/index.js +14 -0
  70. package/dist/preview/overlay.d.ts +10 -0
  71. package/dist/preview/overlay.js +82 -0
  72. package/dist/preview/paint.d.ts +17 -0
  73. package/dist/preview/paint.js +262 -0
  74. package/dist/preview/strings.d.ts +14 -0
  75. package/dist/preview/strings.js +22 -0
  76. package/dist/visual/ArrayField.svelte +362 -0
  77. package/dist/visual/ArrayField.svelte.d.ts +24 -0
  78. package/dist/visual/BooleanField.svelte +88 -0
  79. package/dist/visual/BooleanField.svelte.d.ts +20 -0
  80. package/dist/visual/Card.svelte +849 -0
  81. package/dist/visual/Card.svelte.d.ts +43 -0
  82. package/dist/visual/CardControls.svelte +92 -0
  83. package/dist/visual/CardControls.svelte.d.ts +11 -0
  84. package/dist/visual/DateField.svelte +203 -0
  85. package/dist/visual/DateField.svelte.d.ts +28 -0
  86. package/dist/visual/DiagnosticList.svelte +50 -0
  87. package/dist/visual/DiagnosticList.svelte.d.ts +7 -0
  88. package/dist/visual/EnumField.svelte +169 -0
  89. package/dist/visual/EnumField.svelte.d.ts +25 -0
  90. package/dist/visual/Field.svelte +270 -0
  91. package/dist/visual/Field.svelte.d.ts +37 -0
  92. package/dist/visual/FieldHint.svelte +185 -0
  93. package/dist/visual/FieldHint.svelte.d.ts +13 -0
  94. package/dist/visual/FieldLabel.svelte +141 -0
  95. package/dist/visual/FieldLabel.svelte.d.ts +19 -0
  96. package/dist/visual/FormatPopover.svelte +396 -0
  97. package/dist/visual/FormatPopover.svelte.d.ts +9 -0
  98. package/dist/visual/NumberField.svelte +72 -0
  99. package/dist/visual/NumberField.svelte.d.ts +19 -0
  100. package/dist/visual/ObjectField.svelte +131 -0
  101. package/dist/visual/ObjectField.svelte.d.ts +17 -0
  102. package/dist/visual/ProseArrayElement.svelte +98 -0
  103. package/dist/visual/ProseArrayElement.svelte.d.ts +19 -0
  104. package/dist/visual/ProseField.svelte +213 -0
  105. package/dist/visual/ProseField.svelte.d.ts +50 -0
  106. package/dist/visual/SlashMenu.svelte +126 -0
  107. package/dist/visual/SlashMenu.svelte.d.ts +14 -0
  108. package/dist/visual/TextField.svelte +63 -0
  109. package/dist/visual/TextField.svelte.d.ts +23 -0
  110. package/dist/visual/TipsCard.svelte +191 -0
  111. package/dist/visual/TipsCard.svelte.d.ts +13 -0
  112. package/dist/visual/VisualEditor.svelte +96 -0
  113. package/dist/visual/VisualEditor.svelte.d.ts +14 -0
  114. package/dist/visual/VisualEditorInner.svelte +1032 -0
  115. package/dist/visual/VisualEditorInner.svelte.d.ts +17 -0
  116. package/dist/visual/controls.css +272 -0
  117. package/dist/visual/diagnostics.d.ts +52 -0
  118. package/dist/visual/diagnostics.js +83 -0
  119. package/dist/visual/domid.d.ts +21 -0
  120. package/dist/visual/domid.js +36 -0
  121. package/dist/visual/ext.d.ts +19 -0
  122. package/dist/visual/ext.js +29 -0
  123. package/dist/visual/icons/Icon.svelte +39 -0
  124. package/dist/visual/icons/Icon.svelte.d.ts +12 -0
  125. package/dist/visual/icons/nodes.d.ts +103 -0
  126. package/dist/visual/icons/nodes.js +55 -0
  127. package/dist/visual/index.d.ts +9 -0
  128. package/dist/visual/index.js +24 -0
  129. package/dist/visual/motion.d.ts +18 -0
  130. package/dist/visual/motion.js +53 -0
  131. package/dist/visual/props.d.ts +77 -0
  132. package/dist/visual/props.js +1 -0
  133. package/dist/visual/signals.d.ts +59 -0
  134. package/dist/visual/signals.js +1 -0
  135. package/dist/visual/strings.d.ts +123 -0
  136. package/dist/visual/strings.js +80 -0
  137. package/dist/visual/structure.d.ts +224 -0
  138. package/dist/visual/structure.js +270 -0
  139. package/dist/visual/synced.svelte.d.ts +15 -0
  140. package/dist/visual/synced.svelte.js +34 -0
  141. package/dist/visual/tips.d.ts +24 -0
  142. package/dist/visual/tips.js +50 -0
  143. 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;