Skip to content

Commit 4470f58

Browse files
Gnathonicclaude
andcommitted
docs: record gap-adjust wheel chord in input contracts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent eb0af46 commit 4470f58

2 files changed

Lines changed: 66 additions & 41 deletions

File tree

docs/INPUT-CONTRACTS.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,14 @@ Zoom settles carry a `SettleReason` (`zoom-controller.ts`): only
112112
| Pinch survivor | keeps panning | ignored until fresh press |
113113
| Tap commit | immediate | deferred (300 ms) |
114114
| Swipe-to-flip | yes (mobile setting, edge-gated) | no (panning is the scroll) |
115-
| Wheel | zoom or camera glide | zoom or (native/strip) scroll |
115+
| Wheel | zoom, gap, or camera glide | zoom, gap, or (native/strip) scroll |
116+
117+
Ctrl/meta+shift+wheel is the page-gap adjustment chord on every surface
118+
(paged writes `pagedGap`; scroll readers write `scrollGap` and sync
119+
`pageDividers`), checked before the zoom intent. Binding rule: wheel combos
120+
with a native browser meaning keep that meaning tuned for the reader
121+
(ctrl+wheel zooms, shift+wheel stays a horizontal pan); the gap chord is
122+
deliberately one no browser binds.
116123

117124
## Testing
118125

docs/superpowers/plans/2026-07-05-page-gap-234.md

Lines changed: 58 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@
2525
### Task 1: `pagedGap` setting
2626

2727
**Files:**
28+
2829
- Modify: `src/lib/settings/settings.ts:166` (type), `src/lib/settings/settings.ts:313` (default)
2930
- Test: `src/lib/settings/settings.test.ts`
3031

3132
**Interfaces:**
33+
3234
- Produces: `Settings.pagedGap: number` (default `0`) — read by Tasks 4, 5, 7 as `$settings.pagedGap`.
3335

3436
- [ ] **Step 1: Write the failing test**
@@ -55,8 +57,8 @@ Expected: FAIL — `expected undefined to be 0`
5557
In `src/lib/settings/settings.ts`, after the `scrollGap` type field (line ~166):
5658

5759
```typescript
58-
scrollGap: number; // Pixels of padding between pages in scroll modes
59-
pagedGap: number; // Gap between a paged-mode spread's two pages (image px, scales with zoom)
60+
scrollGap: number; // Pixels of padding between pages in scroll modes
61+
pagedGap: number; // Gap between a paged-mode spread's two pages (image px, scales with zoom)
6062
```
6163

6264
In `defaultSettings` after `scrollGap: 0,` (line ~313):
@@ -83,10 +85,12 @@ git commit -m "feat(settings): add pagedGap setting"
8385
### Task 2: `spreadContentSize` helper
8486

8587
**Files:**
88+
8689
- Modify: `src/lib/reader/paged-zoom-layout.ts` (append after `panEdgeState`)
8790
- Test: `src/lib/reader/paged-zoom-layout.test.ts`
8891

8992
**Interfaces:**
93+
9094
- Consumes: `Size` (already exported from this module).
9195
- Produces: `spreadContentSize(first: Size, second: Size | null, gap: number): Size` — used by Task 4 in `Reader.svelte`.
9296

@@ -159,11 +163,13 @@ git commit -m "feat(reader): spreadContentSize includes the pair gap in fit math
159163
### Task 3: Gap wheel intent helpers
160164

161165
**Files:**
166+
162167
- Modify: `src/lib/reader/zoom-math.ts` (after `normalizeWheelDelta`, line ~120)
163168
- Test: `src/lib/reader/zoom-math.test.ts`
164169
- Modify: `docs/superpowers/specs/2026-07-05-page-gap-234-design.md` (amend helper section)
165170

166171
**Interfaces:**
172+
167173
- Consumes: `normalizeWheelDelta`, `WheelAccumulator` (same module).
168174
- Produces (used by Tasks 5–7):
169175
- `MAX_PAGE_GAP = 100` (const)
@@ -212,7 +218,10 @@ describe('gapWheelSteps', () => {
212218
const acc = new WheelAccumulator(GAP_WHEEL_STEP_SIZE);
213219
let total = 0;
214220
for (let i = 0; i < 4; i++) {
215-
total += gapWheelSteps({ deltaX: 0, deltaY: -6, deltaMode: 0, timeStamp: 1000 + i * 16 }, acc);
221+
total += gapWheelSteps(
222+
{ deltaX: 0, deltaY: -6, deltaMode: 0, timeStamp: 1000 + i * 16 },
223+
acc
224+
);
216225
}
217226
expect(total).toBe(1); // 24 wheel px accumulated -> one 20px step
218227
});
@@ -302,9 +311,11 @@ git commit -m "feat(reader): gap-adjust wheel intent helpers"
302311
### Task 4: Paged rendering + fit math
303312

