@quillmark/svelte 0.20.1 → 0.21.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/THEMING.md +1 -1
- package/dist/core/codec/atoms.js +25 -10
- package/dist/core/codec/blocks.d.ts +5 -5
- package/dist/core/codec/blocks.js +23 -6
- package/dist/core/codec/code.d.ts +3 -3
- package/dist/core/codec/code.js +51 -21
- package/dist/core/codec/field.d.ts +9 -7
- package/dist/core/codec/field.js +22 -11
- package/dist/core/codec/inputrules.d.ts +4 -0
- package/dist/core/codec/inputrules.js +43 -5
- package/dist/core/codec/link-paste.d.ts +11 -0
- package/dist/core/codec/link-paste.js +32 -0
- package/dist/core/codec/lists.d.ts +2 -2
- package/dist/core/codec/lists.js +66 -26
- package/dist/core/codec/prose.css +71 -43
- package/dist/core/codec/schema.js +55 -11
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-view.d.ts +5 -0
- package/dist/core/codec/table-view.js +232 -103
- package/dist/core/rungs.d.ts +6 -0
- package/dist/core/rungs.js +11 -0
- package/dist/core/theme.css +24 -12
- package/dist/visual/ArrayField.svelte +168 -48
- package/dist/visual/BooleanField.svelte +12 -1
- package/dist/visual/Card.svelte +32 -20
- package/dist/visual/Card.svelte.d.ts +1 -0
- package/dist/visual/CardControls.svelte +15 -15
- package/dist/visual/DateField.svelte +93 -64
- package/dist/visual/EnumField.svelte +9 -4
- package/dist/visual/FieldHint.svelte +22 -5
- package/dist/visual/FieldLabel.svelte +3 -3
- package/dist/visual/FormatPopover.svelte +25 -9
- package/dist/visual/MatrixField.svelte +10 -7
- package/dist/visual/ObjectField.svelte +2 -4
- package/dist/visual/TipsCard.svelte +2 -5
- package/dist/visual/VisualEditorInner.svelte +108 -26
- package/dist/visual/controls.css +2 -1
- package/dist/visual/icons/Icon.svelte +3 -2
- package/dist/visual/icons/Icon.svelte.d.ts +3 -2
- package/dist/visual/icons/nodes.d.ts +25 -0
- package/dist/visual/icons/nodes.js +8 -0
- package/dist/visual/motion.d.ts +25 -3
- package/dist/visual/motion.js +58 -9
- package/dist/visual/strings.d.ts +6 -5
- package/dist/visual/strings.js +3 -3
- package/package.json +2 -2
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
A `date` (or `datetime`) field → a styled segmented date field on bits-ui. The
|
|
3
|
-
stored value is a string, `YYYY-MM-DD` or `today`; a cleared control commits
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
stored value is a string, `YYYY-MM-DD` or `today`; a cleared control commits as the
|
|
4
|
+
focus leaves it, the blank `""` over a `default:`, which stays blank, and `undefined`
|
|
5
|
+
(the unset rung) where nothing is declared, the parent removing the field. The value-object a date field lowers to is a render-time
|
|
6
6
|
concern: the editor only sees the stored string.
|
|
7
7
|
|
|
8
8
|
`today` is the render date rather than a written one (canon `SCHEMAS.md`), so its
|
|
@@ -43,8 +43,15 @@
|
|
|
43
43
|
|
|
44
44
|
Entering an unset field seats the default as the primitive's value, still at the
|
|
45
45
|
default rung, so an edit to one segment commits a whole date with the others the
|
|
46
|
-
default's. Leaving without an edit unseats it, and nothing is written
|
|
47
|
-
|
|
46
|
+
default's. Leaving without an edit unseats it, and nothing is written.
|
|
47
|
+
|
|
48
|
+
A segment edit that leaves a segment empty empties the primitive's value, which is
|
|
49
|
+
held rather than committed while the focus stays inside: the first Backspace through
|
|
50
|
+
a date writes nothing unset, and the empty segment shows its hint, not the default's
|
|
51
|
+
digits beside the ones still standing. Leaving settles it: blank where every segment
|
|
52
|
+
is empty, otherwise the stored date back. A field storing no date puts its segments
|
|
53
|
+
back by remounting the primitive, which re-reads an unchanged empty value as nothing
|
|
54
|
+
to sync.
|
|
48
55
|
-->
|
|
49
56
|
<script lang="ts">
|
|
50
57
|
import { DateField as BitsDateField } from 'bits-ui';
|
|
@@ -108,13 +115,18 @@
|
|
|
108
115
|
|
|
109
116
|
const fallbackDate = $derived(toDateValue(fallback?.slice(0, 10) ?? ''));
|
|
110
117
|
/** The default is the primitive's value: from entering an unset field until an
|
|
111
|
-
* edit
|
|
112
|
-
* neither drops a seat nor restores one over the segments a clear left. */
|
|
118
|
+
* edit or leaving. Part of the projection, so a re-derivation keeps it. */
|
|
113
119
|
let seat = $state(false);
|
|
120
|
+
/** An emptied value held while the focus stays inside (see the header). Part of the
|
|
121
|
+
* projection too, so a re-derivation restores neither the stored date nor the seat
|
|
122
|
+
* over the segments it emptied. */
|
|
123
|
+
let held = $state(false);
|
|
124
|
+
/** Remounts the primitive when bumped (see the header). */
|
|
125
|
+
let generation = $state(0);
|
|
114
126
|
// Driven controlled (`value` + `onValueChange`, never `bind:`) so reconciliation
|
|
115
127
|
// stays the package's.
|
|
116
128
|
let local = $derived(
|
|
117
|
-
value?.slice(0, 10) ?? (seat && fallbackDate ? fallbackDate.toString() : '')
|
|
129
|
+
held ? '' : (value?.slice(0, 10) ?? (seat && fallbackDate ? fallbackDate.toString() : ''))
|
|
118
130
|
);
|
|
119
131
|
const parsed = $derived(toDateValue(local));
|
|
120
132
|
// The date the empty segments ghost, or undefined when there is nothing to ghost:
|
|
@@ -122,7 +134,7 @@
|
|
|
122
134
|
// fails to parse is AUTHORED-but-malformed, which the empty field states
|
|
123
135
|
// honestly: the ghost would claim it unset. Held as the parsed value rather than
|
|
124
136
|
// a boolean so the substitution below narrows on the one fact it needs.
|
|
125
|
-
const ghost = $derived(local === '' ? fallbackDate : undefined);
|
|
137
|
+
const ghost = $derived(value == null && local === '' && !held ? fallbackDate : undefined);
|
|
126
138
|
const seated = $derived(value == null && seat && local !== '');
|
|
127
139
|
/** Who put `today` here, while the field follows the render date. */
|
|
128
140
|
const follows = $derived(
|
|
@@ -130,6 +142,7 @@
|
|
|
130
142
|
);
|
|
131
143
|
|
|
132
144
|
function write(next: string): void {
|
|
145
|
+
held = false;
|
|
133
146
|
local = next;
|
|
134
147
|
seat = false;
|
|
135
148
|
onCommit(next);
|
|
@@ -152,7 +165,17 @@
|
|
|
152
165
|
if (!within(e) && value == null && fallbackDate) seat = true;
|
|
153
166
|
}
|
|
154
167
|
function leave(e: FocusEvent): void {
|
|
155
|
-
if (
|
|
168
|
+
if (within(e)) return;
|
|
169
|
+
seat = false;
|
|
170
|
+
if (!held) return;
|
|
171
|
+
held = false;
|
|
172
|
+
const filled = [...(wrapEl?.querySelectorAll('[data-segment]') ?? [])].some(
|
|
173
|
+
(s) => s.getAttribute('data-segment') !== 'literal' && !s.hasAttribute('data-ghosted')
|
|
174
|
+
);
|
|
175
|
+
const blank = fallbackDate ? '' : undefined;
|
|
176
|
+
if (!filled) {
|
|
177
|
+
if (value !== blank) onCommit(blank);
|
|
178
|
+
} else if (!value) generation++;
|
|
156
179
|
}
|
|
157
180
|
|
|
158
181
|
// What one segment prints, and which rung it takes.
|
|
@@ -194,61 +217,67 @@
|
|
|
194
217
|
onfocusout={leave}
|
|
195
218
|
onkeydowncapture={shortcut}
|
|
196
219
|
>
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
digits at full ink while the segments around them ghost. -->
|
|
217
|
-
<BitsDateField.Input
|
|
218
|
-
class="qm-date qm-control-box qm-focus-ring-within"
|
|
219
|
-
aria-label={labelledBy ? undefined : label}
|
|
220
|
-
aria-labelledby={labelledBy}
|
|
221
|
-
aria-describedby={describedBy}
|
|
222
|
-
data-ghosted={ghost ? '' : undefined}
|
|
223
|
-
data-default={seated || follows ? '' : undefined}
|
|
220
|
+
{#key generation}
|
|
221
|
+
<BitsDateField.Root
|
|
222
|
+
value={parsed}
|
|
223
|
+
placeholder={fallbackDate}
|
|
224
|
+
onValueChange={(d) => {
|
|
225
|
+
// `CalendarDate.toString()` is exactly `YYYY-MM-DD`. A cleared or
|
|
226
|
+
// half-typed field yields undefined, held until the focus leaves. The
|
|
227
|
+
// primitive reports its own value again as a segment loses focus, which is
|
|
228
|
+
// no edit, and over a seated default or `today` would write it.
|
|
229
|
+
const next = d?.toString() ?? '';
|
|
230
|
+
if (next === (parsed?.toString() ?? '')) return;
|
|
231
|
+
if (!d) {
|
|
232
|
+
held = true;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
held = false;
|
|
236
|
+
local = next;
|
|
237
|
+
onCommit(next);
|
|
238
|
+
}}
|
|
224
239
|
>
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
{#
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
240
|
+
<!-- `data-ghosted` states the rung the way the enum trigger does. The date
|
|
241
|
+
primitive emits no per-segment placeholder marker (only bits' `select`
|
|
242
|
+
does), so the tone rides the same attribute, set per segment from the
|
|
243
|
+
substitution itself; which is also what keeps a half-typed date's own
|
|
244
|
+
digits at full ink while the segments around them ghost. -->
|
|
245
|
+
<BitsDateField.Input
|
|
246
|
+
class="qm-date qm-control-box qm-focus-ring-within"
|
|
247
|
+
aria-label={labelledBy ? undefined : label}
|
|
248
|
+
aria-labelledby={labelledBy}
|
|
249
|
+
aria-describedby={describedBy}
|
|
250
|
+
data-ghosted={ghost ? '' : undefined}
|
|
251
|
+
data-default={seated || follows ? '' : undefined}
|
|
252
|
+
>
|
|
253
|
+
{#snippet children({ segments })}
|
|
254
|
+
<!-- Keyed by index: `part` repeats (the `literal` separators between
|
|
255
|
+
segments all carry it) so a part-keyed block collides. -->
|
|
256
|
+
{#each segments as seg, i (i)}
|
|
257
|
+
{@const shown = segmentText(seg.part, seg.value)}
|
|
258
|
+
<BitsDateField.Segment
|
|
259
|
+
class="qm-date-segment"
|
|
260
|
+
part={seg.part}
|
|
261
|
+
data-ghosted={shown.ghosted}
|
|
262
|
+
>
|
|
263
|
+
{shown.text}
|
|
264
|
+
</BitsDateField.Segment>
|
|
265
|
+
{/each}
|
|
266
|
+
{#if !datetime}
|
|
267
|
+
<button
|
|
268
|
+
type="button"
|
|
269
|
+
class="qm-chip qm-tap-floor qm-focus-ring qm-date-today"
|
|
270
|
+
aria-pressed={!!follows}
|
|
271
|
+
data-default={follows === 'default' ? '' : undefined}
|
|
272
|
+
onclick={toggle}
|
|
273
|
+
>
|
|
274
|
+
<Icon name={follows ? 'calendar-check' : 'calendar'} />{t.strings.dateToday}
|
|
275
|
+
</button>
|
|
276
|
+
{/if}
|
|
277
|
+
{/snippet}
|
|
278
|
+
</BitsDateField.Input>
|
|
279
|
+
</BitsDateField.Root>
|
|
280
|
+
{/key}
|
|
252
281
|
</span>
|
|
253
282
|
|
|
254
283
|
<style>
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { Select } from 'bits-ui';
|
|
17
17
|
import Icon from './icons/Icon.svelte';
|
|
18
18
|
import { wording } from './strings.js';
|
|
19
|
+
import { spacePx } from '../core/rungs.js';
|
|
19
20
|
import './controls.css';
|
|
20
21
|
|
|
21
22
|
const t = wording();
|
|
@@ -79,6 +80,8 @@
|
|
|
79
80
|
* `none`: it draws at the default rung (theme.css). */
|
|
80
81
|
const ghostsMember = $derived(!optional && !!fallback);
|
|
81
82
|
const ghostText = $derived(optional ? t.strings.optionalGhost : dash(fallback));
|
|
83
|
+
/** The sentinel's tag says what picking it does: return to the default, or unset. */
|
|
84
|
+
const tag = $derived(ghostsMember ? t.strings.enumUnsetTag : t.strings.enumUnsetTagNone);
|
|
82
85
|
/** What the closed trigger shows: the pick, or the ghosted default while unset. */
|
|
83
86
|
const shown = $derived(unset ? ghostText : dash(local));
|
|
84
87
|
|
|
@@ -113,10 +116,10 @@
|
|
|
113
116
|
data-ghosted={unset ? (ghostsMember ? 'default' : '') : undefined}
|
|
114
117
|
>
|
|
115
118
|
<span class="qm-select-shown">{shown}</span>
|
|
116
|
-
<Icon name="chevron-down" class="qm-select-chevron"
|
|
119
|
+
<Icon name="chevron-down" class="qm-select-chevron" />
|
|
117
120
|
</Select.Trigger>
|
|
118
121
|
<Select.Portal to={portalTarget}>
|
|
119
|
-
<Select.Content collisionBoundary={portalTarget ?? []} sideOffset={
|
|
122
|
+
<Select.Content collisionBoundary={portalTarget ?? []} sideOffset={spacePx(wrapEl)}>
|
|
120
123
|
<!-- The list portals out of the trigger's DOM but into the nearest
|
|
121
124
|
`[data-qm-root]`, and carries the marker itself, like FormatPopover.
|
|
122
125
|
That root is the boundary a flip is measured against too, being the box
|
|
@@ -138,11 +141,11 @@
|
|
|
138
141
|
<Select.Item
|
|
139
142
|
class="qm-menu-item qm-select-item qm-select-unset"
|
|
140
143
|
value={UNSET}
|
|
141
|
-
label={optional ? ghostText : `${ghostText} ${
|
|
144
|
+
label={optional ? ghostText : `${ghostText} ${tag}`}
|
|
142
145
|
>
|
|
143
146
|
<span class="qm-select-ghost">{ghostText}</span>
|
|
144
147
|
{#if !optional}
|
|
145
|
-
<span class="qm-select-tag">{
|
|
148
|
+
<span class="qm-select-tag">{tag}</span>
|
|
146
149
|
{/if}
|
|
147
150
|
</Select.Item>
|
|
148
151
|
<!-- A refused option still draws under `'disable'`, and under either
|
|
@@ -202,6 +205,8 @@
|
|
|
202
205
|
}
|
|
203
206
|
.qm-select-wrap :global(.qm-select-chevron) {
|
|
204
207
|
flex-shrink: 0;
|
|
208
|
+
width: var(--_qm-glyph-control);
|
|
209
|
+
height: var(--_qm-glyph-control);
|
|
205
210
|
}
|
|
206
211
|
/* The focus ring rides `.qm-focus-ring` on the trigger (controls.css). */
|
|
207
212
|
/* Shown-never-written: the closed control reads muted while unset, at the rung of
|
|
@@ -180,7 +180,7 @@
|
|
|
180
180
|
e.stopPropagation();
|
|
181
181
|
open = false;
|
|
182
182
|
}
|
|
183
|
-
}}><Icon name="info"
|
|
183
|
+
}}><Icon name="info" /></button
|
|
184
184
|
>
|
|
185
185
|
|
|
186
186
|
<!-- Placement is the description's height. One line rides the label rung beside its own
|
|
@@ -249,8 +249,15 @@
|
|
|
249
249
|
So the box is the glyph and the target is `.qm-tap-floor`'s (controls.css), which
|
|
250
250
|
carries that split and the giving-up of the family floor together.
|
|
251
251
|
|
|
252
|
-
It takes no fill either: the
|
|
253
|
-
|
|
252
|
+
It takes no fill either: the surface itself opens on hover.
|
|
253
|
+
|
|
254
|
+
At rest it recedes to the idle rung, a card describing every field otherwise
|
|
255
|
+
standing a mark beside each label before anything is touched. It lifts with the
|
|
256
|
+
scope that owns its row (the field, the property, the array or matrix, the table
|
|
257
|
+
its column heads, the section a lone field is named by), each reached by child
|
|
258
|
+
steps so a nested field's marker stays at rest under its container's hover, and
|
|
259
|
+
while its surface is open. The keyboard's `Mod-/` and the parked description do not
|
|
260
|
+
depend on the lift. */
|
|
254
261
|
.qm-field-hint {
|
|
255
262
|
/* The marker recedes to the label rung; the surface carries the text. */
|
|
256
263
|
color: var(--_qm-ink-label);
|
|
@@ -258,10 +265,20 @@
|
|
|
258
265
|
cursor: help;
|
|
259
266
|
padding: 0;
|
|
260
267
|
background: none;
|
|
268
|
+
opacity: var(--_qm-opacity-idle);
|
|
269
|
+
transition: opacity var(--_qm-duration-fast) var(--_qm-ease-reverse);
|
|
261
270
|
}
|
|
271
|
+
:global(:is(.qm-field, .qm-object-prop):is(:hover, :focus-within) > .qm-field-label-row)
|
|
272
|
+
> .qm-field-hint,
|
|
273
|
+
:global(:is(.qm-array, .qm-matrix):is(:hover, :focus-within) > * > .qm-field-label-row)
|
|
274
|
+
> .qm-field-hint,
|
|
275
|
+
:global(.qm-array-table:is(:hover, :focus-within) .qm-array-table-col > .qm-field-label-row)
|
|
276
|
+
> .qm-field-hint,
|
|
277
|
+
:global(.qm-group:is(:hover, :focus-within) > .qm-group-head) > .qm-field-hint,
|
|
262
278
|
.qm-field-hint:hover,
|
|
263
|
-
.qm-field-hint:focus-visible
|
|
264
|
-
|
|
279
|
+
.qm-field-hint:focus-visible,
|
|
280
|
+
.qm-field-hint[aria-expanded='true'] {
|
|
281
|
+
opacity: 1;
|
|
265
282
|
}
|
|
266
283
|
/* The lift, the translucency, the blur and the scale-in come from
|
|
267
284
|
`.qm-popover-surface` (controls.css). This surface's own is a measure: guidance
|
|
@@ -116,12 +116,12 @@
|
|
|
116
116
|
.qm-field-label.targeted {
|
|
117
117
|
cursor: pointer;
|
|
118
118
|
}
|
|
119
|
-
/* Required marker:
|
|
120
|
-
the document still edits and renders unmet. */
|
|
119
|
+
/* Required marker: the label's own ink, not an alarm; required-ness is guidance,
|
|
120
|
+
the document still edits and renders unmet, and the danger hue is a diagnostic's. */
|
|
121
121
|
.qm-field-required {
|
|
122
122
|
display: inline-flex;
|
|
123
123
|
align-items: center;
|
|
124
|
-
color: var(--_qm-
|
|
124
|
+
color: var(--_qm-ink-label);
|
|
125
125
|
line-height: 1;
|
|
126
126
|
}
|
|
127
127
|
/* The parked description: in the accessibility tree, out of the layout, and never
|
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
A press holds it down, which is what makes the surface answer a selection rather
|
|
25
25
|
than a pointer: a drag fires `selectionchange` on every frame, so a surface driven
|
|
26
26
|
by that alone rises on the first character and rides the cursor across the text it
|
|
27
|
-
is covering. Keyboard selection holds nothing, so
|
|
27
|
+
is covering. Keyboard selection holds nothing, so it waits instead: a rise with no
|
|
28
|
+
release behind it comes once the selection has stood still for `SETTLE_MS`, so a
|
|
29
|
+
Shift-arrow run, or a select-all on its way to a cut, raises nothing until it stops.
|
|
30
|
+
A lowering never waits, and an open surface follows each step.
|
|
28
31
|
|
|
29
32
|
Nothing here watches scroll. `sync` decides whether the surface is up, which a
|
|
30
33
|
scroll does not change; where it sits is the anchor's own question, and the anchor
|
|
@@ -70,6 +73,7 @@
|
|
|
70
73
|
import { hasMarks, rangeAnchor, type RangeAnchor } from '../core/codec/index.js';
|
|
71
74
|
import type { ActiveProse } from './leaves.js';
|
|
72
75
|
import { clearLink, hrefInSelection, normalizeHref, setLink } from './links.js';
|
|
76
|
+
import { spacePx } from '../core/rungs.js';
|
|
73
77
|
import './controls.css';
|
|
74
78
|
|
|
75
79
|
interface Props {
|
|
@@ -78,7 +82,7 @@
|
|
|
78
82
|
}
|
|
79
83
|
let { getActiveLeaf }: Props = $props();
|
|
80
84
|
|
|
81
|
-
const
|
|
85
|
+
const SETTLE_MS = 450;
|
|
82
86
|
/** The six content formatting marks. `anchor` is a seventh, drawn separately below:
|
|
83
87
|
* a decoration toggle rather than a PM `toggleMark`. One string per mark carries both
|
|
84
88
|
* the tooltip and the accessible name; the anchor button spells the two separately. */
|
|
@@ -120,7 +124,8 @@
|
|
|
120
124
|
return getActiveLeaf()?.views.focusedView();
|
|
121
125
|
}
|
|
122
126
|
|
|
123
|
-
function sync(): void {
|
|
127
|
+
function sync(wait = false): void {
|
|
128
|
+
clearTimeout(settling);
|
|
124
129
|
const insidePopover =
|
|
125
130
|
!!contentEl && !!document.activeElement && contentEl.contains(document.activeElement);
|
|
126
131
|
// Interacting with the popover itself (e.g. the link input): leave state as-is.
|
|
@@ -151,6 +156,10 @@
|
|
|
151
156
|
linkPromptOpen = false;
|
|
152
157
|
return;
|
|
153
158
|
}
|
|
159
|
+
if (wait && !open) {
|
|
160
|
+
settling = setTimeout(() => sync(), SETTLE_MS);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
154
163
|
portalTarget = view.dom.closest<HTMLElement>('[data-qm-root]') ?? undefined;
|
|
155
164
|
const { from, to } = view.state.selection;
|
|
156
165
|
anchor = rangeAnchor(view, from, to);
|
|
@@ -186,12 +195,17 @@
|
|
|
186
195
|
// selection. `pending` collapses a burst to a single scheduled callback.
|
|
187
196
|
let pending = false;
|
|
188
197
|
let rafHandle = 0;
|
|
198
|
+
/** The check in flight answers a pointer's release, which raises at once. */
|
|
199
|
+
let released = false;
|
|
200
|
+
let settling: ReturnType<typeof setTimeout> | undefined;
|
|
189
201
|
function deferredSync(): void {
|
|
190
202
|
if (pending) return;
|
|
191
203
|
pending = true;
|
|
192
204
|
rafHandle = requestAnimationFrame(() => {
|
|
193
205
|
pending = false;
|
|
194
|
-
|
|
206
|
+
const wait = !released;
|
|
207
|
+
released = false;
|
|
208
|
+
sync(wait);
|
|
195
209
|
});
|
|
196
210
|
}
|
|
197
211
|
|
|
@@ -205,14 +219,16 @@
|
|
|
205
219
|
function onPointerUp(): void {
|
|
206
220
|
if (!pressed) return;
|
|
207
221
|
pressed = false;
|
|
222
|
+
released = true;
|
|
208
223
|
deferredSync();
|
|
209
224
|
}
|
|
210
225
|
|
|
211
|
-
// A burst-coalescing rAF may still be in flight at teardown;
|
|
212
|
-
// `sync()` never runs against the destroyed component.
|
|
226
|
+
// A burst-coalescing rAF or a settling rise may still be in flight at teardown;
|
|
227
|
+
// cancel both so `sync()` never runs against the destroyed component.
|
|
213
228
|
onDestroy(() => {
|
|
214
229
|
if (pending) cancelAnimationFrame(rafHandle);
|
|
215
230
|
pending = false;
|
|
231
|
+
clearTimeout(settling);
|
|
216
232
|
});
|
|
217
233
|
|
|
218
234
|
// Any close path that bypasses `sync` (e.g. Escape while the link input holds
|
|
@@ -342,7 +358,7 @@
|
|
|
342
358
|
side="top"
|
|
343
359
|
align="center"
|
|
344
360
|
trapFocus={false}
|
|
345
|
-
sideOffset={
|
|
361
|
+
sideOffset={spacePx(portalTarget, 2)}
|
|
346
362
|
onOpenAutoFocus={(e: Event) => e.preventDefault()}
|
|
347
363
|
onCloseAutoFocus={(e: Event) => e.preventDefault()}
|
|
348
364
|
>
|
|
@@ -411,7 +427,7 @@
|
|
|
411
427
|
title={m.title}
|
|
412
428
|
aria-label={m.title}
|
|
413
429
|
onmousedown={keepFocus}
|
|
414
|
-
onclick={() => toggle(m.name)}><Icon name={m.icon}
|
|
430
|
+
onclick={() => toggle(m.name)}><Icon name={m.icon} /></button
|
|
415
431
|
>
|
|
416
432
|
{/each}
|
|
417
433
|
{#if anchorAvailable}
|
|
@@ -421,7 +437,7 @@
|
|
|
421
437
|
title={t.strings.formatAnchorTitle}
|
|
422
438
|
aria-label={t.strings.formatAnchor}
|
|
423
439
|
onmousedown={keepFocus}
|
|
424
|
-
onclick={toggleAnchor}><Icon name="hash"
|
|
440
|
+
onclick={toggleAnchor}><Icon name="hash" /></button
|
|
425
441
|
>
|
|
426
442
|
{/if}
|
|
427
443
|
</div>
|
|
@@ -525,16 +525,19 @@
|
|
|
525
525
|
background: var(--_qm-danger-tint);
|
|
526
526
|
color: var(--_qm-danger);
|
|
527
527
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
}
|
|
532
|
-
/* The default's set is what prints and nothing written: its titles and every value
|
|
533
|
-
its columns draw take the default rung (theme.css), and the first gesture, writing
|
|
534
|
-
it, takes it off. */
|
|
528
|
+
/* The default's set is what prints and nothing written: its titles, its ticks and
|
|
529
|
+
every value its columns draw take the default rung (theme.css), and the first
|
|
530
|
+
gesture, writing it, takes it off. */
|
|
535
531
|
.qm-matrix-roster[data-default] :global(:is(.qm-input, .ProseMirror, .qm-date)),
|
|
536
532
|
.qm-matrix-roster[data-default] :global(.qm-select:not([data-ghosted])),
|
|
537
533
|
.qm-matrix-roster[data-default] .qm-member.held .qm-member-title {
|
|
538
534
|
color: var(--_qm-ink-default);
|
|
539
535
|
}
|
|
536
|
+
.qm-matrix-roster[data-default] :is(.qm-tick-input:checked, .qm-tick-fixed) {
|
|
537
|
+
background: var(--_qm-accent-default);
|
|
538
|
+
border-color: var(--_qm-accent-default);
|
|
539
|
+
}
|
|
540
|
+
.qm-matrix-roster[data-default] :global(.qm-toggle[data-state='checked']) {
|
|
541
|
+
background: var(--_qm-accent-default);
|
|
542
|
+
}
|
|
540
543
|
</style>
|
|
@@ -457,10 +457,8 @@
|
|
|
457
457
|
/* The vertical and the rung between it and the properties, `--_qm-nest` being the step
|
|
458
458
|
the card and the section hold too.
|
|
459
459
|
|
|
460
|
-
`--_qm-border
|
|
461
|
-
|
|
462
|
-
vertical read. `faint` separates without structuring — a table's interior lines
|
|
463
|
-
under a frame that is doing the structuring — which is the other job.
|
|
460
|
+
`--_qm-border`: this is the stroke that structures (ARCHITECTURE §"A plane is a
|
|
461
|
+
tone"), the one the card's edge and the section's vertical read.
|
|
464
462
|
|
|
465
463
|
`padding-block` is the stroke's end caps, the same rung and the same reason as a
|
|
466
464
|
section's (`Card.svelte`). A cap and not a leading gap: what stands the subform off
|
|
@@ -31,9 +31,6 @@
|
|
|
31
31
|
}
|
|
32
32
|
let { tips, onDismiss, onError }: Props = $props();
|
|
33
33
|
|
|
34
|
-
/** Control-glyph size: the shared rule, as CardControls. */
|
|
35
|
-
const GLYPH = 14;
|
|
36
|
-
|
|
37
34
|
let cursor = $state(0);
|
|
38
35
|
// Clamped: the channel is consumer-authored and can be re-seeded shorter while
|
|
39
36
|
// the card is open, which would otherwise index past the end.
|
|
@@ -82,9 +79,9 @@
|
|
|
82
79
|
{/if}
|
|
83
80
|
<button type="button" class="qm-icon-btn qm-tips-action" onclick={advance}>
|
|
84
81
|
{#if isLast}
|
|
85
|
-
{t.strings.tipsDismiss} <Icon name="trash-2"
|
|
82
|
+
{t.strings.tipsDismiss} <Icon name="trash-2" />
|
|
86
83
|
{:else}
|
|
87
|
-
{t.strings.tipsNext} <Icon name="arrow-right"
|
|
84
|
+
{t.strings.tipsNext} <Icon name="arrow-right" />
|
|
88
85
|
{/if}
|
|
89
86
|
</button>
|
|
90
87
|
</div>
|