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.
This commit is contained in:
Julien Calixte
2026-04-24 23:42:22 +02:00
parent 19495ddf0c
commit e03ff49764
10 changed files with 72 additions and 57 deletions

View File

@@ -9,21 +9,21 @@ Migrate Remanso from a Vue 3 web app to a fully native iOS + Android app built w
## 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 |
| 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
@@ -137,14 +137,18 @@ Zustand replaces Pinia. The store shape is identical to `userRepo.store.ts`:
```ts
const useRepoStore = create<RepoState>((set, get) => ({
user: '',
repo: '',
user: "",
repo: "",
files: [],
userSettings: null,
needToLogin: false,
setRepo: (user, repo) => set({ user, repo }),
loadFiles: async () => { /* Octokit call */ },
loadSettings: async () => { /* MMKV read */ },
loadFiles: async () => {
/* Octokit call */
},
loadSettings: async () => {
/* MMKV read */
}
}))
```
@@ -165,6 +169,7 @@ The markdown-it pipeline (KaTeX, Mermaid, shiki, tabler icons, html5-media, GitH
`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

View File

@@ -25,6 +25,12 @@ const { isATProtoReady } = useATProtoLogin()
overflow-x: auto;
}
@media screen and (max-width: 768px) {
#main-app {
overflow-y: auto;
}
}
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.25s;

View File

@@ -1,12 +1,5 @@
<script lang="ts" setup>
import {
computed,
nextTick,
onMounted,
onUnmounted,
toRefs,
watch
} from "vue"
import { computed, nextTick, onMounted, onUnmounted, toRefs, watch } from "vue"
import HeaderNote from "@/components/HeaderNote.vue"
import SkeletonLoader from "@/components/SkeletonLoader.vue"
@@ -241,6 +234,7 @@ $header-height: 40px;
.flux-note {
.readme {
padding: 0 0.75rem;
position: relative;
}
}

View File

@@ -50,22 +50,14 @@ const fontSize = computed({
<div class="font-change">
<div>
<label for="title-font" class="font-label">t</label>
<select
id="title-font"
class="select"
v-model="titleFont"
>
<select id="title-font" class="select" v-model="titleFont">
<option v-for="font in sortedFontFamilies" :key="font" :value="font">
{{ font }}
</option>
</select>
<label for="body-font" class="font-label">p</label>
<select
id="body-font"
class="select"
v-model="bodyFont"
>
<select id="body-font" class="select" v-model="bodyFont">
<option v-for="font in sortedFontFamilies" :key="font" :value="font">
{{ font }}
</option>
@@ -75,11 +67,7 @@ const fontSize = computed({
<theme-swap />
<label for="font-size" class="font-label">s</label>
<select
id="font-size"
class="select"
v-model="fontSize"
>
<select id="font-size" class="select" v-model="fontSize">
<option v-for="size in fontSizes" :key="size" :value="size">
{{ size }}
</option>

View File

@@ -6,7 +6,10 @@ const goHome = () => router.push({ name: "Home" })
</script>
<template>
<button class="btn btn-ghost btn-circle btn-lg text-base-content" @click="goHome">
<button
class="btn btn-ghost btn-circle btn-lg text-base-content"
@click="goHome"
>
<img src="/favicon.png" alt="Remanso icon" class="remanso-logo" />
</button>
</template>

View File

@@ -16,7 +16,9 @@ const isMarkdown = (filename?: string) => filename?.endsWith(".md") ?? false
const yieldToMain = () =>
"scheduler" in globalThis
? (globalThis as unknown as { scheduler: { yield: () => Promise<void> } }).scheduler.yield()
? (
globalThis as unknown as { scheduler: { yield: () => Promise<void> } }
).scheduler.yield()
: new Promise<void>((r) => setTimeout(r, 0))
export const useComputeBacklinks = () => {
@@ -43,9 +45,10 @@ export const useComputeBacklinks = () => {
}
const fileBacklinkId = generateId(DataType.BacklinkNote, file.sha)
const fileBacklink = await data.get<DataType.BacklinkNote, BacklinkNote>(
fileBacklinkId
)
const fileBacklink = await data.get<
DataType.BacklinkNote,
BacklinkNote
>(fileBacklinkId)
if (fileBacklink) {
continue
}

View File

@@ -37,11 +37,20 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
_persistFonts() {
if (!this.userSettings) return
try {
const { chosenTitleFont, chosenBodyFont, chosenFontSize, chosenFontFamily } =
this.userSettings
const {
chosenTitleFont,
chosenBodyFont,
chosenFontSize,
chosenFontFamily
} = this.userSettings
localStorage.setItem(
`remanso:fonts:${this.user}:${this.repo}`,
JSON.stringify({ chosenTitleFont, chosenBodyFont, chosenFontSize, chosenFontFamily })
JSON.stringify({
chosenTitleFont,
chosenBodyFont,
chosenFontSize,
chosenFontFamily
})
)
} catch {
// ignore
@@ -61,7 +70,8 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
}
if (Object.keys(lsFonts).length) {
if (!this.userSettings) this.userSettings = { $type: DataType.UserSettings }
if (!this.userSettings)
this.userSettings = { $type: DataType.UserSettings }
Object.assign(this.userSettings, lsFonts)
}

4
src/shims-vue.d.ts vendored
View File

@@ -1,5 +1,5 @@
declare module '*.vue' {
import type { DefineComponent } from 'vue'
declare module "*.vue" {
import type { DefineComponent } from "vue"
const component: DefineComponent<{}, {}, any>
export default component
}

View File

@@ -2,6 +2,6 @@
// Update these values to change the light and dark themes
export const themeConfig = {
light: 'emerald',
dark: 'forest'
light: "emerald",
dark: "forest"
}

View File

@@ -8,8 +8,14 @@ import { useRepoList } from "@/modules/repo/hooks/useRepoList.hook"
const { username } = useGitHubLogin()
const { isReady } = useRepos()
const { favoriteRepos, otherRepos, favoriteCheckboxes, toggleCheckbox, canLoadMore, loadMore } =
useRepoList()
const {
favoriteRepos,
otherRepos,
favoriteCheckboxes,
toggleCheckbox,
canLoadMore,
loadMore
} = useRepoList()
</script>
<template>