@quillmark/svelte 0.26.2 → 0.27.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 +36 -2
- package/dist/preset/app.css +69 -0
- package/dist/preset/index.css +5 -2
- package/dist/preset/recipes.css +10 -152
- package/dist/preset/scale.css +10 -1
- package/dist/preset/shell.css +147 -0
- package/dist/preview/Preview.svelte +8 -0
- package/dist/preview/bridge.js +1 -1
- package/dist/preview/viewport.js +20 -15
- package/dist/preview/zoom.d.ts +17 -13
- package/dist/preview/zoom.js +39 -17
- package/dist/visual/BooleanField.svelte +1 -1
- package/dist/visual/Card.svelte +5 -3
- package/package.json +1 -1
package/THEMING.md
CHANGED
|
@@ -127,7 +127,7 @@ import '@quillmark/svelte/preset';
|
|
|
127
127
|
</body>
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
-
This is the stylesheet our own apps import: the playground and
|
|
130
|
+
This is the stylesheet our own apps import: the playground, studio and quillkit's app client draw with these rules and nothing else, so what you get is what we ship, not a reduction of it.
|
|
131
131
|
|
|
132
132
|
It carries a `--qmh-*` scale for the page, derived from the same nine dials and calibrated against the surfaces' own steps, so a plate of yours and a card of ours agree about which way "raised" goes. Turning a dial moves both.
|
|
133
133
|
|
|
@@ -177,9 +177,11 @@ An app that mounts an editor beside a preview over one session needs a screen to
|
|
|
177
177
|
|
|
178
178
|
The narrow shape is one mount at a time, not two of them stacked: half a narrow viewport is under the width either surface reads at. The track you are not showing is `display: none`, so it leaves the tab order, hit-testing and the accessibility tree together: you need no `inert` on it, and no breakpoint in your JS, since the only state you hold is which of the two you last asked for. Keep both mounted: the hidden pane keeps its pixels, its caret and its scroll, and comes back to them — it rasters nothing at a box it has none of, and repaints only where it returns to a box that draws its pages at another scale. A preview scroll asked for while it was hidden is held and run on the way back, so it opens at the caret rather than several edits behind it. A click that lands in the hidden pane is the one thing the shape adds to your wiring, so send the caret and reveal the track together, or the hit goes somewhere the reader cannot see.
|
|
179
179
|
|
|
180
|
+
Every rule behind the five is wrapped in `:where()`, so it computes at specificity zero: a rule of yours on the same element wins at any specificity and in any sheet order, which is what lets a stylesheet written by someone other than the app's author (an `app.css` over quillkit's app client) restyle a band without knowing which shell rules set it.
|
|
181
|
+
|
|
180
182
|
The shape is shared; the look of a band is not. Your wordmark, your nav and the depth of your head are the most app-specific things on the screen, and a preset that drew them would hand every tool built on this the same face, so they read `--qmh-*` like everything else and you write the rules. `.qm-split`'s gap is a default rather than a fact about splitting: close it to `0` and carry a hairline on one pane if you want the two mounts to meet instead of stand apart.
|
|
181
183
|
|
|
182
|
-
It is **unlayered
|
|
184
|
+
It is **unlayered**, and nothing in it outranks a rule of yours aimed at the same element: the page, type, status and control recipes compute at ordinary specificity, so your own rules beat them by ordinary precedence, and the shell and the app chrome compute at specificity zero, as the surfaces do. It is also opt-in and side-effect free until imported: nothing that styles your document arrives with a component.
|
|
183
185
|
|
|
184
186
|
One rule in it earns its place regardless of whether you take the rest, and you want it if you draw a focus ring of your own — cut the mounted surfaces out of the blanket:
|
|
185
187
|
|
|
@@ -191,6 +193,38 @@ One rule in it earns its place regardless of whether you take the rest, and you
|
|
|
191
193
|
|
|
192
194
|
A blanket `:focus-visible` of yours outranks ours, including on the controls that already draw one, so a ring written for the controls that draw none repaints the ones that do. The cut-out leaves yours everywhere else, and `:not(:where(…))` contributes no specificity, so your rule ranks exactly as it would without it.
|
|
193
195
|
|
|
196
|
+
### The app chrome
|
|
197
|
+
|
|
198
|
+
A tool's bands are yours, since a tool's head is the most app-specific thing on its screen. An app whose whole page is one form to fill in has less to say there: a name, the action the page leads to, and a line signing it. Four classes draw that much, quillkit's app client wears them, and a host of its own takes them the same way:
|
|
199
|
+
|
|
200
|
+
| Class | What it is |
|
|
201
|
+
| ----------------- | ------------------------------------------------------------------------------------------------- |
|
|
202
|
+
| `.qm-app-header` | The band naming the app: a plate, a gutter and a hairline under it. Put it beside `.qm-bar`. |
|
|
203
|
+
| `.qm-app-title` | What the app is called, at the title rung: the one line on the screen read as a name. |
|
|
204
|
+
| `.qm-app-footer` | The line signing the app, in meta type under a hairline; its links take the line's own ink. |
|
|
205
|
+
| `.qm-app-primary` | The one action the page leads to: the control wearing `--qm-accent`, standing at the touch floor. |
|
|
206
|
+
|
|
207
|
+
They compute at specificity zero, as the shell does, so `.qm-app-header { background: #00308f; color: #fff; }` is the whole of rebranding the head.
|
|
208
|
+
|
|
209
|
+
## What a stylesheet may aim at
|
|
210
|
+
|
|
211
|
+
A stylesheet you write against a mounted surface, or one an author writes over an app someone else built, may aim at these and nothing else:
|
|
212
|
+
|
|
213
|
+
- the nine dials (§"The dials");
|
|
214
|
+
- the surface roots, `.qm-editor` and `.qm-preview`, and `.qm-pane`;
|
|
215
|
+
- `data-qm-card="<kind>"`, on the box of every card the editor draws, its value the card's kind and `main` for the main card;
|
|
216
|
+
- the preset's classes, on your own DOM, if you import it (§"Match ours").
|
|
217
|
+
|
|
218
|
+
`data-qm-card` is the one hook named by the schema rather than by the package: a rule aimed at a kind lands on that kind's cards wherever the editor puts them, which is the one thing a quill-specific restyle needs and a dial cannot say.
|
|
219
|
+
|
|
220
|
+
```css
|
|
221
|
+
[data-qm-card='indorsement'] {
|
|
222
|
+
border-left: 3px solid var(--qm-accent);
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Nothing names a field or a property. Every other `data-qm-*` attribute the surfaces carry, `data-qm-prop` among them, is something the surface queries itself, and moves when it does.
|
|
227
|
+
|
|
194
228
|
## What is deliberately not public
|
|
195
229
|
|
|
196
230
|
The two root classes and `.qm-pane` are the whole class contract on the surfaces. Every other `qm-*` class inside them is internal and renames without notice: a class contract over the interior would freeze the DOM shape the surfaces are free to re-cut, and the dials already reach the values a restyle wants. The preset's classes are a contract of their own: they land on _your_ DOM, so they freeze nothing of ours.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/* An app's own chrome: the band that names it, the line that signs it, and the one
|
|
2
|
+
action it exists for. The shell gives a band its row; these give the app's bands their
|
|
3
|
+
look, for a host whose page is one form rather than a workroom. quillkit's app client
|
|
4
|
+
wears them, and an author's `app.css` restyles them (THEMING §"The app chrome").
|
|
5
|
+
|
|
6
|
+
Specificity zero throughout, as the shell's: a rule of the host's own on the same
|
|
7
|
+
element wins at any specificity and in any sheet order. `check:style` holds the rank.
|
|
8
|
+
This file mints nothing. */
|
|
9
|
+
|
|
10
|
+
/* The head: the plate the title stands on, a hairline under it, and the ink everything on
|
|
11
|
+
it takes, so a host recolouring the band recolours its name with it. */
|
|
12
|
+
:where(.qm-app-header) {
|
|
13
|
+
padding: var(--qmh-space-2) var(--qmh-space-4);
|
|
14
|
+
color: var(--qmh-ink);
|
|
15
|
+
background: var(--qmh-surface);
|
|
16
|
+
border-block-end: var(--qmh-border-width) solid var(--qmh-border);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* What the app is called. Display type rather than chrome type: it is the one line on
|
|
20
|
+
the screen a reader reads as a name. */
|
|
21
|
+
:where(.qm-app-title) {
|
|
22
|
+
margin: 0;
|
|
23
|
+
font-size: var(--qmh-text-title);
|
|
24
|
+
font-weight: var(--qmh-weight-strong);
|
|
25
|
+
line-height: var(--qmh-leading-tight);
|
|
26
|
+
color: inherit;
|
|
27
|
+
overflow-wrap: anywhere;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* The foot: a line of meta type under a hairline, its links in the line's own ink. */
|
|
31
|
+
:where(.qm-app-footer) {
|
|
32
|
+
padding: var(--qmh-space-2) var(--qmh-space-4);
|
|
33
|
+
border-block-start: var(--qmh-border-width) solid var(--qmh-border);
|
|
34
|
+
font-size: var(--qmh-text-meta);
|
|
35
|
+
line-height: var(--qmh-leading-tight);
|
|
36
|
+
color: var(--qmh-ink-meta);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
:where(.qm-app-footer a) {
|
|
40
|
+
color: inherit;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* The primary action: the one control on the screen that wears the accent, at the touch
|
|
44
|
+
floor, since it is the press the whole page leads to. */
|
|
45
|
+
:where(.qm-app-primary) {
|
|
46
|
+
font: inherit;
|
|
47
|
+
font-size: var(--qmh-text-label);
|
|
48
|
+
font-weight: var(--qmh-weight-strong);
|
|
49
|
+
line-height: var(--qmh-leading-tight);
|
|
50
|
+
min-height: var(--qmh-tap);
|
|
51
|
+
padding: var(--qmh-space-2) var(--qmh-space-4);
|
|
52
|
+
color: var(--qmh-on-accent);
|
|
53
|
+
background: var(--qmh-accent);
|
|
54
|
+
border: var(--qmh-border-width) solid var(--qmh-accent);
|
|
55
|
+
border-radius: var(--qmh-radius-inner);
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
transition: background-color var(--qmh-duration) var(--qmh-ease-reverse);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
:where(.qm-app-primary:hover) {
|
|
61
|
+
background: var(--qmh-accent-hover);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
:where(.qm-app-primary:disabled) {
|
|
65
|
+
color: var(--qmh-ink-meta);
|
|
66
|
+
background: var(--qmh-surface-hover);
|
|
67
|
+
border-color: var(--qmh-border);
|
|
68
|
+
cursor: default;
|
|
69
|
+
}
|
package/dist/preset/index.css
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
the surface barrels pull `core/theme.css` and never this, because this
|
|
4
4
|
styles the document and nothing may put that in a consumer's page uninvited.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
`scale.css` is where the literals are minted and is exempt; the rest are held to
|
|
7
|
+
reading them. `recipes.css` ranks at ordinary specificity, `shell.css` and `app.css`
|
|
8
|
+
at zero. */
|
|
8
9
|
|
|
9
10
|
@import './scale.css';
|
|
10
11
|
@import './recipes.css';
|
|
12
|
+
@import './shell.css';
|
|
13
|
+
@import './app.css';
|
package/dist/preset/recipes.css
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
/* The endorsed look: the rules a host draws around a mounted surface
|
|
2
|
-
|
|
1
|
+
/* The endorsed look: the rules a host draws around a mounted surface, as classes it puts
|
|
2
|
+
on its own elements; the shell it mounts one in is `shell.css` beside this, and an
|
|
3
|
+
app's own chrome is `app.css`. The rungs beside this in
|
|
3
4
|
`scale.css` are the invisible half (a scale alone still leaves a consumer writing
|
|
4
5
|
every declaration before anything on the page looks like anything), so the recipes
|
|
5
6
|
are what the preset is for. This file mints nothing; `check:style` runs its axes here.
|
|
@@ -40,156 +41,6 @@
|
|
|
40
41
|
max-width: var(--qmh-measure);
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
/* ── The workspace ─────────────────────────────────────────────────────────── */
|
|
44
|
-
|
|
45
|
-
/* The rules above dress the page a surface sits on; these four are the shell it is
|
|
46
|
-
mounted in, a tool where `.qm-page` is a document: the screen an app that mounts two
|
|
47
|
-
surfaces over one session is, drawn once (ARCHITECTURE §Styling).
|
|
48
|
-
|
|
49
|
-
Pinned to the viewport rather than sized to it: a page-height box still leaves the
|
|
50
|
-
root scroller counting the overflow its panes clip, and a wheel over the chrome drags
|
|
51
|
-
the whole app off the top. Three bands (a head, the body, an optional foot), and the
|
|
52
|
-
body takes what is left and may shrink below its content, so the panes in it own
|
|
53
|
-
their overflow and hold still while their contents move.
|
|
54
|
-
|
|
55
|
-
The one column is stated for the same reason the rows are: an implicit track is `auto`,
|
|
56
|
-
which is sized by what a band holds, and a `.qm-split` under one contributes its items'
|
|
57
|
-
max-content through its `fr` tracks. The shell would take the width of the widest thing
|
|
58
|
-
mounted in it instead of the viewport's, and every band would be that wide with the
|
|
59
|
-
second pane past the edge. A host holding its content to a maximum never sees it; one
|
|
60
|
-
spending the whole screen sees it on the first surface it mounts. */
|
|
61
|
-
.qm-workspace {
|
|
62
|
-
position: fixed;
|
|
63
|
-
inset: 0;
|
|
64
|
-
display: grid;
|
|
65
|
-
grid-template-columns: minmax(0, 1fr);
|
|
66
|
-
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
67
|
-
overflow: hidden;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/* A gesture past the end of a pane chains out to the viewport and bounces the empty
|
|
71
|
-
document, which is the last way a pinned shell moves. Conditioned on the workspace, so
|
|
72
|
-
a host's other routes keep a page's own feel. */
|
|
73
|
-
html:has(.qm-workspace) {
|
|
74
|
-
overscroll-behavior: none;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/* A band's line: the row a head or a foot puts its parts on, wrapping, centred. That is
|
|
78
|
-
the whole of it. What a band is made of is the host's: the rule under it, the gutter
|
|
79
|
-
beside it, and the depth of it, because where a band ends and how tall it stands are
|
|
80
|
-
page-shell decisions: a site holding its content to a maximum runs the rule past it at
|
|
81
|
-
its own rhythm, and a one-screen tool has no maximum to hold to. */
|
|
82
|
-
.qm-bar {
|
|
83
|
-
display: flex;
|
|
84
|
-
flex-wrap: wrap;
|
|
85
|
-
align-items: center;
|
|
86
|
-
gap: var(--qmh-space-2) var(--qmh-space-4);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/* Two even tracks for two surfaces, and no resizer, since a divider that can be dragged
|
|
90
|
-
is a control competing with what it divides. Each track may shrink below its content,
|
|
91
|
-
which is what lets the frame in it clip rather than grow.
|
|
92
|
-
|
|
93
|
-
The gap is the endorsed default and not a fact about splitting: two surfaces standing
|
|
94
|
-
apart on a page read as two things, which is what a host showing its instruments wants.
|
|
95
|
-
A host that spends the whole screen on the mounts closes it to nothing and carries a
|
|
96
|
-
hairline on one pane instead: one property, and the tracks are untouched. */
|
|
97
|
-
.qm-split {
|
|
98
|
-
display: grid;
|
|
99
|
-
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
100
|
-
grid-template-rows: minmax(0, 1fr);
|
|
101
|
-
gap: var(--qmh-space-4);
|
|
102
|
-
min-height: 0;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/* The band that switches them, and nothing at the widths where both tracks are showing
|
|
106
|
-
and there is nothing to switch. Where it stands is the host's, like any other band; what
|
|
107
|
-
goes in it is a pair of `.qm-control`s, which already carry the on-state a pressed
|
|
108
|
-
control takes. Its visibility is the preset's because the threshold is: a host hiding
|
|
109
|
-
its own tab row writes this file's number a second time, which is the one thing the
|
|
110
|
-
single source is for. */
|
|
111
|
-
.qm-switch {
|
|
112
|
-
display: none;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/* One mount at a time under the threshold, rather than two of them abreast. Half a narrow
|
|
116
|
-
viewport is under the width either surface reads at, so the split stops being one and
|
|
117
|
-
the tracks become views of the same room: the host says which with `data-qm-show`, and
|
|
118
|
-
the attribute names the track rather than what is in it, so nothing here knows an editor
|
|
119
|
-
from a preview. No attribute is the first track.
|
|
120
|
-
|
|
121
|
-
The other track is `display: none` and not a clip, which is the whole of the hiding: it
|
|
122
|
-
leaves the tab order, hit-testing and the accessibility tree in one property, so the
|
|
123
|
-
pane needs no `inert` and the host needs no breakpoint in JS. Both stay mounted (a
|
|
124
|
-
preview binds its handle once, an editor holds a caret, a history and a scroll), so
|
|
125
|
-
the switch is presentational, never conditional. A pane hidden this way keeps the
|
|
126
|
-
pixels it had: the paint loop's band sweep no-ops on an empty visible set, and its
|
|
127
|
-
resize observer holds the last real width rather than rastering at the 0×0 box a hidden
|
|
128
|
-
pane reports, so what comes back at the width it left needs no paint at all. The
|
|
129
|
-
preview's caret follow spans the same window — a trip asked for against a pane with no
|
|
130
|
-
box is held and run when it has one (`preview/bridge.ts`), the pane opening at the
|
|
131
|
-
caret rather than several edits behind it.
|
|
132
|
-
|
|
133
|
-
Height is the second clause because a landscape phone passes the first one: it clears
|
|
134
|
-
the width with room for two tracks and has no body left to put them in. A desktop window
|
|
135
|
-
that short wants one mount for the same reason. */
|
|
136
|
-
@media (width < 50rem), (height < 30rem) {
|
|
137
|
-
.qm-switch {
|
|
138
|
-
display: flex;
|
|
139
|
-
gap: var(--qmh-space);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/* Even halves of the band, at the height a thumb needs. The floor is the recipe's and
|
|
143
|
-
not the host's, because the band exists only at the sizes where the pointer is a
|
|
144
|
-
finger: a host is free to stand the band deeper, and has nothing to gain from
|
|
145
|
-
standing it shallower than the thing it is tapping. */
|
|
146
|
-
.qm-switch > * {
|
|
147
|
-
flex: 1 1 0;
|
|
148
|
-
min-height: var(--qmh-tap);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/* Centred, which is what a control filling half a band reads as; `.qm-control` itself
|
|
152
|
-
reads left, for the menus it also dresses. */
|
|
153
|
-
.qm-switch > .qm-control {
|
|
154
|
-
text-align: center;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
.qm-split {
|
|
158
|
-
grid-template-columns: minmax(0, 1fr);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
.qm-split:not([data-qm-show='2']) > :nth-child(2),
|
|
162
|
-
.qm-split[data-qm-show='2'] > :nth-child(1) {
|
|
163
|
-
display: none;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/* The mounting site: an edge, a corner, and the clip that holds a surface to them, which
|
|
168
|
-
is all a surface painting its own column needs. No tone and no gutter: those are the
|
|
169
|
-
surface's own (THEMING §"Drop it in"). `border-box`, so a stated height is the height
|
|
170
|
-
it draws.
|
|
171
|
-
|
|
172
|
-
Positioned, because a clip reaches only what it is the containing block for: a
|
|
173
|
-
surface's out-of-flow parts (a parked description, an overlay) resolve against the
|
|
174
|
-
nearest positioned ancestor, and with none between them and the document they land at
|
|
175
|
-
their static position unclipped, leaving the page to scroll past its own foot by
|
|
176
|
-
however far the frame's contents run over.
|
|
177
|
-
|
|
178
|
-
The two minimums are what let a frame in a track clip rather than push: a grid or flex
|
|
179
|
-
item floors at its content, so a mount that paints wider than its column widens the
|
|
180
|
-
column instead. They ride the frame rather than the split's children, which keeps every
|
|
181
|
-
rule in this file landing where the host put a class. Outside a track they are the
|
|
182
|
-
initial value and do nothing. */
|
|
183
|
-
.qm-frame {
|
|
184
|
-
box-sizing: border-box;
|
|
185
|
-
position: relative;
|
|
186
|
-
min-width: 0;
|
|
187
|
-
min-height: 0;
|
|
188
|
-
border: var(--qmh-border-width) solid var(--qmh-border);
|
|
189
|
-
border-radius: var(--qmh-radius);
|
|
190
|
-
overflow: hidden;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
44
|
/* ── Type ──────────────────────────────────────────────────────────────────── */
|
|
194
45
|
|
|
195
46
|
/* A run of chrome type: a section's label, a panel's caption, the name over a readout.
|
|
@@ -306,6 +157,13 @@ pre.qm-readout {
|
|
|
306
157
|
background: var(--qmh-surface-hover);
|
|
307
158
|
}
|
|
308
159
|
|
|
160
|
+
/* Centred in the shell's switch band, which is what a control filling half a band reads
|
|
161
|
+
as; `.qm-control` itself reads left, for the menus it also dresses. Here rather than
|
|
162
|
+
beside the band in `shell.css`, which ranks at zero and so could not outrank this. */
|
|
163
|
+
.qm-switch > .qm-control {
|
|
164
|
+
text-align: center;
|
|
165
|
+
}
|
|
166
|
+
|
|
309
167
|
/* One choice means nothing to pick: the control stays, because what it reads is a fact
|
|
310
168
|
worth having, and it recedes to say it is not a choice. */
|
|
311
169
|
.qm-control:disabled {
|
package/dist/preset/scale.css
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
could not be both.
|
|
19
19
|
|
|
20
20
|
Literals live here and nowhere else in the preset: `check:style` runs its axes over
|
|
21
|
-
|
|
21
|
+
the sheets beside it, with this file exempt as the place the defaults are minted.
|
|
22
22
|
|
|
23
23
|
The poles fall back to the system colours, so a host that turns no dial still lands
|
|
24
24
|
where its own unstyled text does — `Canvas` being the plane the UA would have
|
|
@@ -48,6 +48,14 @@
|
|
|
48
48
|
the underline under a link. */
|
|
49
49
|
--qmh-border-strong: color-mix(in oklab, var(--qmh-surface), var(--qmh-ink) 40%);
|
|
50
50
|
|
|
51
|
+
/* The one hue a host's page spends on purpose: the primary action wears it. The
|
|
52
|
+
figures are the surfaces' own default for the same dial, so a page that turns no
|
|
53
|
+
dial and a card's focus ring agree; the ink on it is the plate's, the pole the
|
|
54
|
+
accent's default contrasts against in both schemes. */
|
|
55
|
+
--qmh-accent: var(--qm-accent, light-dark(#71717a, #a1a1aa));
|
|
56
|
+
--qmh-on-accent: var(--qmh-surface);
|
|
57
|
+
--qmh-accent-hover: color-mix(in oklab, var(--qmh-accent), var(--qmh-ink) 15%);
|
|
58
|
+
|
|
51
59
|
/* The hue a host spends on a failure. The figures are the surfaces' own, so a
|
|
52
60
|
page's status line and a card's diagnostic carry one colour meaning error.
|
|
53
61
|
Each pole is its own figure: a hue calibrated against white reads as a stain
|
|
@@ -92,6 +100,7 @@
|
|
|
92
100
|
surfaces' ramp, which is chrome inside a card, and a page's body text is a
|
|
93
101
|
different job at a different size. A host that wants them locked sets both. */
|
|
94
102
|
--qmh-text: 1rem;
|
|
103
|
+
--qmh-text-title: 1.125rem;
|
|
95
104
|
--qmh-text-label: 0.8125rem;
|
|
96
105
|
--qmh-text-meta: 0.6875rem;
|
|
97
106
|
--qmh-weight-strong: 600;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/* The shell: the screen an app mounting two surfaces over one session is, drawn once
|
|
2
|
+
(THEMING §"The shell, if you are building a tool"). `recipes.css` dresses the page a
|
|
3
|
+
surface sits on; this is what it is mounted in, a tool where `.qm-page` is a document.
|
|
4
|
+
|
|
5
|
+
Every selector here computes at specificity zero, as the surfaces' own do: a host's
|
|
6
|
+
rule on the same element wins whatever order the sheets land in, and an app whose
|
|
7
|
+
stylesheet someone else writes (quillkit's app client, under an author's `app.css`)
|
|
8
|
+
needs no knowledge of which of these the client happened to set. `check:style` holds
|
|
9
|
+
the rank. This file mints nothing. */
|
|
10
|
+
|
|
11
|
+
/* Pinned to the viewport rather than sized to it: a page-height box still leaves the
|
|
12
|
+
root scroller counting the overflow its panes clip, and a wheel over the chrome drags
|
|
13
|
+
the whole app off the top. Three bands (a head, the body, an optional foot), and the
|
|
14
|
+
body takes what is left and may shrink below its content, so the panes in it own
|
|
15
|
+
their overflow and hold still while their contents move.
|
|
16
|
+
|
|
17
|
+
The one column is stated for the same reason the rows are: an implicit track is `auto`,
|
|
18
|
+
which is sized by what a band holds, and a `.qm-split` under one contributes its items'
|
|
19
|
+
max-content through its `fr` tracks. The shell would take the width of the widest thing
|
|
20
|
+
mounted in it instead of the viewport's, and every band would be that wide with the
|
|
21
|
+
second pane past the edge. A host holding its content to a maximum never sees it; one
|
|
22
|
+
spending the whole screen sees it on the first surface it mounts. */
|
|
23
|
+
:where(.qm-workspace) {
|
|
24
|
+
position: fixed;
|
|
25
|
+
inset: 0;
|
|
26
|
+
display: grid;
|
|
27
|
+
grid-template-columns: minmax(0, 1fr);
|
|
28
|
+
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
29
|
+
overflow: hidden;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* A gesture past the end of a pane chains out to the viewport and bounces the empty
|
|
33
|
+
document, which is the last way a pinned shell moves. Conditioned on the workspace, so
|
|
34
|
+
a host's other routes keep a page's own feel. */
|
|
35
|
+
:where(html:has(.qm-workspace)) {
|
|
36
|
+
overscroll-behavior: none;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* A band's line: the row a head or a foot puts its parts on, wrapping, centred. That is
|
|
40
|
+
the whole of it. What a band is made of is the host's: the rule under it, the gutter
|
|
41
|
+
beside it, and the depth of it, because where a band ends and how tall it stands are
|
|
42
|
+
page-shell decisions: a site holding its content to a maximum runs the rule past it at
|
|
43
|
+
its own rhythm, and a one-screen tool has no maximum to hold to. */
|
|
44
|
+
:where(.qm-bar) {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-wrap: wrap;
|
|
47
|
+
align-items: center;
|
|
48
|
+
gap: var(--qmh-space-2) var(--qmh-space-4);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Two even tracks for two surfaces, and no resizer, since a divider that can be dragged
|
|
52
|
+
is a control competing with what it divides. Each track may shrink below its content,
|
|
53
|
+
which is what lets the frame in it clip rather than grow.
|
|
54
|
+
|
|
55
|
+
The gap is the endorsed default and not a fact about splitting: two surfaces standing
|
|
56
|
+
apart on a page read as two things, which is what a host showing its instruments wants.
|
|
57
|
+
A host that spends the whole screen on the mounts closes it to nothing and carries a
|
|
58
|
+
hairline on one pane instead: one property, and the tracks are untouched. */
|
|
59
|
+
:where(.qm-split) {
|
|
60
|
+
display: grid;
|
|
61
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
62
|
+
grid-template-rows: minmax(0, 1fr);
|
|
63
|
+
gap: var(--qmh-space-4);
|
|
64
|
+
min-height: 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* The band that switches them, and nothing at the widths where both tracks are showing
|
|
68
|
+
and there is nothing to switch. Where it stands is the host's, like any other band; what
|
|
69
|
+
goes in it is a pair of `.qm-control`s, which already carry the on-state a pressed
|
|
70
|
+
control takes. Its visibility is the preset's because the threshold is: a host hiding
|
|
71
|
+
its own tab row writes this file's number a second time, which is the one thing the
|
|
72
|
+
single source is for. */
|
|
73
|
+
:where(.qm-switch) {
|
|
74
|
+
display: none;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* One mount at a time under the threshold, rather than two of them abreast. Half a narrow
|
|
78
|
+
viewport is under the width either surface reads at, so the split stops being one and
|
|
79
|
+
the tracks become views of the same room: the host says which with `data-qm-show`, and
|
|
80
|
+
the attribute names the track rather than what is in it, so nothing here knows an editor
|
|
81
|
+
from a preview. No attribute is the first track.
|
|
82
|
+
|
|
83
|
+
The other track is `display: none` and not a clip, which is the whole of the hiding: it
|
|
84
|
+
leaves the tab order, hit-testing and the accessibility tree in one property, so the
|
|
85
|
+
pane needs no `inert` and the host needs no breakpoint in JS. Both stay mounted (a
|
|
86
|
+
preview binds its handle once, an editor holds a caret, a history and a scroll), so
|
|
87
|
+
the switch is presentational, never conditional. A pane hidden this way keeps the
|
|
88
|
+
pixels it had: the paint loop's band sweep no-ops on an empty visible set, and its
|
|
89
|
+
resize observer holds the last real width rather than rastering at the 0×0 box a hidden
|
|
90
|
+
pane reports, so what comes back at the width it left needs no paint at all. The
|
|
91
|
+
preview's caret follow spans the same window — a trip asked for against a pane with no
|
|
92
|
+
box is held and run when it has one (`preview/bridge.ts`), the pane opening at the
|
|
93
|
+
caret rather than several edits behind it.
|
|
94
|
+
|
|
95
|
+
Height is the second clause because a landscape phone passes the first one: it clears
|
|
96
|
+
the width with room for two tracks and has no body left to put them in. A desktop window
|
|
97
|
+
that short wants one mount for the same reason. */
|
|
98
|
+
@media (width < 50rem), (height < 30rem) {
|
|
99
|
+
:where(.qm-switch) {
|
|
100
|
+
display: flex;
|
|
101
|
+
gap: var(--qmh-space);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Even halves of the band, at the height a thumb needs. The floor is the recipe's and
|
|
105
|
+
not the host's, because the band exists only at the sizes where the pointer is a
|
|
106
|
+
finger: a host is free to stand the band deeper, and has nothing to gain from
|
|
107
|
+
standing it shallower than the thing it is tapping. */
|
|
108
|
+
:where(.qm-switch > *) {
|
|
109
|
+
flex: 1 1 0;
|
|
110
|
+
min-height: var(--qmh-tap);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
:where(.qm-split) {
|
|
114
|
+
grid-template-columns: minmax(0, 1fr);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
:where(.qm-split:not([data-qm-show='2']) > :nth-child(2)),
|
|
118
|
+
:where(.qm-split[data-qm-show='2'] > :nth-child(1)) {
|
|
119
|
+
display: none;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* The mounting site: an edge, a corner, and the clip that holds a surface to them, which
|
|
124
|
+
is all a surface painting its own column needs. No tone and no gutter: those are the
|
|
125
|
+
surface's own (THEMING §"Drop it in"). `border-box`, so a stated height is the height
|
|
126
|
+
it draws.
|
|
127
|
+
|
|
128
|
+
Positioned, because a clip reaches only what it is the containing block for: a
|
|
129
|
+
surface's out-of-flow parts (a parked description, an overlay) resolve against the
|
|
130
|
+
nearest positioned ancestor, and with none between them and the document they land at
|
|
131
|
+
their static position unclipped, leaving the page to scroll past its own foot by
|
|
132
|
+
however far the frame's contents run over.
|
|
133
|
+
|
|
134
|
+
The two minimums are what let a frame in a track clip rather than push: a grid or flex
|
|
135
|
+
item floors at its content, so a mount that paints wider than its column widens the
|
|
136
|
+
column instead. They ride the frame rather than the split's children, which keeps every
|
|
137
|
+
rule in this file landing where the host put a class. Outside a track they are the
|
|
138
|
+
initial value and do nothing. */
|
|
139
|
+
:where(.qm-frame) {
|
|
140
|
+
box-sizing: border-box;
|
|
141
|
+
position: relative;
|
|
142
|
+
min-width: 0;
|
|
143
|
+
min-height: 0;
|
|
144
|
+
border: var(--qmh-border-width) solid var(--qmh-border);
|
|
145
|
+
border-radius: var(--qmh-radius);
|
|
146
|
+
overflow: hidden;
|
|
147
|
+
}
|
|
@@ -178,6 +178,14 @@
|
|
|
178
178
|
pointer-events: none;
|
|
179
179
|
transition: opacity var(--_qm-reveal);
|
|
180
180
|
}
|
|
181
|
+
/* A screen with no hover has no pointer to bring it back, and the pill is the way back
|
|
182
|
+
to the fit there, so it stays. */
|
|
183
|
+
@media (hover: none) {
|
|
184
|
+
:global(:where(.qm-preview-zoom[data-idle])) {
|
|
185
|
+
opacity: 1;
|
|
186
|
+
pointer-events: auto;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
181
189
|
:global(:where(.qm-preview-zoom:focus-visible)) {
|
|
182
190
|
opacity: 1;
|
|
183
191
|
pointer-events: auto;
|
package/dist/preview/bridge.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { boxesForField, rectToPercent, clickToPdfPt, pxToPt, applyPercentRect } from './geometry.js';
|
|
2
|
-
/** A pane hidden by the shell's narrow switch (`preset/
|
|
2
|
+
/** A pane hidden by the shell's narrow switch (`preset/shell.css`) measures 0×0, and so
|
|
3
3
|
* does every box on it: the clearance arithmetic reads that as a caret clear of both
|
|
4
4
|
* edges, and a centring computes its trip out of zeros. Neither is an answer about where
|
|
5
5
|
* the caret is. */
|
package/dist/preview/viewport.js
CHANGED
|
@@ -42,15 +42,17 @@ export function createViewport(container, slots, setScale, resetLabel) {
|
|
|
42
42
|
// was hidden at stands, so a return to the same box re-rasters nothing.
|
|
43
43
|
return width > 0 && height > 0 ? { width, height } : undefined;
|
|
44
44
|
}
|
|
45
|
-
// What the pages are drawn at, the zoom held against this pane's floor,
|
|
45
|
+
// What the pages are drawn at, the zoom held against this pane's floor, the floor, and
|
|
46
|
+
// the default fit.
|
|
46
47
|
function bounds() {
|
|
47
48
|
const box = pane();
|
|
48
49
|
if (!box || !slots.length)
|
|
49
50
|
return undefined;
|
|
50
51
|
const sizes = slots.map((s) => s.size);
|
|
51
52
|
const floor = floorZoom(sizes, box);
|
|
52
|
-
const
|
|
53
|
-
|
|
53
|
+
const fit = fitZoom(sizes, box);
|
|
54
|
+
const current = zoom === undefined ? fit : clampZoom(zoom, floor);
|
|
55
|
+
return { current, floor, fit };
|
|
54
56
|
}
|
|
55
57
|
function apply(settle) {
|
|
56
58
|
const b = bounds();
|
|
@@ -113,11 +115,11 @@ export function createViewport(container, slots, setScale, resetLabel) {
|
|
|
113
115
|
// A re-fit holds the line at the pane's top edge, so a dragged divider leaves the
|
|
114
116
|
// reader where they were. The desk turns the browser's own scroll anchoring off
|
|
115
117
|
// (`Preview.svelte`), which would correct the same move a second time.
|
|
116
|
-
function refit() {
|
|
118
|
+
function refit(settle) {
|
|
117
119
|
if (!container.scrollTop && !container.scrollLeft)
|
|
118
|
-
return apply();
|
|
120
|
+
return apply(settle);
|
|
119
121
|
const r = container.getBoundingClientRect();
|
|
120
|
-
holding(r.left + r.width / 2, r.top, () => apply());
|
|
122
|
+
holding(r.left + r.width / 2, r.top, () => apply(settle));
|
|
121
123
|
}
|
|
122
124
|
function centre() {
|
|
123
125
|
const r = container.getBoundingClientRect();
|
|
@@ -125,27 +127,29 @@ export function createViewport(container, slots, setScale, resetLabel) {
|
|
|
125
127
|
}
|
|
126
128
|
// The pointer while it is over the pane, else the pane's centre.
|
|
127
129
|
const anchor = () => pointer ?? centre();
|
|
130
|
+
// A zoom landing on the fit is the default again, and follows the pane from there.
|
|
131
|
+
const settled = (next, b) => next === b.fit ? undefined : next;
|
|
128
132
|
function step(next, at = anchor()) {
|
|
129
133
|
const b = bounds();
|
|
130
134
|
if (!b)
|
|
131
135
|
return;
|
|
132
136
|
run = undefined;
|
|
133
|
-
zoomTo(next(b), at.x, at.y);
|
|
137
|
+
zoomTo(settled(next(b), b), at.x, at.y);
|
|
134
138
|
}
|
|
135
139
|
// About the pane's centre, the pointer being on the pill.
|
|
136
140
|
const reset = () => step(() => undefined, centre());
|
|
137
141
|
// The raw zoom a gesture starting now carries from: the run's own while it is still
|
|
138
142
|
// the one on screen.
|
|
139
143
|
function from(b) {
|
|
140
|
-
return run && run.shown === b.current ? run : { raw: b.current, shown: b.current };
|
|
144
|
+
return run && run.shown === b.current ? run : { raw: b.current, shown: b.current, slack: 0 };
|
|
141
145
|
}
|
|
142
146
|
function glideTo(raw, x, y) {
|
|
143
147
|
const b = bounds();
|
|
144
148
|
if (!b)
|
|
145
149
|
return;
|
|
146
150
|
const start = from(b);
|
|
147
|
-
run = glide(start, raw(start), b.floor);
|
|
148
|
-
zoomTo(run.shown, x, y, SETTLE_MS);
|
|
151
|
+
run = glide(start, raw(start), b.floor, b.fit);
|
|
152
|
+
zoomTo(settled(run.shown, b), x, y, SETTLE_MS);
|
|
149
153
|
}
|
|
150
154
|
// Ctrl-wheel is a mouse's zoom, and the trackpad pinch Chromium and Firefox send as
|
|
151
155
|
// `deltaY = -100 ln(scale)`, in deltas smaller than a notch's: those track the fingers.
|
|
@@ -169,7 +173,7 @@ export function createViewport(container, slots, setScale, resetLabel) {
|
|
|
169
173
|
if ((!dir && e.key !== '0') || !bounds())
|
|
170
174
|
return;
|
|
171
175
|
e.preventDefault();
|
|
172
|
-
step((b) => (dir ? stepZoom(b.current, dir, b.floor) : undefined));
|
|
176
|
+
step((b) => (dir ? stepZoom(b.current, dir, b.floor, b.fit) : undefined));
|
|
173
177
|
}
|
|
174
178
|
let pinch;
|
|
175
179
|
// The fingers on this pane, not one resting on the editor beside it.
|
|
@@ -270,13 +274,14 @@ export function createViewport(container, slots, setScale, resetLabel) {
|
|
|
270
274
|
];
|
|
271
275
|
for (const [target, type, fn, opts] of listeners)
|
|
272
276
|
target.addEventListener(type, fn, opts);
|
|
273
|
-
// The pane's box moving: a dragged divider, a resized window, a hidden track shown.
|
|
274
|
-
// Guarded for jsdom, which
|
|
275
|
-
|
|
277
|
+
// The pane's box moving: a dragged divider, a resized window, a hidden track shown. A
|
|
278
|
+
// drag is a gesture, so its pages re-raster once it rests. Guarded for jsdom, which
|
|
279
|
+
// ships no ResizeObserver.
|
|
280
|
+
const resizeObserver = typeof ResizeObserver === 'undefined' ? undefined : new ResizeObserver(() => refit(SETTLE_MS));
|
|
276
281
|
resizeObserver?.observe(container);
|
|
277
282
|
apply();
|
|
278
283
|
return {
|
|
279
|
-
update: refit,
|
|
284
|
+
update: () => refit(),
|
|
280
285
|
setZoom(next) {
|
|
281
286
|
const valid = next !== undefined && Number.isFinite(next) && next > 0 ? next : undefined;
|
|
282
287
|
if (bounds())
|
package/dist/preview/zoom.d.ts
CHANGED
|
@@ -20,22 +20,26 @@ export declare function fitZoom(pages: readonly PageSize[], pane: Pane): number;
|
|
|
20
20
|
* default, which on a pane past the cap is already smaller than that. */
|
|
21
21
|
export declare function floorZoom(pages: readonly PageSize[], pane: Pane): number;
|
|
22
22
|
export declare function clampZoom(zoom: number, floor: number): number;
|
|
23
|
-
/** The next level in `dir` from `zoom`, skipping levels under `floor`.
|
|
24
|
-
* levels the first press lands on the nearer one in that direction;
|
|
25
|
-
* above the floor, the floor. Half a percent of slack, so a zoom
|
|
26
|
-
* it. */
|
|
27
|
-
export declare function stepZoom(zoom: number, dir: 1 | -1, floor: number): number;
|
|
23
|
+
/** The next level in `dir` from `zoom`, `fit` among them, skipping levels under `floor`.
|
|
24
|
+
* From between two levels the first press lands on the nearer one in that direction;
|
|
25
|
+
* past the last level above the floor, the floor. Half a percent of slack, so a zoom
|
|
26
|
+
* shown as a level is at it. */
|
|
27
|
+
export declare function stepZoom(zoom: number, dir: 1 | -1, floor: number, fit: number): number;
|
|
28
28
|
/**
|
|
29
|
-
* A continuous gesture's run:
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
29
|
+
* A continuous gesture's run: the zoom its events have asked for (`raw`), what it shows,
|
|
30
|
+
* and, while it shows a hold, how far past the hold the events have carried (`slack`, a
|
|
31
|
+
* log of the ratio). The run moves `shown` by each event's ratio rather than snapping
|
|
32
|
+
* `raw`, so a trackpad's small deltas carry it out of a hold as smoothly as they carried
|
|
33
|
+
* it in.
|
|
33
34
|
*/
|
|
34
35
|
export interface Glide {
|
|
35
36
|
raw: number;
|
|
36
37
|
shown: number;
|
|
38
|
+
slack: number;
|
|
37
39
|
}
|
|
38
|
-
/** One event of a continuous gesture, carrying the run to `raw
|
|
39
|
-
*
|
|
40
|
-
* stops there
|
|
41
|
-
|
|
40
|
+
/** One event of a continuous gesture, carrying the run to `raw`. It stops at `fit` or
|
|
41
|
+
* 100% when it crosses one, at the first it meets, so one wheel notch across either
|
|
42
|
+
* stops there; it leaves a hold once carried {@link DETENT} past, moving on from the
|
|
43
|
+
* hold rather than jumping to where the events would have put it. 100% gives way to a
|
|
44
|
+
* `fit` within the detent of it: two holds that close would pin the run between them. */
|
|
45
|
+
export declare function glide(run: Glide, raw: number, floor: number, fit: number): Glide;
|
package/dist/preview/zoom.js
CHANGED
|
@@ -12,8 +12,10 @@ const FLOOR_FILL = 0.9;
|
|
|
12
12
|
/** What `Mod +` and `Mod −` step through: a subset of the levels a browser's own zoom
|
|
13
13
|
* keys land on, so a step lands somewhere a reader has been before. */
|
|
14
14
|
export const STEPS = [0.67, 0.75, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2];
|
|
15
|
-
/** How far
|
|
15
|
+
/** How far, as a log of the zoom's ratio to a hold, a continuous gesture carries past it
|
|
16
|
+
* before it leaves it: a ratio, so a hold is as wide at a phone's fit as at 100%. */
|
|
16
17
|
const DETENT = 0.04;
|
|
18
|
+
const near = (zoom, hold) => Math.abs(Math.log(zoom / hold)) <= DETENT;
|
|
17
19
|
/** The default: the widest page fits the pane's width, capped at {@link FIT_CAP}. A
|
|
18
20
|
* portrait-only document lands at the pane's width, as a narrow pane always drew it. */
|
|
19
21
|
export function fitZoom(pages, pane) {
|
|
@@ -30,23 +32,43 @@ export function floorZoom(pages, pane) {
|
|
|
30
32
|
export function clampZoom(zoom, floor) {
|
|
31
33
|
return Math.min(MAX_ZOOM, Math.max(floor, zoom));
|
|
32
34
|
}
|
|
33
|
-
/** The
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
/** The levels a step lands on: `STEPS` with `fit` among them, which takes the
|
|
36
|
+
* place of any level within {@link DETENT} of it, since fit is the one that follows the
|
|
37
|
+
* pane. */
|
|
38
|
+
function stops(fit) {
|
|
39
|
+
return [...STEPS.filter((s) => !near(s, fit)), fit].sort((a, b) => a - b);
|
|
40
|
+
}
|
|
41
|
+
/** The next level in `dir` from `zoom`, `fit` among them, skipping levels under `floor`.
|
|
42
|
+
* From between two levels the first press lands on the nearer one in that direction;
|
|
43
|
+
* past the last level above the floor, the floor. Half a percent of slack, so a zoom
|
|
44
|
+
* shown as a level is at it. */
|
|
45
|
+
export function stepZoom(zoom, dir, floor, fit) {
|
|
46
|
+
const levels = stops(fit);
|
|
38
47
|
const next = dir > 0
|
|
39
|
-
?
|
|
40
|
-
:
|
|
48
|
+
? levels.find((s) => s > zoom + 0.005)
|
|
49
|
+
: levels.reverse().find((s) => s < zoom - 0.005 && s >= floor);
|
|
41
50
|
return clampZoom(next ?? (dir > 0 ? MAX_ZOOM : floor), floor);
|
|
42
51
|
}
|
|
43
|
-
/** One event of a continuous gesture, carrying the run to `raw
|
|
44
|
-
*
|
|
45
|
-
* stops there
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
+
/** One event of a continuous gesture, carrying the run to `raw`. It stops at `fit` or
|
|
53
|
+
* 100% when it crosses one, at the first it meets, so one wheel notch across either
|
|
54
|
+
* stops there; it leaves a hold once carried {@link DETENT} past, moving on from the
|
|
55
|
+
* hold rather than jumping to where the events would have put it. 100% gives way to a
|
|
56
|
+
* `fit` within the detent of it: two holds that close would pin the run between them. */
|
|
57
|
+
export function glide(run, raw, floor, fit) {
|
|
58
|
+
const holds = near(fit, 1) ? [fit] : [fit, 1];
|
|
59
|
+
const step = Math.log(raw / run.raw);
|
|
60
|
+
let next;
|
|
61
|
+
if (holds.includes(run.shown)) {
|
|
62
|
+
const slack = run.slack + step;
|
|
63
|
+
if (Math.abs(slack) <= DETENT)
|
|
64
|
+
return { raw, shown: run.shown, slack };
|
|
65
|
+
next = run.shown * Math.exp(slack - Math.sign(slack) * DETENT);
|
|
66
|
+
}
|
|
67
|
+
else
|
|
68
|
+
next = run.shown * Math.exp(step);
|
|
69
|
+
next = clampZoom(next, floor);
|
|
70
|
+
const crossed = holds
|
|
71
|
+
.filter((t) => (run.shown - t) * (next - t) < 0)
|
|
72
|
+
.sort((a, b) => Math.abs(a - run.shown) - Math.abs(b - run.shown));
|
|
73
|
+
return { raw, shown: crossed[0] ?? next, slack: 0 };
|
|
52
74
|
}
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
|
|
53
53
|
<!-- Not `.qm-switch`: that name is the preset's pane band, whose narrow-viewport rule
|
|
54
54
|
stands every child of it at `--qmh-tap`, so a track wearing it grows a thumb three times
|
|
55
|
-
its own height over the labels around it (`preset/
|
|
55
|
+
its own height over the labels around it (`preset/shell.css`, held by `check:style`). -->
|
|
56
56
|
<span
|
|
57
57
|
class="qm-toggle-wrap"
|
|
58
58
|
data-unset={unset ? '' : undefined}
|
package/dist/visual/Card.svelte
CHANGED
|
@@ -211,12 +211,14 @@
|
|
|
211
211
|
</script>
|
|
212
212
|
|
|
213
213
|
<!-- `data-qm-card` names the island for a floating surface that has to stay inside one
|
|
214
|
-
({@link FieldHint})
|
|
215
|
-
ancestor and nothing else marks the box its guidance belongs in.
|
|
214
|
+
({@link FieldHint}): a surface portals to `[data-qm-root]`, so the card is not its
|
|
215
|
+
ancestor and nothing else marks the box its guidance belongs in. Its value is the card's
|
|
216
|
+
kind, `main` for the main card, which is the one schema-named hook a host's stylesheet
|
|
217
|
+
is promised (THEMING §"What a stylesheet may aim at"). -->
|
|
216
218
|
<section
|
|
217
219
|
bind:this={el}
|
|
218
220
|
class={['qm-card', { 'qm-main': card.isMain, 'qm-unschemable': card.unschemable }]}
|
|
219
|
-
data-qm-card
|
|
221
|
+
data-qm-card={card.kind}
|
|
220
222
|
>
|
|
221
223
|
{#if card.unschemable}
|
|
222
224
|
{@render recoveryShell()}
|