Commit Graph

241 Commits

Author SHA1 Message Date
Julien Calixte
54c52feeba refactor(notes): return failure status from pullLatest
Callers had to read back the side-effected status ref to know why a
pull failed, which also broke TS narrowing on the freshness badge
handler. Return { raw, failureStatus } so the reason flows through
the return value.
2026-05-25 21:25:54 +02:00
Julien Calixte
6a0f0d08d2 feat(notes): add image upload button when editing a markdown note 2026-05-25 21:17:46 +02:00
Julien Calixte
01bb4b8c70 refactor(github): extract putRaw and add uploadBinaryFile 2026-05-25 21:17:38 +02:00
Julien Calixte
151a4d9137 fix(github): auto-retry GitHub calls on 401 and expose unauthorized state
fetchLatestSha and queryFileContent were silently catching every error
(including expired-token 401s) and returning null, so the freshness
badge could stay on "offline" with no UI hint that re-auth was needed.
Wrap both calls in runWithAuthRetry to recover from refreshable tokens
transparently, and switch fetchLatestSha to a tagged result so the
freshness hook can distinguish auth failure from network failure.
2026-05-17 21:10:20 +02:00
Julien Calixte
7a2ccc1bd1 fix(github): silence contents endpoint deprecation warnings
Use RFC 6570 reserved expansion ({+path}) so slashes stay literal in
the request URL, and pin the contents call to API version 2026-03-10
to clear the Sunset: 2028-03-10 header on the deprecated 2022-11-28
version. The only breaking change in 2026-03-10 (submodule type in
directory listings) doesn't affect us — fetchLatestSha is per-file
and the array branch already short-circuits.
2026-05-17 00:34:03 +02:00
Julien Calixte
2994839ee5 feat(offline): track structured failure details during caching
Replace the failure counter with a list of typed failures (fetch,
build, save, readme, unknown) carrying path, sha, message, and
affected doc ids so callers can surface diagnostics instead of just
a count.
2026-05-17 00:07:13 +02:00
Julien Calixte
0a2445a06d fix(repo): paginate repo list alphabetically server-side
Switch from /search/repositories (best-match order) to /user/repos
with sort=full_name so infinite scroll appends in A–Z order instead
of injecting new pages into earlier letters.
2026-05-16 23:59:04 +02:00
Julien Calixte
f3ed5e063f perf(offline): parallelise note caching with batched writes
Fetch blobs with a concurrency of 8 instead of one at a time, batch
PouchDB writes via bulkUpdate, filter already-cached files up front,
and run the README fetch in parallel with the worker loop.
2026-05-16 23:48:06 +02:00
Julien Calixte
0ad747b69f fix(offline): pre-cache home README under its synthetic key
The home page reads README via getCachedMainReadme, which uses
Note-{owner}-{repo}-README, not the blob SHA. Cache-all only wrote
SHA-keyed entries, so the homepage README was empty offline.
2026-05-16 23:31:58 +02:00
Julien Calixte
c5236b2587 fix(offline): don't abort cache loop on a single fetch failure
A single null from queryFileContent silently exited cacheAllNotes,
leaving every later note uncached while the success toast still fired.
Skip the failing file, count failures, and surface them via errorMessage.
2026-05-16 23:31:42 +02:00
Julien Calixte
de7ac8d096 fix(freshness): hold checking state for 400ms minimum
Cached GitHub responses resolved so fast that the spinner state was
overwritten before Vue could render it, so clicking the badge from
"Not checked" looked like nothing happened.
2026-05-16 18:38:59 +02:00
Julien Calixte
9d1b6552cb feat(markdown): add hover download buttons on rendered SVGs
Attach SVG and PNG (×2) download buttons to every rendered SVG (TikZ,
Mermaid, …) once post-render finishes. Exports always use the original
colors with a white background baked in, regardless of theme.
2026-05-16 12:17:04 +02:00
Julien Calixte
572a05962c fix(markdown): load TikZ font stylesheet to keep text aligned
The obsidian-tikzjax bundle emits SVG referencing Computer Modern font
families like cmbx7 but ships the @font-face declarations in a separate
styles.css. Without it the browser substitutes a default font whose
glyph widths don't match TeX's, leaving visible gaps between each
kerned text run. Lazy-load the stylesheet alongside the engine on
first TikZ render so cache hits and misses both see correct fonts.
2026-05-15 16:25:55 +02:00
Julien Calixte
33285d723a refactor(markdown): extract post-render effects into composable
Four components duplicated the watch -> nextTick -> listenToClick -> runTikz
shape. Collapse to useMarkdownPostRender, which also accepts optional flags
for Mermaid, Shikiji and image hydration so StackedNote no longer needs its
own inline conditional chain.
2026-05-15 15:48:12 +02:00
Julien Calixte
813f16655c Merge branch 'main' of ssh://git.apoena.dev:22222/remanso-space/remanso 2026-05-15 15:40:21 +02:00
Julien Calixte
bf19e982fe feat(markdown): render TikZ fenced blocks to SVG
Adds a markdown-it extractor for ```tikz blocks that emits a sanitized
placeholder, plus a runTikz() that lazy-loads the obsidian-tikzjax 0.5.2
bundle from jsdelivr on first encounter, sanitizes the produced SVG with
DOMPurify, and caches it in PouchDB keyed by sha256(source) so repeat
views are instant and offline-capable.
2026-05-15 15:38:48 +02:00
Julien Calixte
816c3687d8 fix(auth): clear stale credential error after github re-auth
The 401 flag and cached repo list were module-level and only reset
after a 20-min stale window, so re-authenticating left the
"credentials are invalid or expired" message pinned on. Watch the
access token: reset state and refetch on change. Also await
saveCredentials before redirecting so refs are settled.
2026-05-14 13:04:44 +02:00
Julien Calixte
aad07184fd fix(freshness): surface silent failures when pulling latest
queryFileContent threw on octokit errors (stale SHA 404, expired token,
network blip) and the rejection bubbled up unhandled through pullLatest
and onBadgeClick, leaving the badge stuck on "Outdated" with no log or
toast. Wrap the octokit call, log on failure, clear the cached SHA so
the next click re-resolves it, and show an error toast.

Also fix a dead `if (!user || !repo) { null }` that did nothing.
2026-05-06 22:02:50 +02:00
Julien Calixte
e425be5c96 refactor(freshness): drop time-based stale-known status
The 2-minute timer + tick ref decayed verified to stale-known and rendered
a clock icon, but the user can always click the badge to re-check. Removing
the timer simplifies the hook and the badge has one fewer visual state.
2026-05-04 23:53:48 +02:00
Julien Calixte
84803c45dd refactor(scroll): clean up debug overlay and pass anchor by param
Removes the temporary on-screen scroll diagnosis panel and the global
window.__scrollAtClick stash. The anchor scrollTop is now captured
synchronously at addStackedNote entry and threaded through
scrollToFocusedNote and scrollToNoteElement to scrollToElement, so no
state survives across calls — nothing to reset on repo or page change.
2026-05-04 23:02:12 +02:00
Julien Calixte
a526a9f6af fix(scroll): snap to click-time scrollTop before smooth scroll
Capture mainApp.scrollTop synchronously when addStackedNote runs and
snap the scroll back to that value before scrollIntoView fires, so
the smooth scroll begins from where the user actually tapped rather
than from a position drifted by momentum or async work.
2026-05-04 19:57:00 +02:00
Julien Calixte
c88340d5f1 chore(debug): add temporary scroll overlay for mobile diagnosis 2026-05-04 19:02:35 +02:00
Julien Calixte
2f05b93f51 fix(stacked-note): size mobile notes with svh to stabilize scroll target
Dynamic viewport units rescale every note when the mobile address bar
grows or shrinks, shifting the scroll target by the address-bar height
mid-flight. Small viewport units stay constant across address-bar
transitions so the smooth scroll lands where it was aimed.
2026-05-04 18:45:45 +02:00
Julien Calixte
cc266eac7c refactor(scroll): delegate note scroll to scrollIntoView
Native scrollIntoView reads the element position at scroll time and
picks the right scrollable ancestor itself, sidestepping iOS Safari
quirks with scrollTo on overflow containers and visual-viewport shifts.
2026-05-04 18:29:05 +02:00
Julien Calixte
be006f08b4 fix(stacked-note): align mobile scroll target to element rect
Replace the (index + 1) * clientHeight math and 80ms setTimeout with a
scrollToElement helper that reads getBoundingClientRect inside rAF, so
the smooth scroll starts from the user's actual position even when the
note is freshly mounted.
2026-05-04 18:15:10 +02:00
Julien Calixte
efe9c01e63 chore(github-content): pin api version on fetchLatestSha request
Silences the @octokit/request deprecation warning that prints whenever
the unversioned /repos/{owner}/{repo}/contents/{path} call fires.
2026-05-03 23:37:24 +02:00
Julien Calixte
d31c774ace feat(stacked-note): surface note freshness and guard saves on conflict
Adds a Tabler-icon badge in the stacked-note action bar showing whether
the loaded copy still matches GitHub HEAD (verified / outdated / offline
/ checking / unknown / stale-known). The save flow now re-checks before
the PUT and opens a conflict modal when GitHub has moved on, with three
explicit choices: discard local edits and pull, overwrite anyway, or
cancel. Race-condition 409s from the PUT itself are routed through the
same modal.
2026-05-03 23:32:54 +02:00
Julien Calixte
d8a59467a0 refactor(github-content): expose conflict info and add latest-sha lookup
updateFile/createFile now return { sha, conflict } so 409/422 from GitHub
can drive a UI flow instead of being swallowed as a generic save error.
Also adds fetchLatestSha(path) for cheap freshness checks against HEAD.
2026-05-03 23:32:37 +02:00
Julien Calixte
e715fb02d3 fix(markdown): cache Shikiji init promise to avoid race on parallel callers
The boolean guard flipped synchronously before the async plugin load
resolved, so concurrent callers (e.g. multiple stacked non-markdown
notes mounting on reload) returned early and rendered before
markdown-it-shikiji was attached to the shared md instance. Cache the
in-flight promise instead so all callers await the same resolution.
2026-04-30 11:03:29 +02:00
Julien Calixte
7b4c7947aa fix: remove bottom padding 2026-04-29 22:06:58 +02:00
Julien Calixte
68022971cd refactor(notes): restore fixed mobile heights for scroll math
Re-pin .note and .stacked-note to 100dvh on mobile and bring back the
container height in useResizeContainer so (index + 1) * height has a
reachable scroll target. Switch the polled scroll helper to that same
formula instead of offsetTop.
2026-04-29 11:32:23 +02:00
Julien Calixte
3e9418285f refactor(notes): let mobile notes size to content 2026-04-29 11:09:31 +02:00
Julien Calixte
17f015b686 fix(notes): wait for stacked note element before scrolling on mobile
A single nextTick is not enough for a freshly added stacked note to be
in the DOM, so the mobile scroll target was computed against a null
element. Poll with requestAnimationFrame (mirroring scrollToHashInNote)
and use offsetTop, with an (index + 1) * height fallback.
2026-04-29 10:52:07 +02:00
Julien Calixte
adb1bd5945 fix: fix height on mobile 2026-04-29 10:34:46 +02:00
Julien Calixte
cf5567de7c refactor(notes): use options object for renderCodeFile params 2026-04-27 20:36:46 +02:00
Julien Calixte
9d6f70546e feat(notes): render code files with Shikiji syntax highlighting
Non-markdown files opened as stacked notes are now highlighted using
the existing markdown-it-shikiji pipeline (4-backtick fence wrapping)
with a h1 filename heading. Edit controls are hidden for code files.
Adds alloy language grammar and a fileLanguage utility mapping
extensions to Shikiji language IDs.
2026-04-27 19:57:15 +02:00
Julien Calixte
812f393283 design: reduce padding for pre in tabs 2026-04-27 18:22:48 +02:00
Julien Calixte
37b39a6d96 design: box for tabs instead 2026-04-27 15:34:12 +02:00
Julien Calixte
df8bda0130 feat(markdown): render tabs as DaisyUI radio input pattern
Use @mdit/plugin-tab custom renderers to emit DaisyUI tabs-lift
structure (radio inputs + tab-content divs) instead of unstyled
default output. CSS-driven, no JS required.
2026-04-27 15:28:10 +02:00
Julien Calixte
74491a45a9 fix(repoList): prevent duplicate entries from concurrent loadMore calls
Add isLoading guard so concurrent fetches are rejected, and include
isLoading in canLoadMore so vInfiniteScroll waits before firing again.
2026-04-27 10:33:31 +02:00
Julien Calixte
da4fada8a1 fix(repoList): handle Bad credentials error from GitHub API
Catch 401 responses in useRepos loadMore and expose hasCredentialError,
then show a sign-in prompt in RepoList instead of an unhandled rejection.
2026-04-27 10:32:37 +02:00
Julien Calixte
836b480ea6 fix(navigation): resolve clicked anchor when target is a nested element
A click on a child of an <a> (e.g. nested <strong>, <em>, <code>, icon)
made event.target a non-anchor, so getAttribute('href') returned null
and the handler bailed without preventDefault. The browser then
performed the native navigation, which for relative links like
'../note.md' resolved against the current /:user/:repo URL and the SPA
re-routed treating the destination as a new repo.
2026-04-26 13:58:48 +02:00
Julien Calixte
181ffd1e5c feat(navigation): smooth scroll for in-note anchor links
Pure-fragment links (#heading) used to fall through to the browser's
default jump. Handle them in the click listener and scope the lookup
to the same stacked note so identical heading ids in other notes
don't win, with smooth scroll behavior to match cross-note anchors
into already-stacked notes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 09:59:12 +02:00
Julien Calixte
c00065ce4a refactor(navigation): scrollToFocusedNote takes an options object
Smooth-scroll for the anchor jump when the target note is already
stacked, instant otherwise. While threading the new flag, the four
positional params got hard to read, so collapse them into
{ noteId, notes, hash, smoothHash } and update all call sites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 09:56:32 +02:00
Julien Calixte
4ce8c30649 fix(navigation): support anchor fragments in note links
Links like `path/to/note.md#heading` previously errored with "Note not
found" because the full href (including `#hash`) was matched against
file paths. Split the fragment off in the link handler, plumb it through
the event bus, and scroll the matching heading into view once the
target note is in place. Headings now get GitHub-style ids via
markdown-it-anchor + github-slugger so the anchors actually exist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 09:40:30 +02:00
Julien Calixte
e03ff49764 fix(mobile): restore overflow-y and unstick readme on vertical scroll
- Restore explicit overflow-y:auto on #main-app for mobile (removed in
  63f5d64) — implicit coercion from overflow-x:auto is not reliable in
  all Safari/WebKit versions.
