@quillmark/svelte 0.16.0 → 0.18.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 +4 -2
- package/dist/core/codec/index.d.ts +0 -3
- package/dist/core/rebind.svelte.js +5 -8
- package/dist/core/theme.css +12 -1
- package/dist/preset/index.d.ts +1 -0
- package/dist/preview/Preview.svelte +14 -19
- package/dist/preview/Preview.svelte.d.ts +3 -3
- package/dist/preview/index.d.ts +0 -1
- package/dist/visual/ArrayField.svelte +44 -41
- package/dist/visual/ArrayField.svelte.d.ts +0 -1
- package/dist/visual/BooleanField.svelte +16 -14
- package/dist/visual/BooleanField.svelte.d.ts +0 -1
- package/dist/visual/Card.svelte +65 -35
- package/dist/visual/CardControls.svelte.d.ts +0 -1
- package/dist/visual/DateField.svelte +14 -17
- package/dist/visual/DateField.svelte.d.ts +0 -1
- package/dist/visual/EnumField.svelte +9 -12
- package/dist/visual/EnumField.svelte.d.ts +0 -1
- package/dist/visual/Field.svelte +17 -10
- package/dist/visual/Field.svelte.d.ts +3 -0
- package/dist/visual/FieldHint.svelte.d.ts +0 -1
- package/dist/visual/FieldLabel.svelte +1 -2
- package/dist/visual/FormatPopover.svelte +22 -27
- package/dist/visual/FormatPopover.svelte.d.ts +0 -1
- package/dist/visual/MatrixAdd.svelte +210 -0
- package/dist/visual/MatrixAdd.svelte.d.ts +15 -0
- package/dist/visual/MatrixField.svelte +329 -84
- package/dist/visual/MatrixField.svelte.d.ts +8 -6
- package/dist/visual/NumberField.svelte +6 -10
- package/dist/visual/NumberField.svelte.d.ts +0 -1
- package/dist/visual/ObjectField.svelte +30 -17
- package/dist/visual/ObjectField.svelte.d.ts +3 -1
- package/dist/visual/ProseField.svelte +42 -41
- package/dist/visual/ProseField.svelte.d.ts +0 -1
- package/dist/visual/ProseValue.svelte +56 -56
- package/dist/visual/ProseValue.svelte.d.ts +0 -1
- package/dist/visual/SlashMenu.svelte +5 -15
- package/dist/visual/SlashMenu.svelte.d.ts +0 -1
- package/dist/visual/TextField.svelte +5 -9
- package/dist/visual/TextField.svelte.d.ts +0 -1
- package/dist/visual/TipsCard.svelte +10 -11
- package/dist/visual/TipsCard.svelte.d.ts +0 -1
- package/dist/visual/VisualEditorInner.svelte +21 -14
- package/dist/visual/VisualEditorInner.svelte.d.ts +0 -1
- package/dist/visual/checklists.d.ts +82 -0
- package/dist/visual/checklists.js +171 -0
- package/dist/visual/index.d.ts +0 -1
- package/dist/visual/strings.d.ts +15 -1
- package/dist/visual/strings.js +6 -0
- package/dist/visual/structure.d.ts +63 -31
- package/dist/visual/structure.js +122 -44
- package/package.json +20 -16
- package/dist/visual/synced.svelte.d.ts +0 -15
- package/dist/visual/synced.svelte.js +0 -34
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
ProseValue}) over the boundary's nested read, and a container property — an `array`,
|
|
6
6
|
an `object`, a `matrix`, a variant — mounts its own control here, at the next rung of
|
|
7
7
|
the same figure. Depth is bounded by what is open, not by the schema: a repeater inside
|
|
8
|
-
a subform is collapsed rows again,
|
|
9
|
-
|
|
8
|
+
a subform is collapsed rows again, so an open row under an open row is one figure two
|
|
9
|
+
rungs in.
|
|
10
10
|
|
|
11
11
|
The nesting is a vertical at `--_qm-border`, the ladder's last stroke: the card's edge,
|
|
12
12
|
an open section's vertical one `--_qm-nest` in where the field is in one, this one a rung
|
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
member's id. So a cell reads at whatever depth it sits, which nothing here knows.
|
|
31
31
|
|
|
32
32
|
`bare` is the table's row: the same cells with no labels and no figure of their own,
|
|
33
|
-
laid onto the tracks the table's header names ({@link ArrayField}).
|
|
33
|
+
laid onto the tracks the table's header names ({@link ArrayField}). `flat` is
|
|
34
|
+
`ui.layout: flat`: the cells keep their labels and lose the figure, standing at the
|
|
35
|
+
depth of whatever holds the subform.
|
|
34
36
|
-->
|
|
35
37
|
<script lang="ts">
|
|
36
38
|
import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
|
|
@@ -39,12 +41,14 @@
|
|
|
39
41
|
baseType,
|
|
40
42
|
controlKind,
|
|
41
43
|
declaredContent,
|
|
44
|
+
flatObject,
|
|
42
45
|
humanize,
|
|
43
46
|
isContainer,
|
|
44
47
|
mountKey,
|
|
45
48
|
obliged,
|
|
46
49
|
optionalCell,
|
|
47
50
|
printedText,
|
|
51
|
+
renderedBoolean,
|
|
48
52
|
shortCell
|
|
49
53
|
} from './structure.js';
|
|
50
54
|
import { splitDeep, unrouted, type DeepDiagnostic } from './diagnostics.js';
|
|
@@ -93,6 +97,9 @@
|
|
|
93
97
|
/** A table's row: the cells alone, on the tracks the table's header names, with
|
|
94
98
|
* no labels and no vertical of their own. */
|
|
95
99
|
bare?: boolean;
|
|
100
|
+
/** `ui.layout: flat`: no vertical and no rung, the properties at the depth of the
|
|
101
|
+
* list the subform stands in. */
|
|
102
|
+
flat?: boolean;
|
|
96
103
|
/** Raw keydown off a text or prose cell, for a row whose own keys run through its
|
|
97
104
|
* cells: the table's Enter and Backspace ({@link ArrayField}). */
|
|
98
105
|
onCellKey?: (e: KeyboardEvent, key: string) => void;
|
|
@@ -111,6 +118,7 @@
|
|
|
111
118
|
contentAt,
|
|
112
119
|
onCommit,
|
|
113
120
|
bare = false,
|
|
121
|
+
flat = false,
|
|
114
122
|
onCellKey,
|
|
115
123
|
diagnostics
|
|
116
124
|
}: Props = $props();
|
|
@@ -248,8 +256,7 @@
|
|
|
248
256
|
|
|
249
257
|
<div
|
|
250
258
|
bind:this={rootEl}
|
|
251
|
-
class=
|
|
252
|
-
class:qm-object-bare={bare}
|
|
259
|
+
class={['qm-object', { 'qm-object-bare': bare, 'qm-object-flat': flat }]}
|
|
253
260
|
role="group"
|
|
254
261
|
aria-labelledby={labelledBy}
|
|
255
262
|
aria-describedby={describedBy}
|
|
@@ -278,9 +285,7 @@
|
|
|
278
285
|
(`packable`). A block prose cell spans it too, and keeps its label row. -->
|
|
279
286
|
{@const block = kind === 'prose' && !shortCell(sub)}
|
|
280
287
|
<div
|
|
281
|
-
class=
|
|
282
|
-
class:qm-prop-full={isContainer(kind)}
|
|
283
|
-
class:qm-prop-wide={block}
|
|
288
|
+
class={['qm-object-prop', { 'qm-prop-full': isContainer(kind), 'qm-prop-wide': block }]}
|
|
284
289
|
data-qm-prop={key}
|
|
285
290
|
>
|
|
286
291
|
{#if ids && kind !== 'array' && kind !== 'matrix'}
|
|
@@ -322,7 +327,7 @@
|
|
|
322
327
|
label={named}
|
|
323
328
|
id={ids?.control}
|
|
324
329
|
describedBy={describes}
|
|
325
|
-
value={obj[key]
|
|
330
|
+
value={renderedBoolean(obj[key])}
|
|
326
331
|
fallback={sub.default as boolean | undefined}
|
|
327
332
|
optional={optionalCell(sub)}
|
|
328
333
|
onCommit={(v) => commitProp(key, v)}
|
|
@@ -367,9 +372,8 @@
|
|
|
367
372
|
{:else if kind === 'array'}
|
|
368
373
|
<!-- The repeater owns its label row, the add chip sharing it (`Field` skips
|
|
369
374
|
its own label for one too), so the names are handed down rather than
|
|
370
|
-
drawn here. Its rows are collapsed rows again
|
|
371
|
-
|
|
372
|
-
and one figure. -->
|
|
375
|
+
drawn here. Its rows are collapsed rows again: the depth an open row
|
|
376
|
+
under an open row costs is two rungs of the ladder and one figure. -->
|
|
373
377
|
<ArrayField
|
|
374
378
|
bind:this={nestedEls[key]}
|
|
375
379
|
value={obj[key] as unknown[] | undefined}
|
|
@@ -392,6 +396,7 @@
|
|
|
392
396
|
bind:this={nestedEls[key]}
|
|
393
397
|
value={obj[key] as Record<string, unknown> | undefined}
|
|
394
398
|
properties={sub.properties}
|
|
399
|
+
flat={flatObject(sub)}
|
|
395
400
|
label={ids ? undefined : fallbackName(key, sub)}
|
|
396
401
|
idBase={ids?.control}
|
|
397
402
|
labelledBy={ids?.label}
|
|
@@ -471,6 +476,8 @@
|
|
|
471
476
|
}
|
|
472
477
|
.qm-object:not(.qm-object-bare) {
|
|
473
478
|
container-type: inline-size;
|
|
479
|
+
}
|
|
480
|
+
.qm-object:not(.qm-object-bare, .qm-object-flat) {
|
|
474
481
|
border-inline-start: var(--_qm-vertical-width) solid var(--_qm-border);
|
|
475
482
|
padding-inline-start: var(--_qm-nest);
|
|
476
483
|
padding-block: var(--_qm-space);
|
|
@@ -481,7 +488,14 @@
|
|
|
481
488
|
.qm-object-grid {
|
|
482
489
|
display: grid;
|
|
483
490
|
grid-template-columns: repeat(var(--cols), 1fr);
|
|
484
|
-
gap: var(--_qm-space-2);
|
|
491
|
+
column-gap: var(--_qm-space-2);
|
|
492
|
+
--row-gap: var(--_qm-space-2);
|
|
493
|
+
row-gap: var(--row-gap);
|
|
494
|
+
}
|
|
495
|
+
/* Flat, the rows keep the rhythm of the list the subform stands in: a section's
|
|
496
|
+
(`Card.svelte`) or an enclosing subform's, inherited rather than restated. */
|
|
497
|
+
.qm-object-flat > .qm-object-grid {
|
|
498
|
+
--row-gap: inherit;
|
|
485
499
|
}
|
|
486
500
|
/* A property measures like a field: two tracks over the subform's own rows, so a
|
|
487
501
|
label wrapping in one column leaves its control on the row's baseline rather than a
|
|
@@ -546,10 +560,9 @@
|
|
|
546
560
|
grid-row: auto;
|
|
547
561
|
grid-template-rows: none;
|
|
548
562
|
row-gap: var(--_qm-space-half);
|
|
549
|
-
/* A row
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
align-items: center;
|
|
563
|
+
/* A row reads along its first line: a prose cell that wraps grows downward, and
|
|
564
|
+
the cells beside it stay on the line it started on. */
|
|
565
|
+
align-items: start;
|
|
553
566
|
}
|
|
554
567
|
/* What the row could not hand down stands across it: a diagnostic in the first
|
|
555
568
|
column alone would read as that column's. */
|
|
@@ -2,7 +2,6 @@ import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
|
|
|
2
2
|
import { type DeepDiagnostic } from './diagnostics.js';
|
|
3
3
|
import type { LandingBox } from './leaves.js';
|
|
4
4
|
import ObjectField from './ObjectField.svelte';
|
|
5
|
-
import './controls.css';
|
|
6
5
|
interface Props {
|
|
7
6
|
value: Record<string, unknown> | undefined;
|
|
8
7
|
properties: Record<string, QuillFieldSchema> | undefined;
|
|
@@ -31,6 +30,9 @@ interface Props {
|
|
|
31
30
|
/** A table's row: the cells alone, on the tracks the table's header names, with
|
|
32
31
|
* no labels and no vertical of their own. */
|
|
33
32
|
bare?: boolean;
|
|
33
|
+
/** `ui.layout: flat`: no vertical and no rung, the properties at the depth of the
|
|
34
|
+
* list the subform stands in. */
|
|
35
|
+
flat?: boolean;
|
|
34
36
|
/** Raw keydown off a text or prose cell, for a row whose own keys run through its
|
|
35
37
|
* cells: the table's Enter and Backspace ({@link ArrayField}). */
|
|
36
38
|
onCellKey?: (e: KeyboardEvent, key: string) => void;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
this leaf's commits with the caret riding the untouched view.
|
|
6
6
|
-->
|
|
7
7
|
<script lang="ts">
|
|
8
|
-
import {
|
|
8
|
+
import { untrack } from 'svelte';
|
|
9
9
|
import { createField, type FieldController, type SlashState } from '../core/codec/index.js';
|
|
10
10
|
import type { LeafRegistry } from './leaves.js';
|
|
11
11
|
import SlashMenu from './SlashMenu.svelte';
|
|
@@ -82,7 +82,6 @@
|
|
|
82
82
|
diagnostics
|
|
83
83
|
}: Props = $props();
|
|
84
84
|
|
|
85
|
-
let containerEl: HTMLDivElement | undefined = $state();
|
|
86
85
|
let held = $state(false);
|
|
87
86
|
const uid = $props.id();
|
|
88
87
|
const heldId = `${uid}-held`;
|
|
@@ -108,41 +107,43 @@
|
|
|
108
107
|
controller?.focus();
|
|
109
108
|
}
|
|
110
109
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
110
|
+
function mountField(container: HTMLDivElement): () => void {
|
|
111
|
+
return untrack(() => {
|
|
112
|
+
const mounted = createField({
|
|
113
|
+
doc,
|
|
114
|
+
quill,
|
|
115
|
+
addr,
|
|
116
|
+
container,
|
|
117
|
+
inline,
|
|
118
|
+
plaintext,
|
|
119
|
+
label,
|
|
120
|
+
labelledBy,
|
|
121
|
+
describedBy,
|
|
122
|
+
fallback,
|
|
123
|
+
placeholder,
|
|
124
|
+
placeholderUntilEdit,
|
|
125
|
+
tableStrings: () => t.strings,
|
|
126
|
+
onSlash: (next) => {
|
|
127
|
+
slash = next;
|
|
128
|
+
},
|
|
129
|
+
onFocus,
|
|
130
|
+
onCaretMove,
|
|
131
|
+
onChange,
|
|
132
|
+
onError,
|
|
133
|
+
heldNoteId: heldId,
|
|
134
|
+
onHold: (next) => {
|
|
135
|
+
held = next;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
controller = mounted;
|
|
139
|
+
leaves?.registerProse(leafKey, mounted);
|
|
140
|
+
return () => {
|
|
141
|
+
leaves?.unregisterProse(leafKey);
|
|
142
|
+
mounted.destroy();
|
|
143
|
+
controller = undefined;
|
|
144
|
+
};
|
|
138
145
|
});
|
|
139
|
-
|
|
140
|
-
return () => {
|
|
141
|
-
leaves?.unregisterProse(leafKey);
|
|
142
|
-
controller?.destroy();
|
|
143
|
-
controller = undefined;
|
|
144
|
-
};
|
|
145
|
-
});
|
|
146
|
+
}
|
|
146
147
|
|
|
147
148
|
// A retype does not remount the leaf (its key is the card's session id), so the new
|
|
148
149
|
// kind's ghost and default are pushed into the live view rather than paid for with
|
|
@@ -162,11 +163,11 @@
|
|
|
162
163
|
</script>
|
|
163
164
|
|
|
164
165
|
<div
|
|
165
|
-
|
|
166
|
-
class=
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
{@attach mountField}
|
|
167
|
+
class={[
|
|
168
|
+
'qm-prose',
|
|
169
|
+
{ 'qm-control-box': !unframed, 'qm-focus-ring-within': !unframed, 'qm-prose-block': block }
|
|
170
|
+
]}
|
|
170
171
|
data-leaf-key={leafKey}
|
|
171
172
|
>
|
|
172
173
|
{#if held}
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
schema, read-only, with a note inside its box, and commits nothing.
|
|
32
32
|
-->
|
|
33
33
|
<script lang="ts">
|
|
34
|
-
import {
|
|
34
|
+
import { untrack } from 'svelte';
|
|
35
35
|
import { EditorState, Selection } from 'prosemirror-state';
|
|
36
36
|
import { EditorView } from 'prosemirror-view';
|
|
37
37
|
import {
|
|
@@ -101,7 +101,6 @@
|
|
|
101
101
|
const t = wording();
|
|
102
102
|
const uid = $props.id();
|
|
103
103
|
const heldId = `${uid}-held`;
|
|
104
|
-
let editorEl: HTMLDivElement | undefined = $state();
|
|
105
104
|
let held = $state(false);
|
|
106
105
|
let view: EditorView | undefined;
|
|
107
106
|
/** Take the caret: what a parent placing focus on this leaf calls. The view's
|
|
@@ -134,61 +133,62 @@
|
|
|
134
133
|
view.dispatch(view.state.tr.setSelection(sel).scrollIntoView());
|
|
135
134
|
}
|
|
136
135
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
136
|
+
function mountView(mount: HTMLDivElement): () => void {
|
|
137
|
+
return untrack(() => {
|
|
138
|
+
// The same keymap and plugin stack a `createField` leaf mounts (shared
|
|
139
|
+
// `proseLeafPlugins`), minus the anchor-position plugin: anchors are dropped on
|
|
140
|
+
// the parent's value write, per the header.
|
|
141
|
+
const inline = !block;
|
|
142
|
+
const stored = content();
|
|
143
|
+
const rt = stored ?? fallback ?? emptyContent();
|
|
144
|
+
let defaulted = stored === undefined && !!fallback;
|
|
145
|
+
held = !fitsLeaf(rt, { inline, plaintext });
|
|
146
|
+
const named = held
|
|
147
|
+
? heldAttributes({ label, labelledBy, describedBy }, heldId)
|
|
148
|
+
: (proseAttributes({ label, labelledBy, describedBy }) ?? {});
|
|
149
|
+
const defaultNamed = { ...named, 'data-default': '' };
|
|
150
|
+
const schema = held ? blockSchema : leafSchema({ plaintext, inline });
|
|
151
|
+
const state = EditorState.create({
|
|
152
|
+
doc: decode(rt, schema),
|
|
153
|
+
plugins: held
|
|
154
|
+
? []
|
|
155
|
+
: proseLeafPlugins(schema, {
|
|
156
|
+
inline,
|
|
157
|
+
placeholder: () => placeholder,
|
|
158
|
+
placeholderUntilEdit: true
|
|
159
|
+
})
|
|
160
|
+
});
|
|
161
|
+
const mounted = new EditorView(
|
|
162
|
+
{ mount },
|
|
163
|
+
{
|
|
164
|
+
state,
|
|
165
|
+
editable: () => !held,
|
|
166
|
+
// Which of `aria-label` / `aria-labelledby` wins is the codec's one answer
|
|
167
|
+
// (`proseAttributes`), so a cell carrying a label element and a row carrying
|
|
168
|
+
// none cannot name their regions by different rules.
|
|
169
|
+
attributes: () => (defaulted ? defaultNamed : named),
|
|
170
|
+
dispatchTransaction(tr) {
|
|
171
|
+
const next = mounted.state.apply(tr);
|
|
172
|
+
const edit = tr.docChanged && !held;
|
|
173
|
+
if (edit) defaulted = false;
|
|
174
|
+
mounted.updateState(next);
|
|
175
|
+
if (edit) onChange(pmToContent(next.doc));
|
|
176
|
+
},
|
|
177
|
+
handleDOMEvents: {
|
|
178
|
+
keydown: (_v, e) => {
|
|
179
|
+
onKey?.(e);
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
};
|
|
191
|
-
}
|
|
184
|
+
);
|
|
185
|
+
view = mounted;
|
|
186
|
+
return () => {
|
|
187
|
+
view = undefined;
|
|
188
|
+
mounted.destroy();
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
192
|
</script>
|
|
193
193
|
|
|
194
194
|
<!-- `.qm-control-box` (controls.css) is the whole box, so an array of `richtext` and
|
|
@@ -197,7 +197,7 @@
|
|
|
197
197
|
line of prose measure one line. Width is the row's or the cell's to give: the leaf
|
|
198
198
|
fills the track it is placed in. -->
|
|
199
199
|
<div class="qm-prose-value qm-control-box qm-focus-ring-within">
|
|
200
|
-
<div
|
|
200
|
+
<div {@attach mountView}></div>
|
|
201
201
|
<!-- Inside the box, so the cell or row holds one child either way: a subform cell's
|
|
202
202
|
subgrid has a row for the label and one for the box, and an array row's slabs
|
|
203
203
|
stand the box's height. -->
|
|
@@ -33,30 +33,20 @@
|
|
|
33
33
|
}
|
|
34
34
|
let { menu, leaf, label }: Props = $props();
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
* derived, because the primitive owns this prop: its own dismissal layer (an
|
|
38
|
-
* outside press) writes it, and a derived it cannot write to is an error rather
|
|
39
|
-
* than a dismissal. The next report re-syncs, so the codec stays the authority on
|
|
40
|
-
* whether a run is live. */
|
|
41
|
-
let open = $state(false);
|
|
42
|
-
$effect(() => {
|
|
43
|
-
open = !!menu && menu.items.length > 0;
|
|
44
|
-
});
|
|
36
|
+
let open = $derived(!!menu && menu.items.length > 0);
|
|
45
37
|
|
|
46
38
|
/** The root to portal into, resolved from the leaf's own DOM so the menu lands
|
|
47
39
|
* inside whichever `[data-qm-root]` raised it and inherits the consumer's dials;
|
|
48
40
|
* `undefined` falls back to bits-ui's `document.body`. */
|
|
49
41
|
const portalTarget = $derived(leaf()?.el.closest<HTMLElement>('[data-qm-root]') ?? undefined);
|
|
50
42
|
|
|
51
|
-
let surface = $state<HTMLElement | undefined>();
|
|
52
|
-
|
|
53
43
|
/** Keep the keyboard's cursor in the port: the arrows move an index rather than a
|
|
54
44
|
* focus, so a row the list scrolled past has nothing bringing it back. `nearest`
|
|
55
45
|
* scrolls the least that works, and no ancestor the row is already visible in. */
|
|
56
|
-
|
|
46
|
+
function followHighlight(surface: HTMLElement): void {
|
|
57
47
|
void menu?.index;
|
|
58
|
-
surface
|
|
59
|
-
}
|
|
48
|
+
surface.querySelector('[data-highlighted]')?.scrollIntoView({ block: 'nearest' });
|
|
49
|
+
}
|
|
60
50
|
|
|
61
51
|
/** Swallow the item's own mousedown: without it the browser focuses the item and
|
|
62
52
|
* blurs the leaf, taking the trigger run's caret with it. */
|
|
@@ -98,7 +88,7 @@
|
|
|
98
88
|
a thing to click, tab into, or read. -->
|
|
99
89
|
<div
|
|
100
90
|
{...props}
|
|
101
|
-
|
|
91
|
+
{@attach followHighlight}
|
|
102
92
|
class="qm-slash-menu qm-menu-surface"
|
|
103
93
|
data-qm-root
|
|
104
94
|
role="group"
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { FieldController, SlashState } from '../core/codec/index.js';
|
|
2
|
-
import './controls.css';
|
|
3
2
|
interface Props {
|
|
4
3
|
/** The live menu, or `undefined` when closed: the codec's report channel. Named
|
|
5
4
|
* `menu` rather than `state`, which is the rune's word in this file. */
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
unanswered, and an optional cell returns to `none`.
|
|
15
15
|
-->
|
|
16
16
|
<script lang="ts">
|
|
17
|
-
import { syncedLocal } from './synced.svelte.js';
|
|
18
17
|
import './controls.css';
|
|
19
18
|
|
|
20
19
|
interface Props {
|
|
@@ -40,11 +39,8 @@
|
|
|
40
39
|
}
|
|
41
40
|
let { value, fallback, placeholder, label, id, describedBy, onCommit, onKey }: Props = $props();
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// `syncedLocal`).
|
|
46
|
-
const local = syncedLocal(() => value ?? fallback ?? '');
|
|
47
|
-
const defaulted = $derived(value == null && !!fallback && local.value === fallback);
|
|
42
|
+
let local = $derived(value ?? fallback ?? '');
|
|
43
|
+
const defaulted = $derived(value == null && !!fallback && local === fallback);
|
|
48
44
|
|
|
49
45
|
let inputEl: HTMLInputElement | undefined = $state();
|
|
50
46
|
/** Take the caret: what a parent placing focus on this control calls. */
|
|
@@ -57,7 +53,7 @@
|
|
|
57
53
|
bind:this={inputEl}
|
|
58
54
|
class="qm-input qm-focus-ring"
|
|
59
55
|
type="text"
|
|
60
|
-
value={local
|
|
56
|
+
value={local}
|
|
61
57
|
{id}
|
|
62
58
|
{placeholder}
|
|
63
59
|
data-default={defaulted ? '' : undefined}
|
|
@@ -65,7 +61,7 @@
|
|
|
65
61
|
aria-describedby={describedBy}
|
|
66
62
|
onkeydown={onKey}
|
|
67
63
|
oninput={(e) => {
|
|
68
|
-
local
|
|
69
|
-
onCommit(local
|
|
64
|
+
local = (e.currentTarget as HTMLInputElement).value;
|
|
65
|
+
onCommit(local !== '' ? local : fallback ? '' : undefined);
|
|
70
66
|
}}
|
|
71
67
|
/>
|
|
@@ -40,20 +40,19 @@
|
|
|
40
40
|
const index = $derived(Math.min(cursor, tips.length - 1));
|
|
41
41
|
const isLast = $derived(index >= tips.length - 1);
|
|
42
42
|
|
|
43
|
-
// The tip string is derived before the
|
|
44
|
-
// Reading `tips[index]` inside the
|
|
45
|
-
// dependency (a fresh object every `revision` bump) so the
|
|
46
|
-
// on every unrelated commit, re-crossing the WASM boundary
|
|
47
|
-
// `aria-live` region per keystroke. A derived string
|
|
43
|
+
// The tip string is derived before the attachment reads it, and that is
|
|
44
|
+
// load-bearing. Reading `tips[index]` inside the attachment makes the parent's
|
|
45
|
+
// `model` derive the dependency (a fresh object every `revision` bump) so the
|
|
46
|
+
// attachment would re-run on every unrelated commit, re-crossing the WASM boundary
|
|
47
|
+
// and rebuilding this `aria-live` region per keystroke. A derived string
|
|
48
|
+
// short-circuits on `===`.
|
|
48
49
|
const tip = $derived(tips[index] ?? '');
|
|
49
50
|
|
|
50
51
|
// The tip is DOM, not text: `renderTip` returns a fragment the codec's own
|
|
51
52
|
// `toDOM` built, so it is written in rather than interpolated.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (el) el.replaceChildren(renderTip(tip, onError));
|
|
56
|
-
});
|
|
53
|
+
function paintTip(body: HTMLDivElement): void {
|
|
54
|
+
body.replaceChildren(renderTip(tip, onError));
|
|
55
|
+
}
|
|
57
56
|
|
|
58
57
|
function advance(): void {
|
|
59
58
|
if (isLast) onDismiss();
|
|
@@ -64,7 +63,7 @@
|
|
|
64
63
|
<aside class="qm-tips" aria-label={t.strings.tipsLabel}>
|
|
65
64
|
<!-- Advancing swaps the text under a button that keeps focus, so the region
|
|
66
65
|
announces rather than the change passing silently. -->
|
|
67
|
-
<div class="qm-tips-body" aria-live="polite"
|
|
66
|
+
<div class="qm-tips-body" aria-live="polite" {@attach paintTip}></div>
|
|
68
67
|
<div class="qm-tips-foot">
|
|
69
68
|
<!-- One tip is its own whole set, so a lone dot reports nothing the card does
|
|
70
69
|
not already show. -->
|