@quillmark/svelte 0.3.0 → 0.3.1

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.
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * An address's boxes: `fieldBoxes`'s union, else its own `regions()` rects, else the
3
- * rects of everything under it. The one rule the overlay and the scroll both read, so
4
- * an address `regions()` names never draws nothing (PREVIEW.md §"Three
5
- * responsibilities").
3
+ * rects of everything under it. What the scroll reads, so an address `regions()`
4
+ * names is never one the preview cannot place (PREVIEW.md §"Two responsibilities").
6
5
  *
7
6
  * `fieldBoxes` answers `[]` for a scalar reference the plate places without tracking
8
7
  * its content and for a `richtext[]` element; runtime.d.ts says what such a field's
@@ -20,8 +19,8 @@ export function boxesForField(field, boxes, regions) {
20
19
  }
21
20
  /**
22
21
  * Forward: a PDF-pt rect -> CSS % of the page box. Percent positioning tracks
23
- * the page's displayed size across DPR and container resize for free; this is
24
- * what the overlay draws (overlay.ts).
22
+ * the page's displayed size across DPR and container resize for free, so the
23
+ * scroll's measurement needs no zoom or DPR term of its own (bridge.ts).
25
24
  */
26
25
  export function rectToPercent(rect, pageSize) {
27
26
  const [x0, y0, x1, y1] = rect;
@@ -34,10 +33,9 @@ export function rectToPercent(rect, pageSize) {
34
33
  };
35
34
  }
36
35
  /**
37
- * Position `el` absolutely at `pct` (the % rect this module produces); the one
38
- * `PercentRect` → `element.style` write, shared by the overlay's field boxes and
39
- * the bridge's scroll marker so the positioning convention lives in one place.
40
- * The caller sets any box chrome (border, background) on top.
36
+ * Position `el` absolutely at `pct` (the % rect this module produces): the one
37
+ * `PercentRect` → `element.style` write, so the positioning convention lives beside
38
+ * the transform that produces it rather than at the caller placing a marker.
41
39
  */
42
40
  export function applyPercentRect(el, pct) {
43
41
  Object.assign(el.style, {
@@ -5,7 +5,7 @@
5
5
  // (the codec, the VisualEditor) so a `@quillmark/preview` promotion stays a
6
6
  // re-export, not a refactor: it may reach `/core` (the shared WASM boundary) and
7
7
  // nothing else. Enforced by `check:deps`, which walks the subpath's import graph.
8
- // The theme derivation, which the page slots and the overlay read through `var()`.
8
+ // The theme derivation, which the page slots read through `var()`.
9
9
  // Imported at the barrel because a subpath is what a consumer gets: this surface
10
10
  // reaches modules inside `core/` and never its entry, so a sheet hanging off that
11
11
  // entry arrives only for a consumer importing `/core` for some other reason. The
@@ -1,5 +1,5 @@
1
1
  import type { LiveSession, PageSize } from '@quillmark/wasm';
2
- /** One page's DOM slot: the box overlay.ts/bridge.ts position against, plus its cached geometry. */
2
+ /** One page's DOM slot: the box bridge.ts measures against, plus its cached geometry. */
3
3
  export interface PageSlot {
4
4
  readonly page: number;
5
5
  readonly size: PageSize;
@@ -36,9 +36,9 @@ onPaintError) {
36
36
  // The sheet is a plane and the desk is the one under it: paper sits at
37
37
  // `--_qm-surface` and the container paints `--_qm-surface-sunken`, so the
38
38
  // edge is where two tones meet and no stroke is drawn around it
39
- // (ARCHITECTURE §Styling). It leaves the overlay's no-ink-at-rest rule
40
- // untouched, since a boundary that is not painted is not a mark on the page
41
- // (PREVIEW §Overlay). `border-box` costs nothing here and keeps a slot a
39
+ // (ARCHITECTURE §Styling). It leaves the no-ink rule untouched, since a
40
+ // boundary that is not painted is not a mark on the page (PREVIEW §"Two
41
+ // responsibilities"). `border-box` costs nothing here and keeps a slot a
42
42
  // consumer insets from measuring wider than its paper.
43
43
  const el = document.createElement('div');
44
44
  el.className = 'qm-page';
@@ -151,7 +151,7 @@ onPaintError) {
151
151
  slots[i] = { ...slots[i], size };
152
152
  // Keep the page box's shape in step with the re-read size: a recompile
153
153
  // can change a page's dimensions without changing the count, and every
154
- // %-space overlay/click transform assumes box shape matches PageSize.
154
+ // %-space click/scroll transform assumes box shape matches PageSize.
155
155
  slots[i].el.style.aspectRatio = `${size.widthPt} / ${size.heightPt}`;
156
156
  }
157
157
  }
@@ -164,7 +164,7 @@ onPaintError) {
164
164
  // Every paint freezes `canvas.style.width/height` to the box width at that
165
165
  // paint (paintSlot). A page that stays mounted while the container's CSS width
166
166
  // or `devicePixelRatio` shifts would otherwise keep a stale raster the %-space
167
- // overlay/click math silently drifts off of: the box tracks the container, the
167
+ // click/scroll math silently drifts off of: the box tracks the container, the
168
168
  // ink does not. Two observers close the gap by repainting mounted pages from
169
169
  // their current box; canvases are `position:absolute`, so a repaint can't feed
170
170
  // back into layout (and thus can't re-trigger the resize observer).
@@ -1,8 +1,7 @@
1
1
  <!--
2
- One card block: the header (composable cards only), the field list grouped by
3
- `ui.group`/`ui.compact`, and the body prose leaf. `main` is headerless with no
4
- controls. Every prose leaf takes a parent-built live address, so a card reorder
5
- re-targets its commits without a remount.
2
+ One card block, its fields grouped by `ui.group`/`ui.compact`. `main` is headerless
3
+ and takes no controls. Every prose leaf takes a parent-built live address, so a card
4
+ reorder re-targets its commits without a remount.
6
5
  -->
7
6
  <script lang="ts">
8
7
  import { wording } from './strings.js';
@@ -43,7 +42,6 @@
43
42
  card: CardModel;
44
43
  doc: Document;
45
44
  quill: Quill;
46
- index: number;
47
45
  isFirst: boolean;
48
46
  isLast: boolean;
49
47
  kinds: string[];
@@ -61,7 +59,6 @@
61
59
  card,
62
60
  doc,
63
61
  quill,
64
- index,
65
62
  isFirst,
66
63
  isLast,
67
64
  kinds,
@@ -91,8 +88,8 @@
91
88
  (e.currentTarget as HTMLInputElement).select();
92
89
  }
93
90
  // A press on an unfocused title would place a caret on mouseup, collapsing the
94
- // focus-time select-all. Take focus manually and suppress that caret; a press while
95
- // already focused stays normal, so clicking mid-title to place the caret still works.
91
+ // focus-time select-all. A press while already focused stays normal, so clicking
92
+ // mid-title to place the caret still works.
96
93
  function onTitleMousedown(e: MouseEvent): void {
97
94
  const el = e.currentTarget as HTMLInputElement;
98
95
  if (document.activeElement !== el) {
@@ -100,9 +97,8 @@
100
97
  el.focus();
101
98
  }
102
99
  }
103
- // A press on the region's empty width enters the title edit; preventDefault keeps it
104
- // from starting a text selection on the wrapper. A press on the input itself is left
105
- // to `onTitleMousedown`, which owns the already-focused caret-placement case.
100
+ // A press on the region's empty width enters the title edit, and starts no text
101
+ // selection on the wrapper. A press on the input itself is `onTitleMousedown`'s.
106
102
  function onRenameMousedown(e: MouseEvent): void {
107
103
  const input = (e.currentTarget as HTMLElement).querySelector('input');
108
104
  if (!input || e.target === input) return;
@@ -136,10 +132,10 @@
136
132
  // paint as a single stroke of twice the width.
137
133
  const hasMeta = $derived(ungrouped.length > 0 || grouped.length > 0);
138
134
 
139
- // Ephemeral session state, seeded once from the card's shape. Card is keyed by stable
140
- // id, so this survives the VisualEditor re-derive that reassigns `card` and resets
141
- // only on a remount. Not reconciled to later section changes: a retype is the one
142
- // reshape, and the open group stays where it still exists.
135
+ // Seeded once from the card's shape. Card is keyed by stable id, so this survives the
136
+ // VisualEditor re-derive that reassigns `card` and resets only on a remount. Not
137
+ // reconciled to later section changes: a retype is the one reshape, and the open
138
+ // group stays where it still exists.
143
139
  // svelte-ignore state_referenced_locally
144
140
  let expanded = $state<string | null>(initialExpandedGroup(card.sections));
145
141
  let headers = $state<Record<string, HTMLButtonElement | undefined>>({});
@@ -162,10 +158,10 @@
162
158
  }
163
159
 
164
160
  /**
165
- * Open the group holding leaf `key`, if this card has one: a caret placed in a
166
- * collapsed panel does not land at all. `VisualEditor.setCaret` calls this on every
167
- * card and then waits a flush before landing, since `inert` clears when the panel
168
- * renders open, not when `expanded` is assigned.
161
+ * Open the group holding leaf `key`: a caret placed in a collapsed panel does not
162
+ * land at all. `VisualEditor.setCaret` calls this on every card and then waits a
163
+ * flush before landing, since `inert` clears when the panel renders open, not when
164
+ * `expanded` is assigned.
169
165
  *
170
166
  * A call, not a prop: a reveal is an event, and modelling it as state needs a
171
167
  * fresh-identity wrapper to re-fire and an `untrack` to keep the per-keystroke
@@ -232,11 +228,10 @@
232
228
  {/if}
233
229
 
234
230
  <div class="qm-card-body">
235
- <!-- Every field the card has, ungrouped and grouped alike, inside one element
236
- whose top and bottom edges are the bracket's two horizontals. Anchored here
237
- rather than on the header and on the accordion so an open section's vertical
238
- reaches them, and so a card with no groups still terminates above its body. An
239
- empty wrapper between the two rules would collapse them onto each other. -->
231
+ <!-- One element whose top and bottom edges are the bracket's two horizontals.
232
+ Anchored here rather than on the header and on the accordion so an open section's
233
+ vertical reaches them, and so a card with no groups still terminates above its
234
+ body. -->
240
235
  {#if hasMeta}
241
236
  <div
242
237
  class="qm-card-meta"
@@ -442,13 +437,17 @@
442
437
  }
443
438
  /* Autosize: the ::after mirrors the text into the same grid cell as the input, so the
444
439
  overlaid input tracks its content width. Both carry the same nothing for a box model
445
- and inherit the same type tokens, which is what makes the two measure alike. */
440
+ and inherit the same type tokens, which is what makes the two measure alike.
441
+
442
+ The type is the group header's, down to the ink and its hover step below: a card
443
+ title and a section header both name a block of fields, so they read as one register
444
+ and what ranks them is position rather than a size step. */
446
445
  .qm-card-title-sizer {
447
446
  display: inline-grid;
448
447
  align-items: center;
449
448
  min-width: 0;
450
449
  max-width: 100%;
451
- font-size: var(--_qm-text-title);
450
+ font-size: var(--_qm-text-body);
452
451
  font-weight: var(--_qm-weight-mid);
453
452
  line-height: var(--_qm-leading-tight);
454
453
  font-family: inherit;
@@ -620,11 +619,17 @@
620
619
  The clip box takes the inline inset, which costs a shut panel no height and holds the
621
620
  queried width still. The block inset goes on the content, where a `0fr` track clips
622
621
  it: declared one box out it collapses the content but not itself, and stands under a
623
- shut header as dead space. Neither is animated, and neither is qualified by open. */
622
+ shut header as dead space. Neither is animated, and neither is qualified by open.
623
+
624
+ A field ends on the clip box at the end edge, where no inset hides its ring the way
625
+ the start edge's does. The pad carries the clip past it and the margin takes the same
626
+ distance back, so the width the fields are packed and queried at is unmoved. */
624
627
  .qm-group-panel-inner {
625
628
  min-height: 0;
626
629
  overflow: hidden;
627
630
  padding-inline-start: var(--_qm-space-4);
631
+ padding-inline-end: var(--_qm-ring-reach);
632
+ margin-inline-end: calc(-1 * var(--_qm-ring-reach));
628
633
  }
629
634
  .qm-group-panel-inner > .qm-fields {
630
635
  padding-block: var(--_qm-space-2);
@@ -641,7 +646,7 @@
641
646
  }
642
647
  /* One typographic role with the editable title above. */
643
648
  .qm-card-title-static {
644
- font-size: var(--_qm-text-title);
649
+ font-size: var(--_qm-text-body);
645
650
  font-weight: var(--_qm-weight-mid);
646
651
  line-height: var(--_qm-leading-tight);
647
652
  color: var(--_qm-ink-label);
@@ -22,7 +22,6 @@ interface Props {
22
22
  card: CardModel;
23
23
  doc: Document;
24
24
  quill: Quill;
25
- index: number;
26
25
  isFirst: boolean;
27
26
  isLast: boolean;
28
27
  kinds: string[];
@@ -1,9 +1,8 @@
1
1
  <!--
2
- One content prose leaf: a Svelte mount of the codec's `createField`
3
- (VISUAL_EDITOR §Surface), which owns the PM state, the history and the
4
- per-keystroke commit, so this wrapper is wiring and registration. `addr` is a
5
- live object the parent builds, so a card reorder re-targets this leaf's commits
6
- with the caret riding the untouched view.
2
+ A Svelte mount of the codec's `createField` (VISUAL_EDITOR §Surface), which owns the
3
+ PM state, the history and the per-keystroke commit, leaving this wrapper wiring and
4
+ registration. `addr` is a live object the parent builds, so a card reorder re-targets
5
+ this leaf's commits with the caret riding the untouched view.
7
6
  -->
8
7
  <script lang="ts">
9
8
  import { onMount } from 'svelte';
@@ -41,8 +40,7 @@
41
40
  /** Ghost shown on the empty leaf: an inline leaf's resolved `default:` or
42
41
  * nothing, a body's always text (`resolveBodyGhost`). */
43
42
  placeholder?: string;
44
- /** Stable identity for the registry, stamped on the DOM node so a remount is
45
- * visible as one. */
43
+ /** Registry identity, stamped on the DOM node so a remount is visible as one. */
46
44
  leafKey: string;
47
45
  onFocus?: (addr: Addr) => void;
48
46
  onCaretMove?: (addr: Addr, pos: number) => void;
@@ -79,9 +77,9 @@
79
77
  // locale swap reaches a mounted table without remounting the leaf and losing the caret.
80
78
  const t = wording();
81
79
 
82
- /** The insert menu's live state, pushed by the leaf's trigger plugin: the menu is
83
- * the leaf's surface rather than the shell's (VISUAL_EDITOR §Chrome). A constrained
84
- * leaf never receives one, the trigger mounting on the block schema alone. */
80
+ /** Pushed by the leaf's trigger plugin: the menu is the leaf's surface rather than
81
+ * the shell's (VISUAL_EDITOR §Chrome). A constrained leaf never receives one, the
82
+ * trigger mounting on the block schema alone. */
85
83
  let slash: SlashState | undefined = $state();
86
84
 
87
85
  // The codec's own schema predicate (`createField`: `plaintext` implies `inline`), so
@@ -91,8 +89,8 @@
91
89
 
92
90
  let controller: FieldController | undefined;
93
91
 
94
- /** Take the caret: what the label's click calls. The controller's focus, not the
95
- * container's, which leaves the PM selection unplaced. */
92
+ /** What the label's click calls. The controller's focus, not the container's, which
93
+ * leaves the PM selection unplaced. */
96
94
  export function focus(): void {
97
95
  controller?.focus();
98
96
  }
@@ -153,16 +151,15 @@
153
151
  /* Containing block for the arrival wash's inset child (`core/bloom.ts`). */
154
152
  position: relative;
155
153
  }
156
- /* A leaf the inline schema does not constrain is prose rather than a cell in a row of
157
- controls, so it opens at three line boxes and grows: the ghost takes the first,
158
- leaving room under it. `1em`, not a size rung, which is the rule below's to name.
154
+ /* A leaf the inline schema does not constrain grows, and opens at one line box: the
155
+ empty leaf is the height its first line will take, so the first keystroke displaces
156
+ nothing below it. `1em`, not a size rung, which is the rule below's to name.
159
157
 
160
- On the editable rather than on the wrapper, which is what makes those lines part of
161
- the body: height the wrapper holds is height outside the `contenteditable`, where a
162
- press lands no caret. Inside it, the browser answers a press past the last line at
163
- the end of it. */
158
+ On the editable rather than on the wrapper, which is what makes that line part of the
159
+ body: height the wrapper holds is height outside the `contenteditable`, where a press
160
+ lands no caret. */
164
161
  .qm-prose-block :global(.ProseMirror) {
165
- min-height: calc(1em * var(--_qm-leading-body) * 3);
162
+ min-height: calc(1em * var(--_qm-leading-body));
166
163
  }
167
164
  /* Paper, and the withheld box is what says so. Not `block` alone: `inline` is the
168
165
  quill's to declare, so a schema omitting it on a richtext field puts a block leaf in
@@ -28,8 +28,7 @@ interface Props {
28
28
  /** Ghost shown on the empty leaf: an inline leaf's resolved `default:` or
29
29
  * nothing, a body's always text (`resolveBodyGhost`). */
30
30
  placeholder?: string;
31
- /** Stable identity for the registry, stamped on the DOM node so a remount is
32
- * visible as one. */
31
+ /** Registry identity, stamped on the DOM node so a remount is visible as one. */
33
32
  leafKey: string;
34
33
  onFocus?: (addr: Addr) => void;
35
34
  onCaretMove?: (addr: Addr, pos: number) => void;
@@ -81,13 +81,11 @@
81
81
  {/each}
82
82
  </div>
83
83
  {/if}
84
- <!-- One button, and its action is where the cursor stands: the set is walked,
85
- and its end is the exit. -->
86
84
  <button type="button" class="qm-icon-btn qm-tips-action" onclick={advance}>
87
85
  {#if isLast}
88
- {t.strings.tipsDismiss} <Icon name="x" size={GLYPH} />
86
+ {t.strings.tipsDismiss} <Icon name="trash-2" size={GLYPH} />
89
87
  {:else}
90
- {t.strings.tipsNext} <Icon name="chevron-right" size={GLYPH} />
88
+ {t.strings.tipsNext} <Icon name="arrow-right" size={GLYPH} />
91
89
  {/if}
92
90
  </button>
93
91
  </div>
@@ -100,41 +98,36 @@
100
98
  guidance is: the main card's, not a card of its own. Still in-flow, no edge, no
101
99
  lift.
102
100
 
103
- A typewritten slip, and that is the whole of the treatment: the monospace face and
104
- a warmth over the card's own surface, which together say draft rather than
105
- document. Both come off the tips rungs (`core/theme.css`), which are mixes over
106
- the surface and the ink the consumer already tuned, so the card cannot end up less
107
- legible than the editor around it.
101
+ The bottom corners take the inner rung where the card above takes the outer: a slip
102
+ is cut crisper than the thing it is tucked under, at every setting of the dial.
108
103
 
109
- The top inset takes the tuck back, so the space above the tip is the space below
110
- the foot.
111
-
112
- The reading rungs, size and leading both: a tip is a passage that wraps, so it
113
- takes the size a field's value is read at rather than the label rung a line of
114
- chrome would take. */
104
+ The insets are asymmetric so the ink is not: the top takes the tuck back, and the
105
+ bottom gives up a rung to the foot's control, whose box stands on the tap floor
106
+ rather than on its own one-line label. */
115
107
  .qm-tips {
116
108
  margin-top: calc(var(--_qm-radius) * -1);
117
- border-radius: 0 0 var(--_qm-radius) var(--_qm-radius);
118
- padding: calc(var(--_qm-space-3) + var(--_qm-radius)) var(--_qm-space-3) var(--_qm-space-3);
109
+ border-radius: 0 0 var(--_qm-radius-inner) var(--_qm-radius-inner);
110
+ padding: calc(var(--_qm-space-3) + var(--_qm-radius)) var(--_qm-space-3) var(--_qm-space-2);
119
111
  background: var(--_qm-tips-surface);
120
112
  display: flex;
121
113
  flex-direction: column;
122
114
  gap: var(--_qm-space-2);
123
- font-family: var(--_qm-font-mono);
115
+ font-family: var(--_qm-tips-font);
124
116
  font-size: var(--_qm-text-body);
125
117
  line-height: var(--_qm-leading-body);
126
118
  color: var(--_qm-tips-ink);
127
119
  }
120
+ /* One line held open, so the foot does not step up under a shorter tip. */
121
+ .qm-tips-body {
122
+ min-height: calc(var(--_qm-leading-body) * 1em);
123
+ }
128
124
  /* The rendered tip is injected DOM (the codec's `toDOM` output), so its element
129
125
  styles are `:global`: the compiler never sees these tags in the markup. Restated
130
126
  from `core/codec/prose.css`, which is where they are argued and which cannot reach
131
- here: its selectors are scoped to `.ProseMirror` and a tip is not a view. Two
132
- deliberate divergences, and both follow from the card being read rather than
133
- edited: the link takes no underline, since a hidden `href` costs its reader
134
- nothing; and `code` takes a chip where a leaf gives it a face, since the face is
135
- already the card's own and a token like `- ` has nothing else to set it apart.
136
- `pre-wrap` is what keeps the significant space inside such a token: collapsed, the
137
- chip closes on the word after it. */
127
+ here: its selectors are scoped to `.ProseMirror` and a tip is not a view. The one
128
+ divergence: `code` takes a chip and a warmer ink where a leaf gives it a face, the
129
+ face here being the card's own already. `pre-wrap` keeps the significant space
130
+ inside a token like `- `; collapsed, the chip closes on the word after it. */
138
131
  .qm-tips-body :global(p) {
139
132
  margin: 0;
140
133
  }
@@ -142,12 +135,14 @@
142
135
  font-family: inherit;
143
136
  font-size: inherit;
144
137
  white-space: pre-wrap;
145
- padding: 0 var(--_qm-space-half);
138
+ padding: 0 var(--_qm-space);
146
139
  border-radius: var(--_qm-radius-inner);
147
- background: var(--_qm-tips-chip);
140
+ background: var(--_qm-tips-fill);
141
+ color: var(--_qm-tips-ink-warm);
148
142
  }
149
143
  .qm-tips-body :global(a) {
150
144
  color: inherit;
145
+ text-decoration: underline;
151
146
  }
152
147
  .qm-tips-foot {
153
148
  display: flex;
@@ -158,49 +153,51 @@
158
153
  .qm-tips-dots {
159
154
  display: flex;
160
155
  align-items: center;
156
+ gap: var(--_qm-space-half);
161
157
  margin-right: auto;
162
158
  }
163
- /* Random access to the set, and the pitch is the tap floor: 2.5.8's spacing
164
- exception measures centre distance, which is the number the floor already fixes,
165
- so a tighter row of dots buys nothing it does not immediately owe back. Target and
166
- mark are therefore not one rectangle (the floor is the button, the dot is drawn
167
- inside it), which is also why the hover reads on the mark rather than filling the
168
- box a `.qm-icon-btn` would. */
159
+ /* Random access to the set, drawn as one cluster: a row of marks spaced to the tap
160
+ floor reads as a row of controls. So the target is the mark plus its padding, under
161
+ the floor, which 2.5.8 allows on the equivalent-control exception rather than on
162
+ the spacing one — the foot's button clears the floor and walks the whole set from
163
+ the tip the card opens on, so no tip is reachable only by a dot. State is three
164
+ steps of one ink on the recede ladder, never a second hue and never a size: a dot
165
+ that grew would move the row it sits in. */
169
166
  .qm-tips-dot {
170
- display: flex;
171
- align-items: center;
172
- justify-content: center;
173
- box-sizing: border-box;
174
- min-width: var(--_qm-tap-min);
175
- min-height: var(--_qm-tap-min);
176
- border: none;
177
- background: transparent;
178
- cursor: pointer;
179
- }
180
- /* Three steps of one ink on the recede ladder (rest, hover, current), never a
181
- second hue and never a size: a dot that grew would move the row it sits in. */
182
- .qm-tips-dot::before {
183
- content: '';
167
+ box-sizing: content-box;
184
168
  width: 5px;
185
169
  height: 5px;
170
+ padding: var(--_qm-space-half);
171
+ border: none;
186
172
  border-radius: var(--_qm-radius-pill);
187
173
  background: currentColor;
174
+ background-clip: content-box;
188
175
  opacity: var(--_qm-opacity-idle);
176
+ cursor: pointer;
189
177
  transition: opacity var(--_qm-duration-fast) var(--_qm-ease-reverse);
190
178
  }
191
- .qm-tips-dot:hover::before {
179
+ .qm-tips-dot:hover {
192
180
  opacity: var(--_qm-opacity-muted);
193
181
  }
194
- .qm-tips-dot[aria-current='true']::before {
182
+ .qm-tips-dot[aria-current='true'] {
195
183
  opacity: 1;
196
184
  }
197
- /* Box and disabled state come from `.qm-icon-btn` (controls.css); the foot's
198
- button carries a label, so it widens its inset and stays on the card's own type
199
- and ink rather than the family's body rung and the callers' own colours. */
185
+ /* Box and disabled state come from `.qm-icon-btn` (controls.css); what this
186
+ overrides is everything the family sets for a glyph among callers of its own
187
+ colour, a labelled control on a warm card wanting none of it. The hover included:
188
+ the family's neutral step would read as lent from the document above. Unlayered,
189
+ so it beats the base rule without out-specifying it. */
200
190
  .qm-tips-action {
201
191
  padding: var(--_qm-space-half) var(--_qm-space-2);
202
192
  font-family: inherit;
203
193
  font-size: inherit;
204
194
  color: inherit;
195
+ transition:
196
+ color var(--_qm-duration-fast) var(--_qm-ease-reverse),
197
+ background-color var(--_qm-duration-fast) var(--_qm-ease-reverse);
198
+ }
199
+ .qm-tips-action:hover {
200
+ color: var(--_qm-tips-ink-warm);
201
+ background: var(--_qm-tips-fill);
205
202
  }
206
203
  </style>