- Override position:sticky on .readme to position:relative on mobile.
  The desktop sticky (left:0) is correct for horizontal scroll, but on
  mobile vertical scroll it pinned the 100dvh-tall readme across the
  entire viewport, hiding all stacked notes behind it.
2026-04-24 23:42:22 +02:00
Julien Calixte
19495ddf0c feat(scroll): use smooth scrollTo instead of direct property assignment 2026-04-23 18:07:44 +02:00
Julien Calixte
63bc3f4d5d fix(mobile): scroll #main-app instead of body on mobile
body/html have overflow:hidden so scrollTop is a no-op on them.
#main-app is the actual scroll container; use overflow-y:auto on
mobile and target it directly in scrollToNote and the scroll listener.
2026-04-23 18:01:30 +02:00
Julien Calixte
449a16f791 fix(layout): prevent document-level scroll-y when stacked notes overflow
Contain horizontal overflow within #main-app instead of leaking to the
document, which caused a horizontal scrollbar to consume viewport height
and trigger an unwanted vertical scrollbar. Also fix note pane height
to use 100% instead of 100vh, and switch useResizeContainer to minWidth
so the flex container can grow when the window is wider than the notes.
Add a window resize listener to keep the value accurate on resize.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 23:52:03 +02:00
Julien Calixte
8d9134a062 perf: cache repo list with 20-minute stale time
Hoist useRepos state to module scope so all callers share one instance, and skip re-fetching until data is older than 20 minutes.
2026-04-06 14:59:12 +02:00