304313
**Files:**
314+
305315
- Modify: `src/lib/components/Reader/Reader.svelte:376-389` (pagedContentSize), `:1204-1211` (flex row), imports at top of `<script>`.
306316

307317
**Interfaces:**
318+
308319
- Consumes: `spreadContentSize` (Task 2), `$settings.pagedGap` (Task 1).
309320
- Produces: paged spread renders with `column-gap`; `pagedContentSize` includes the gap (PagedViewport re-fits automatically via its `contentSize` signature).
310321

@@ -367,10 +378,12 @@ git commit -m "feat(reader): render pagedGap between spread pages (#234)"
367378
### Task 5: Paged surface wheel chord + toast
368379

369380
**Files:**
381+
370382
- Modify: `src/lib/components/Reader/PagedViewport.svelte` (Props, imports, `handleWheel`)
371383
- Modify: `src/lib/components/Reader/Reader.svelte` (`handleGapChange` near `showNotification` line ~887; `onGapChange` prop at the `<PagedViewport` usage line ~1171)
372384

373385
**Interfaces:**
386+
374387
- Consumes: `wheelIntentIsGapAdjust`, `gapWheelSteps`, `WheelAccumulator`, `GAP_WHEEL_STEP_SIZE`, `MAX_PAGE_GAP` (Task 3); `updateSetting` from `$lib/settings`.
375388
- Produces: `PagedViewport` prop `onGapChange?: (px: number) => void`; Reader's `handleGapChange(px: number)` (reused by Task 6).
376389

@@ -406,34 +419,34 @@ and add `onGapChange` to the destructuring.
406419
Above `handleWheel`, add:
407420

408421
```typescript
409-
const gapAccumulator = new WheelAccumulator(GAP_WHEEL_STEP_SIZE);
422+
const gapAccumulator = new WheelAccumulator(GAP_WHEEL_STEP_SIZE);
410423

411-
function adjustGap(delta: number) {
412-
if (delta === 0) return;
413-
const next = Math.max(0, Math.min(MAX_PAGE_GAP, ($settings.pagedGap ?? 0) + delta));
414-
updateSetting('pagedGap', next);
415-
onGapChange?.(next);
416-
}
424+
function adjustGap(delta: number) {
425+
if (delta === 0) return;
426+
const next = Math.max(0, Math.min(MAX_PAGE_GAP, ($settings.pagedGap ?? 0) + delta));
427+
updateSetting('pagedGap', next);
428+
onGapChange?.(next);
429+
}
417430
```
418431

419432
At the TOP of `handleWheel` (before `const modifier = …`):
420433

421434
```typescript
422-
if (wheelIntentIsGapAdjust(e)) {
423-
e.preventDefault();
424-
adjustGap(gapWheelSteps(e, gapAccumulator));
425-
return;
426-
}
435+
if (wheelIntentIsGapAdjust(e)) {
436+
e.preventDefault();
437+
adjustGap(gapWheelSteps(e, gapAccumulator));
438+
return;
439+
}
427440
```
428441

429442
- [ ] **Step 2: Wire the toast in Reader**
430443

431444
In `src/lib/components/Reader/Reader.svelte`, after the `showNotification` function (line ~901):
432445

433446
```typescript
434-
function handleGapChange(px: number) {
435-
showNotification(`Page gap: ${px}px`, 'page-gap');
436-
}
447+
function handleGapChange(px: number) {
448+
showNotification(`Page gap: ${px}px`, 'page-gap');
449+
}
437450
```
438451

439452
At the `<PagedViewport` usage (line ~1171), add the prop:
@@ -468,11 +481,13 @@ git commit -m "feat(reader): ctrl+shift+scroll adjusts pagedGap in paged mode"
468481
### Task 6: Scroll surfaces wheel chord
469482

470483
**Files:**
484+
471485
- Modify: `src/lib/components/Reader/HorizontalScrollReader.svelte` (Props line ~21, imports, `handleWheel` line ~346)
472486
- Modify: `src/lib/components/Reader/VerticalScrollReader.svelte` (Props line ~21, imports, `handleWheel` line ~343)
473487
- Modify: `src/lib/components/Reader/Reader.svelte` (pass `onGapChange={handleGapChange}` to both usages, lines ~1140-1166)
474488

