@quillmark/svelte 0.13.0 → 0.15.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/field.d.ts +3 -14
- package/dist/core/codec/field.js +11 -25
- package/dist/core/codec/prose.css +4 -10
- package/dist/core/theme.css +4 -4
- package/dist/visual/Card.svelte +1 -0
- package/dist/visual/DateField.svelte +96 -13
- package/dist/visual/DateField.svelte.d.ts +5 -2
- package/dist/visual/Field.svelte +10 -13
- package/dist/visual/NumberField.svelte +43 -16
- package/dist/visual/ObjectField.svelte +1 -6
- package/dist/visual/ProseField.svelte +1 -10
- package/dist/visual/ProseField.svelte.d.ts +1 -5
- package/dist/visual/ProseValue.svelte +3 -8
- package/dist/visual/ProseValue.svelte.d.ts +2 -5
- package/dist/visual/TextField.svelte +2 -11
- package/dist/visual/TextField.svelte.d.ts +0 -3
- package/dist/visual/VisualEditorInner.svelte +0 -2
- package/dist/visual/controls.css +1 -1
- package/dist/visual/icons/nodes.d.ts +28 -0
- package/dist/visual/icons/nodes.js +14 -1
- package/dist/visual/strings.d.ts +3 -0
- package/dist/visual/strings.js +1 -0
- package/dist/visual/structure.d.ts +7 -23
- package/dist/visual/structure.js +19 -49
- package/package.json +2 -2
package/NOTICE
CHANGED
|
@@ -6,9 +6,10 @@ Copyright 2026 Nibs
|
|
|
6
6
|
This package contains SVG glyph geometry derived from Lucide, carried in source
|
|
7
7
|
rather than installed.
|
|
8
8
|
|
|
9
|
-
`visual/icons/nodes.ts` holds
|
|
9
|
+
`visual/icons/nodes.ts` holds nineteen verbatim from @lucide/svelte 1.28.0:
|
|
10
10
|
chevron-right, arrow-right, chevron-down, chevron-up, bold, italic, underline,
|
|
11
|
-
strikethrough, code, link, hash, info, plus, x, trash-2
|
|
11
|
+
strikethrough, code, link, hash, info, plus, x, trash-2, minus, check, calendar
|
|
12
|
+
and calendar-check.
|
|
12
13
|
|
|
13
14
|
`core/codec/table-view.ts` holds a plus and four text-alignment glyphs derived
|
|
14
15
|
from an earlier release of the same project.
|
|
@@ -71,16 +71,10 @@ export interface CreateFieldOpts {
|
|
|
71
71
|
* a body's invitation. The initial value; {@link FieldController.setPlaceholder}
|
|
72
72
|
* moves it after mount. Empty/absent shows no ghost. */
|
|
73
73
|
placeholder?: string;
|
|
74
|
-
/** The placeholder
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* whenever the leaf is empty. */
|
|
74
|
+
/** The placeholder goes at the leaf's first edit: the edit answers the leaf, and
|
|
75
|
+
* emptied it holds an empty answer, which prints empty. Absent, the placeholder
|
|
76
|
+
* returns whenever the leaf is empty. */
|
|
78
77
|
placeholderUntilEdit?: boolean;
|
|
79
|
-
/** Ghost text shown on the empty leaf until its first edit: an unset field's
|
|
80
|
-
* `example:`, at rest where there is no placeholder and in the placeholder's stead
|
|
81
|
-
* while the leaf holds the focus. The initial value; {@link FieldController.setExample}
|
|
82
|
-
* moves it after mount. */
|
|
83
|
-
example?: string;
|
|
84
78
|
onFocus?(addr: Addr): void;
|
|
85
79
|
/** Fired with the new USV caret after an edit or a selection move. */
|
|
86
80
|
onCaretMove?(addr: Addr, pos: number): void;
|
|
@@ -118,9 +112,6 @@ export interface FieldController {
|
|
|
118
112
|
* a transaction would fire `onCaretMove` at a moment the caret did not move.
|
|
119
113
|
*/
|
|
120
114
|
setPlaceholder(text: string | undefined): void;
|
|
121
|
-
/** Move the example ghost after mount, the way {@link setPlaceholder} moves the
|
|
122
|
-
* other one. */
|
|
123
|
-
setExample(text: string | undefined): void;
|
|
124
115
|
/** Move the default an unset leaf holds after mount: a retype or a quill swap
|
|
125
116
|
* re-seats an unset leaf on the new kind's default, and leaves a written one be. */
|
|
126
117
|
setFallback(content: Content | undefined): void;
|
|
@@ -220,7 +211,5 @@ export declare function proseLeafPlugins(schema: Schema, opts: {
|
|
|
220
211
|
placeholder?: () => string | undefined;
|
|
221
212
|
/** {@link CreateFieldOpts.placeholderUntilEdit}. */
|
|
222
213
|
placeholderUntilEdit?: boolean;
|
|
223
|
-
/** The focused leaf's ghost, read live for the same reason. */
|
|
224
|
-
example?: () => string | undefined;
|
|
225
214
|
afterHistory?: Plugin[];
|
|
226
215
|
}): Plugin[];
|
package/dist/core/codec/field.js
CHANGED
|
@@ -146,10 +146,9 @@ export function createField(opts) {
|
|
|
146
146
|
const reconciler = createReconciler(shown());
|
|
147
147
|
let index; // rebuilt on every structural change
|
|
148
148
|
let view;
|
|
149
|
-
// The
|
|
150
|
-
//
|
|
149
|
+
// The ghost's live cell: the ghost plugin reads it per pass, so moving it is an
|
|
150
|
+
// assignment plus a re-render rather than a rebuilt plugin stack.
|
|
151
151
|
let placeholderText = opts.placeholder;
|
|
152
|
-
let exampleText = opts.example;
|
|
153
152
|
// The views nested inside this leaf: one per table cell (`table-view.ts`). The
|
|
154
153
|
// leaf holds the set because chrome asks the leaf, not the island, which view
|
|
155
154
|
// holds the caret: the format popover raises over a cell exactly as over the
|
|
@@ -269,7 +268,6 @@ export function createField(opts) {
|
|
|
269
268
|
// given one later; the plugin draws nothing while the text is empty.
|
|
270
269
|
placeholder: () => placeholderText,
|
|
271
270
|
placeholderUntilEdit: opts.placeholderUntilEdit,
|
|
272
|
-
example: () => exampleText,
|
|
273
271
|
afterHistory: [anchorPlugin(seededAnchors)]
|
|
274
272
|
})
|
|
275
273
|
});
|
|
@@ -418,12 +416,6 @@ export function createField(opts) {
|
|
|
418
416
|
// transaction, so nothing commits and no caret is reported.
|
|
419
417
|
view.setProps({});
|
|
420
418
|
},
|
|
421
|
-
setExample(text) {
|
|
422
|
-
if (text === exampleText)
|
|
423
|
-
return;
|
|
424
|
-
exampleText = text;
|
|
425
|
-
view.setProps({});
|
|
426
|
-
},
|
|
427
419
|
setFallback(content) {
|
|
428
420
|
if (content === fallback || (content && fallback && contentEqual(content, fallback)))
|
|
429
421
|
return;
|
|
@@ -533,7 +525,7 @@ export function proseLeafPlugins(schema, opts) {
|
|
|
533
525
|
list.push(gapCursor(), pastAtomPlugin(), islandPastePlugin());
|
|
534
526
|
if (schema === plainSchema)
|
|
535
527
|
list.push(plainClipboardPlugin());
|
|
536
|
-
if (opts.placeholder
|
|
528
|
+
if (opts.placeholder)
|
|
537
529
|
list.push(ghostPlugin(opts));
|
|
538
530
|
return list;
|
|
539
531
|
}
|
|
@@ -590,18 +582,16 @@ function pastAtomPlugin() {
|
|
|
590
582
|
});
|
|
591
583
|
}
|
|
592
584
|
/**
|
|
593
|
-
* The empty leaf's
|
|
594
|
-
* class and the ghost text, which `prose.css` draws from the
|
|
585
|
+
* The empty leaf's ghost. A node decoration stamps the sole empty textblock with a
|
|
586
|
+
* class and the ghost text, which `prose.css` draws from the attribute: so the text
|
|
595
587
|
* never enters the document, the caret path, or a `pmToContent` export, and it
|
|
596
588
|
* vanishes the instant the leaf holds any content (the emptiness test fails).
|
|
597
589
|
*
|
|
598
|
-
* `
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
* that says what the unset field prints (`placeholderUntilEdit`). An inline leaf's
|
|
602
|
-
* ghost keeps to the one line the leaf is (`qm-prose-placeholder-line`).
|
|
590
|
+
* Under `placeholderUntilEdit` it is stamped only until the state's first edit, after
|
|
591
|
+
* which the leaf has been answered. An inline leaf's ghost keeps to the one line the
|
|
592
|
+
* leaf is (`qm-prose-placeholder-line`).
|
|
603
593
|
*
|
|
604
|
-
* The
|
|
594
|
+
* The text is read per decoration pass rather than closed over, so moving the ghost is
|
|
605
595
|
* a re-render and never a document edit.
|
|
606
596
|
*/
|
|
607
597
|
function ghostPlugin(opts) {
|
|
@@ -616,8 +606,7 @@ function ghostPlugin(opts) {
|
|
|
616
606
|
decorations(state) {
|
|
617
607
|
const edited = key.getState(state);
|
|
618
608
|
const text = edited && opts.placeholderUntilEdit ? undefined : opts.placeholder?.();
|
|
619
|
-
|
|
620
|
-
if (!text && !example)
|
|
609
|
+
if (!text)
|
|
621
610
|
return null;
|
|
622
611
|
const { doc } = state;
|
|
623
612
|
const first = doc.firstChild;
|
|
@@ -629,10 +618,7 @@ function ghostPlugin(opts) {
|
|
|
629
618
|
? 'qm-prose-placeholder qm-prose-placeholder-line'
|
|
630
619
|
: 'qm-prose-placeholder'
|
|
631
620
|
};
|
|
632
|
-
|
|
633
|
-
attrs['data-placeholder'] = text;
|
|
634
|
-
if (example)
|
|
635
|
-
attrs['data-example'] = example;
|
|
621
|
+
attrs['data-placeholder'] = text;
|
|
636
622
|
return DecorationSet.create(doc, [Decoration.node(0, first.nodeSize, attrs)]);
|
|
637
623
|
}
|
|
638
624
|
}
|
|
@@ -196,21 +196,19 @@ li.ProseMirror-selectednode::after {
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
/* The empty leaf's ghost (`field.ts`, `ghostPlugin`): a node decoration's data
|
|
199
|
-
|
|
199
|
+
attribute, so the text stays out of the document. At the rung every control's
|
|
200
200
|
placeholder takes; the italic is what a leaf with no written neighbour has instead
|
|
201
201
|
of the step to `ink`.
|
|
202
202
|
|
|
203
203
|
After the empty line rather than before it, and pulled up one line box onto it: the
|
|
204
204
|
caret keeps the start of that line, and a ghost's lines past the first grow the
|
|
205
205
|
leaf rather than hang over what follows it. A block leaf's ghost wraps and keeps the
|
|
206
|
-
breaks it was written with, a
|
|
207
|
-
leaf's keeps to the one line the leaf is.
|
|
208
|
-
is no placeholder, and stands in for the placeholder while the leaf holds the focus.
|
|
206
|
+
breaks it was written with, a body `default:` being a document's worth; an inline
|
|
207
|
+
leaf's keeps to the one line the leaf is.
|
|
209
208
|
|
|
210
209
|
Drawn only where there is text to draw: an empty box pulled up a line would take
|
|
211
210
|
the empty line with it. */
|
|
212
|
-
.ProseMirror .qm-prose-placeholder[data-placeholder]::after
|
|
213
|
-
.ProseMirror .qm-prose-placeholder[data-example]::after {
|
|
211
|
+
.ProseMirror .qm-prose-placeholder[data-placeholder]::after {
|
|
214
212
|
content: attr(data-placeholder);
|
|
215
213
|
display: block;
|
|
216
214
|
margin-top: -1lh;
|
|
@@ -219,10 +217,6 @@ li.ProseMirror-selectednode::after {
|
|
|
219
217
|
font-style: italic;
|
|
220
218
|
pointer-events: none;
|
|
221
219
|
}
|
|
222
|
-
.ProseMirror .qm-prose-placeholder[data-example]:not([data-placeholder])::after,
|
|
223
|
-
.ProseMirror-focused .qm-prose-placeholder[data-example]::after {
|
|
224
|
-
content: attr(data-example);
|
|
225
|
-
}
|
|
226
220
|
.ProseMirror .qm-prose-placeholder.qm-prose-placeholder-line::after {
|
|
227
221
|
white-space: nowrap;
|
|
228
222
|
overflow: hidden;
|
package/dist/core/theme.css
CHANGED
|
@@ -164,12 +164,12 @@
|
|
|
164
164
|
stacked on one. */
|
|
165
165
|
--_qm-border-faint: color-mix(in oklab, var(--_qm-bg), var(--_qm-border) 50%);
|
|
166
166
|
/* Three rungs, and nothing under the last: a step below `label` lands under 3:1 on
|
|
167
|
-
the well a field draws it against, and what would take it — an
|
|
168
|
-
|
|
167
|
+
the well a field draws it against, and what would take it — an unset value's word,
|
|
168
|
+
an invitation — is content a reader has to read (WCAG 1.4.3), not decoration.
|
|
169
169
|
`ink` is written. `default` is the resolved `default:` drawn as the value it
|
|
170
170
|
prints: near enough to `ink` to read as an answer, a step off it so it never
|
|
171
|
-
reads as a written one. `label` is words about a value rather than the value —
|
|
172
|
-
`
|
|
171
|
+
reads as a written one. `label` is words about a value rather than the value —
|
|
172
|
+
`None`, a format hint, an invitation — and the chrome that names fields; in italic
|
|
173
173
|
where it is a leaf's ghost, which has no written neighbour to be read against. */
|
|
174
174
|
--_qm-ink: var(--_qm-fg);
|
|
175
175
|
--_qm-ink-default: color-mix(in oklab, var(--_qm-fg), var(--_qm-bg) 20%);
|
package/dist/visual/Card.svelte
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
A `date` (or `datetime`) field → a styled segmented date field on bits-ui. The
|
|
3
|
-
stored value is a string
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
stored value is a string, `YYYY-MM-DD` or `today`; a cleared control commits
|
|
4
|
+
`undefined` (the unset rung): the parent removes the field, and the quill's
|
|
5
|
+
`default:` applies. The value-object a date field lowers to is a render-time
|
|
6
|
+
concern: the editor only sees the stored string.
|
|
7
|
+
|
|
8
|
+
`today` is the render date rather than a written one (canon `SCHEMAS.md`), so its
|
|
9
|
+
digits take the default rung: they print, and nothing wrote them. The digits are
|
|
10
|
+
the local date, which is what `resolve` and a session opened without a date both
|
|
11
|
+
render. The Today toggle writes `today`, and pressed, writes those digits; `T` in a
|
|
12
|
+
segment writes `today` too. An edit to a segment writes a whole date, the other
|
|
13
|
+
segments the render date's. A `default: today` presses the toggle at the default
|
|
14
|
+
rung while the field is unset.
|
|
8
15
|
|
|
9
16
|
Styled rather than a native `<input type="date">`: that control's calendar popup
|
|
10
17
|
is UA-owned and reaches no dial. `DateField` (segments, no
|
|
@@ -41,14 +48,20 @@
|
|
|
41
48
|
-->
|
|
42
49
|
<script lang="ts">
|
|
43
50
|
import { DateField as BitsDateField } from 'bits-ui';
|
|
44
|
-
import { parseDate, type DateValue } from '@internationalized/date';
|
|
51
|
+
import { getLocalTimeZone, parseDate, today, type DateValue } from '@internationalized/date';
|
|
52
|
+
import Icon from './icons/Icon.svelte';
|
|
53
|
+
import { wording } from './strings.js';
|
|
45
54
|
import { syncedLocal } from './synced.svelte.js';
|
|
46
55
|
import './controls.css';
|
|
47
56
|
|
|
57
|
+
const t = wording();
|
|
58
|
+
const TODAY = 'today';
|
|
59
|
+
const renderDate = today(getLocalTimeZone());
|
|
60
|
+
|
|
48
61
|
interface Props {
|
|
49
62
|
value: string | undefined;
|
|
50
|
-
/** The
|
|
51
|
-
*
|
|
63
|
+
/** The declared `default:` (`YYYY-MM-DD` or `today`): parsed for display only,
|
|
64
|
+
* shown while unset, never written. */
|
|
52
65
|
fallback?: string;
|
|
53
66
|
/** Accessible name for a field nothing else names: an object property, whose
|
|
54
67
|
* name is the field label plus the property's. A field's own date takes
|
|
@@ -65,9 +78,12 @@
|
|
|
65
78
|
labelledBy?: string;
|
|
66
79
|
/** The parked `description` (FieldLabel): announced after the name. */
|
|
67
80
|
describedBy?: string;
|
|
81
|
+
/** A `datetime`: `today` is no value of its type, so it draws no toggle and
|
|
82
|
+
* takes no `T`. */
|
|
83
|
+
datetime?: boolean;
|
|
68
84
|
onCommit: (v: string | undefined) => void;
|
|
69
85
|
}
|
|
70
|
-
let { value, fallback, label, labelledBy, describedBy, onCommit }: Props = $props();
|
|
86
|
+
let { value, fallback, label, labelledBy, describedBy, datetime, onCommit }: Props = $props();
|
|
71
87
|
|
|
72
88
|
let wrapEl: HTMLElement | undefined = $state();
|
|
73
89
|
/** Take the caret: what the label click, and a parent placing focus here, calls.
|
|
@@ -83,6 +99,7 @@
|
|
|
83
99
|
// throws on a malformed authored value: an empty field is the honest render.
|
|
84
100
|
function toDateValue(s: string): DateValue | undefined {
|
|
85
101
|
if (!s) return undefined;
|
|
102
|
+
if (s === TODAY) return renderDate;
|
|
86
103
|
try {
|
|
87
104
|
return parseDate(s);
|
|
88
105
|
} catch {
|
|
@@ -110,6 +127,27 @@
|
|
|
110
127
|
// a boolean so the substitution below narrows on the one fact it needs.
|
|
111
128
|
const ghost = $derived(local.value === '' ? fallbackDate : undefined);
|
|
112
129
|
const seated = $derived(value == null && seat && local.value !== '');
|
|
130
|
+
/** Who put `today` here, while the field follows the render date. */
|
|
131
|
+
const follows = $derived(
|
|
132
|
+
local.value === TODAY ? 'authored' : value == null && fallback === TODAY ? 'default' : undefined
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
function write(next: string): void {
|
|
136
|
+
local.value = next;
|
|
137
|
+
seat = false;
|
|
138
|
+
onCommit(next);
|
|
139
|
+
}
|
|
140
|
+
function toggle(): void {
|
|
141
|
+
write(follows ? renderDate.toString() : TODAY);
|
|
142
|
+
}
|
|
143
|
+
function shortcut(e: KeyboardEvent): void {
|
|
144
|
+
if (datetime || e.key.toLowerCase() !== 't' || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
145
|
+
const seg = (e.target as HTMLElement).dataset.segment;
|
|
146
|
+
if (!seg || seg === 'literal') return;
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
e.stopPropagation();
|
|
149
|
+
write(TODAY);
|
|
150
|
+
}
|
|
113
151
|
|
|
114
152
|
/** Focus moving between segments is neither an entry nor a leaving. */
|
|
115
153
|
const within = (e: FocusEvent): boolean => !!wrapEl?.contains(e.relatedTarget as Node | null);
|
|
@@ -152,7 +190,13 @@
|
|
|
152
190
|
}
|
|
153
191
|
</script>
|
|
154
192
|
|
|
155
|
-
<span
|
|
193
|
+
<span
|
|
194
|
+
class="qm-date-wrap"
|
|
195
|
+
bind:this={wrapEl}
|
|
196
|
+
onfocusin={enter}
|
|
197
|
+
onfocusout={leave}
|
|
198
|
+
onkeydowncapture={shortcut}
|
|
199
|
+
>
|
|
156
200
|
<BitsDateField.Root
|
|
157
201
|
value={parsed}
|
|
158
202
|
placeholder={fallbackDate}
|
|
@@ -160,9 +204,9 @@
|
|
|
160
204
|
// `CalendarDate.toString()` is exactly `YYYY-MM-DD`. A cleared or
|
|
161
205
|
// half-typed field yields undefined: the unset rung. The primitive reports
|
|
162
206
|
// its own value again as a segment loses focus, which is no edit, and over a
|
|
163
|
-
// seated default would write it.
|
|
207
|
+
// seated default or `today` would write it.
|
|
164
208
|
const next = d?.toString() ?? '';
|
|
165
|
-
if (next ===
|
|
209
|
+
if (next === (parsed?.toString() ?? '')) return;
|
|
166
210
|
local.value = next;
|
|
167
211
|
if (!d) seat = false;
|
|
168
212
|
onCommit(d?.toString());
|
|
@@ -179,7 +223,7 @@
|
|
|
179
223
|
aria-labelledby={labelledBy}
|
|
180
224
|
aria-describedby={describedBy}
|
|
181
225
|
data-ghosted={ghost ? '' : undefined}
|
|
182
|
-
data-default={seated ? '' : undefined}
|
|
226
|
+
data-default={seated || follows ? '' : undefined}
|
|
183
227
|
>
|
|
184
228
|
{#snippet children({ segments })}
|
|
185
229
|
<!-- Keyed by index: `part` repeats (the `literal` separators between
|
|
@@ -194,6 +238,17 @@
|
|
|
194
238
|
{shown.text}
|
|
195
239
|
</BitsDateField.Segment>
|
|
196
240
|
{/each}
|
|
241
|
+
{#if !datetime}
|
|
242
|
+
<button
|
|
243
|
+
type="button"
|
|
244
|
+
class="qm-chip qm-tap-floor qm-focus-ring qm-date-today"
|
|
245
|
+
aria-pressed={!!follows}
|
|
246
|
+
data-default={follows === 'default' ? '' : undefined}
|
|
247
|
+
onclick={toggle}
|
|
248
|
+
>
|
|
249
|
+
<Icon name={follows ? 'calendar-check' : 'calendar'} />{t.strings.dateToday}
|
|
250
|
+
</button>
|
|
251
|
+
{/if}
|
|
197
252
|
{/snippet}
|
|
198
253
|
</BitsDateField.Input>
|
|
199
254
|
</BitsDateField.Root>
|
|
@@ -235,4 +290,32 @@
|
|
|
235
290
|
.qm-date-wrap :global(.qm-date[data-default] .qm-date-segment) {
|
|
236
291
|
color: var(--_qm-ink-default);
|
|
237
292
|
}
|
|
293
|
+
/* The toggle's state is its fill and its glyph, never the pointer: a hover fill
|
|
294
|
+
would read as pressed. Off, it rests hidden where a pointer can reveal it, still
|
|
295
|
+
holding its width so the digits never shift. It sits inside the box's line, so
|
|
296
|
+
the box stands as tall as the inputs beside it. */
|
|
297
|
+
.qm-date-today {
|
|
298
|
+
margin-inline-start: auto;
|
|
299
|
+
padding-block: var(--_qm-space-half);
|
|
300
|
+
line-height: 1;
|
|
301
|
+
border-radius: var(--_qm-radius-inner);
|
|
302
|
+
}
|
|
303
|
+
.qm-date-today:hover {
|
|
304
|
+
background: transparent;
|
|
305
|
+
color: var(--_qm-ink-label);
|
|
306
|
+
}
|
|
307
|
+
.qm-date-today[aria-pressed='true'] {
|
|
308
|
+
background: var(--_qm-accent-tint);
|
|
309
|
+
color: var(--_qm-ink);
|
|
310
|
+
}
|
|
311
|
+
.qm-date-today[data-default] {
|
|
312
|
+
background: transparent;
|
|
313
|
+
box-shadow: inset 0 0 0 var(--_qm-border-width) var(--_qm-border);
|
|
314
|
+
color: var(--_qm-ink-default);
|
|
315
|
+
}
|
|
316
|
+
@media (hover: hover) {
|
|
317
|
+
.qm-date-wrap:not(:hover, :focus-within) .qm-date-today[aria-pressed='false'] {
|
|
318
|
+
opacity: 0;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
238
321
|
</style>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import './controls.css';
|
|
2
2
|
interface Props {
|
|
3
3
|
value: string | undefined;
|
|
4
|
-
/** The
|
|
5
|
-
*
|
|
4
|
+
/** The declared `default:` (`YYYY-MM-DD` or `today`): parsed for display only,
|
|
5
|
+
* shown while unset, never written. */
|
|
6
6
|
fallback?: string;
|
|
7
7
|
/** Accessible name for a field nothing else names: an object property, whose
|
|
8
8
|
* name is the field label plus the property's. A field's own date takes
|
|
@@ -19,6 +19,9 @@ interface Props {
|
|
|
19
19
|
labelledBy?: string;
|
|
20
20
|
/** The parked `description` (FieldLabel): announced after the name. */
|
|
21
21
|
describedBy?: string;
|
|
22
|
+
/** A `datetime`: `today` is no value of its type, so it draws no toggle and
|
|
23
|
+
* takes no `T`. */
|
|
24
|
+
datetime?: boolean;
|
|
22
25
|
onCommit: (v: string | undefined) => void;
|
|
23
26
|
}
|
|
24
27
|
declare const DateField: import("svelte").Component<Props, {
|
package/dist/visual/Field.svelte
CHANGED
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
VARIANT_DISCRIMINANT,
|
|
34
34
|
arrayLayout,
|
|
35
35
|
baseType,
|
|
36
|
-
exampleGhost,
|
|
37
36
|
ghostDefault,
|
|
38
37
|
isContainer,
|
|
39
38
|
optionalCell,
|
|
@@ -115,25 +114,24 @@
|
|
|
115
114
|
|
|
116
115
|
// What an unset field shows of its `default:`: the resolved value (provenance,
|
|
117
116
|
// `source === 'default'`). `ghost` is the raw typed value (enum/number/boolean
|
|
118
|
-
// fallbacks, a content leaf's `Content`), `defaultStr` its string form
|
|
119
|
-
//
|
|
120
|
-
//
|
|
117
|
+
// fallbacks, a content leaf's `Content`), `defaultStr` its string form; an
|
|
118
|
+
// object-valued default has none. An array draws its declared literal instead,
|
|
119
|
+
// which is what a take writes (`ArrayField`).
|
|
121
120
|
// A default that prints stands in the control as the value it is, at the default
|
|
122
121
|
// rung, and the first edit takes it (VISUAL_EDITOR §"The commitment ladder"). Where
|
|
123
|
-
// nothing prints, a control draws words: the `none` an optional cell prints
|
|
124
|
-
// a free-text field's `example:`.
|
|
122
|
+
// nothing prints, a control draws words: the `none` an optional cell prints.
|
|
125
123
|
const ghost = $derived(ghostDefault(provenance));
|
|
126
124
|
const defaultStr = $derived(
|
|
127
125
|
field.control === 'prose' ? titleText(ghost) || undefined : stringifyGhost(ghost)
|
|
128
126
|
);
|
|
129
127
|
const printed = $derived(printedText(defaultStr));
|
|
130
|
-
// A text or
|
|
131
|
-
// the row stops naming a default once the field is written, and emptying a
|
|
132
|
-
// field still has to know that the default prints.
|
|
128
|
+
// A text, number or date control reads its default off the schema, not the resolved
|
|
129
|
+
// row: the row stops naming a default once the field is written, and emptying a
|
|
130
|
+
// written field still has to know that the default prints. A date's `default: today`
|
|
131
|
+
// resolves to digits, and the control has to know it follows the render date.
|
|
133
132
|
const declared = $derived(printedText(field.schema.default));
|
|
134
133
|
const optional = $derived(optionalCell(field.schema));
|
|
135
134
|
const none = $derived(optional && value == null ? t.strings.optionalGhost : undefined);
|
|
136
|
-
const example = $derived(value == null ? exampleGhost(field.schema) : undefined);
|
|
137
135
|
// A variant resolves as one rung whose value is the whole container, so the
|
|
138
136
|
// discriminant's ghost is that container's own discriminant cell.
|
|
139
137
|
const ghostMember = $derived(
|
|
@@ -287,7 +285,6 @@
|
|
|
287
285
|
fallback={printed ? (ghost as Content) : undefined}
|
|
288
286
|
placeholder={none}
|
|
289
287
|
placeholderUntilEdit
|
|
290
|
-
{example}
|
|
291
288
|
labelledBy={domIds.label}
|
|
292
289
|
{describedBy}
|
|
293
290
|
{leafKey}
|
|
@@ -350,7 +347,8 @@
|
|
|
350
347
|
<DateField
|
|
351
348
|
bind:this={dateEl}
|
|
352
349
|
value={value as string | undefined}
|
|
353
|
-
fallback={
|
|
350
|
+
fallback={stringifyGhost(field.schema.default)}
|
|
351
|
+
datetime={baseType(field.schema) === 'datetime'}
|
|
354
352
|
labelledBy={domIds.label}
|
|
355
353
|
{describedBy}
|
|
356
354
|
onCommit={onCommitScalar}
|
|
@@ -405,7 +403,6 @@
|
|
|
405
403
|
value={value as string | undefined}
|
|
406
404
|
fallback={declared}
|
|
407
405
|
placeholder={none}
|
|
408
|
-
{example}
|
|
409
406
|
id={domIds.control}
|
|
410
407
|
{describedBy}
|
|
411
408
|
onCommit={onCommitScalar}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
A `number` / `integer` field → numeric input (fixture `font_size` = 11.5).
|
|
3
3
|
Commits at `change` (blur/Enter), not per keystroke: a partial numeric entry
|
|
4
|
-
(`-`, `1
|
|
4
|
+
(`-`, `1.`) is never a document state worth a boundary round-trip, and
|
|
5
5
|
committing it live flashes a coercion diagnostic + `console.error` on every
|
|
6
6
|
intermediate prefix, announced by `DiagnosticList`'s `role="status"` live
|
|
7
7
|
region.
|
|
@@ -12,15 +12,21 @@
|
|
|
12
12
|
so a blank entry commits `undefined`, the unset rung: the parent removes the field,
|
|
13
13
|
the engine renders the default, and the input holds it again.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
`
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
The entry grammar is plain decimal: an optional sign and digits, and for a `number`
|
|
16
|
+
a `.` and an exponent, the form `String()` gives a value too small or large for
|
|
17
|
+
digits. A keystroke that leaves the text outside a prefix of it does not land, and in
|
|
18
|
+
a `number` a `,` lands as `.`, the decimal key of a comma locale's keypad. A deletion
|
|
19
|
+
always lands, so a stored value outside the grammar (`abc`) can be cleared, and a
|
|
20
|
+
paste, a drop or an IME composition lands as it comes, judged when it settles.
|
|
21
|
+
`type="text"`, not `type="number"`: a native number input sanitizes an invalid
|
|
22
|
+
string to `""` rather than showing it, and steps on a scroll-wheel. `inputmode`
|
|
23
|
+
keeps the numeric mobile keyboard.
|
|
24
|
+
|
|
25
|
+
A settled entry in the grammar commits as a number. Anything else forwards the raw
|
|
26
|
+
string to `onCommit`: the boundary's `writer.set` coercion is the judge (throws a
|
|
27
|
+
`QuillmarkError` the parent turns into a field diagnostic, VISUAL_EDITOR
|
|
28
|
+
§Diagnostics). `Number()` alone is no gate: it reads `0x1F` and `Infinity` as
|
|
29
|
+
numbers, and `1.000` as a whole one.
|
|
24
30
|
-->
|
|
25
31
|
<script lang="ts">
|
|
26
32
|
import { syncedLocal } from './synced.svelte.js';
|
|
@@ -53,22 +59,42 @@
|
|
|
53
59
|
const local = syncedLocal(() => (value != null ? String(value) : (fallback ?? '')));
|
|
54
60
|
const defaulted = $derived(value == null && !!fallback && local.value === fallback);
|
|
55
61
|
|
|
62
|
+
const partial = $derived(
|
|
63
|
+
integer ? /^\s*[+-]?\d*\s*$/ : /^\s*[+-]?\d*\.?\d*(?:[eE][+-]?\d*)?\s*$/
|
|
64
|
+
);
|
|
65
|
+
const whole = $derived(integer ? /^[+-]?\d+$/ : /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/);
|
|
66
|
+
|
|
67
|
+
function filter(e: InputEvent): void {
|
|
68
|
+
if (e.inputType !== 'insertText' || e.data == null) return;
|
|
69
|
+
const el = e.currentTarget as HTMLInputElement;
|
|
70
|
+
const from = el.selectionStart ?? el.value.length;
|
|
71
|
+
const to = el.selectionEnd ?? el.value.length;
|
|
72
|
+
const data = !integer && e.data === ',' ? '.' : e.data;
|
|
73
|
+
const next = el.value.slice(0, from) + data + el.value.slice(to);
|
|
74
|
+
if (!partial.test(next)) return e.preventDefault();
|
|
75
|
+
if (data === e.data) return;
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
// `insertText` lands on the undo stack and fires `input`; `setRangeText` does neither.
|
|
78
|
+
if (document.execCommand?.('insertText', false, data)) return;
|
|
79
|
+
el.setRangeText(data, from, to, 'end');
|
|
80
|
+
local.value = el.value;
|
|
81
|
+
}
|
|
82
|
+
|
|
56
83
|
// Parse a settled entry and emit it; `local` is owned by `oninput`. Blank →
|
|
57
84
|
// `undefined`, and the input takes the default back: nothing else reconciles it, the
|
|
58
85
|
// projection having read the default throughout. Onto the element as well, since the
|
|
59
86
|
// attribute Svelte last wrote may already be the default, and it writes no repeat.
|
|
60
87
|
function commit(el: HTMLInputElement): void {
|
|
61
88
|
const raw = el.value;
|
|
62
|
-
|
|
89
|
+
const entry = raw.trim();
|
|
90
|
+
if (entry === '') {
|
|
63
91
|
onCommit(undefined);
|
|
64
92
|
local.value = el.value = fallback ?? '';
|
|
65
93
|
return;
|
|
66
94
|
}
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const n = Number(raw);
|
|
71
|
-
onCommit(Number.isNaN(n) ? raw : n);
|
|
95
|
+
// Finite as well: a digit run past `Number.MAX_VALUE` reads as `Infinity`.
|
|
96
|
+
const n = whole.test(entry) ? Number(entry) : NaN;
|
|
97
|
+
onCommit(Number.isFinite(n) ? n : raw);
|
|
72
98
|
}
|
|
73
99
|
</script>
|
|
74
100
|
|
|
@@ -82,6 +108,7 @@
|
|
|
82
108
|
data-default={defaulted ? '' : undefined}
|
|
83
109
|
aria-label={id ? undefined : label}
|
|
84
110
|
aria-describedby={describedBy}
|
|
111
|
+
onbeforeinput={filter}
|
|
85
112
|
oninput={(e) => {
|
|
86
113
|
local.value = (e.currentTarget as HTMLInputElement).value;
|
|
87
114
|
}}
|
|
@@ -39,7 +39,6 @@
|
|
|
39
39
|
baseType,
|
|
40
40
|
controlKind,
|
|
41
41
|
declaredContent,
|
|
42
|
-
exampleGhost,
|
|
43
42
|
humanize,
|
|
44
43
|
isContainer,
|
|
45
44
|
mountKey,
|
|
@@ -134,9 +133,6 @@
|
|
|
134
133
|
* cell ghosts nothing, an empty answer printing empty. */
|
|
135
134
|
const noneOf = (key: string, sub: QuillFieldSchema): string | undefined =>
|
|
136
135
|
obj[key] == null && optionalCell(sub) ? t.strings.optionalGhost : undefined;
|
|
137
|
-
/** What an unset free-text cell ghosts where nothing prints ({@link exampleGhost}). */
|
|
138
|
-
const exampleOf = (key: string, sub: QuillFieldSchema): string | undefined =>
|
|
139
|
-
obj[key] == null ? exampleGhost(sub) : undefined;
|
|
140
136
|
/** A scalar cell's `default:` as the text an unset cell holds, where it prints. */
|
|
141
137
|
const textDefault = (sub: QuillFieldSchema): string | undefined => printedText(sub.default);
|
|
142
138
|
/** A content cell's `default:` as the content an unset leaf holds, where it prints. */
|
|
@@ -338,6 +334,7 @@
|
|
|
338
334
|
describedBy={describes}
|
|
339
335
|
value={obj[key] as string | undefined}
|
|
340
336
|
fallback={sub.default != null ? String(sub.default) : undefined}
|
|
337
|
+
datetime={baseType(sub) === 'datetime'}
|
|
341
338
|
onCommit={(v) => commitProp(key, v)}
|
|
342
339
|
/>
|
|
343
340
|
{:else if kind === 'text'}
|
|
@@ -348,7 +345,6 @@
|
|
|
348
345
|
value={obj[key] as string | undefined}
|
|
349
346
|
fallback={textDefault(sub)}
|
|
350
347
|
placeholder={noneOf(key, sub)}
|
|
351
|
-
example={exampleOf(key, sub)}
|
|
352
348
|
onCommit={(v) => commitProp(key, v)}
|
|
353
349
|
onKey={onCellKey ? (e) => onCellKey(e, key) : undefined}
|
|
354
350
|
/>
|
|
@@ -361,7 +357,6 @@
|
|
|
361
357
|
fallback={contentDefault(sub)}
|
|
362
358
|
plaintext={baseType(sub) === 'plaintext'}
|
|
363
359
|
placeholder={noneOf(key, sub)}
|
|
364
|
-
example={exampleOf(key, sub)}
|
|
365
360
|
{block}
|
|
366
361
|
label={named}
|
|
367
362
|
labelledBy={ids?.label}
|
|
@@ -43,12 +43,8 @@
|
|
|
43
43
|
/** Ghost shown on the empty leaf: the `none` an optional field prints unset, or
|
|
44
44
|
* nothing, and a body's always text (`resolveBodyGhost`). */
|
|
45
45
|
placeholder?: string;
|
|
46
|
-
/**
|
|
46
|
+
/** The placeholder goes at the leaf's first edit (`createField`). */
|
|
47
47
|
placeholderUntilEdit?: boolean;
|
|
48
|
-
/** Ghost shown on the empty leaf until its first edit, and in the placeholder's
|
|
49
|
-
* stead while the leaf holds the focus: an unset field's `example:`
|
|
50
|
-
* (`exampleGhost`). */
|
|
51
|
-
example?: string;
|
|
52
48
|
/** Registry identity, stamped on the DOM node so a remount is visible as one. */
|
|
53
49
|
leafKey: string;
|
|
54
50
|
onFocus?: (addr: Addr) => void;
|
|
@@ -77,7 +73,6 @@
|
|
|
77
73
|
fallback,
|
|
78
74
|
placeholder,
|
|
79
75
|
placeholderUntilEdit,
|
|
80
|
-
example,
|
|
81
76
|
leafKey,
|
|
82
77
|
onFocus,
|
|
83
78
|
onCaretMove,
|
|
@@ -128,7 +123,6 @@
|
|
|
128
123
|
fallback,
|
|
129
124
|
placeholder,
|
|
130
125
|
placeholderUntilEdit,
|
|
131
|
-
example,
|
|
132
126
|
tableStrings: () => t.strings,
|
|
133
127
|
onSlash: (next) => {
|
|
134
128
|
slash = next;
|
|
@@ -156,9 +150,6 @@
|
|
|
156
150
|
$effect(() => {
|
|
157
151
|
controller?.setPlaceholder(placeholder);
|
|
158
152
|
});
|
|
159
|
-
$effect(() => {
|
|
160
|
-
controller?.setExample(example);
|
|
161
|
-
});
|
|
162
153
|
$effect(() => {
|
|
163
154
|
controller?.setFallback(fallback);
|
|
164
155
|
});
|
|
@@ -31,12 +31,8 @@ interface Props {
|
|
|
31
31
|
/** Ghost shown on the empty leaf: the `none` an optional field prints unset, or
|
|
32
32
|
* nothing, and a body's always text (`resolveBodyGhost`). */
|
|
33
33
|
placeholder?: string;
|
|
34
|
-
/**
|
|
34
|
+
/** The placeholder goes at the leaf's first edit (`createField`). */
|
|
35
35
|
placeholderUntilEdit?: boolean;
|
|
36
|
-
/** Ghost shown on the empty leaf until its first edit, and in the placeholder's
|
|
37
|
-
* stead while the leaf holds the focus: an unset field's `example:`
|
|
38
|
-
* (`exampleGhost`). */
|
|
39
|
-
example?: string;
|
|
40
36
|
/** Registry identity, stamped on the DOM node so a remount is visible as one. */
|
|
41
37
|
leafKey: string;
|
|
42
38
|
onFocus?: (addr: Addr) => void;
|
|
@@ -76,12 +76,9 @@
|
|
|
76
76
|
labelledBy?: string;
|
|
77
77
|
/** The parked `description` (FieldLabel) → `aria-describedby`. */
|
|
78
78
|
describedBy?: string;
|
|
79
|
-
/** The empty leaf's
|
|
80
|
-
* `none` an optional cell prints unset
|
|
81
|
-
* placeholder and in its stead while the leaf holds the focus (`createField`'s
|
|
82
|
-
* pair). Read per decoration pass. */
|
|
79
|
+
/** The empty leaf's ghost, a subform cell's, until the leaf's first edit: the
|
|
80
|
+
* `none` an optional cell prints unset. Read per decoration pass. */
|
|
83
81
|
placeholder?: string;
|
|
84
|
-
example?: string;
|
|
85
82
|
onChange: (rt: Content) => void;
|
|
86
83
|
/** Raw keydown, for a container whose own keys run through this leaf: the
|
|
87
84
|
* array repeater's Enter/Backspace (`ArrayField`). Fires before the view's own
|
|
@@ -97,7 +94,6 @@
|
|
|
97
94
|
labelledBy,
|
|
98
95
|
describedBy,
|
|
99
96
|
placeholder,
|
|
100
|
-
example,
|
|
101
97
|
onChange,
|
|
102
98
|
onKey
|
|
103
99
|
}: Props = $props();
|
|
@@ -160,8 +156,7 @@
|
|
|
160
156
|
: proseLeafPlugins(schema, {
|
|
161
157
|
inline,
|
|
162
158
|
placeholder: () => placeholder,
|
|
163
|
-
placeholderUntilEdit: true
|
|
164
|
-
example: () => example
|
|
159
|
+
placeholderUntilEdit: true
|
|
165
160
|
})
|
|
166
161
|
});
|
|
167
162
|
const mounted = new EditorView(
|
|
@@ -25,12 +25,9 @@ interface Props {
|
|
|
25
25
|
labelledBy?: string;
|
|
26
26
|
/** The parked `description` (FieldLabel) → `aria-describedby`. */
|
|
27
27
|
describedBy?: string;
|
|
28
|
-
/** The empty leaf's
|
|
29
|
-
* `none` an optional cell prints unset
|
|
30
|
-
* placeholder and in its stead while the leaf holds the focus (`createField`'s
|
|
31
|
-
* pair). Read per decoration pass. */
|
|
28
|
+
/** The empty leaf's ghost, a subform cell's, until the leaf's first edit: the
|
|
29
|
+
* `none` an optional cell prints unset. Read per decoration pass. */
|
|
32
30
|
placeholder?: string;
|
|
33
|
-
example?: string;
|
|
34
31
|
onChange: (rt: Content) => void;
|
|
35
32
|
/** Raw keydown, for a container whose own keys run through this leaf: the
|
|
36
33
|
* array repeater's Enter/Backspace (`ArrayField`). Fires before the view's own
|
|
@@ -24,9 +24,6 @@
|
|
|
24
24
|
/** Words at rest about an unset field that holds no text: the `None` an optional
|
|
25
25
|
* cell prints. */
|
|
26
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;
|
|
30
27
|
/** Accessible name for an input nothing else names: an array element, whose
|
|
31
28
|
* name is the field label plus its 1-based index. A field's own input takes
|
|
32
29
|
* `id` instead and is named by the `<label for>` beside it. */
|
|
@@ -41,8 +38,7 @@
|
|
|
41
38
|
* array repeater's Enter/Backspace (`ArrayField`). */
|
|
42
39
|
onKey?: (e: KeyboardEvent) => void;
|
|
43
40
|
}
|
|
44
|
-
let { value, fallback, placeholder,
|
|
45
|
-
$props();
|
|
41
|
+
let { value, fallback, placeholder, label, id, describedBy, onCommit, onKey }: Props = $props();
|
|
46
42
|
|
|
47
43
|
// Local input state synced to `value`, or to the default an unset field holds:
|
|
48
44
|
// own-typing stays local, only an external change reconciles back in (see
|
|
@@ -50,9 +46,6 @@
|
|
|
50
46
|
const local = syncedLocal(() => value ?? fallback ?? '');
|
|
51
47
|
const defaulted = $derived(value == null && !!fallback && local.value === fallback);
|
|
52
48
|
|
|
53
|
-
let focused = $state(false);
|
|
54
|
-
const ghost = $derived(focused ? (example ?? placeholder) : (placeholder ?? example));
|
|
55
|
-
|
|
56
49
|
let inputEl: HTMLInputElement | undefined = $state();
|
|
57
50
|
/** Take the caret: what a parent placing focus on this control calls. */
|
|
58
51
|
export function focus(): void {
|
|
@@ -66,13 +59,11 @@
|
|
|
66
59
|
type="text"
|
|
67
60
|
value={local.value}
|
|
68
61
|
{id}
|
|
69
|
-
placeholder
|
|
62
|
+
{placeholder}
|
|
70
63
|
data-default={defaulted ? '' : undefined}
|
|
71
64
|
aria-label={id ? undefined : label}
|
|
72
65
|
aria-describedby={describedBy}
|
|
73
66
|
onkeydown={onKey}
|
|
74
|
-
onfocus={() => (focused = true)}
|
|
75
|
-
onblur={() => (focused = false)}
|
|
76
67
|
oninput={(e) => {
|
|
77
68
|
local.value = (e.currentTarget as HTMLInputElement).value;
|
|
78
69
|
onCommit(local.value !== '' ? local.value : fallback ? '' : undefined);
|
|
@@ -6,9 +6,6 @@ interface Props {
|
|
|
6
6
|
/** Words at rest about an unset field that holds no text: the `None` an optional
|
|
7
7
|
* cell prints. */
|
|
8
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;
|
|
12
9
|
/** Accessible name for an input nothing else names: an array element, whose
|
|
13
10
|
* name is the field label plus its 1-based index. A field's own input takes
|
|
14
11
|
* `id` instead and is named by the `<label for>` beside it. */
|
|
@@ -63,7 +63,6 @@
|
|
|
63
63
|
provenanceMap,
|
|
64
64
|
resolvedByCardIndex,
|
|
65
65
|
ghostDefault,
|
|
66
|
-
declaredGhost,
|
|
67
66
|
titleText,
|
|
68
67
|
resolveBodyGhost,
|
|
69
68
|
NO_RESOLVED_ROWS,
|
|
@@ -612,7 +611,6 @@
|
|
|
612
611
|
bodyGhost: hasBody
|
|
613
612
|
? resolveBodyGhost(
|
|
614
613
|
titleText(ghostDefault(rows.body ?? undefined)) || undefined,
|
|
615
|
-
declaredGhost(cardSchema?.body?.example, true),
|
|
616
614
|
merged.bodyPlaceholder?.({ cardId: id, kind, isMain }),
|
|
617
615
|
merged.bodyGhost
|
|
618
616
|
)
|
package/dist/visual/controls.css
CHANGED
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
color: var(--_qm-ink-default);
|
|
113
113
|
}
|
|
114
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 —
|
|
115
|
+
ghosts is words about a value it does not hold — `None` — which is
|
|
116
116
|
content and reads at a content's contrast, the same step the prose leaf's ghost
|
|
117
117
|
reads (`prose.css`). */
|
|
118
118
|
:where(.qm-input)::placeholder {
|
|
@@ -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,6 +47,9 @@ 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;
|
|
52
55
|
/** What an unset optional cell (`t?`) ghosts at rest: the `none` it prints
|
package/dist/visual/strings.js
CHANGED
|
@@ -37,6 +37,7 @@ 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',
|
|
41
42
|
optionalGhost: 'None',
|
|
42
43
|
proseHeld: 'Holds more than this field can edit here.',
|
|
@@ -138,32 +138,23 @@ export interface BodyPlaceholderContext {
|
|
|
138
138
|
*/
|
|
139
139
|
export type BodyPlaceholder = (ctx: BodyPlaceholderContext) => string | undefined;
|
|
140
140
|
/**
|
|
141
|
-
* The empty body's ghost: the resolved body `default:`, else the
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* there is no promise; the quill's own comes first, being the one an agent working
|
|
147
|
-
* from the blueprint sees too.
|
|
141
|
+
* The empty body's ghost: the resolved body `default:`, else the consumer's wording,
|
|
142
|
+
* else the flat built-in. The `default:` wins because it is the only one that
|
|
143
|
+
* describes the render: it promises what prints if nothing is written, and wording
|
|
144
|
+
* placed over it would make that promise unreadable. The rest are invitations, and an
|
|
145
|
+
* invitation belongs only where there is no promise.
|
|
148
146
|
*/
|
|
149
|
-
export declare function resolveBodyGhost(resolvedDefault: string | undefined,
|
|
147
|
+
export declare function resolveBodyGhost(resolvedDefault: string | undefined, custom: string | undefined, builtIn: string): string;
|
|
150
148
|
/** Whether a cell is optional: a `t?` type, which renders `none` unanswered and so
|
|
151
149
|
* never declares a `default:` (canon `SCHEMAS.md` §"Optional cells"). */
|
|
152
150
|
export declare function optionalCell(f: QuillFieldSchema): boolean;
|
|
153
151
|
/** The type a cell's control reads: the declared one, an optional cell's `?` stripped.
|
|
154
152
|
* The `?` moves the render floor and nothing else, so every read of a type is this. */
|
|
155
153
|
export declare function baseType(f: QuillFieldSchema): QuillFieldType;
|
|
156
|
-
/**
|
|
157
|
-
* A value the schema declares, as ghost text: a `default:`, an `example:` or a
|
|
158
|
-
* `body.example`. Markdown — a `richtext` value — ghosts as the text it renders, one
|
|
159
|
-
* block to a line, as a resolved default does ({@link titleText}); anything else as
|
|
160
|
-
* itself. `undefined` for a blank or a non-scalar.
|
|
161
|
-
*/
|
|
162
|
-
export declare function declaredGhost(v: unknown, markdown: boolean): string | undefined;
|
|
163
154
|
/**
|
|
164
155
|
* A scalar `default:` as the text an unset control holds, where it prints: as
|
|
165
156
|
* declared, `undefined` for none, a blank or a non-scalar. A default of spaces prints
|
|
166
|
-
* nothing a reader sees, so a control holds none
|
|
157
|
+
* nothing a reader sees, so a control holds none.
|
|
167
158
|
*/
|
|
168
159
|
export declare function printedText(v: unknown): string | undefined;
|
|
169
160
|
/**
|
|
@@ -174,13 +165,6 @@ export declare function printedText(v: unknown): string | undefined;
|
|
|
174
165
|
* a blank or a non-scalar.
|
|
175
166
|
*/
|
|
176
167
|
export declare function declaredContent(v: unknown, markdown: boolean): Content | undefined;
|
|
177
|
-
/**
|
|
178
|
-
* The `example:` an unset free-text cell ghosts: a `string`, `plaintext` or `richtext`
|
|
179
|
-
* whose `default:` prints nothing. Every other type takes none. A default that prints
|
|
180
|
-
* dominates, standing in the control as the value it is; a type-empty one, the
|
|
181
|
-
* skippable marker, prints nothing and leaves the example the only thing to draw.
|
|
182
|
-
*/
|
|
183
|
-
export declare function exampleGhost(f: QuillFieldSchema): string | undefined;
|
|
184
168
|
/** A field's `{#each}` key: its name and what a mounted leaf reads once, its type and
|
|
185
169
|
* `inline` and its elements'. A retype or a quill swap that changes any of them under a
|
|
186
170
|
* name remounts the field, where a kept leaf would commit on the old schema. */
|
package/dist/visual/structure.js
CHANGED
|
@@ -48,16 +48,14 @@ export function stringifyGhost(ghost) {
|
|
|
48
48
|
return ghost != null && typeof ghost !== 'object' ? String(ghost) : undefined;
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
|
-
* The empty body's ghost: the resolved body `default:`, else the
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* there is no promise; the quill's own comes first, being the one an agent working
|
|
57
|
-
* from the blueprint sees too.
|
|
51
|
+
* The empty body's ghost: the resolved body `default:`, else the consumer's wording,
|
|
52
|
+
* else the flat built-in. The `default:` wins because it is the only one that
|
|
53
|
+
* describes the render: it promises what prints if nothing is written, and wording
|
|
54
|
+
* placed over it would make that promise unreadable. The rest are invitations, and an
|
|
55
|
+
* invitation belongs only where there is no promise.
|
|
58
56
|
*/
|
|
59
|
-
export function resolveBodyGhost(resolvedDefault,
|
|
60
|
-
return resolvedDefault ||
|
|
57
|
+
export function resolveBodyGhost(resolvedDefault, custom, builtIn) {
|
|
58
|
+
return resolvedDefault || custom || builtIn;
|
|
61
59
|
}
|
|
62
60
|
/** Whether a cell is optional: a `t?` type, which renders `none` unanswered and so
|
|
63
61
|
* never declares a `default:` (canon `SCHEMAS.md` §"Optional cells"). */
|
|
@@ -69,22 +67,10 @@ export function optionalCell(f) {
|
|
|
69
67
|
export function baseType(f) {
|
|
70
68
|
return (optionalCell(f) ? f.type.slice(0, -1) : f.type);
|
|
71
69
|
}
|
|
72
|
-
/**
|
|
73
|
-
* A value the schema declares, as ghost text: a `default:`, an `example:` or a
|
|
74
|
-
* `body.example`. Markdown — a `richtext` value — ghosts as the text it renders, one
|
|
75
|
-
* block to a line, as a resolved default does ({@link titleText}); anything else as
|
|
76
|
-
* itself. `undefined` for a blank or a non-scalar.
|
|
77
|
-
*/
|
|
78
|
-
export function declaredGhost(v, markdown) {
|
|
79
|
-
const text = stringifyGhost(v)?.trim();
|
|
80
|
-
if (!text)
|
|
81
|
-
return undefined;
|
|
82
|
-
return (markdown ? core().importMarkdown(text).text.trim() : text) || undefined;
|
|
83
|
-
}
|
|
84
70
|
/**
|
|
85
71
|
* A scalar `default:` as the text an unset control holds, where it prints: as
|
|
86
72
|
* declared, `undefined` for none, a blank or a non-scalar. A default of spaces prints
|
|
87
|
-
* nothing a reader sees, so a control holds none
|
|
73
|
+
* nothing a reader sees, so a control holds none.
|
|
88
74
|
*/
|
|
89
75
|
export function printedText(v) {
|
|
90
76
|
const text = stringifyGhost(v);
|
|
@@ -114,21 +100,6 @@ export function declaredContent(v, markdown) {
|
|
|
114
100
|
islands: []
|
|
115
101
|
};
|
|
116
102
|
}
|
|
117
|
-
/**
|
|
118
|
-
* The `example:` an unset free-text cell ghosts: a `string`, `plaintext` or `richtext`
|
|
119
|
-
* whose `default:` prints nothing. Every other type takes none. A default that prints
|
|
120
|
-
* dominates, standing in the control as the value it is; a type-empty one, the
|
|
121
|
-
* skippable marker, prints nothing and leaves the example the only thing to draw.
|
|
122
|
-
*/
|
|
123
|
-
export function exampleGhost(f) {
|
|
124
|
-
const kind = controlKind(f);
|
|
125
|
-
if (kind !== 'text' && kind !== 'prose')
|
|
126
|
-
return undefined;
|
|
127
|
-
const markdown = baseType(f) === 'richtext';
|
|
128
|
-
if (declaredGhost(f.default, markdown) !== undefined)
|
|
129
|
-
return undefined;
|
|
130
|
-
return declaredGhost(f.example, markdown);
|
|
131
|
-
}
|
|
132
103
|
/** A field's `{#each}` key: its name and what a mounted leaf reads once, its type and
|
|
133
104
|
* `inline` and its elements'. A retype or a quill swap that changes any of them under a
|
|
134
105
|
* name remounts the field, where a kept leaf would commit on the old schema. */
|
|
@@ -323,21 +294,20 @@ export function shortCell(sub) {
|
|
|
323
294
|
return (kind === 'text' || kind === 'enum' || kind === 'number' || kind === 'boolean' || kind === 'date');
|
|
324
295
|
}
|
|
325
296
|
/**
|
|
326
|
-
* The first short text cell's words
|
|
327
|
-
* `
|
|
328
|
-
*
|
|
329
|
-
*
|
|
297
|
+
* The first short text cell's words — the first `string`, or inline `richtext` /
|
|
298
|
+
* `plaintext`, in declaration order — read through {@link titleText}; `undefined` where
|
|
299
|
+
* there is no such cell or it is empty. The cell is picked by the schema alone, so an
|
|
300
|
+
* instance is named by one cell whatever the others hold. What names an instance from
|
|
301
|
+
* its own values: a collapsed row, and a card whose kind declares no `title`.
|
|
330
302
|
*/
|
|
331
303
|
function firstShortText(cells, values) {
|
|
332
|
-
|
|
304
|
+
const named = Object.entries(cells ?? {}).find(([, sub]) => {
|
|
333
305
|
const kind = controlKind(sub);
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
return undefined;
|
|
306
|
+
return kind === 'text' || (kind === 'prose' && shortCell(sub));
|
|
307
|
+
});
|
|
308
|
+
if (!named)
|
|
309
|
+
return undefined;
|
|
310
|
+
return titleText(ownValue(values, named[0])).trim() || undefined;
|
|
341
311
|
}
|
|
342
312
|
/** A collapsed row's own words ({@link firstShortText}); `undefined` while the row has
|
|
343
313
|
* nothing to say for itself. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quillmark/svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Editor, live-preview and source surfaces over a Quillmark WASM session.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"check": "svelte-check --tsconfig ./tsconfig.json"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
|
-
"@quillmark/wasm": ">=0.
|
|
69
|
+
"@quillmark/wasm": ">=0.117.0-0",
|
|
70
70
|
"svelte": "^5.0.0"
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|