Three issues found in review, all around switching to a different note
while the editor is already open (via a note-link or backlink chip):
- TiptapEditor's onTransaction/onUpdate had no guard against writing
state after the component is torn down. Removing a focused editor's
DOM node fires a native blur, which ProseMirror turns into one last
transaction synchronously during that same teardown pass, crashing
with state_unsafe_mutation. Deferred the onTransaction write to a
microtask (checked against a destroyed flag) so it lands after
Svelte's current pass finishes; guarded onUpdate synchronously.
- openEditor/openNoteById in NotesView.svelte awaited a network fetch
before ever showing the editor, for every note open (not just
link-driven navigation) — a regression for an offline-first app, and
the source of a real race: two rapid note-link clicks left two
fetches in flight with no ordering guarantee, so whichever resolved
last won, not whichever was clicked last. Reverted to opening
instantly from local/IndexedDB data (with a request-id guard against
the residual race), and instead fetch backlinks lazily in
NoteEditor.svelte only if not already present — mirroring the
existing attachments-fetch pattern already in that file. Also
applied the same post-teardown guard to that pre-existing attachments
effect, since remounting on note-switch newly exposes it to the same
class of race.
- FormattingToolbar's closeDropdowns() cleared search results but never
cancelled the pending debounce timer, so a stale query could
repopulate results after the dropdown had already closed. Added the
missing clearTimeout, plus a try/catch around the search fetch for
consistency with the pattern used elsewhere in the app.
Verified: pnpm check (0 errors), full unit suite (316/316), and the
existing e2e suite including all note-linking scenarios (19/19) all
pass against these changes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bumps deps to clear all 69 pnpm audit findings (nodemailer 8->9 for the
raw-option SSRF/arbitrary-file-read CVE; svelte, hono/express/qs chain,
linkify-it, and the rest resolved within their existing semver ranges
via a lockfile refresh). Caps the previously-unbounded cookie override
at <1 so it can't silently jump to the API-incompatible 2.x line again.
Hardens the MCP upload_image tool, which fetched an arbitrary caller-
supplied URL and stored whatever content-type came back with no checks:
- validates every resolved DNS record (not just the first) against
private/loopback/link-local/multicast ranges, then pins the actual
connection to the validated address so a second, unvalidated lookup
at connect time can't be used for DNS-rebinding SSRF
- rejects non-image content-types and enforces the same 10MB cap as
the manual upload endpoint
- adds X-Content-Type-Options: nosniff to both attachment-serving
routes as defense in depth
Also validates the admin role field server-side (previously trusted
the client-supplied string), and fixes two incidental regressions
surfaced by the dependency bump: StarterKit now bundles its own
Link/Underline extensions (disabled to avoid duplicate registration),
and FormattingToolbar could call methods on an already-destroyed
editor instance during the markdown-mode toggle race.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Separates --hover-wash from --border so the softened dark-mode border grey
no longer collapses hover/active tint washes to near-invisibility (measured
5% wash on --bg-surface: 4.7 -> 17.0 RGB distance). Light mode is unaffected
since --hover-wash falls back to var(--border) in :root.
Also fixes stale/incorrect claims surfaced by the final review: the
checkbox rule and missing CSS variables in CLAUDE.md, the "warm parchment"
description of dark mode in FEATURES.md, the chip contrast claim in the
palette design doc (chips are 3:1 UI, not 4:1 text), and adds a measured
lightness-floor test guarding against chips regressing to card lightness.
The mobile editor is fullscreen with no backdrop to tap, so the Android
back button / iOS back swipe now dismisses it instead of leaving the
page. The editor pushes a shallow-routing history entry on mount and
closes when it is popped; UI closes pop the entry themselves so history
stays clean. NotesView's hash rewriting now goes through SvelteKit's
replaceState so the router keeps handling popstate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C1N5Vi12zTez7LNqRvYjUD
Address review feedback on the search-results preservation change:
- reconcileSearchResults now keeps a result absent from the canonical
notes store (a server hit not yet loaded locally, e.g. from another
device) instead of dropping it, so valid matches no longer vanish
mid-session. Locally deleted/left notes are still pruned at their call
sites; trashed notes are still dropped.
- SearchBar commits searchQuery only once its results arrive, so the view
no longer flashes an empty state over the current notes while a request
is in flight, and searchQuery stays consistent with searchResults on a
failed request.
- Expand reconcile unit tests (keep-absent, trashed-drop, mixed order,
empty) and add an e2e test asserting notes stay visible during an
in-flight search.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keep canonical notes separate from active search results so periodic
reloads no longer replace the filtered view.
Cancel superseded requests and ignore stale responses to prevent older
queries from overwriting newer results. Reconcile results with canonical
notes so updates propagate and removed or trashed notes disappear.
Add unit and end-to-end coverage for reconciliation, background sync,
and out-of-order search responses.
Co-authored-by: Google Gemini <gemini@google.com>
Intercept touch checkbox changes before TipTap's focusing handler and
update the task-item transaction directly. This prevents the mobile
keyboard from starting to open while preserving checkbox state changes.
Strengthen the mobile regression test to assert that checking and
unchecking emit no editor focus events.
Co-authored-by: Codex <codex@openai.com>
Add interactive-widget=resizes-content to the viewport meta so the
Android keyboard shrinks the layout viewport through the native inset
path. Chromium's visualViewport.height can under-report the visible
height by the system nav-bar height, which left the editor toolbar
floating above the keyboard; the native resize path is accurate. iOS
ignores the attribute and keeps using the visualViewport tracking in
NoteEditor.
Position toolbar dropdown portals with window.innerHeight instead of
visualViewport.height: CSS bottom on position:fixed and
getBoundingClientRect() both use layout-viewport coordinates, so
mixing in the visual-viewport height pushed dropdowns behind the
keyboard while it was open.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Restore portal dropdown test IDs so the formatting e2e suite can
find heading and link controls again.
Let the mobile formatting toolbar pan horizontally, keep dropdown
portals from drifting by closing them on scroll or resize, and use
visualViewport height for keyboard-aware positioning.
Remove the app-wide maximum-scale lock, keep editor focus targets at
16px on mobile to avoid input auto-zoom, and move toolbar scrollbar
styling to a top-level scoped style block.
Align the remove-link action with the destructive design token, use
the standard 150ms editor slide-up animation, and replace
save/restore body overflow locking with a resilient body class.
Make editor full-screen natively on mobile with back button. Add
visualViewport tracking to keep bottom toolbars above the software keyboard.
disable pull-to-refresh for native PWA feel
prevent auto-zoom on input focus in mobile Safari
prevent background scrolling when editor is open
prevent touch scroll on editor toolbars to stop background scrolling
hide visual gap at bottom of editor when iOS keyboard opens
move note actions to top-right menu on mobile devices
wrap background colors in mobile menu to reduce width
make formatting toolbar scrollable on mobile and extract dropdowns
remove empty else block causing svelte compilation error
add missing data-dropdown attributes to fix click interception in toolbar portals
keep mobile keyboard open when using toolbar menus and prevent right-edge overflow
Co-authored-by: Google Gemini <gemini@google.com>
Use e.currentTarget instead of e.target so the reference is always the
contenteditable div, not a descendant <a> tag. Use el.contains() instead
of strict equality to avoid linkifying while focus is still inside the
editable element.
https://claude.ai/code/session_01VZceWPhHMHCA6SELBdCPfN
When a contenteditable loses focus, the onblur handler immediately modified
innerHTML to linkify URLs. On mobile, this DOM mutation causes a layout reflow
between touchstart and click, making the browser cancel taps on the adjacent
delete button. Deferring via requestAnimationFrame ensures the click event
fires before the DOM is modified.
https://claude.ai/code/session_01VZceWPhHMHCA6SELBdCPfN
- Add noreferrer to rel attribute to prevent Referer header leaks
- Strip newlines from pasted text to prevent checklist corruption
- Add aria-placeholder for screen reader accessibility
- Dismiss LinkPopover on scroll/resize
- Stop keydown propagation on links in NoteCard preview
The reactive {#html} binding inside contenteditable replaced innerHTML on
every keystroke, destroying the cursor and causing text duplication.
Instead, use a use:editableContent action that sets innerHTML on mount
and only syncs when the element is NOT focused. Linkification happens
on blur, matching Google Keep's behavior. Update e2e tests to blur
before asserting link visibility.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add entity decoding (& < > ") to unlinkifyHtml to
prevent double-encoding on contenteditable edit cycles
- Fix indentation of LinkPopover import and state declaration
- Move LinkPopover inside root div
- Add unit test for entity decoding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On installed PWAs, closing the editor by tapping the overlay would cause
the browser's delayed synthetic click to land on the NoteCard underneath,
immediately reopening a different note. Fixed by keeping a transparent
shield in the DOM for 400ms after close to absorb the ghost click.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two issues fixed:
1. Attachment operations (upload, feature, remove) only updated the local
editor state, not the notes store. Before auto-save, this was masked
because saveAndClose always called updateNote, whose PATCH response
refreshed the store. With auto-save skipping redundant saves, the store
was never updated with attachment changes. Now attachment handlers
update the store directly.
2. TiptapEditor fires onUpdate on init with round-tripped markdown that
can differ from the original. Added an autoSaveReady guard to absorb
this initial normalization as the baseline, preventing spurious saves
when reopening existing notes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Snapshot fields at start of performSave to avoid race with user edits
during async save (fields could change between await and lastSaved update)
- Only update lastSaved tracking on successful save, so failed saves
are retried on the next debounce cycle
- Stop restoring title after Untitled fallback in autoSave(), which was
re-triggering the auto-save effect
- Capture note count before opening editor in empty-note e2e test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Notes are now automatically saved 2 seconds after the user stops editing,
preventing data loss when the browser tab is closed without closing the
editor first. A beforeunload handler provides an additional safety net.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a reusable `use:tooltip` Svelte action as a drop-in replacement for
native title attributes. Styled to match the retro parchment theme with
JetBrains Mono font, dark background, and 400ms hover delay.
Replace all title attributes in NoteEditor, NoteCard, and NotesView
with the custom tooltip. The action supports top/bottom positioning
and auto-clamps to viewport edges.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a checkbox icon next to the "Add a crumb..." button that opens a new
note directly in checklist mode, similar to Google Keep's quick-create
bar. The NoteEditor accepts a new initialChecklistMode prop.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The popover was opening upward (bottom-full) which got clipped by the
NoteCard's overflow-hidden. Changed to open downward (top-full) so it
stays within the card's visual area.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add vertical dots (⋮) overflow menu to NoteEditor bottom action bar
- New checklist actions: "Delete checked" and "Uncheck all"
- Move secondary actions to menu: checklist toggle, markdown toggle,
version history, move to trash
- Promote archive to the action bar for quick access
- Keep in bar: color picker, image upload, share, archive, ⋮, close
- Menu renders as fixed-position portal to escape card overflow-hidden
- Click-outside handler closes the menu
- Update e2e tests to use overflow menu for checklist/markdown toggles
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove unused flushSync import
- Fix removeItem single-item branch: emit change and trigger reactivity
- Remove misleading role="button" + tabindex from drag handle
- Use anchored regex for checked detection instead of line.includes('[x]')
in NoteCard and public share page (prevents false positive on item text)
- Fix NoteCard preview E2E test: scope assertion to specific card, use
unique title to avoid collision
- Restore arrow key navigation E2E test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update checklist preview parsers to detect 2-space child prefix and
render indented items with extra left padding.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Checklist component changes:
- Import logic from checklist utility module
- Tab/Shift+Tab indent/outdent with sibling adoption
- Indented rendering (pl-8) for child items in active section
- Done section groups checked children under read-only parent labels
- Enter on child creates sibling at same indent level
- Backspace on parent orphans children to top-level
- Directional lock drag: horizontal = indent/outdent, vertical = reorder
(uses window capture-phase listeners + pending indent applied after
DnD finalize to avoid corrupting svelte-dnd-action internal state)
- flushSync for state updates from window event listeners
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two issues caused checked items to revert after closing:
1. loadNotes() blindly overwrote the store with notes.set(), so a stale
response from the periodic sync (every 30s) could replace a just-saved
note with older server data. Now merges by version number, keeping the
higher-versioned note.
2. Checklist component ignored external content prop changes (from sync or
history restore). Added $effect that re-parses items when the content
prop changes from an external source, using lastEmitted tracking to
distinguish self-originated vs external updates.
https://claude.ai/code/session_01TdPpATUkmdY7DhBkmqrvWs
ArrowUp/ArrowDown move focus between active checklist inputs.
At list boundaries, focus stays on the current item (no wrap).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The addItem() focus logic used positional indexing (inputs[afterIndex+1])
but afterIndex came from the full items[] array while DOM inputs only
render active (unchecked) items. On reopened notes, checked items are
interleaved in the parsed content, causing the index offset to be wrong.
Fix: use data-item-id attribute for targeted querySelector lookup.
Test: reopened note with interleaved checked/unchecked items.
Introduced in e5688c9 (active/done split).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Wrap readFile in try/catch, return 404 for missing attachment files
- Remove unused CollaboratorPopover import from NoteCard
- Make darkMode a proper $state rune for reactivity (add svelte-runes
polyfill for Vitest so $state works in unit tests)
- Add error toasts for failed share link toggle (non-2xx responses)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Switch NoteCard, NoteEditor, and ColorPicker from static NOTE_COLORS
lookups to getNoteColor(color, getIsDarkMode()), so inline background
styles react to the active theme. Also fix the theme preference button
type cast in settings/preferences to resolve the svelte-check error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Strip server paths from public attachment responses (new PublicAttachment type)
- Use $page.url.origin instead of window.location.origin (SSR-safe)
- Fix toggle switch left-4.5 → left-[1.125rem] (valid Tailwind)
- Add aria-label to SharingIndicator button for screen readers
- Add opportunistic pruning to IP rate limiter to prevent unbounded growth
- Reduce shared attachment cache to 1 hour (respect share revocation)
- Update OpenAPI schema with PublicAttachment type
- Add unit test asserting server paths are not leaked
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Create SharingIndicator component with Globe/Users icon priority
- NoteCard shows Globe icon (gold) for public shares, Users for collabs
- NoteEditor share button reflects public link state
- Share token propagated through store on create/revoke
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add toggle switch, URL display, and copy button for public share
links above the existing collaborators section. Gold border when
active, retro-styled toggle with hard shadows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add max-h-[17rem] overflow-hidden on note cards so checklist notes
don't force text notes in the same grid row to stretch with
truncated content.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On mobile, tapping a note card could trigger action buttons in the
editor that rendered at the same coordinates (ghost click). Suppress
pointer events on both toolbars for 150ms after mount — matching the
pop-in animation — so the synthetic click passes harmlessly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The editor card's onpointerdown stopPropagation prevented the
FormattingToolbar's document-level pointerdown listener from
detecting clicks outside the dropdown. Since both overlay handlers
already use e.target === e.currentTarget to ignore bubbled events,
no stopPropagation is needed on the card.
https://claude.ai/code/session_018DYwSysqysMmaRi5XkqTpo
Pointer events are never synthetically re-dispatched from touch
(unlike mouse events), so the tap that opened the editor cannot
ghost-click the overlay. This eliminates the mobile first-load bug
without relying on an arbitrary delay.
Both pointerdown and pointerup must target the overlay itself to
trigger close, which also handles the text-selection-outside-editor
edge case.
https://claude.ai/code/session_018DYwSysqysMmaRi5XkqTpo
On first page load on mobile, the synthetic mousedown from a tap can
arrive ~300ms after the touch event — well after a single
requestAnimationFrame (~16ms). Replace the rAF-based overlayReady
guard with a 400ms timestamp check from mount time to reliably
ignore stale synthetic events on slow first loads.
https://claude.ai/code/session_018DYwSysqysMmaRi5XkqTpo