@quillmark/svelte 0.12.0 → 0.14.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/NOTICE +3 -2
- package/dist/core/codec/decode.d.ts +17 -0
- package/dist/core/codec/decode.js +57 -4
- package/dist/core/codec/field.d.ts +63 -15
- package/dist/core/codec/field.js +201 -65
- package/dist/core/codec/index.d.ts +3 -3
- package/dist/core/codec/index.js +3 -3
- package/dist/core/codec/keymap.d.ts +5 -4
- package/dist/core/codec/keymap.js +5 -4
- package/dist/core/codec/prose.css +42 -0
- package/dist/core/codec/schema.d.ts +24 -5
- package/dist/core/codec/schema.js +40 -9
- package/dist/core/errors.d.ts +3 -2
- package/dist/core/theme.css +9 -6
- package/dist/preview/paint.js +21 -17
- package/dist/visual/ArrayField.svelte +66 -22
- package/dist/visual/ArrayField.svelte.d.ts +2 -0
- package/dist/visual/BooleanField.svelte +28 -6
- package/dist/visual/BooleanField.svelte.d.ts +2 -0
- package/dist/visual/Card.svelte +4 -3
- package/dist/visual/DateField.svelte +158 -40
- package/dist/visual/DateField.svelte.d.ts +5 -2
- package/dist/visual/EnumField.svelte +27 -9
- package/dist/visual/EnumField.svelte.d.ts +5 -0
- package/dist/visual/Field.svelte +48 -11
- package/dist/visual/FormatPopover.svelte +4 -3
- package/dist/visual/MatrixField.svelte +3 -2
- package/dist/visual/NumberField.svelte +73 -29
- package/dist/visual/NumberField.svelte.d.ts +5 -1
- package/dist/visual/ObjectField.svelte +63 -15
- package/dist/visual/ProseField.svelte +53 -23
- package/dist/visual/ProseField.svelte.d.ts +15 -3
- package/dist/visual/ProseValue.svelte +102 -46
- package/dist/visual/ProseValue.svelte.d.ts +17 -5
- package/dist/visual/TextField.svelte +35 -18
- package/dist/visual/TextField.svelte.d.ts +7 -0
- package/dist/visual/VariantField.svelte +1 -0
- package/dist/visual/VisualEditorInner.svelte +24 -24
- package/dist/visual/controls.css +16 -5
- package/dist/visual/icons/nodes.d.ts +28 -0
- package/dist/visual/icons/nodes.js +14 -1
- package/dist/visual/strings.d.ts +11 -1
- package/dist/visual/strings.js +3 -0
- package/dist/visual/structure.d.ts +77 -43
- package/dist/visual/structure.js +138 -64
- package/package.json +2 -2
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
`object`'s property, a variant's cell ({@link ObjectField}). None of them is
|
|
5
5
|
`applyChange`-addressable: `Addr.field` is a flat name, so `keywords[0]` and
|
|
6
6
|
`contact.note` have no op address. So this is not a `createField` leaf: it mounts a
|
|
7
|
-
minimal PM view over the codec's decode/encode
|
|
7
|
+
minimal PM view over the codec's decode/encode and a leaf schema, and on every edit
|
|
8
8
|
hands the re-encoded `Content` up to the parent — {@link ArrayField}, which commits
|
|
9
9
|
the whole array, or {@link ObjectField}, which commits the whole container
|
|
10
|
-
(`writer.set(field, next)`). Anchors inside a value are dropped on that write
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
(`writer.set(field, next)`). Anchors inside a value are dropped on that write.
|
|
11
|
+
Mounts once per stable key (an element's session id, a property's name), so the
|
|
12
|
+
parent's re-derive leaves the caret where it is.
|
|
13
13
|
|
|
14
14
|
The content is read, not passed: `reader.getContentAt(addr, path)` decodes through
|
|
15
15
|
the codec the leaf's own declared type names, so what it rests as — the content
|
|
@@ -17,37 +17,55 @@
|
|
|
17
17
|
being the row's or the cell's business. Read once, at mount, since that is when this
|
|
18
18
|
leaf takes its state.
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
inline
|
|
22
|
-
`
|
|
23
|
-
|
|
20
|
+
The schema is the declared type's where the parent has room for it: a subform cell
|
|
21
|
+
declaring no `inline` takes the full row ({@link ObjectField}), a `richtext` one on the
|
|
22
|
+
block schema and a `plaintext` one on the plain schema, paragraphs and hard breaks.
|
|
23
|
+
An array element is one textblock whatever it declares, since Enter there is the
|
|
24
|
+
repeater's. There is no slash menu at either width, and an island draws as its
|
|
25
|
+
placeholder: an atom a keystroke can delete and no view edits.
|
|
26
|
+
|
|
27
|
+
A leaf over content its schema cannot hold is held: a narrowed one over anything but
|
|
28
|
+
one plain paragraph (`fitsInline`), a plain one over anything but plain lines
|
|
29
|
+
(`fitsPlain`). Its decode would join or drop what is left over, and the first
|
|
30
|
+
keystroke would write that loss back, so a held leaf draws its content on the block
|
|
31
|
+
schema, read-only, with a note inside its box, and commits nothing.
|
|
24
32
|
-->
|
|
25
33
|
<script lang="ts">
|
|
26
34
|
import { onMount } from 'svelte';
|
|
27
35
|
import { EditorState, Selection } from 'prosemirror-state';
|
|
28
36
|
import { EditorView } from 'prosemirror-view';
|
|
29
37
|
import {
|
|
38
|
+
blockSchema,
|
|
30
39
|
decode,
|
|
40
|
+
emptyContent,
|
|
41
|
+
fitsLeaf,
|
|
42
|
+
heldAttributes,
|
|
43
|
+
leafSchema,
|
|
31
44
|
pmToContent,
|
|
32
|
-
inlineSchema,
|
|
33
|
-
plaintextSchema,
|
|
34
45
|
proseAttributes,
|
|
35
46
|
proseLeafPlugins,
|
|
36
47
|
buildLineIndex,
|
|
37
48
|
usvToPM
|
|
38
49
|
} from '../core/codec/index.js';
|
|
50
|
+
import { wording } from './strings.js';
|
|
39
51
|
import './controls.css';
|
|
40
52
|
import type { Content } from '@quillmark/wasm';
|
|
41
53
|
|
|
42
54
|
interface Props {
|
|
43
|
-
/** This leaf's content, read at mount (the parent's boundary read)
|
|
44
|
-
* rather than a value: the parent re-derives
|
|
45
|
-
* its state once, so a value prop would be a
|
|
46
|
-
* all but one of them discarded. */
|
|
47
|
-
content: () => Content;
|
|
55
|
+
/** This leaf's content, read at mount (the parent's boundary read), `undefined`
|
|
56
|
+
* where nothing is stored. A thunk rather than a value: the parent re-derives
|
|
57
|
+
* per revision and this leaf takes its state once, so a value prop would be a
|
|
58
|
+
* boundary read per leaf per render, all but one of them discarded. */
|
|
59
|
+
content: () => Content | undefined;
|
|
60
|
+
/** The default an unset leaf holds, where it prints: drawn at the default rung,
|
|
61
|
+
* and handed up whole with the first edit in it, as `createField` takes one. */
|
|
62
|
+
fallback?: Content;
|
|
48
63
|
/** The mark-free schema (a `plaintext` leaf): literal text, no formatting,
|
|
49
64
|
* exactly as the scalar field of that type mounts. */
|
|
50
65
|
plaintext?: boolean;
|
|
66
|
+
/** A multi-block schema, for a cell the parent draws at full width: the block
|
|
67
|
+
* schema, or the plain one with `plaintext`. Absent, the leaf is one textblock. */
|
|
68
|
+
block?: boolean;
|
|
51
69
|
/** Accessible name for the editable region, where nothing else names it: an
|
|
52
70
|
* array has no per-element label. A cell with a label element takes
|
|
53
71
|
* `labelledBy`. */
|
|
@@ -58,6 +76,12 @@
|
|
|
58
76
|
labelledBy?: string;
|
|
59
77
|
/** The parked `description` (FieldLabel) → `aria-describedby`. */
|
|
60
78
|
describedBy?: string;
|
|
79
|
+
/** The empty leaf's ghosts, a subform cell's, until the leaf's first edit: the
|
|
80
|
+
* `none` an optional cell prints unset, and the `example:` drawn where there is no
|
|
81
|
+
* placeholder and in its stead while the leaf holds the focus (`createField`'s
|
|
82
|
+
* pair). Read per decoration pass. */
|
|
83
|
+
placeholder?: string;
|
|
84
|
+
example?: string;
|
|
61
85
|
onChange: (rt: Content) => void;
|
|
62
86
|
/** Raw keydown, for a container whose own keys run through this leaf: the
|
|
63
87
|
* array repeater's Enter/Backspace (`ArrayField`). Fires before the view's own
|
|
@@ -66,15 +90,23 @@
|
|
|
66
90
|
}
|
|
67
91
|
let {
|
|
68
92
|
content,
|
|
93
|
+
fallback,
|
|
69
94
|
plaintext = false,
|
|
95
|
+
block = false,
|
|
70
96
|
label,
|
|
71
97
|
labelledBy,
|
|
72
98
|
describedBy,
|
|
99
|
+
placeholder,
|
|
100
|
+
example,
|
|
73
101
|
onChange,
|
|
74
102
|
onKey
|
|
75
103
|
}: Props = $props();
|
|
76
104
|
|
|
77
|
-
|
|
105
|
+
const t = wording();
|
|
106
|
+
const uid = $props.id();
|
|
107
|
+
const heldId = `${uid}-held`;
|
|
108
|
+
let editorEl: HTMLDivElement | undefined = $state();
|
|
109
|
+
let held = $state(false);
|
|
78
110
|
let view: EditorView | undefined;
|
|
79
111
|
/** Take the caret: what a parent placing focus on this leaf calls. The view's
|
|
80
112
|
* focus, not the element's: a PM view restores its selection, where a bare DOM
|
|
@@ -107,39 +139,55 @@
|
|
|
107
139
|
}
|
|
108
140
|
|
|
109
141
|
onMount(() => {
|
|
110
|
-
if (!
|
|
111
|
-
//
|
|
112
|
-
// `
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
142
|
+
if (!editorEl) return;
|
|
143
|
+
// The same keymap and plugin stack a `createField` leaf mounts (shared
|
|
144
|
+
// `proseLeafPlugins`), minus the anchor-position plugin: anchors are dropped on
|
|
145
|
+
// the parent's value write, per the header.
|
|
146
|
+
const inline = !block;
|
|
147
|
+
const stored = content();
|
|
148
|
+
const rt = stored ?? fallback ?? emptyContent();
|
|
149
|
+
let defaulted = stored === undefined && !!fallback;
|
|
150
|
+
held = !fitsLeaf(rt, { inline, plaintext });
|
|
151
|
+
const named = held
|
|
152
|
+
? heldAttributes({ label, labelledBy, describedBy }, heldId)
|
|
153
|
+
: (proseAttributes({ label, labelledBy, describedBy }) ?? {});
|
|
154
|
+
const defaultNamed = { ...named, 'data-default': '' };
|
|
155
|
+
const schema = held ? blockSchema : leafSchema({ plaintext, inline });
|
|
121
156
|
const state = EditorState.create({
|
|
122
|
-
doc:
|
|
123
|
-
plugins:
|
|
157
|
+
doc: decode(rt, schema),
|
|
158
|
+
plugins: held
|
|
159
|
+
? []
|
|
160
|
+
: proseLeafPlugins(schema, {
|
|
161
|
+
inline,
|
|
162
|
+
placeholder: () => placeholder,
|
|
163
|
+
placeholderUntilEdit: true,
|
|
164
|
+
example: () => example
|
|
165
|
+
})
|
|
124
166
|
});
|
|
125
|
-
const mounted = new EditorView(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
167
|
+
const mounted = new EditorView(
|
|
168
|
+
{ mount: editorEl },
|
|
169
|
+
{
|
|
170
|
+
state,
|
|
171
|
+
editable: () => !held,
|
|
172
|
+
// Which of `aria-label` / `aria-labelledby` wins is the codec's one answer
|
|
173
|
+
// (`proseAttributes`), so a cell carrying a label element and a row carrying
|
|
174
|
+
// none cannot name their regions by different rules.
|
|
175
|
+
attributes: () => (defaulted ? defaultNamed : named),
|
|
176
|
+
dispatchTransaction(tr) {
|
|
177
|
+
const next = mounted.state.apply(tr);
|
|
178
|
+
const edit = tr.docChanged && !held;
|
|
179
|
+
if (edit) defaulted = false;
|
|
180
|
+
mounted.updateState(next);
|
|
181
|
+
if (edit) onChange(pmToContent(next.doc));
|
|
182
|
+
},
|
|
183
|
+
handleDOMEvents: {
|
|
184
|
+
keydown: (_v, e) => {
|
|
185
|
+
onKey?.(e);
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
140
188
|
}
|
|
141
189
|
}
|
|
142
|
-
|
|
190
|
+
);
|
|
143
191
|
view = mounted;
|
|
144
192
|
return () => {
|
|
145
193
|
view = undefined;
|
|
@@ -153,7 +201,15 @@
|
|
|
153
201
|
like the text cell beside it. No floor: the reset in `core/codec/prose.css` makes one
|
|
154
202
|
line of prose measure one line. Width is the row's or the cell's to give: the leaf
|
|
155
203
|
fills the track it is placed in. -->
|
|
156
|
-
<div
|
|
204
|
+
<div class="qm-prose-value qm-control-box qm-focus-ring-within">
|
|
205
|
+
<div bind:this={editorEl}></div>
|
|
206
|
+
<!-- Inside the box, so the cell or row holds one child either way: a subform cell's
|
|
207
|
+
subgrid has a row for the label and one for the box, and an array row's slabs
|
|
208
|
+
stand the box's height. -->
|
|
209
|
+
{#if held}
|
|
210
|
+
<span id={heldId} class="qm-prose-held-note">{t.strings.proseHeld}</span>
|
|
211
|
+
{/if}
|
|
212
|
+
</div>
|
|
157
213
|
|
|
158
214
|
<style>
|
|
159
215
|
/* Caret-primary, matching ProseField: the contenteditable's own outline is
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import './controls.css';
|
|
2
2
|
import type { Content } from '@quillmark/wasm';
|
|
3
3
|
interface Props {
|
|
4
|
-
/** This leaf's content, read at mount (the parent's boundary read)
|
|
5
|
-
* rather than a value: the parent re-derives
|
|
6
|
-
* its state once, so a value prop would be a
|
|
7
|
-
* all but one of them discarded. */
|
|
8
|
-
content: () => Content;
|
|
4
|
+
/** This leaf's content, read at mount (the parent's boundary read), `undefined`
|
|
5
|
+
* where nothing is stored. A thunk rather than a value: the parent re-derives
|
|
6
|
+
* per revision and this leaf takes its state once, so a value prop would be a
|
|
7
|
+
* boundary read per leaf per render, all but one of them discarded. */
|
|
8
|
+
content: () => Content | undefined;
|
|
9
|
+
/** The default an unset leaf holds, where it prints: drawn at the default rung,
|
|
10
|
+
* and handed up whole with the first edit in it, as `createField` takes one. */
|
|
11
|
+
fallback?: Content;
|
|
9
12
|
/** The mark-free schema (a `plaintext` leaf): literal text, no formatting,
|
|
10
13
|
* exactly as the scalar field of that type mounts. */
|
|
11
14
|
plaintext?: boolean;
|
|
15
|
+
/** A multi-block schema, for a cell the parent draws at full width: the block
|
|
16
|
+
* schema, or the plain one with `plaintext`. Absent, the leaf is one textblock. */
|
|
17
|
+
block?: boolean;
|
|
12
18
|
/** Accessible name for the editable region, where nothing else names it: an
|
|
13
19
|
* array has no per-element label. A cell with a label element takes
|
|
14
20
|
* `labelledBy`. */
|
|
@@ -19,6 +25,12 @@ interface Props {
|
|
|
19
25
|
labelledBy?: string;
|
|
20
26
|
/** The parked `description` (FieldLabel) → `aria-describedby`. */
|
|
21
27
|
describedBy?: string;
|
|
28
|
+
/** The empty leaf's ghosts, a subform cell's, until the leaf's first edit: the
|
|
29
|
+
* `none` an optional cell prints unset, and the `example:` drawn where there is no
|
|
30
|
+
* placeholder and in its stead while the leaf holds the focus (`createField`'s
|
|
31
|
+
* pair). Read per decoration pass. */
|
|
32
|
+
placeholder?: string;
|
|
33
|
+
example?: string;
|
|
22
34
|
onChange: (rt: Content) => void;
|
|
23
35
|
/** Raw keydown, for a container whose own keys run through this leaf: the
|
|
24
36
|
* array repeater's Enter/Backspace (`ArrayField`). Fires before the view's own
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
A `string` field → text input. Commits
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
A `string` field → text input. Commits every edit live (on input) via the parent's
|
|
3
|
+
typed `writer.set`, so the preview tracks typing.
|
|
4
|
+
|
|
5
|
+
An unset field whose `default:` prints holds that default as its text, at the
|
|
6
|
+
default rung (theme.css): the value it prints, drawn as one and a step off a written
|
|
7
|
+
one. Nothing is written until an edit, which writes the whole text as authored: the
|
|
8
|
+
default taken, whatever the edit left of it. Tabbing through writes nothing; a
|
|
9
|
+
keystroke typed and removed pins the default.
|
|
10
|
+
|
|
11
|
+
An emptied input writes what empty means for this field (VISUAL_EDITOR §"The
|
|
12
|
+
commitment ladder"): `""` over a default that prints, the only answer that prints empty there;
|
|
13
|
+
the unset rung elsewhere, where unset already prints nothing and the field stays
|
|
14
|
+
unanswered, and an optional cell returns to `none`.
|
|
10
15
|
-->
|
|
11
16
|
<script lang="ts">
|
|
12
17
|
import { syncedLocal } from './synced.svelte.js';
|
|
@@ -14,7 +19,14 @@
|
|
|
14
19
|
|
|
15
20
|
interface Props {
|
|
16
21
|
value: string | undefined;
|
|
22
|
+
/** The resolved `default:` where it prints: the text an unset field holds. */
|
|
23
|
+
fallback?: string;
|
|
24
|
+
/** Words at rest about an unset field that holds no text: the `None` an optional
|
|
25
|
+
* cell prints. */
|
|
17
26
|
placeholder?: string;
|
|
27
|
+
/** An unset field's `example:` (`exampleGhost`): drawn wherever the field holds no
|
|
28
|
+
* text, and in `placeholder`'s stead while the input holds the focus. */
|
|
29
|
+
example?: string;
|
|
18
30
|
/** Accessible name for an input nothing else names: an array element, whose
|
|
19
31
|
* name is the field label plus its 1-based index. A field's own input takes
|
|
20
32
|
* `id` instead and is named by the `<label for>` beside it. */
|
|
@@ -29,11 +41,17 @@
|
|
|
29
41
|
* array repeater's Enter/Backspace (`ArrayField`). */
|
|
30
42
|
onKey?: (e: KeyboardEvent) => void;
|
|
31
43
|
}
|
|
32
|
-
let { value, placeholder, label, id, describedBy, onCommit, onKey }: Props =
|
|
44
|
+
let { value, fallback, placeholder, example, label, id, describedBy, onCommit, onKey }: Props =
|
|
45
|
+
$props();
|
|
33
46
|
|
|
34
|
-
// Local input state synced to `value
|
|
35
|
-
// change reconciles back in (see
|
|
36
|
-
|
|
47
|
+
// Local input state synced to `value`, or to the default an unset field holds:
|
|
48
|
+
// own-typing stays local, only an external change reconciles back in (see
|
|
49
|
+
// `syncedLocal`).
|
|
50
|
+
const local = syncedLocal(() => value ?? fallback ?? '');
|
|
51
|
+
const defaulted = $derived(value == null && !!fallback && local.value === fallback);
|
|
52
|
+
|
|
53
|
+
let focused = $state(false);
|
|
54
|
+
const ghost = $derived(focused ? (example ?? placeholder) : (placeholder ?? example));
|
|
37
55
|
|
|
38
56
|
let inputEl: HTMLInputElement | undefined = $state();
|
|
39
57
|
/** Take the caret: what a parent placing focus on this control calls. */
|
|
@@ -48,16 +66,15 @@
|
|
|
48
66
|
type="text"
|
|
49
67
|
value={local.value}
|
|
50
68
|
{id}
|
|
51
|
-
{
|
|
69
|
+
placeholder={ghost}
|
|
70
|
+
data-default={defaulted ? '' : undefined}
|
|
52
71
|
aria-label={id ? undefined : label}
|
|
53
72
|
aria-describedby={describedBy}
|
|
54
73
|
onkeydown={onKey}
|
|
74
|
+
onfocus={() => (focused = true)}
|
|
75
|
+
onblur={() => (focused = false)}
|
|
55
76
|
oninput={(e) => {
|
|
56
77
|
local.value = (e.currentTarget as HTMLInputElement).value;
|
|
57
|
-
|
|
58
|
-
if (local.value !== '') onCommit(local.value);
|
|
59
|
-
}}
|
|
60
|
-
onchange={() => {
|
|
61
|
-
if (local.value === '') onCommit(undefined);
|
|
78
|
+
onCommit(local.value !== '' ? local.value : fallback ? '' : undefined);
|
|
62
79
|
}}
|
|
63
80
|
/>
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import './controls.css';
|
|
2
2
|
interface Props {
|
|
3
3
|
value: string | undefined;
|
|
4
|
+
/** The resolved `default:` where it prints: the text an unset field holds. */
|
|
5
|
+
fallback?: string;
|
|
6
|
+
/** Words at rest about an unset field that holds no text: the `None` an optional
|
|
7
|
+
* cell prints. */
|
|
4
8
|
placeholder?: string;
|
|
9
|
+
/** An unset field's `example:` (`exampleGhost`): drawn wherever the field holds no
|
|
10
|
+
* text, and in `placeholder`'s stead while the input holds the focus. */
|
|
11
|
+
example?: string;
|
|
5
12
|
/** Accessible name for an input nothing else names: an array element, whose
|
|
6
13
|
* name is the field label plus its 1-based index. A field's own input takes
|
|
7
14
|
* `id` instead and is named by the `<label for>` beside it. */
|
|
@@ -50,20 +50,21 @@
|
|
|
50
50
|
IdSeq,
|
|
51
51
|
controlKind,
|
|
52
52
|
isContainer,
|
|
53
|
+
shortCell,
|
|
53
54
|
schemaAt,
|
|
54
55
|
fieldModels,
|
|
55
56
|
groupOrder,
|
|
56
57
|
groupSections,
|
|
57
58
|
groupLabel,
|
|
58
59
|
cardTitle,
|
|
59
|
-
|
|
60
|
+
kindTitle,
|
|
60
61
|
fieldValues,
|
|
61
62
|
bodyEnabled,
|
|
62
|
-
humanize,
|
|
63
63
|
provenanceMap,
|
|
64
64
|
resolvedByCardIndex,
|
|
65
65
|
ghostDefault,
|
|
66
|
-
|
|
66
|
+
declaredGhost,
|
|
67
|
+
titleText,
|
|
67
68
|
resolveBodyGhost,
|
|
68
69
|
NO_RESOLVED_ROWS,
|
|
69
70
|
type CardModel,
|
|
@@ -286,24 +287,22 @@
|
|
|
286
287
|
onChange?.({ source: 'prose', cardId, path: pathFor(plain) });
|
|
287
288
|
}
|
|
288
289
|
|
|
289
|
-
/** A card's
|
|
290
|
-
* one piece of the model that lane
|
|
291
|
-
* the next bump, which rebuilds it from the document. The
|
|
292
|
-
*
|
|
293
|
-
* names the field. */
|
|
290
|
+
/** A header named by the card's own values, after a prose commit to one that can name
|
|
291
|
+
* it: the one piece of the model that lane moves, held beside the model and dropped
|
|
292
|
+
* on the next bump, which rebuilds it from the document. The card is read only for a
|
|
293
|
+
* one-line prose field of a kind declaring no `title` (`cardTitle`). */
|
|
294
294
|
let liveTitles = $state<Record<CardId, string>>({});
|
|
295
295
|
function refreshTitle(addr: Addr, cardId: CardId): void {
|
|
296
296
|
const { card: at, field } = addr;
|
|
297
|
-
if (field == null) return;
|
|
298
|
-
const shown =
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (!live) return;
|
|
297
|
+
if (at == null || field == null) return;
|
|
298
|
+
const shown = model.cards[at];
|
|
299
|
+
const cardSchema = shown && quill.schema.card_kinds?.[shown.kind];
|
|
300
|
+
if (!cardSchema || cardSchema.title?.trim()) return;
|
|
301
|
+
const sub = Object.hasOwn(cardSchema.fields, field) ? cardSchema.fields[field] : undefined;
|
|
302
|
+
if (!sub || !shortCell(sub)) return;
|
|
304
303
|
liveTitles = {
|
|
305
304
|
...liveTitles,
|
|
306
|
-
[cardId]: cardTitle(cardSchema, shown.kind, fieldValues(
|
|
305
|
+
[cardId]: cardTitle(cardSchema, shown.kind, fieldValues(doc.card(at).payloadItems), undefined)
|
|
307
306
|
};
|
|
308
307
|
}
|
|
309
308
|
function titled(c: CardModel): CardModel {
|
|
@@ -328,11 +327,11 @@
|
|
|
328
327
|
const keyStr = fieldKeyToString(key);
|
|
329
328
|
try {
|
|
330
329
|
if (value === undefined) {
|
|
331
|
-
// The unset rung
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
//
|
|
330
|
+
// The unset rung (VISUAL_EDITOR §"The commitment ladder"): removing the field
|
|
331
|
+
// leaves the engine's authored › `default:` › blank-fill resolve to render the
|
|
332
|
+
// default, where writing one bakes a snapshot the schema cannot track (canon
|
|
333
|
+
// SCHEMAS.md: the engine never writes a default; the editor writes one only
|
|
334
|
+
// as the edit that takes it). Removal writes no value, so there is
|
|
336
335
|
// nothing for a schema to conform and the lane is the quill-free one.
|
|
337
336
|
if (isMain) {
|
|
338
337
|
doc.removeField(name);
|
|
@@ -612,7 +611,8 @@
|
|
|
612
611
|
// still across a re-derive because the function does.
|
|
613
612
|
bodyGhost: hasBody
|
|
614
613
|
? resolveBodyGhost(
|
|
615
|
-
|
|
614
|
+
titleText(ghostDefault(rows.body ?? undefined)) || undefined,
|
|
615
|
+
declaredGhost(cardSchema?.body?.example, true),
|
|
616
616
|
merged.bodyPlaceholder?.({ cardId: id, kind, isMain }),
|
|
617
617
|
merged.bodyGhost
|
|
618
618
|
)
|
|
@@ -858,7 +858,7 @@
|
|
|
858
858
|
<div class="qm-primary">
|
|
859
859
|
<Card
|
|
860
860
|
bind:this={mainCard}
|
|
861
|
-
card={
|
|
861
|
+
card={model.main}
|
|
862
862
|
{doc}
|
|
863
863
|
{quill}
|
|
864
864
|
isFirst={true}
|
|
@@ -957,7 +957,7 @@
|
|
|
957
957
|
<div class="qm-menu-surface" data-qm-root>
|
|
958
958
|
{#each kinds as k (k)}
|
|
959
959
|
<DropdownMenu.Item class="qm-menu-item" onSelect={() => addCard(atIndex, k)}
|
|
960
|
-
>{
|
|
960
|
+
>{kindTitle(quill.schema.card_kinds?.[k], k)}</DropdownMenu.Item
|
|
961
961
|
>
|
|
962
962
|
{/each}
|
|
963
963
|
</div>
|
package/dist/visual/controls.css
CHANGED
|
@@ -105,14 +105,25 @@
|
|
|
105
105
|
width: 100%;
|
|
106
106
|
box-sizing: border-box;
|
|
107
107
|
}
|
|
108
|
-
/*
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
/* An unset field's text where its `default:` prints (`TextField`, `NumberField`, a
|
|
109
|
+
prose leaf): the value, at the default rung, so it reads as an answer and never as a
|
|
110
|
+
written one. The first edit writes it and the rung goes with the attribute. */
|
|
111
|
+
:where(.qm-input[data-default], .ProseMirror[data-default]) {
|
|
112
|
+
color: var(--_qm-ink-default);
|
|
113
|
+
}
|
|
114
|
+
/* The native placeholder is the label rung, not the UA's own grey: what an input
|
|
115
|
+
ghosts is words about a value it does not hold — an `example:`, `None` — which is
|
|
116
|
+
content and reads at a content's contrast, the same step the prose leaf's ghost
|
|
117
|
+
reads (`prose.css`). */
|
|
113
118
|
:where(.qm-input)::placeholder {
|
|
114
119
|
color: var(--_qm-ink-label);
|
|
115
120
|
}
|
|
121
|
+
/* The note a held prose leaf draws inside its box, under the text it is about. */
|
|
122
|
+
:where(.qm-prose-held-note) {
|
|
123
|
+
display: block;
|
|
124
|
+
font-size: var(--_qm-text-label);
|
|
125
|
+
color: var(--_qm-ink-label);
|
|
126
|
+
}
|
|
116
127
|
|
|
117
128
|
/* The second recipe, and it is not the box above: a field is a box because you
|
|
118
129
|
type into it. A button rests with no fill; the surface arrives on hover, off the
|
|
@@ -119,6 +119,34 @@ export declare const ICONS: {
|
|
|
119
119
|
readonly check: readonly [readonly ["path", {
|
|
120
120
|
readonly d: "M20 6 9 17l-5-5";
|
|
121
121
|
}]];
|
|
122
|
+
readonly calendar: readonly [readonly ["path", {
|
|
123
|
+
readonly d: "M8 2v3";
|
|
124
|
+
}], readonly ["path", {
|
|
125
|
+
readonly d: "M16 2v3";
|
|
126
|
+
}], readonly ["rect", {
|
|
127
|
+
readonly x: "3";
|
|
128
|
+
readonly y: "3";
|
|
129
|
+
readonly width: "18";
|
|
130
|
+
readonly height: "18";
|
|
131
|
+
readonly rx: "2";
|
|
132
|
+
}], readonly ["path", {
|
|
133
|
+
readonly d: "M3 9h18";
|
|
134
|
+
}]];
|
|
135
|
+
readonly 'calendar-check': readonly [readonly ["path", {
|
|
136
|
+
readonly d: "M8 2v3";
|
|
137
|
+
}], readonly ["path", {
|
|
138
|
+
readonly d: "M16 2v3";
|
|
139
|
+
}], readonly ["rect", {
|
|
140
|
+
readonly x: "3";
|
|
141
|
+
readonly y: "3";
|
|
142
|
+
readonly width: "18";
|
|
143
|
+
readonly height: "18";
|
|
144
|
+
readonly rx: "2";
|
|
145
|
+
}], readonly ["path", {
|
|
146
|
+
readonly d: "M3 9h18";
|
|
147
|
+
}], readonly ["path", {
|
|
148
|
+
readonly d: "m9 15 2 2 4-4";
|
|
149
|
+
}]];
|
|
122
150
|
};
|
|
123
151
|
/** The closed set. A name that is not here is a compile error at the call site,
|
|
124
152
|
* which is what a string key buys over thirteen imported components. */
|
|
@@ -64,5 +64,18 @@ export const ICONS = {
|
|
|
64
64
|
['path', { d: 'M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2' }]
|
|
65
65
|
],
|
|
66
66
|
minus: [['path', { d: 'M5 12h14' }]],
|
|
67
|
-
check: [['path', { d: 'M20 6 9 17l-5-5' }]]
|
|
67
|
+
check: [['path', { d: 'M20 6 9 17l-5-5' }]],
|
|
68
|
+
calendar: [
|
|
69
|
+
['path', { d: 'M8 2v3' }],
|
|
70
|
+
['path', { d: 'M16 2v3' }],
|
|
71
|
+
['rect', { x: '3', y: '3', width: '18', height: '18', rx: '2' }],
|
|
72
|
+
['path', { d: 'M3 9h18' }]
|
|
73
|
+
],
|
|
74
|
+
'calendar-check': [
|
|
75
|
+
['path', { d: 'M8 2v3' }],
|
|
76
|
+
['path', { d: 'M16 2v3' }],
|
|
77
|
+
['rect', { x: '3', y: '3', width: '18', height: '18', rx: '2' }],
|
|
78
|
+
['path', { d: 'M3 9h18' }],
|
|
79
|
+
['path', { d: 'm9 15 2 2 4-4' }]
|
|
80
|
+
]
|
|
68
81
|
};
|
package/dist/visual/strings.d.ts
CHANGED
|
@@ -47,8 +47,17 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
|
|
|
47
47
|
enumUnsetTag: string;
|
|
48
48
|
/** The count in the matrix's label row: how many of the roster are held. */
|
|
49
49
|
matrixHeld: (held: number, total: number) => string;
|
|
50
|
+
/** The date field's toggle, pressed while the field holds `today`: the render
|
|
51
|
+
* date rather than a written one. */
|
|
52
|
+
dateToday: string;
|
|
50
53
|
/** The required marker's accessible name; the glyph itself is a `*`. */
|
|
51
54
|
fieldRequired: string;
|
|
55
|
+
/** What an unset optional cell (`t?`) ghosts at rest: the `none` it prints
|
|
56
|
+
* unanswered, in the product's words. */
|
|
57
|
+
optionalGhost: string;
|
|
58
|
+
/** Under a prose leaf holding more structure than it can edit (paragraphs, a list,
|
|
59
|
+
* an island in a one-line cell), which it draws read-only rather than flatten. */
|
|
60
|
+
proseHeld: string;
|
|
52
61
|
formatGroup: string;
|
|
53
62
|
formatBold: string;
|
|
54
63
|
formatEmphasis: string;
|
|
@@ -84,7 +93,8 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
|
|
|
84
93
|
bodyGhost: string;
|
|
85
94
|
/**
|
|
86
95
|
* Per-CARD wording, in place of {@link VisualStrings.bodyGhost}; `undefined`
|
|
87
|
-
* takes it.
|
|
96
|
+
* takes it. A body `default:` and the kind's `body.example` both outrank it
|
|
97
|
+
* (`resolveBodyGhost`). Pure and uncached ({@link BodyPlaceholder}).
|
|
88
98
|
*/
|
|
89
99
|
bodyPlaceholder: BodyPlaceholder;
|
|
90
100
|
}
|
package/dist/visual/strings.js
CHANGED
|
@@ -37,7 +37,10 @@ export const DEFAULT_VISUAL_STRINGS = {
|
|
|
37
37
|
elementUntitled: (label, index) => `${label} ${index}`,
|
|
38
38
|
enumUnsetTag: 'default',
|
|
39
39
|
matrixHeld: (held, total) => `${held} of ${total} held`,
|
|
40
|
+
dateToday: 'Today',
|
|
40
41
|
fieldRequired: 'required',
|
|
42
|
+
optionalGhost: 'None',
|
|
43
|
+
proseHeld: 'Holds more than this field can edit here.',
|
|
41
44
|
formatGroup: 'Formatting',
|
|
42
45
|
formatBold: 'Bold (Mod-B)',
|
|
43
46
|
formatEmphasis: 'Emphasis (Mod-I)',
|