From 5d5d86c533e9d7bd7818bb60c2256146b60f0b4e Mon Sep 17 00:00:00 2001 From: agent Date: Sun, 27 Sep 2026 09:37:15 +0000 Subject: [PATCH 01/16] Fix imported PDFs not rendering in the Docker image nginx has no MIME type for .mjs, so the pdf.js worker was served as application/octet-stream and browsers refused to load it. Every imported PDF page then showed "Source PDF ... is not available", which looked like missing files. Serve .mjs as text/javascript. The page background now also tells the two failures apart: "not stored on this device" when the source bytes are missing, and "could not display" (with the error logged) when pdf.js fails. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VsorPV3JeJRoZ1mD1pdvtL --- docker/nginx.conf | 6 ++++++ src/lib/components/PdfBackground.svelte | 22 ++++++++++++++-------- src/lib/pdf/render.ts | 5 ++++- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index f86d149..bee09c1 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -1,3 +1,9 @@ +# This file is included at http level, so this adds to nginx's MIME table. +# Browsers refuse to run module scripts (the pdf.js worker) served as octet-stream. +types { + text/javascript mjs; +} + server { listen 80; server_name _; diff --git a/src/lib/components/PdfBackground.svelte b/src/lib/components/PdfBackground.svelte index 4c070ba..3d3494e 100644 --- a/src/lib/components/PdfBackground.svelte +++ b/src/lib/components/PdfBackground.svelte @@ -1,5 +1,5 @@ - -{#if failed} -
Source PDF “{origin.name}” is not available
+ +{#if failure === 'missing'} +
Source PDF “{origin.name}” is not stored on this device
+{:else if failure === 'error'} +
Could not display “{origin.name}” (see the browser console)
{/if} From 3d29255d0c6f16e2ffd219dca9d2061fbd167758 Mon Sep 17 00:00:00 2001 From: agent Date: Sun, 27 Sep 2026 21:31:12 +0000 Subject: [PATCH 14/16] Add background grid settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Appearance now has a toggle for the background grid and a choice of subdivisions (none, 2×2 up to 16×16) that split each page-sized cell into fainter lines. Subdivisions are left out when zoomed so far out that they would be under 8 px apart. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GaoZtJQZwLXkEgWs8twCia --- src/app.css | 2 ++ src/lib/components/CanvasView.svelte | 28 ++++++++++++++++++------- src/lib/components/SettingsModal.svelte | 14 +++++++++++++ src/lib/state/settings.svelte.ts | 9 ++++++++ 4 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/app.css b/src/app.css index 76440c5..912d602 100644 --- a/src/app.css +++ b/src/app.css @@ -61,6 +61,7 @@ --shadow: 0 1px 2px rgba(0, 0, 0, 0.06), 0 4px 16px rgba(0, 0, 0, 0.08); --page-line: rgba(0, 0, 0, 0.18); --grid-line: rgba(0, 0, 0, 0.05); + --grid-line-minor: rgba(0, 0, 0, 0.028); --ghost: rgba(0, 0, 0, 0.045); --ghost-border: rgba(0, 0, 0, 0.12); --radius: 6px; @@ -88,6 +89,7 @@ --shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 6px 20px rgba(0, 0, 0, 0.35); --page-line: rgba(255, 255, 255, 0.16); --grid-line: rgba(255, 255, 255, 0.04); + --grid-line-minor: rgba(255, 255, 255, 0.022); --ghost: rgba(255, 255, 255, 0.04); --ghost-border: rgba(255, 255, 255, 0.14); color-scheme: dark; diff --git a/src/lib/components/CanvasView.svelte b/src/lib/components/CanvasView.svelte index 5b2b27f..4746268 100644 --- a/src/lib/components/CanvasView.svelte +++ b/src/lib/components/CanvasView.svelte @@ -30,11 +30,28 @@ let stage: HTMLDivElement; const cssVars = pageCssVars(); - // Background grid: one cell per active page, aligned to its edges (A4 at the origin if none). - const grid = $derived.by(() => { + // Background grid: one cell per active page, aligned to its edges (A4 at the origin if none), + // optionally split into fainter subdivisions. + const gridStyle = $derived.by(() => { + const g = settings.data.grid; + if (!g.show) return ''; const r = (doc.activeId && doc.rects.get(doc.activeId)) || { x: 0, y: 0, ...A4 }; const s = viewport.scale; - return { w: r.width * s, h: r.height * s, x: viewport.x + r.x * s, y: viewport.y + r.y * s }; + const w = r.width * s; + const h = r.height * s; + const lines = (color: string) => [ + `linear-gradient(to right, ${color} 1px, transparent 1px)`, + `linear-gradient(to bottom, ${color} 1px, transparent 1px)` + ]; + const images = lines('var(--grid-line)'); + const sizes = [`${w}px ${h}px`, `${w}px ${h}px`]; + // Leave out subdivisions once they'd be too dense to read. + const n = g.divisions; + if (n > 1 && Math.min(w, h) / n >= 8) { + images.push(...lines('var(--grid-line-minor)')); + sizes.push(`${w / n}px ${h / n}px`, `${w / n}px ${h / n}px`); + } + return `background-image: ${images.join(', ')}; background-size: ${sizes.join(', ')}; background-position: ${viewport.x + r.x * s}px ${viewport.y + r.y * s}px`; }); // ---- autosave -------------------------------------------------------- @@ -605,7 +622,7 @@ class:panning class:erasing={tools.tool === 'eraser'} bind:this={stage} - style="background-size: {grid.w}px {grid.h}px; background-position: {grid.x}px {grid.y}px" + style={gridStyle} onpointerdown={onPointerDown} onpointermove={onPointerMove} onpointerup={onPointerUp} @@ -760,9 +777,6 @@ user-select: none; -webkit-user-select: none; background-color: var(--bg-canvas); - background-image: - linear-gradient(to right, var(--grid-line) 1px, transparent 1px), - linear-gradient(to bottom, var(--grid-line) 1px, transparent 1px); } .stage :global(.cm-content) { user-select: text; diff --git a/src/lib/components/SettingsModal.svelte b/src/lib/components/SettingsModal.svelte index 6b92676..9c8f886 100644 --- a/src/lib/components/SettingsModal.svelte +++ b/src/lib/components/SettingsModal.svelte @@ -83,6 +83,20 @@ +
+
Background grid
Faint lines behind the pages, one cell per active page.
+ +
+
+
Grid subdivisions
Split each page-sized cell into a finer grid for sketching and lining things up.
+ +
{:else if tab === 'input'}

Pen & touch

diff --git a/src/lib/state/settings.svelte.ts b/src/lib/state/settings.svelte.ts index 4fa5724..92cee0b 100644 --- a/src/lib/state/settings.svelte.ts +++ b/src/lib/state/settings.svelte.ts @@ -6,6 +6,12 @@ import { DEFAULT_PALM, type PalmOptions } from '$lib/ink/palm'; export type ThemePref = 'system' | 'light' | 'dark'; export type PageStylePref = 'paper' | 'match'; +export interface GridSettings { + show: boolean; + /** Minor lines per page cell side (1 = page-sized cells only). */ + divisions: number; +} + export interface GitHubSettings { token: string; owner: string; @@ -18,6 +24,7 @@ export interface GitHubSettings { interface SettingsData { theme: ThemePref; pageStyle: PageStylePref; + grid: GridSettings; fingerDraw: boolean; palm: PalmOptions; github: GitHubSettings; @@ -30,6 +37,7 @@ interface SettingsData { const DEFAULTS: SettingsData = { theme: 'system', pageStyle: 'match', + grid: { show: true, divisions: 1 }, fingerDraw: true, palm: DEFAULT_PALM, github: { token: '', owner: '', repo: '', branch: 'main', dir: '' }, @@ -48,6 +56,7 @@ function load(): SettingsData { return { ...DEFAULTS, ...saved, + grid: { ...DEFAULTS.grid, ...saved.grid }, palm: { ...DEFAULTS.palm, ...saved.palm }, github: { ...DEFAULTS.github, ...saved.github } }; From 2df91b561de29a46d588d4e020144b4f5f09cd35 Mon Sep 17 00:00:00 2001 From: agent Date: Sun, 27 Sep 2026 21:31:54 +0000 Subject: [PATCH 15/16] Bring spec and README up to date The spec still listed undo, the eraser, colours and smoothing as missing. Describe the ink engine and palm rejection as built (draw vs navigate intents, settings toggles, cancelled pointers), the plus buttons, fling offer, grid and page size dialog, note that sync is GitHub-only, and replace the stale open item with the real ones. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GaoZtJQZwLXkEgWs8twCia --- README.md | 3 ++- spec.md | 32 ++++++++++++++++++++++---------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 824eec3..a3149e3 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ 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 grey "+" tiles next to the active page add a page there; `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 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. +- **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. - **Sync**: Settings → Sync. Use a fine-grained token with *Contents: read & write* on the repo. Edits autosave locally; pushes happen every N minutes and on `Ctrl+S` / the sync button. Conflicts: newer wins. diff --git a/spec.md b/spec.md index 5125de1..aaedd63 100644 --- a/spec.md +++ b/spec.md @@ -9,10 +9,11 @@ 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. -- Hidden/unopened neighbor pages render as a gray tile with a "+" in a circle; clicking (or a shortcut) instantiates them as real pages. +- 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. +- 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. -- Pages default to A4, or auto-match the aspect ratio of an imported PDF page; resizable manually otherwise. +- Pages default to A4, or auto-match the aspect ratio of an imported PDF page; resizable manually otherwise (page menu: presets, or a custom size in mm). ## 2. Flatten Algorithm (tree → linear page order, e.g. for PDF page order / print) @@ -45,7 +46,7 @@ Canvases (PDF files) live in a **file tree** you define, e.g. `school/english/le ## 5. Sync & Storage -- Repo-backed (GitHub/GitLab), one file per canvas. +- Repo-backed (GitHub for now; GitLab not implemented), one file per canvas. - PWA: offline-first, caches locally, explicit save/sync pushes to repo. - Multi-device: git pull/push is the sync mechanism (not real-time collab). @@ -54,7 +55,7 @@ Canvases (PDF files) live in a **file tree** you define, e.g. `school/english/le - **Frontend**: SvelteKit, deployed on your VPS, installable as PWA. - **Ink input**: Pointer Events + pressure (your existing artifact — pending). - **PDF read/write**: pdf.js + pdf-lib. -- **Storage**: Git repo via GitHub/GitLab API (or local backend proxy holding the token). +- **Storage**: IndexedDB locally; Git repo via the GitHub REST API (token in the browser, no proxy). ## 7. Confirmed Product Features @@ -79,12 +80,23 @@ Source: user-provided `palm-rejection-test.html` test harness. The acceptance po - A `touch` pointer is rejected if any of: a pen stroke is active or was active within the last ~5s, its contact ellipse (`width`/`height`) exceeds ~35px (palm heel vs. fingertip), it's within 150ms of the last pen activity, or a pen is currently hovering (armed lockout before it even touches down). - A touch stroke already in progress gets cut off mid-stroke if pen activity appears. -**Gaps before this is the real ink engine** (currently a raster test, not vector): -- `draw()` writes straight to canvas via `ctx.lineTo`/`stroke` with pressure-scaled line width, and throws points away. Needs to become `recordPoint()`: push `{x, y, pressure, tiltX, tiltY, t}` into a `Stroke.points[]` array per stroke; rendering = replaying that array. This is what makes strokes resizable/erasable/undoable and exportable to both the PDF Ink-annotation layer and the JSON attachment layer. -- `tiltX`/`tiltY` exist on the pen's `PointerEvent` but aren't read anywhere yet — needed for tilt support. -- No smoothing yet (raw straight segments) — fine as a v1, can layer in curve smoothing later without changing point storage. -- No undo/redo, multi-color, eraser yet — needs a `Stroke[]` array per page's ink layer with add/remove, which undo/redo just pops/pushes. +**How the app applies it** (`src/lib/ink/palm.ts`): +- Each touch is evaluated with an intent. `draw` (finger ink) gets the full policy above. `navigate` (pan, pinch, and taps on canvas buttons such as the add-page "+") skips the 5 s pen session and the hover lockout, so fingers can move around and tap between pen strokes; pen-down, wide contacts and the 150 ms window still reject it. +- Once a pen has been used, fingers only navigate. "Draw with finger" in Settings turns finger ink off entirely. +- Each rule (pen session, wide contact, timing, hover) can be switched off in Settings → Pen & touch. +- An interrupted pointer (`pointercancel`) drops the stroke or erase in progress instead of committing it. + +**Ink engine (implemented)**: +- `StrokeRecorder.recordPoint()` stores `x, y, pressure, tiltX, tiltY, t` per sample in a flat `Stroke.points` array (stride 6); rendering replays it. Tilt is recorded but not yet used for rendering. +- Outlines are smoothed with `perfect-freehand`; mouse and finger strokes simulate pressure. +- Pen, highlighter (translucent, no thinning) and stroke eraser, with configurable colour palettes; the pen's eraser end erases too. +- Undo/redo covers ink, erasing and page-tree changes. +- Strokes are written to the PDF both as Ink annotations and as JSON attachments. ## 10. Open Items -- [ ] Nothing blocking — ready to move into implementation planning (component breakdown, page-tree data structures in Svelte, PDF encode/decode module) +- [ ] Conflict copies: sync currently lets the newer version win silently; keep the losing version as a separate file. +- [ ] Tell the user when a new version of the app has been deployed (the service worker updates, but the open tab keeps old code until reload). +- [ ] CI that runs `check`, `test` and `build` on every pull request. +- [ ] Use recorded tilt for rendering. +- [ ] GitLab sync. From 5fb11e49286c29149cf9b1bba7dcabce675ef47c Mon Sep 17 00:00:00 2001 From: agent Date: Sun, 27 Sep 2026 21:43:09 +0000 Subject: [PATCH 16/16] Let the page fling work anywhere, with a setting to turn it off MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A fling toward a free side of the active page now offers a page at any zoom or pan position, not only once that edge is on screen. If the side's plus is visible it pulses as before; otherwise an "Add page" button appears at that edge of the screen, tapped through the same palm-checked gesture as the plus. Settings → Pen & touch gets "Swipe to add pages" (on by default). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GaoZtJQZwLXkEgWs8twCia --- README.md | 2 +- spec.md | 2 +- src/lib/components/CanvasView.svelte | 71 +++++++++++++++++++++---- src/lib/components/SettingsModal.svelte | 4 ++ src/lib/state/settings.svelte.ts | 3 ++ 5 files changed, 71 insertions(+), 11 deletions(-) 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,