475489
**Interfaces:**
490+
476491
- Consumes: Task 3 helpers; Reader's `handleGapChange` (Task 5); `updateSetting` from `$lib/settings` (both files already import `settings`; extend that import).
477492
- Produces: both scroll readers accept `onGapChange?: (px: number) => void`; the chord writes `scrollGap` and syncs `pageDividers = gap > 0`.
478493

@@ -492,28 +507,28 @@ Add to `interface Props` (after `onOverlayToggle?`) and to the `$props()` destru
492507
Add above `handleWheel`:
493508

494509
```typescript
495-
const gapAccumulator = new WheelAccumulator(GAP_WHEEL_STEP_SIZE);
496-
497-
// The chord drives the continuous-mode dividers: gap > 0 means dividers on,
498-
// reaching 0 turns them off (the M toggle keeps working independently).
499-
function adjustGap(delta: number) {
500-
if (delta === 0) return;
501-
const next = Math.max(0, Math.min(MAX_PAGE_GAP, $settings.scrollGap + delta));
502-
updateSetting('scrollGap', next);
503-
const dividers = next > 0;
504-
if ($settings.pageDividers !== dividers) updateSetting('pageDividers', dividers);
505-
onGapChange?.(next);
506-
}
510+
const gapAccumulator = new WheelAccumulator(GAP_WHEEL_STEP_SIZE);
511+
512+
// The chord drives the continuous-mode dividers: gap > 0 means dividers on,
513+
// reaching 0 turns them off (the M toggle keeps working independently).
514+
function adjustGap(delta: number) {
515+
if (delta === 0) return;
516+
const next = Math.max(0, Math.min(MAX_PAGE_GAP, $settings.scrollGap + delta));
517+
updateSetting('scrollGap', next);
518+
const dividers = next > 0;
519+
if ($settings.pageDividers !== dividers) updateSetting('pageDividers', dividers);
520+
onGapChange?.(next);
521+
}
507522
```
508523

509524
In `handleWheel`, directly after the `if (!scrollContainer) return;` guard (both files):
510525

511526
```typescript
512-
if (wheelIntentIsGapAdjust(e)) {
513-
e.preventDefault();
514-
adjustGap(gapWheelSteps(e, gapAccumulator));
515-
return;
516-
}
527+
if (wheelIntentIsGapAdjust(e)) {
528+
e.preventDefault();
529+
adjustGap(gapWheelSteps(e, gapAccumulator));
530+
return;
531+
}
517532
```
518533

519534
(In VerticalScrollReader the `preventDefault` matters doubly: the surface scrolls natively, and without it the browser would treat ctrl+wheel as page zoom.)
@@ -541,9 +556,11 @@ git commit -m "feat(reader): ctrl+shift+scroll adjusts dividers in continuous mo
541556
### Task 7: Settings UI
542557

543558
**Files:**
559+
544560
- Modify: `src/lib/components/Settings/Reader/ReaderSettings.svelte` (isPaged block line ~90-104; divider slider label line ~127)
545561

546562
**Interfaces:**
563+
547564
- Consumes: `$settings.pagedGap`, `updateSetting` (already imported), `MAX_PAGE_GAP` (Task 3), `Range`/`Label` (already imported).
548565

549566
- [ ] **Step 1: Add the paged gap slider**
@@ -574,10 +591,10 @@ Inside the `{#if isPaged}` block, after the page-view-mode `</div>` (line ~103),
574591
Change the divider-size label (line ~127) to:
575592

576593
```svelte
577-
<Label class="text-gray-900 dark:text-white">
578-
Divider size: {$settings.scrollGap}px
579-
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400">(Ctrl+Shift+Scroll)</span>
580-
</Label>
594+
<Label class="text-gray-900 dark:text-white">
595+
Divider size: {$settings.scrollGap}px
596+
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400">(Ctrl+Shift+Scroll)</span>
597+
</Label>
581598
```
582599

583600
and change that slider's `max={100}` to `max={MAX_PAGE_GAP}`.
@@ -599,14 +616,15 @@ git commit -m "feat(settings): page gap slider + chord hints"
599616
### Task 8: Input contracts doc + full verification
600617

601618
**Files:**
619+
602620
- Modify: `docs/INPUT-CONTRACTS.md:115` (wheel row) + new paragraph after the matrix
603621

604622
- [ ] **Step 1: Update the contracts doc**
605623

606624
Change the wheel row of the per-surface policy matrix:
607625

608626
```markdown
609-
| Wheel | zoom, gap, or camera glide | zoom, gap, or (native/strip) scroll |
627+
| Wheel | zoom, gap, or camera glide | zoom, gap, or (native/strip) scroll |
610628
```
611629

612630
After the matrix table, add:

0 commit comments

Comments
 (0)