diff --git a/README.md b/README.md index a3149e3..0cbf95a 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ The image builds the PWA and serves it with nginx. Put an HTTPS reverse proxy in ## Using it - **Tools** (bottom bar): Text `T`, Pen `P`, Highlighter `H`, Eraser `E`, Import PDF, Undo/Redo, zoom. The pen's eraser end erases too. -- **Pages**: the "+" circles beside the active page add a page there (a quick finger fling past a free edge highlights that side's "+"); `Alt+Arrow` moves to (or creates) the neighbour. Right-click a page or use its `⋯` button to insert, resize or delete it. +- **Pages**: the "+" circles beside the active page add a page there (a quick finger flick toward a free side offers one there; turn it off under Settings → Pen & touch); `Alt+Arrow` moves to (or creates) the neighbour. Right-click a page or use its `⋯` button to insert, resize or delete it. - **Grid**: the faint background grid has one cell per active page; Settings → Appearance can hide it or subdivide it. - **Importing a PDF** into a non-empty canvas shows every free spot; click one to place the chain. Into a blank canvas it simply replaces the empty page. The Files panel can also import a PDF as a new canvas. - **View**: wheel/trackpad pans, `Ctrl`+wheel or pinch zooms, `Ctrl+0` fits the page, `Ctrl+9` shows the whole tree, space-drag or middle-drag pans. diff --git a/spec.md b/spec.md index aaedd63..c2f5235 100644 --- a/spec.md +++ b/spec.md @@ -9,7 +9,7 @@ One canvas = one tree = one `.pdf` file. - A branch can only ever continue in the direction it started (strictly linear — no sub-branches off a branch). - A trunk page can have at most one left branch and one right branch. - **PDF import**: creates a chain of pages in whatever direction you pick when placing it (up/down = extends the trunk, left/right = becomes a branch off the trunk page you clicked). Only one trunk exists per canvas — additional imported PDFs must attach as a branch off some trunk page, or extend an existing branch further outward in its established direction. -- Each free side of the active page shows a small "+" circle just off its edge; tapping it (or `Alt+Arrow`) adds a blank page there. A quick finger fling past a free edge highlights that side's "+" for a few seconds, but never adds a page by itself. +- Each free side of the active page shows a small "+" circle just off its edge; tapping it (or `Alt+Arrow`) adds a blank page there. A quick finger fling toward a free side offers a page there for a few seconds: that side's "+" pulses, or, if it is off screen, an "Add page" button appears at that edge of the screen. The fling never adds a page by itself, and can be turned off in Settings. - The canvas background is a faint grid with one cell per active page, aligned to its edges; Settings can hide it or subdivide it. - Deleting a page removes it (undo via Ctrl+Z); no permanent trash. - Default zoom: one page fills the screen. Zoom out reveals the tree. diff --git a/src/lib/components/CanvasView.svelte b/src/lib/components/CanvasView.svelte index 4746268..6031c8a 100644 --- a/src/lib/components/CanvasView.svelte +++ b/src/lib/components/CanvasView.svelte @@ -119,7 +119,8 @@ const placeSlots = $derived(tools.placing ? freeSlots(doc.tree) : []); - // A finger fling past the active page's free edge highlights the plus on that side. + // A finger fling toward a free side of the active page offers a new page there: the plus + // pulses if it's on screen, otherwise a button appears at that edge of the screen. let offer = $state(null); let offerTimer: ReturnType | undefined; @@ -130,18 +131,21 @@ } function offerAfterFling(samples: Sample[]) { + if (!settings.data.flingToAdd) return; const dir = flingDir(samples); const id = doc.activeId; - const r = id && doc.rects.get(id); const slot = dir && ghostSlots.find((s) => s.anchor === id && s.dir === dir); - if (!r || !slot) return; - // Only once that edge has come into view, i.e. the fling went past the end. + if (slot) setOffer(slot); + } + + /** The offered plus is off screen, so the offer shows as an edge-of-screen button instead. */ + const offerOffscreen = $derived.by(() => { + const r = offer && plusRect(offer); + if (!r) return false; const a = viewport.toScreen(r.x, r.y); const b = viewport.toScreen(r.x + r.width, r.y + r.height); - const edge = { left: a.x, right: b.x, up: a.y, down: b.y }[slot.dir]; - const size = slot.dir === 'left' || slot.dir === 'right' ? viewport.width : viewport.height; - if (edge >= 0 && edge <= size) setOffer(slot); - } + return b.x < 0 || b.y < 0 || a.x > viewport.width || a.y > viewport.height; + }); // Show every possible spot when an imported PDF is waiting to be placed. $effect(() => { @@ -338,7 +342,7 @@ const fingerInks = tools.inking && settings.data.fingerDraw && !palm.penSeen; // Taps on an add-page button are palm-checked like navigation: still rejected for // pen-down, palm-sized contacts and right after pen use, but not for the 5 s pen session. - const plus = target.closest('.ghost'); + const plus = target.closest('.ghost, .offer-pill'); if (e.pointerType === 'touch') { const decision = palm.evaluate(e, touchIntent(fingerInks, !!plus)); if (!decision.accept) return; // palm: ignore entirely @@ -707,6 +711,20 @@ {/if} + {#if offer && offerOffscreen} + + + {/if} + {#if eraserAt && tools.tool === 'eraser'}
{/if} @@ -834,6 +852,41 @@ transform: scale(1.3); } } + .offer-pill { + position: absolute; + display: flex; + align-items: center; + gap: 6px; + padding: 10px 16px 10px 12px; + border: 1px solid var(--accent); + border-radius: 999px; + background: var(--bg); + color: var(--accent); + box-shadow: var(--shadow); + cursor: pointer; + white-space: nowrap; + } + .offer-pill[data-side='right'] { + right: 12px; + top: 50%; + transform: translateY(-50%); + } + .offer-pill[data-side='left'] { + left: 12px; + top: 50%; + transform: translateY(-50%); + } + .offer-pill[data-side='up'] { + top: 12px; + left: 50%; + transform: translateX(-50%); + } + .offer-pill[data-side='down'] { + /* Clear of the toolbar. */ + bottom: 72px; + left: 50%; + transform: translateX(-50%); + } @media (prefers-reduced-motion: reduce) { .ghost.offer .plus { animation: none; diff --git a/src/lib/components/SettingsModal.svelte b/src/lib/components/SettingsModal.svelte index 9c8f886..a6be752 100644 --- a/src/lib/components/SettingsModal.svelte +++ b/src/lib/components/SettingsModal.svelte @@ -103,6 +103,10 @@
Draw with finger
When off, a single finger always pans; only pen and mouse draw. Once you use a pen, fingers only pan and zoom.
+
+
Swipe to add pages
A quick one-finger flick toward a free side of the active page offers to add a page there.
+ +

Palm rejection

Pen session lockout
Ignore touches while the pen is writing, and finger ink for 5 seconds after it was used.
diff --git a/src/lib/state/settings.svelte.ts b/src/lib/state/settings.svelte.ts index 92cee0b..ceb10c2 100644 --- a/src/lib/state/settings.svelte.ts +++ b/src/lib/state/settings.svelte.ts @@ -26,6 +26,8 @@ interface SettingsData { pageStyle: PageStylePref; grid: GridSettings; fingerDraw: boolean; + /** A finger fling toward a free side of the active page offers a new page there. */ + flingToAdd: boolean; palm: PalmOptions; github: GitHubSettings; /** Minutes between automatic pushes (0 = manual only). */ @@ -39,6 +41,7 @@ const DEFAULTS: SettingsData = { pageStyle: 'match', grid: { show: true, divisions: 1 }, fingerDraw: true, + flingToAdd: true, palm: DEFAULT_PALM, github: { token: '', owner: '', repo: '', branch: 'main', dir: '' }, pushInterval: 5,