Show add-page indicator as a bar and palm-check its taps
The add-page ghost is now a thin bar along the active page's free edge instead of a full page-sized block. Pointer taps on it go through the stage gesture handling, so touches are evaluated by the palm rejector with the same 'draw' policy as writing, including being cut off if the pen shows up mid-tap. The native click only handles keyboard activation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
138b797506
commit
8295894b32
1 changed files with 83 additions and 15 deletions
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import CirclePlusIcon from '@lucide/svelte/icons/circle-plus';
|
||||
import DownloadIcon from '@lucide/svelte/icons/download';
|
||||
import EllipsisIcon from '@lucide/svelte/icons/ellipsis';
|
||||
import ArrowUpIcon from '@lucide/svelte/icons/arrow-up';
|
||||
|
|
@ -118,6 +117,17 @@
|
|||
return rectBeside(a, slot.dir, s);
|
||||
}
|
||||
|
||||
/** Hit area of the add-page bar: a strip hugging the anchor's free edge. */
|
||||
function barRect(slot: Slot): Rect | null {
|
||||
const a = doc.rectOf(slot.anchor);
|
||||
if (!a) return null;
|
||||
const t = 20 / viewport.scale;
|
||||
if (slot.dir === 'right') return { x: a.x + a.width, y: a.y, width: t, height: a.height };
|
||||
if (slot.dir === 'left') return { x: a.x - t, y: a.y, width: t, height: a.height };
|
||||
if (slot.dir === 'down') return { x: a.x, y: a.y + a.height, width: a.width, height: t };
|
||||
return { x: a.x, y: a.y - t, width: a.width, height: t };
|
||||
}
|
||||
|
||||
/** Run a tree change without the anchor page jumping on screen. */
|
||||
function keepAnchor(anchor: string, fn: () => void) {
|
||||
const before = doc.rectOf(anchor);
|
||||
|
|
@ -227,6 +237,7 @@
|
|||
| { kind: 'draw'; pointerId: number; pointerType: string; pageId: string; rec: StrokeRecorder }
|
||||
| { kind: 'erase'; pointerId: number; pointerType: string; hits: Map<string, Set<string>> }
|
||||
| { kind: 'pan'; pointerId: number; lastX: number; lastY: number; startX: number; startY: number; moved: boolean; tap: boolean }
|
||||
| { kind: 'add'; pointerId: number; pointerType: string; startX: number; startY: number; slot: Slot }
|
||||
| { kind: 'pinch' };
|
||||
|
||||
let gesture: Gesture | null = null;
|
||||
|
|
@ -280,8 +291,10 @@
|
|||
|
||||
// Fingers ink only when enabled and no pen has been used; otherwise they pan/zoom.
|
||||
const fingerInks = tools.inking && settings.data.fingerDraw && !palm.penSeen;
|
||||
// Taps on an add-page bar get the same palm policy as writing.
|
||||
const bar = target.closest<HTMLElement>('.ghost');
|
||||
if (e.pointerType === 'touch') {
|
||||
const decision = palm.evaluate(e, fingerInks ? 'draw' : 'navigate');
|
||||
const decision = palm.evaluate(e, fingerInks || bar ? 'draw' : 'navigate');
|
||||
if (!decision.accept) return; // palm: ignore entirely
|
||||
touches.set(e.pointerId, p);
|
||||
if (touches.size >= 2) {
|
||||
|
|
@ -292,6 +305,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
if (bar && e.button === 0) {
|
||||
e.preventDefault();
|
||||
const slot = { anchor: bar.dataset.anchor!, dir: bar.dataset.dir as Dir };
|
||||
gesture = { kind: 'add', pointerId: e.pointerId, pointerType: e.pointerType, startX: p.x, startY: p.y, slot };
|
||||
capture(e.pointerId);
|
||||
return;
|
||||
}
|
||||
|
||||
const onPage = pageAt(p.x, p.y);
|
||||
const inEditor = !!target.closest('.cm-editor');
|
||||
// Pressing anywhere outside the text leaves the editor (preventDefault
|
||||
|
|
@ -303,10 +324,10 @@
|
|||
e.button === 1 ||
|
||||
spaceDown ||
|
||||
(e.pointerType === 'touch' && (!fingerInks || !onPage)) ||
|
||||
(e.pointerType === 'mouse' && e.button === 0 && !onPage && !target.closest('.ghost, .slot'));
|
||||
(e.pointerType === 'mouse' && e.button === 0 && !onPage && !target.closest('.slot'));
|
||||
if (wantsPan) {
|
||||
if (onPage && !inEditor) doc.activeId = onPage;
|
||||
gesture = { kind: 'pan', pointerId: e.pointerId, lastX: p.x, lastY: p.y, startX: p.x, startY: p.y, moved: false, tap: inEditor || !!target.closest('.ghost, .slot') };
|
||||
gesture = { kind: 'pan', pointerId: e.pointerId, lastX: p.x, lastY: p.y, startX: p.x, startY: p.y, moved: false, tap: inEditor || !!target.closest('.slot') };
|
||||
if (e.pointerType !== 'touch' || !gesture.tap) {
|
||||
if (e.pointerType !== 'touch') e.preventDefault();
|
||||
capture(e.pointerId);
|
||||
|
|
@ -375,6 +396,12 @@
|
|||
return;
|
||||
}
|
||||
|
||||
if (gesture.kind === 'add') {
|
||||
// Dragging off the bar is not a tap.
|
||||
if (Math.hypot(p.x - gesture.startX, p.y - gesture.startY) > 12) gesture = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (gesture.kind === 'erase') {
|
||||
eraseAt(e);
|
||||
return;
|
||||
|
|
@ -402,7 +429,9 @@
|
|||
if (!gesture || gesture.pointerId !== e.pointerId) return;
|
||||
const g = gesture;
|
||||
gesture = null;
|
||||
if (g.kind === 'draw') {
|
||||
if (g.kind === 'add') {
|
||||
if (e.type === 'pointerup' && (g.pointerType !== 'touch' || palm.evaluate(e).accept)) addPage(g.slot.anchor, g.slot.dir);
|
||||
} else if (g.kind === 'draw') {
|
||||
live = null;
|
||||
if (e.type === 'pointerup') doc.addStroke(g.pageId, g.rec.stroke);
|
||||
} else if (g.kind === 'erase') {
|
||||
|
|
@ -564,18 +593,17 @@
|
|||
{/each}
|
||||
|
||||
{#each ghostSlots as slot (slot.anchor + slot.dir)}
|
||||
{@const r = slotRect(slot)}
|
||||
{@const r = barRect(slot)}
|
||||
{#if r}
|
||||
<!-- Pointer taps go through the palm-checked gesture; onclick only serves the keyboard. -->
|
||||
<button
|
||||
class="ghost"
|
||||
data-anchor={slot.anchor}
|
||||
data-dir={slot.dir}
|
||||
style="left:{r.x}px; top:{r.y}px; width:{r.width}px; height:{r.height}px"
|
||||
title="Add page ({slot.dir})"
|
||||
onclick={() => addPage(slot.anchor, slot.dir)}
|
||||
>
|
||||
<span class="plus" style="--icon: {Math.min(r.width, r.height) * 0.14}px">
|
||||
<CirclePlusIcon size="100%" strokeWidth={1.25} />
|
||||
</span>
|
||||
</button>
|
||||
onclick={(e) => e.detail === 0 && addPage(slot.anchor, slot.dir)}
|
||||
></button>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
|
|
@ -696,7 +724,49 @@
|
|||
top: 0;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
.ghost,
|
||||
.ghost {
|
||||
position: absolute;
|
||||
background: none;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* The visible bar: 4 screen px thick, 6 px off the page edge. */
|
||||
.ghost::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background: var(--ghost-border);
|
||||
border-radius: calc(2px / var(--s));
|
||||
transition: background 0.12s;
|
||||
}
|
||||
.ghost[data-dir='left']::before,
|
||||
.ghost[data-dir='right']::before {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: calc(4px / var(--s));
|
||||
}
|
||||
.ghost[data-dir='up']::before,
|
||||
.ghost[data-dir='down']::before {
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: calc(4px / var(--s));
|
||||
}
|
||||
.ghost[data-dir='right']::before {
|
||||
left: calc(6px / var(--s));
|
||||
}
|
||||
.ghost[data-dir='left']::before {
|
||||
right: calc(6px / var(--s));
|
||||
}
|
||||
.ghost[data-dir='down']::before {
|
||||
top: calc(6px / var(--s));
|
||||
}
|
||||
.ghost[data-dir='up']::before {
|
||||
bottom: calc(6px / var(--s));
|
||||
}
|
||||
.ghost:hover::before,
|
||||
.ghost:focus-visible::before {
|
||||
background: var(--accent);
|
||||
}
|
||||
.slot {
|
||||
position: absolute;
|
||||
display: grid;
|
||||
|
|
@ -709,7 +779,6 @@
|
|||
padding: 0;
|
||||
transition: background 0.12s, color 0.12s;
|
||||
}
|
||||
.ghost:hover,
|
||||
.slot:hover {
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
|
|
@ -720,7 +789,6 @@
|
|||
color: var(--accent);
|
||||
border-color: color-mix(in srgb, var(--accent) 50%, transparent);
|
||||
}
|
||||
.plus,
|
||||
.arrow {
|
||||
display: grid;
|
||||
width: var(--icon);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue