@quillmark/svelte 0.20.0 → 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 +8 -8
package/dist/core/theme.css
CHANGED
|
@@ -117,9 +117,9 @@
|
|
|
117
117
|
|
|
118
118
|
Two planes sink. `sunken` is the ground — the column behind the cards, the
|
|
119
119
|
desk behind the sheets — and lifting the plane above it takes very little
|
|
120
|
-
at the light pole. `well` is the hole cut into a card: a field,
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
at the light pole. `well` is the hole cut into a card: a field, the one
|
|
121
|
+
native select, neither of which draws an edge, so the step is the whole of
|
|
122
|
+
what says a value is typed there. The order at the
|
|
123
123
|
light pole is card, ground, well; at the dark pole the two sink to within
|
|
124
124
|
a channel of each other, which is where a card with little light left to
|
|
125
125
|
give up leaves them.
|
|
@@ -148,6 +148,10 @@
|
|
|
148
148
|
dark. */
|
|
149
149
|
--_qm-surface-raised: color-mix(in oklab, var(--_qm-bg), #fff 8%);
|
|
150
150
|
--_qm-surface-hover: color-mix(in oklab, var(--_qm-bg), var(--_qm-fg) 8%);
|
|
151
|
+
/* A band laid on the card rather than cut into it: a table's header, and the pill a
|
|
152
|
+
grip draws. It steps toward the ink at both poles, a shade under white in light
|
|
153
|
+
and a shade over the card in dark, where `well` sinks to a black stripe. */
|
|
154
|
+
--_qm-surface-band: color-mix(in oklab, var(--_qm-bg), var(--_qm-fg) 6%);
|
|
151
155
|
/* The one rung whose arms are not a single ratio: a stroke is read against what it
|
|
152
156
|
separates, and the mix that reads as a hairline on the dark default draws the
|
|
153
157
|
light card in a line heavier than the type it frames. The light arm is
|
|
@@ -157,12 +161,11 @@
|
|
|
157
161
|
color-mix(in oklab, var(--_qm-bg), var(--_qm-fg) 6%),
|
|
158
162
|
color-mix(in oklab, var(--_qm-bg), var(--_qm-fg) 17%)
|
|
159
163
|
);
|
|
160
|
-
/* A
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
--_qm-border-faint: color-mix(in oklab, var(--_qm-bg), var(--_qm-border) 50%);
|
|
164
|
+
/* A stroke ruled through content rather than round it: a table's frame and every
|
|
165
|
+
line between its cells. The light arm of `border` is a card's edge against the
|
|
166
|
+
ground, and drawn between lines of type it vanishes, so a grid takes one ratio at
|
|
167
|
+
both poles, which lands within a few points of `border` in dark. */
|
|
168
|
+
--_qm-rule: color-mix(in oklab, var(--_qm-bg), var(--_qm-fg) 14%);
|
|
166
169
|
/* Three rungs, and nothing under the last: a step below `label` lands under 3:1 on
|
|
167
170
|
the well a field draws it against, and what would take it — an unset value's word,
|
|
168
171
|
an invitation — is content a reader has to read (WCAG 1.4.3), not decoration.
|
|
@@ -186,6 +189,11 @@
|
|
|
186
189
|
stacks over whichever surface rung the cell already draws instead of replacing
|
|
187
190
|
it. */
|
|
188
191
|
--_qm-accent-tint: color-mix(in oklab, var(--_qm-accent) 12%, transparent);
|
|
192
|
+
/* The accent at the default rung: a mark's fill where the `default:` supplies it, a
|
|
193
|
+
switch's checked track and a held tick's box, stepped toward the card by the
|
|
194
|
+
ratio `default` steps the ink. What is drawn over it in the card's colour, the
|
|
195
|
+
thumb and the check, holds 3:1 at both poles, at 3.3:1 and 4.2:1. */
|
|
196
|
+
--_qm-accent-default: color-mix(in oklab, var(--_qm-accent), var(--_qm-bg) 20%);
|
|
189
197
|
/* The danger hue held: what a destructive control fills with where the family's
|
|
190
198
|
neutral hover rung would read as one more button. Mixed at the accent tint's
|
|
191
199
|
strength, so a held tint is a held tint whichever hue it carries. */
|
|
@@ -281,6 +289,10 @@
|
|
|
281
289
|
arrival settles into its rest instead of stopping at it, and long enough that a
|
|
282
290
|
control resting at nothing reads as arriving rather than as switching. */
|
|
283
291
|
--_qm-reveal: 500ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
292
|
+
/* A wait rather than a trip: how long a caret rests on an empty line before the
|
|
293
|
+
slash hint comes up. Longer than the gap between two keystrokes, so typing on
|
|
294
|
+
through an Enter never draws it. */
|
|
295
|
+
--_qm-hint-wait: 500ms;
|
|
284
296
|
|
|
285
297
|
/* ── Geometry ──────────────────────────────────────────────────────────*/
|
|
286
298
|
/* One stroke width for every edge the chrome draws and every rule it ranks
|
|
@@ -342,9 +354,9 @@
|
|
|
342
354
|
it does not move when `--qm-space` does. Every button family reads it, so the
|
|
343
355
|
floor is one number instead of one expression per control. */
|
|
344
356
|
--_qm-tap-min: 24px;
|
|
345
|
-
/* The chrome glyph: a chip's,
|
|
346
|
-
through the icon's `size`, so a stylesheet beats the SVG presentation
|
|
347
|
-
and one rung is the whole of what a control draws. */
|
|
357
|
+
/* The chrome glyph: a chip's, a glyph button's, the enum's chevron. Sized in CSS
|
|
358
|
+
rather than through the icon's `size`, so a stylesheet beats the SVG presentation
|
|
359
|
+
attribute and one rung is the whole of what a control draws. */
|
|
348
360
|
--_qm-glyph-control: 14px;
|
|
349
361
|
/* The narrowest track a control that fills its width stays usable in: a table's
|
|
350
362
|
column floors at it, rises no further than its content, and a row of them scrolls
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
container or a block prose cell.
|
|
30
30
|
|
|
31
31
|
The row's controls are inside the element: a slab over the end of the element's own
|
|
32
|
-
box, taking its two end-side corners
|
|
33
|
-
|
|
32
|
+
box, taking its two end-side corners: the remove. An `object` row's drag handle hangs
|
|
33
|
+
off its start edge instead, as a table grip does. So a row's box is the element's box, and an array's rows end where
|
|
34
34
|
every other field's control does. On an object element that box is the summary rather
|
|
35
35
|
than the open row: a destructive control belongs to the line it sits on, not to
|
|
36
36
|
everything that line has unfolded. A table's row carries the remove alone, in its last
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
Keys carry the list without the mouse: Enter inserts a sibling below and takes the
|
|
42
42
|
caret there, Backspace on an empty element removes it and hands focus back up the
|
|
43
43
|
list, Alt+↑/↓ anywhere in an `object` row of the list moves it, the nearest row
|
|
44
|
-
answering and never the record around a nested one
|
|
44
|
+
answering and never the record around a nested one, and is the twin of the handle's
|
|
45
|
+
drag. A move is one splice of the ids
|
|
45
46
|
and of the values together, the mechanism insert and remove use, so an element keeps
|
|
46
47
|
its id for life and no prose leaf inside it remounts; the open row stays open across
|
|
47
48
|
its own move, and `animate:reorder` holds the moving row as it holds a card.
|
|
@@ -328,15 +329,13 @@
|
|
|
328
329
|
void focusAfterFlush(next[Math.max(k - 1, 0)], column, fromRemove);
|
|
329
330
|
}
|
|
330
331
|
/**
|
|
331
|
-
* Move element `k`
|
|
332
|
-
* the element keeps its id and everything mounted under it. A no-op
|
|
333
|
-
* The control that
|
|
334
|
-
* flush has moved it: a node moved in the DOM is a node the browser blurred.
|
|
335
|
-
* the move carried to its edge is disabled and takes no focus, so its twin does.
|
|
332
|
+
* Move element `k` to slot `to`: one splice of the ids and of the values together, so
|
|
333
|
+
* the element keeps its id and everything mounted under it. A no-op outside the list.
|
|
334
|
+
* The control that held the focus rides with the row, so it is focused again once the
|
|
335
|
+
* flush has moved it: a node moved in the DOM is a node the browser blurred.
|
|
336
336
|
*/
|
|
337
|
-
function move(k: number,
|
|
338
|
-
|
|
339
|
-
if (to < 0 || to >= ids.length) return;
|
|
337
|
+
function move(k: number, to: number): void {
|
|
338
|
+
if (to === k || to < 0 || to >= ids.length) return;
|
|
340
339
|
const pressed = document.activeElement;
|
|
341
340
|
arm.arm();
|
|
342
341
|
const nextIds = ids.slice();
|
|
@@ -349,14 +348,8 @@
|
|
|
349
348
|
onCommit(next);
|
|
350
349
|
void (async () => {
|
|
351
350
|
if (!(await span.resumes(tick()))) return;
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
pressed instanceof HTMLButtonElement && pressed.disabled
|
|
355
|
-
? [pressed.previousElementSibling, pressed.nextElementSibling].find(
|
|
356
|
-
(b) => b instanceof HTMLButtonElement && !b.disabled
|
|
357
|
-
)
|
|
358
|
-
: pressed;
|
|
359
|
-
if (target instanceof HTMLElement) target.focus();
|
|
351
|
+
if (pressed instanceof HTMLElement && pressed.isConnected && rowEls[id]?.contains(pressed))
|
|
352
|
+
pressed.focus();
|
|
360
353
|
})();
|
|
361
354
|
}
|
|
362
355
|
/** Take the caret: the first element, or the add affordance when the list is empty;
|
|
@@ -538,7 +531,64 @@
|
|
|
538
531
|
const dir = e.key === 'ArrowUp' ? -1 : 1;
|
|
539
532
|
if (k + dir < 0 || k + dir >= ids.length) return;
|
|
540
533
|
e.preventDefault();
|
|
541
|
-
move(k, dir);
|
|
534
|
+
move(k, k + dir);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ── The drag handle ──────────────────────────────────────────────────────────
|
|
538
|
+
// Press-and-drag the handle moves its row; a press that does not travel past the dead
|
|
539
|
+
// zone does nothing. The rows are measured once the drag engages, nothing reflowing
|
|
540
|
+
// until the release (the lift is a tone and the drop rule is out of flow), and the
|
|
541
|
+
// release is the one splice `move` makes.
|
|
542
|
+
const DEAD_ZONE = 3;
|
|
543
|
+
type RowDrag = {
|
|
544
|
+
k: number;
|
|
545
|
+
origin: number;
|
|
546
|
+
engaged: boolean;
|
|
547
|
+
drop: number;
|
|
548
|
+
heads: { start: number; end: number }[];
|
|
549
|
+
};
|
|
550
|
+
let drag = $state<RowDrag | undefined>();
|
|
551
|
+
function onHandleDown(e: PointerEvent, k: number): void {
|
|
552
|
+
if (e.button !== 0) return;
|
|
553
|
+
e.preventDefault();
|
|
554
|
+
(e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);
|
|
555
|
+
drag = { k, origin: e.clientY, engaged: false, drop: k, heads: [] };
|
|
556
|
+
}
|
|
557
|
+
function onHandleMove(e: PointerEvent): void {
|
|
558
|
+
if (!drag) return;
|
|
559
|
+
if (!drag.engaged) {
|
|
560
|
+
if (Math.abs(e.clientY - drag.origin) < DEAD_ZONE) return;
|
|
561
|
+
drag.engaged = true;
|
|
562
|
+
drag.heads = ids.map((id) => {
|
|
563
|
+
const box = rowEls[id]?.getBoundingClientRect();
|
|
564
|
+
return { start: box?.top ?? 0, end: box?.bottom ?? 0 };
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
drag.drop = dropIndex(drag, e.clientY);
|
|
568
|
+
}
|
|
569
|
+
/** The row the pointer is over, or the nearest one past either end. */
|
|
570
|
+
function dropIndex(d: RowDrag, y: number): number {
|
|
571
|
+
let best = d.k;
|
|
572
|
+
let nearest = Infinity;
|
|
573
|
+
d.heads.forEach((head, i) => {
|
|
574
|
+
const gap = y < head.start ? head.start - y : y > head.end ? y - head.end : 0;
|
|
575
|
+
if (gap < nearest) {
|
|
576
|
+
nearest = gap;
|
|
577
|
+
best = i;
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
return best;
|
|
581
|
+
}
|
|
582
|
+
function onHandleUp(e: PointerEvent): void {
|
|
583
|
+
const done = drag;
|
|
584
|
+
drag = undefined;
|
|
585
|
+
if (!done?.engaged || e.type !== 'pointerup') return;
|
|
586
|
+
move(done.k, done.drop);
|
|
587
|
+
}
|
|
588
|
+
/** Where the drop rule draws on row `k`, while a drag would land the row there. */
|
|
589
|
+
function dropSide(k: number): 'before' | 'after' | undefined {
|
|
590
|
+
if (!drag?.engaged || drag.drop !== k || drag.drop === drag.k) return undefined;
|
|
591
|
+
return drag.drop > drag.k ? 'after' : 'before';
|
|
542
592
|
}
|
|
543
593
|
</script>
|
|
544
594
|
|
|
@@ -639,7 +689,11 @@
|
|
|
639
689
|
<!-- The handler catches a key from the controls inside the row and adds no
|
|
640
690
|
interaction of the row's own: the row is no tab stop. -->
|
|
641
691
|
<div
|
|
642
|
-
class={[
|
|
692
|
+
class={[
|
|
693
|
+
'qm-array-row qm-element',
|
|
694
|
+
{ open: open.has(id), lifted: drag?.engaged && drag.k === k }
|
|
695
|
+
]}
|
|
696
|
+
data-drop={dropSide(k)}
|
|
643
697
|
bind:this={rowEls[id]}
|
|
644
698
|
animate:reorder={arm.armed}
|
|
645
699
|
onkeydown={(e) => onRowKey(e, k)}
|
|
@@ -649,6 +703,7 @@
|
|
|
649
703
|
end-side corners) needs corners to take. So a list of records measures
|
|
650
704
|
like a list of inputs whatever the element type. -->
|
|
651
705
|
<div class="qm-element-head">
|
|
706
|
+
{@render handle(k)}
|
|
652
707
|
<button
|
|
653
708
|
type="button"
|
|
654
709
|
class="qm-control-box qm-focus-ring qm-element-summary"
|
|
@@ -666,7 +721,7 @@
|
|
|
666
721
|
{/if}
|
|
667
722
|
</button>
|
|
668
723
|
<div class="qm-row-actions">
|
|
669
|
-
{@render
|
|
724
|
+
{@render removeButton(k)}
|
|
670
725
|
</div>
|
|
671
726
|
</div>
|
|
672
727
|
{#if open.has(id)}
|
|
@@ -738,26 +793,22 @@
|
|
|
738
793
|
>
|
|
739
794
|
{/snippet}
|
|
740
795
|
|
|
741
|
-
<!-- An `object` row's
|
|
742
|
-
|
|
743
|
-
|
|
796
|
+
<!-- An `object` row's drag handle, a table grip's twin hung off the row's start edge.
|
|
797
|
+
It holds no focus, as a grip does not: its keyboard twin is Alt+↑/↓ from anywhere in
|
|
798
|
+
the row. -->
|
|
799
|
+
{#snippet handle(k: number)}
|
|
744
800
|
<button
|
|
745
801
|
type="button"
|
|
746
|
-
class="qm-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
802
|
+
class="qm-row-handle"
|
|
803
|
+
tabindex="-1"
|
|
804
|
+
title={t.strings.arrayDrag}
|
|
805
|
+
aria-label={rowAction(t.strings.arrayDrag, k)}
|
|
806
|
+
onpointerdown={(e) => onHandleDown(e, k)}
|
|
807
|
+
onpointermove={onHandleMove}
|
|
808
|
+
onpointerup={onHandleUp}
|
|
809
|
+
onpointercancel={onHandleUp}
|
|
810
|
+
><span class="qm-row-handle-bar"><Icon name="grip-vertical" /></span></button
|
|
751
811
|
>
|
|
752
|
-
<button
|
|
753
|
-
type="button"
|
|
754
|
-
class="qm-icon-btn qm-row-btn qm-focus-ring"
|
|
755
|
-
title={t.strings.arrayMoveDown}
|
|
756
|
-
aria-label={rowAction(t.strings.arrayMoveDown, k)}
|
|
757
|
-
disabled={k === ids.length - 1}
|
|
758
|
-
onclick={() => move(k, 1)}><Icon name="chevron-down" /></button
|
|
759
|
-
>
|
|
760
|
-
{@render removeButton(k)}
|
|
761
812
|
{/snippet}
|
|
762
813
|
|
|
763
814
|
<style>
|
|
@@ -807,12 +858,16 @@
|
|
|
807
858
|
display: none;
|
|
808
859
|
}
|
|
809
860
|
/* The default's rows are what prints and nothing written: every value they draw takes
|
|
810
|
-
the default rung (theme.css),
|
|
861
|
+
the default rung (theme.css), a checked switch's track included, and the first
|
|
862
|
+
gesture, writing them, takes it off. */
|
|
811
863
|
.qm-array-rows[data-default] :global(:is(.qm-input, .ProseMirror, .qm-date)),
|
|
812
864
|
.qm-array-rows[data-default] :global(.qm-select:not([data-ghosted])),
|
|
813
865
|
.qm-array-rows[data-default] .qm-element-title:not(.untitled) {
|
|
814
866
|
color: var(--_qm-ink-default);
|
|
815
867
|
}
|
|
868
|
+
.qm-array-rows[data-default] :global(.qm-toggle[data-state='checked']) {
|
|
869
|
+
background: var(--_qm-accent-default);
|
|
870
|
+
}
|
|
816
871
|
/* A grid rather than a block: an `<input>` is inline-level and would sit on a
|
|
817
872
|
baseline, standing the row a descender taller than the box the slab measures itself
|
|
818
873
|
against. `minmax(0, …)` because a long unbroken value grows an `auto` track, and the
|
|
@@ -830,7 +885,7 @@
|
|
|
830
885
|
/* The end inset the slabs stand in, taken off whichever box the element drew:
|
|
831
886
|
`.qm-input` is the text element, `.qm-control-box` the prose one — the row's own
|
|
832
887
|
child, so a table's cells, which are boxes too, keep their inset — and an object
|
|
833
|
-
row's summary, which stands
|
|
888
|
+
row's summary, which stands one slab off its end. `:global`, because the box
|
|
834
889
|
belongs to the child component's markup and the scope class stops at this
|
|
835
890
|
component's. The longhand beats the family's `padding` shorthand without a
|
|
836
891
|
specificity fight: `controls.css` ranks the family at zero inside `:where()`. */
|
|
@@ -839,7 +894,7 @@
|
|
|
839
894
|
padding-inline-end: var(--_qm-tap-min);
|
|
840
895
|
}
|
|
841
896
|
.qm-element-summary {
|
|
842
|
-
padding-inline-end:
|
|
897
|
+
padding-inline-end: var(--_qm-tap-min);
|
|
843
898
|
}
|
|
844
899
|
/* The box's end wall, floor to ceiling: a height of its own would leave a sliver of
|
|
845
900
|
well above or below, and the corners it takes are the box's — the end-side pair keeps
|
|
@@ -847,8 +902,7 @@
|
|
|
847
902
|
another gives up its corners on both sides.
|
|
848
903
|
|
|
849
904
|
It comes up on its own row rather than on the field: a destructive control is offered
|
|
850
|
-
by the row the pointer is on, not by every row at once
|
|
851
|
-
it comes up with it. Hover is where the remove says destructive, ink with fill, a
|
|
905
|
+
by the row the pointer is on, not by every row at once. Hover is where the remove says destructive, ink with fill, a
|
|
852
906
|
tint alone being a wash under a label-toned glyph. */
|
|
853
907
|
.qm-row-actions {
|
|
854
908
|
position: absolute;
|
|
@@ -872,10 +926,6 @@
|
|
|
872
926
|
border-start-end-radius: var(--_qm-radius-inner);
|
|
873
927
|
border-end-end-radius: var(--_qm-radius-inner);
|
|
874
928
|
}
|
|
875
|
-
.qm-row-btn :global(svg) {
|
|
876
|
-
width: var(--_qm-glyph-control);
|
|
877
|
-
height: var(--_qm-glyph-control);
|
|
878
|
-
}
|
|
879
929
|
/* Child combinators: a nested array shares this scope, and a row's hover is not its
|
|
880
930
|
open subform's rows'. */
|
|
881
931
|
.qm-array-row:hover > .qm-row-actions,
|
|
@@ -888,6 +938,71 @@
|
|
|
888
938
|
background: var(--_qm-danger-tint);
|
|
889
939
|
color: var(--_qm-danger);
|
|
890
940
|
}
|
|
941
|
+
/* The handle is a table grip's twin (prose.css §"One grip per line"): a pill carrying
|
|
942
|
+
the grip's marks, hung off the row's start edge in the `--_qm-nest` every row stands
|
|
943
|
+
in from the stroke outside it, which a group's panel clips at. It rests
|
|
944
|
+
at nothing, where the remove rests faint, and comes up with the row the pointer is
|
|
945
|
+
on, staying up through the drag it carries; a band flush with the row keeps the
|
|
946
|
+
pointer on the row all the way to it. The pointer owns the gesture, not the page's
|
|
947
|
+
scroll. */
|
|
948
|
+
.qm-row-handle {
|
|
949
|
+
position: absolute;
|
|
950
|
+
inset-block: 0;
|
|
951
|
+
inset-inline-end: 100%;
|
|
952
|
+
width: var(--_qm-nest);
|
|
953
|
+
display: flex;
|
|
954
|
+
align-items: center;
|
|
955
|
+
justify-content: center;
|
|
956
|
+
padding: 0;
|
|
957
|
+
border: none;
|
|
958
|
+
background: none;
|
|
959
|
+
color: var(--_qm-ink-label);
|
|
960
|
+
cursor: grab;
|
|
961
|
+
touch-action: none;
|
|
962
|
+
opacity: 0;
|
|
963
|
+
transition: opacity var(--_qm-duration-fast) var(--_qm-ease-reverse);
|
|
964
|
+
}
|
|
965
|
+
.qm-row-handle-bar {
|
|
966
|
+
display: flex;
|
|
967
|
+
align-items: center;
|
|
968
|
+
justify-content: center;
|
|
969
|
+
width: 100%;
|
|
970
|
+
height: var(--_qm-tap-min);
|
|
971
|
+
border-radius: var(--_qm-radius-pill);
|
|
972
|
+
background: var(--_qm-surface-band);
|
|
973
|
+
}
|
|
974
|
+
.qm-row-handle-bar :global(svg) {
|
|
975
|
+
width: var(--_qm-space-3);
|
|
976
|
+
height: var(--_qm-space-3);
|
|
977
|
+
}
|
|
978
|
+
.qm-array-row:hover > .qm-element-head > .qm-row-handle,
|
|
979
|
+
.qm-element.lifted > .qm-element-head > .qm-row-handle {
|
|
980
|
+
opacity: 1;
|
|
981
|
+
}
|
|
982
|
+
.qm-element.lifted > .qm-element-head > .qm-row-handle {
|
|
983
|
+
cursor: grabbing;
|
|
984
|
+
}
|
|
985
|
+
/* The lifted row is a tone, and the drop is one rule on the edge it would land
|
|
986
|
+
against, drawn out of flow so nothing reflows under the drag: the table island's
|
|
987
|
+
own pair. */
|
|
988
|
+
.qm-element.lifted > .qm-element-head > .qm-element-summary {
|
|
989
|
+
opacity: var(--_qm-opacity-muted);
|
|
990
|
+
}
|
|
991
|
+
.qm-element[data-drop]::after {
|
|
992
|
+
content: '';
|
|
993
|
+
position: absolute;
|
|
994
|
+
inset-inline: 0;
|
|
995
|
+
height: var(--_qm-ring-width);
|
|
996
|
+
background: var(--_qm-accent);
|
|
997
|
+
border-radius: var(--_qm-radius-pill);
|
|
998
|
+
pointer-events: none;
|
|
999
|
+
}
|
|
1000
|
+
.qm-element[data-drop='before']::after {
|
|
1001
|
+
top: calc(-0.5 * (var(--_qm-space) + var(--_qm-ring-width)));
|
|
1002
|
+
}
|
|
1003
|
+
.qm-element[data-drop='after']::after {
|
|
1004
|
+
bottom: calc(-0.5 * (var(--_qm-space) + var(--_qm-ring-width)));
|
|
1005
|
+
}
|
|
891
1006
|
/* ── An object element ──────────────────────────────────────────────────────
|
|
892
1007
|
The row is a summary and the subform hangs under it. `row-gap` rather than a margin
|
|
893
1008
|
on the subform, because the distance between a control and what it has unfolded
|
|
@@ -1106,10 +1221,15 @@
|
|
|
1106
1221
|
.qm-array.empty .qm-add-el {
|
|
1107
1222
|
opacity: 1;
|
|
1108
1223
|
}
|
|
1224
|
+
/* No hover to come up on: the controls stand at the muted rung, the handle with
|
|
1225
|
+
them. */
|
|
1109
1226
|
@media (hover: none) {
|
|
1110
1227
|
.qm-add-el,
|
|
1111
1228
|
.qm-row-actions {
|
|
1112
1229
|
opacity: var(--_qm-opacity-muted);
|
|
1113
1230
|
}
|
|
1231
|
+
.qm-row-handle {
|
|
1232
|
+
opacity: 1;
|
|
1233
|
+
}
|
|
1114
1234
|
}
|
|
1115
1235
|
</style>
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
`mixed` there reads as false), so an optional boolean's control is a `checkbox`, the
|
|
11
11
|
toggle role that does, announcing `mixed` while unset. A press from unset writes
|
|
12
12
|
`true`; nothing here returns the cell to unset.
|
|
13
|
+
|
|
14
|
+
An unset switch draws its `default:` at the default rung, as every control draws a
|
|
15
|
+
printed default: a checked track takes the accent's default rung.
|
|
13
16
|
-->
|
|
14
17
|
<script lang="ts">
|
|
15
18
|
import { Switch } from 'bits-ui';
|
|
@@ -41,6 +44,7 @@
|
|
|
41
44
|
// the unset optional cell, the one state with no default to stand in for it.
|
|
42
45
|
let local = $derived<boolean | undefined>(value ?? fallback ?? (optional ? undefined : false));
|
|
43
46
|
const unset = $derived(local === undefined);
|
|
47
|
+
const defaulted = $derived(value === undefined && fallback !== undefined);
|
|
44
48
|
const tristate = $derived(
|
|
45
49
|
optional ? { role: 'checkbox', 'aria-checked': unset ? ('mixed' as const) : !!local } : {}
|
|
46
50
|
);
|
|
@@ -49,7 +53,11 @@
|
|
|
49
53
|
<!-- Not `.qm-switch`: that name is the preset's pane band, whose narrow-viewport rule
|
|
50
54
|
stands every child of it at `--qmh-tap`, so a track wearing it grows a thumb three times
|
|
51
55
|
its own height over the labels around it (`preset/recipes.css`, held by `check:style`). -->
|
|
52
|
-
<span
|
|
56
|
+
<span
|
|
57
|
+
class="qm-toggle-wrap"
|
|
58
|
+
data-unset={unset ? '' : undefined}
|
|
59
|
+
data-default={defaulted ? '' : undefined}
|
|
60
|
+
>
|
|
53
61
|
<Switch.Root
|
|
54
62
|
class="qm-toggle qm-focus-ring qm-tap-floor"
|
|
55
63
|
checked={!!local}
|
|
@@ -101,6 +109,9 @@
|
|
|
101
109
|
.qm-toggle-wrap :global(.qm-toggle[data-state='checked']) {
|
|
102
110
|
background: var(--_qm-accent);
|
|
103
111
|
}
|
|
112
|
+
.qm-toggle-wrap[data-default] :global(.qm-toggle[data-state='checked']) {
|
|
113
|
+
background: var(--_qm-accent-default);
|
|
114
|
+
}
|
|
104
115
|
/* The focus ring rides `.qm-focus-ring` on the switch (controls.css). */
|
|
105
116
|
/* The thumb reads on tone alone: the base surface over the track's `hover` rung
|
|
106
117
|
unchecked, and over `--_qm-accent` checked. No edge of its own: a hairline around
|
package/dist/visual/Card.svelte
CHANGED
|
@@ -72,13 +72,23 @@
|
|
|
72
72
|
leaves
|
|
73
73
|
}: Props = $props();
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
// The rename in flight, written once on blur or Enter. Held apart from the stored
|
|
76
|
+
// title because a re-derive mid-edit hands `card` back with the title it stored.
|
|
77
|
+
let draft = $state<string | undefined>(undefined);
|
|
78
|
+
const shownTitle = $derived(draft ?? card.titleOverride);
|
|
76
79
|
|
|
77
|
-
//
|
|
78
|
-
|
|
80
|
+
// A focus the shell hands over (`focusHeader`) is not armed for replacement: the key
|
|
81
|
+
// that pressed a delete, pressed again, would retitle the card it landed on.
|
|
82
|
+
let handed = false;
|
|
79
83
|
function onTitleFocus(e: FocusEvent): void {
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
draft = card.titleOverride;
|
|
85
|
+
const input = e.currentTarget as HTMLInputElement;
|
|
86
|
+
if (handed) input.setSelectionRange(input.value.length, input.value.length);
|
|
87
|
+
else input.select();
|
|
88
|
+
}
|
|
89
|
+
function onTitleBlur(): void {
|
|
90
|
+
if (draft !== undefined && draft !== card.titleOverride) ops.rename(draft);
|
|
91
|
+
draft = undefined;
|
|
82
92
|
}
|
|
83
93
|
// A press on an unfocused title would place a caret on mouseup, collapsing the
|
|
84
94
|
// focus-time select-all. A press while already focused stays normal, so clicking
|
|
@@ -102,13 +112,10 @@
|
|
|
102
112
|
const el = e.currentTarget as HTMLInputElement;
|
|
103
113
|
if (e.key === 'Enter') {
|
|
104
114
|
e.preventDefault();
|
|
105
|
-
el.blur();
|
|
115
|
+
el.blur();
|
|
106
116
|
} else if (e.key === 'Escape') {
|
|
107
117
|
e.preventDefault();
|
|
108
|
-
|
|
109
|
-
localTitle = titleAtFocus;
|
|
110
|
-
ops.rename(titleAtFocus); // roll back the live edits
|
|
111
|
-
}
|
|
118
|
+
draft = undefined;
|
|
112
119
|
el.blur();
|
|
113
120
|
}
|
|
114
121
|
}
|
|
@@ -193,6 +200,13 @@
|
|
|
193
200
|
const reduce = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
194
201
|
el?.scrollIntoView({ behavior: reduce ? 'auto' : 'smooth', block });
|
|
195
202
|
}
|
|
203
|
+
/** Focus the title with the caret at its end, or the header's first live control on a
|
|
204
|
+
* recovery shell, which has no title to focus. */
|
|
205
|
+
export function focusHeader(): void {
|
|
206
|
+
handed = true;
|
|
207
|
+
el?.querySelector<HTMLElement>('.qm-card-header :is(input, button:not(:disabled))')?.focus();
|
|
208
|
+
handed = false;
|
|
209
|
+
}
|
|
196
210
|
</script>
|
|
197
211
|
|
|
198
212
|
<!-- `data-qm-card` names the island for a floating surface that has to stay inside one
|
|
@@ -210,20 +224,18 @@
|
|
|
210
224
|
<header class="qm-card-header">
|
|
211
225
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
212
226
|
<div class="qm-card-rename" onmousedown={onRenameMousedown}>
|
|
213
|
-
<span class="qm-card-title-sizer" data-value={
|
|
227
|
+
<span class="qm-card-title-sizer" data-value={shownTitle || card.titlePlaceholder}>
|
|
214
228
|
<input
|
|
215
229
|
class="qm-card-title qm-focus-ring"
|
|
216
|
-
value={
|
|
230
|
+
value={shownTitle}
|
|
217
231
|
placeholder={card.titlePlaceholder}
|
|
218
232
|
aria-label={t.strings.cardTitle}
|
|
219
233
|
size="1"
|
|
220
234
|
onmousedown={onTitleMousedown}
|
|
221
235
|
onfocus={onTitleFocus}
|
|
236
|
+
onblur={onTitleBlur}
|
|
222
237
|
onkeydown={onTitleKeydown}
|
|
223
|
-
oninput={(e) =>
|
|
224
|
-
localTitle = (e.currentTarget as HTMLInputElement).value;
|
|
225
|
-
ops.rename(localTitle);
|
|
226
|
-
}}
|
|
238
|
+
oninput={(e) => (draft = (e.currentTarget as HTMLInputElement).value)}
|
|
227
239
|
/>
|
|
228
240
|
</span>
|
|
229
241
|
</div>
|
|
@@ -463,10 +475,10 @@
|
|
|
463
475
|
what crosses the line. */
|
|
464
476
|
margin-inline-end: calc(-1 * (var(--_qm-tap-min) - var(--_qm-glyph-control)) / 2);
|
|
465
477
|
}
|
|
466
|
-
/* Focus as well as hover, so a caret in any leaf, the title, or a
|
|
467
|
-
reveal: hover alone would strand the
|
|
468
|
-
.qm-card:hover :global(.qm-card-
|
|
469
|
-
.qm-card:focus-within :global(.qm-card-
|
|
478
|
+
/* Focus as well as hover, so a caret in any leaf, the title, or a control holds the
|
|
479
|
+
reveal: hover alone would strand the row on the keyboard. */
|
|
480
|
+
.qm-card:hover :global(.qm-card-controls),
|
|
481
|
+
.qm-card:focus-within :global(.qm-card-controls) {
|
|
470
482
|
opacity: 1;
|
|
471
483
|
}
|
|
472
484
|
/* The rename hit region is the header's free width at full height, so a press
|
|
@@ -38,6 +38,7 @@ interface Props {
|
|
|
38
38
|
declare const Card: import("svelte").Component<Props, {
|
|
39
39
|
revealLeaf: (key: string) => void;
|
|
40
40
|
scrollIntoViewCard: (block: ScrollLogicalPosition) => void;
|
|
41
|
+
focusHeader: () => void;
|
|
41
42
|
}, "">;
|
|
42
43
|
type Card = ReturnType<typeof Card>;
|
|
43
44
|
export default Card;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
Card controls, composable cards only.
|
|
3
|
-
Right-aligned in the header: a move-up/move-down chevron pair
|
|
4
|
-
|
|
3
|
+
Right-aligned in the header: a move-up/move-down chevron pair (each disabled at its
|
|
4
|
+
edge), then delete, the row revealed while the pointer or the caret is in the card.
|
|
5
5
|
Reorder is buttons, not drag (V1). All three are chips (controls.css): fill arrives
|
|
6
|
-
on hover, and delete
|
|
7
|
-
that reveal, so its condition lives in the parent's CSS; the
|
|
8
|
-
guarantee here, that a focused
|
|
6
|
+
on hover, and delete draws faded danger, full under the pointer. The card scopes
|
|
7
|
+
that reveal, so its condition lives in the parent's CSS; the row keeps the narrower
|
|
8
|
+
guarantee here, that a focused control is a visible one.
|
|
9
9
|
-->
|
|
10
10
|
<script lang="ts">
|
|
11
11
|
import { wording } from './strings.js';
|
|
@@ -56,21 +56,21 @@
|
|
|
56
56
|
display: flex;
|
|
57
57
|
align-items: center;
|
|
58
58
|
gap: var(--_qm-space-half);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
opacity: 0;
|
|
60
|
+
transition: opacity var(--_qm-duration-fast) var(--_qm-ease-reverse);
|
|
61
|
+
/* One compositor layer for the row at rest as well as mid-fade: a box handed
|
|
62
|
+
back to the document paint when its fade ends snaps off the header's
|
|
63
|
+
half-pixel as it lands. */
|
|
62
64
|
transform: translateZ(0);
|
|
63
65
|
}
|
|
64
66
|
.qm-card-reorder {
|
|
65
67
|
display: flex;
|
|
66
|
-
opacity: 0;
|
|
67
|
-
transition: opacity var(--_qm-duration-fast) var(--_qm-ease-reverse);
|
|
68
68
|
}
|
|
69
|
-
/* Opacity hides the
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
hidden state is. */
|
|
73
|
-
.qm-card-
|
|
69
|
+
/* Opacity hides the row without taking it out of the tab order, which keeps
|
|
70
|
+
delete the keyboard's way to remove a card, so the component holds its own
|
|
71
|
+
floor: a control that has focus is drawn, and its ring with it. The card's
|
|
72
|
+
reveal already covers this; the rule is here because the hidden state is. */
|
|
73
|
+
.qm-card-controls:focus-within {
|
|
74
74
|
opacity: 1;
|
|
75
75
|
}
|
|
76
76
|
/* Squares: the chip's inline pad is for a label. Width and height are one rung
|