Files
remanso/docs/superpowers/specs/2026-04-20-react-native-migration-design.md
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

237 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Remanso React Native Migration — Design Spec
**Date:** 2026-04-20
**Status:** Approved
## Overview
Migrate Remanso from a Vue 3 web app to a fully native iOS + Android app built with Expo (React Native). The primary motivation is native feel: fluid stack animations, swipe-back gestures, and native navigation chrome. The scope is a full replacement of the web app.
## Tech Stack
| Concern | Current (Vue) | React Native |
| ---------------- | ---------------------- | ---------------------------------------------------------- |
| Framework | Vue 3 + Vite | Expo SDK (managed workflow) |
| Routing | Vue Router | Expo Router (file-system routing over React Navigation v7) |
| State | Pinia | Zustand |
| Server state | TanStack Vue Query | TanStack Query (same library) |
| Styling | DaisyUI + Tailwind CSS | NativeWind v4 |
| Local DB | PouchDB (IndexedDB) | Expo SQLite |
| Simple KV store | localStorage | MMKV |
| Auth | OAuth redirects | expo-auth-session |
| GitHub API | @octokit/rest | @octokit/rest (unchanged) |
| i18n | vue-i18n | react-i18next |
| Date utils | date-fns | date-fns (unchanged) |
| Markdown content | markdown-it (DOM) | react-native-webview |
| Fonts | CSS custom properties | expo-font |
## Navigation Structure
Expo Router generates a React Navigation v7 tree from the file system. The stacked note pattern — the core native feel win — maps to a nested Stack navigator where each backlink push adds a note with a native slide animation and swipe-left pops it.
```
Root Stack
├── Auth screens (unauthenticated, no tab bar)
│ ├── Home / Welcome
│ ├── GitHub OAuth callback
│ └── ATProto OAuth callback
└── Authenticated App — Bottom Tabs
├── Tab: Feed (Stack)
│ ├── Repo picker
│ └── Note Stack (nested Stack)
│ ├── Note (root)
│ ├── Note (pushed via backlink) ← swipe-back to pop
│ └── Note (pushed via backlink) ← swipe-back to pop
├── Tab: Inbox / Drafts / Todos (Stack)
│ └── Note list → Note detail
├── Tab: Public Notes (Stack)
│ ├── Public note list
│ └── Public note detail
└── Tab: Settings (Stack)
├── Settings root
└── Font picker (presented as modal)
```
History and Spaced Repetition are accessible as sections within the Feed tab or as additional tabs — to be decided during implementation.
## Data Layer
PouchDB is replaced by **Expo SQLite** for structured data and **MMKV** for simple key-value preferences.
### Expo SQLite schema
```sql
CREATE TABLE IF NOT EXISTS github_tokens (
id TEXT PRIMARY KEY,
access_token TEXT NOT NULL,
refresh_token TEXT,
expires_at INTEGER
);
CREATE TABLE IF NOT EXISTS atproto_sessions (
id TEXT PRIMARY KEY,
did TEXT NOT NULL
-- full session_json stored in Expo SecureStore keyed by id
);
CREATE TABLE IF NOT EXISTS saved_repos (
id TEXT PRIMARY KEY,
user TEXT NOT NULL,
repo TEXT NOT NULL,
files_json TEXT NOT NULL,
cached_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS history (
id TEXT PRIMARY KEY,
user TEXT NOT NULL,
repo TEXT NOT NULL,
note_path TEXT NOT NULL,
content TEXT NOT NULL,
created_at INTEGER NOT NULL
);
```
The existing `DataApi` interface (`add`, `update`, `remove`, `get`, `getAll`) is re-implemented as a thin wrapper over Expo SQLite. No ORM — plain async SQL with `CREATE TABLE IF NOT EXISTS` on first open handles schema initialization at this scale.
Sensitive data (access tokens, refresh tokens, ATProto sessions) is stored in **Expo SecureStore** rather than plain SQLite. SQLite holds only metadata and content cache.
### MMKV
Replaces localStorage for user settings: font family, font size, theme preference. Synchronous reads, no async overhead.
### No Web Worker
Database calls move to the main thread via Expo SQLite's async API. The performance concern that justified the Web Worker on web does not apply on mobile.
## Auth Flows
### GitHub OAuth
The existing auth server (`api.remanso.space/auth/github`) handles code exchange — the client flow is unchanged:
1. `expo-auth-session` opens an in-app browser tab with the GitHub authorize URL
2. OAuth redirect captured by Expo's deep link handler
3. Code sent to `api.remanso.space/auth/github?code=...` for token exchange
4. Access token + refresh token stored in Expo SecureStore
5. Token refresh logic (15-minute pre-expiry check) stays the same; HTTP calls use `fetch`
### ATProto / Bluesky OAuth
`@atproto/oauth-client-browser` is browser-only (IndexedDB, `window.crypto`, browser redirects). There is no official React Native client. A custom client (~200300 lines) is implemented using:
- `expo-auth-session` for the OAuth redirect flow (PKCE)
- `expo-crypto` for PKCE code verifier/challenge generation
- Expo SecureStore for session persistence
- The same Bluesky API endpoints as the browser client
This keeps ATProto auth fully client-side, consistent with the app's current architecture.
## State Management
Zustand replaces Pinia. The store shape is identical to `userRepo.store.ts`:
```ts
const useRepoStore = create<RepoState>((set, get) => ({
user: "",
repo: "",
files: [],
userSettings: null,
needToLogin: false,
setRepo: (user, repo) => set({ user, repo }),
loadFiles: async () => {
/* Octokit call */
},
loadSettings: async () => {
/* MMKV read */
}
}))
```
TanStack Query handles all GitHub API server state (file fetching, README, repo listing) — same library, same patterns as today.
## Styling
NativeWind v4 provides Tailwind utility classes in React Native. DaisyUI is web-only and has no React Native equivalent — all component styling (buttons, cards, modals) is hand-written using NativeWind utilities.
The two DaisyUI themes (`retro` light, `coffee` dark) are translated into a custom NativeWind theme in `tailwind.config.ts` with the same color tokens. System appearance (`useColorScheme`) drives theme selection.
Font customization uses `expo-font` for loading custom fonts and React Native's `fontFamily` style prop, replacing the CSS custom property approach.
## Markdown Rendering
The markdown-it pipeline (KaTeX, Mermaid, shiki, tabler icons, html5-media, GitHub alerts, checkboxes) runs in the React Native JS context unchanged — same code, same output. The resulting HTML string is passed to a `NoteWebView` component built on `react-native-webview`.
`NoteWebView` is a native UIView/View wrapper around a WebView engine. It is a React Native component — not a web app. The surrounding app (navigation chrome, tab bar, headers, settings, auth screens) is 100% native. Only the note content pane renders HTML. This is the standard pattern for rich content in React Native (used by GitHub Mobile, Linear, and others).
The WebView communicates back to the native layer via `postMessage` for:
- Internal note link taps (trigger React Navigation push)
- External URL taps (open in system browser)
- Backlink detection events
## Project Structure
```
src/
├── app/ # Expo Router — file-system screens
│ ├── _layout.tsx # Root Stack navigator
│ ├── index.tsx # Home / Welcome
│ └── (tabs)/ # Authenticated tab navigator
│ ├── _layout.tsx
│ ├── feed/ # Feed + Note Stack screens
│ ├── inbox/
│ ├── public/
│ └── settings/
├── modules/ # Feature domains (mirrors current structure)
│ ├── note/ # Note models, hooks, caching
│ ├── repo/ # Zustand store, Octokit service
│ ├── user/
│ │ ├── auth/ # GitHub + ATProto OAuth hooks
│ │ └── fonts.ts # Font downloading (was utils/downloadFont.ts)
│ ├── card/ # Spaced repetition
│ ├── history/ # Edit history
│ ├── atproto/ # Custom ATProto OAuth client, DID resolution
│ └── post/ # ts-rest API client (unchanged)
├── components/ # Shared UI components
├── rendering/ # Markdown pipeline — first-class module
│ ├── pipeline.ts # markdown-it setup + plugin registration
│ ├── plugins/ # Custom plugins
│ │ ├── html5-media.ts
│ │ ├── regexp.ts
│ │ └── tabler-icons.ts
│ └── NoteWebView.tsx # react-native-webview wrapper
├── lib/ # Shared low-level, stateless helpers
│ ├── text.ts # slugify, noteTitle, displayLanguage
│ ├── links.ts # link.ts + youtube.ts
│ ├── encoding.ts # decodeBase64ToUTF8
│ └── notifications.ts # notif.ts
├── hooks/ # React hooks
├── data/ # Expo SQLite wrapper (same DataApi interface)
├── locales/ # i18n strings (unchanged)
└── constants/ # Theme tokens, note width constants
```
## Key Risks
1. **ATProto custom OAuth client** — no official SDK; requires careful PKCE implementation and session lifecycle management.
2. **DaisyUI → NativeWind component styling** — no 1:1 mapping; all themed components need to be rebuilt. Most labor-intensive non-feature work.
3. **NoteWebView ↔ native bridge** — link tap handling and scroll coordination between the WebView and the native Stack navigator require careful implementation to feel seamless.
4. **Mermaid in WebView** — Mermaid is JS-heavy; initial render may be slow on lower-end Android. May need lazy rendering or a timeout fallback.
## Out of Scope
- PWA / service worker (not applicable to native)
- Web Worker (replaced by async SQLite)
- Comlink (not applicable)
- Server-side rendering