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:
@@ -9,21 +9,21 @@ Migrate Remanso from a Vue 3 web app to a fully native iOS + Android app built w
|
|||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
| Concern | Current (Vue) | React Native |
|
| Concern | Current (Vue) | React Native |
|
||||||
|---|---|---|
|
| ---------------- | ---------------------- | ---------------------------------------------------------- |
|
||||||
| Framework | Vue 3 + Vite | Expo SDK (managed workflow) |
|
| Framework | Vue 3 + Vite | Expo SDK (managed workflow) |
|
||||||
| Routing | Vue Router | Expo Router (file-system routing over React Navigation v7) |
|
| Routing | Vue Router | Expo Router (file-system routing over React Navigation v7) |
|
||||||
| State | Pinia | Zustand |
|
| State | Pinia | Zustand |
|
||||||
| Server state | TanStack Vue Query | TanStack Query (same library) |
|
| Server state | TanStack Vue Query | TanStack Query (same library) |
|
||||||
| Styling | DaisyUI + Tailwind CSS | NativeWind v4 |
|
| Styling | DaisyUI + Tailwind CSS | NativeWind v4 |
|
||||||
| Local DB | PouchDB (IndexedDB) | Expo SQLite |
|
| Local DB | PouchDB (IndexedDB) | Expo SQLite |
|
||||||
| Simple KV store | localStorage | MMKV |
|
| Simple KV store | localStorage | MMKV |
|
||||||
| Auth | OAuth redirects | expo-auth-session |
|
| Auth | OAuth redirects | expo-auth-session |
|
||||||
| GitHub API | @octokit/rest | @octokit/rest (unchanged) |
|
| GitHub API | @octokit/rest | @octokit/rest (unchanged) |
|
||||||
| i18n | vue-i18n | react-i18next |
|
| i18n | vue-i18n | react-i18next |
|
||||||
| Date utils | date-fns | date-fns (unchanged) |
|
| Date utils | date-fns | date-fns (unchanged) |
|
||||||
| Markdown content | markdown-it (DOM) | react-native-webview |
|
| Markdown content | markdown-it (DOM) | react-native-webview |
|
||||||
| Fonts | CSS custom properties | expo-font |
|
| Fonts | CSS custom properties | expo-font |
|
||||||
|
|
||||||
## Navigation Structure
|
## Navigation Structure
|
||||||
|
|
||||||
@@ -137,14 +137,18 @@ Zustand replaces Pinia. The store shape is identical to `userRepo.store.ts`:
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
const useRepoStore = create<RepoState>((set, get) => ({
|
const useRepoStore = create<RepoState>((set, get) => ({
|
||||||
user: '',
|
user: "",
|
||||||
repo: '',
|
repo: "",
|
||||||
files: [],
|
files: [],
|
||||||
userSettings: null,
|
userSettings: null,
|
||||||
needToLogin: false,
|
needToLogin: false,
|
||||||
setRepo: (user, repo) => set({ user, repo }),
|
setRepo: (user, repo) => set({ user, repo }),
|
||||||
loadFiles: async () => { /* Octokit call */ },
|
loadFiles: async () => {
|
||||||
loadSettings: async () => { /* MMKV read */ },
|
/* 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).
|
`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:
|
The WebView communicates back to the native layer via `postMessage` for:
|
||||||
|
|
||||||
- Internal note link taps (trigger React Navigation push)
|
- Internal note link taps (trigger React Navigation push)
|
||||||
- External URL taps (open in system browser)
|
- External URL taps (open in system browser)
|
||||||
- Backlink detection events
|
- Backlink detection events
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ const { isATProtoReady } = useATProtoLogin()
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
#main-app {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
::view-transition-old(root),
|
::view-transition-old(root),
|
||||||
::view-transition-new(root) {
|
::view-transition-new(root) {
|
||||||
animation-duration: 0.25s;
|
animation-duration: 0.25s;
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import { computed, nextTick, onMounted, onUnmounted, toRefs, watch } from "vue"
|
||||||
computed,
|
|
||||||
nextTick,
|
|
||||||
onMounted,
|
|
||||||
onUnmounted,
|
|
||||||
toRefs,
|
|
||||||
watch
|
|
||||||
} from "vue"
|
|
||||||
|
|
||||||
import HeaderNote from "@/components/HeaderNote.vue"
|
import HeaderNote from "@/components/HeaderNote.vue"
|
||||||
import SkeletonLoader from "@/components/SkeletonLoader.vue"
|
import SkeletonLoader from "@/components/SkeletonLoader.vue"
|
||||||
@@ -241,6 +234,7 @@ $header-height: 40px;
|
|||||||
.flux-note {
|
.flux-note {
|
||||||
.readme {
|
.readme {
|
||||||
padding: 0 0.75rem;
|
padding: 0 0.75rem;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,22 +50,14 @@ const fontSize = computed({
|
|||||||
<div class="font-change">
|
<div class="font-change">
|
||||||
<div>
|
<div>
|
||||||
<label for="title-font" class="font-label">t</label>
|
<label for="title-font" class="font-label">t</label>
|
||||||
<select
|
<select id="title-font" class="select" v-model="titleFont">
|
||||||
id="title-font"
|
|
||||||
class="select"
|
|
||||||
v-model="titleFont"
|
|
||||||
>
|
|
||||||
<option v-for="font in sortedFontFamilies" :key="font" :value="font">
|
<option v-for="font in sortedFontFamilies" :key="font" :value="font">
|
||||||
{{ font }}
|
{{ font }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<label for="body-font" class="font-label">p</label>
|
<label for="body-font" class="font-label">p</label>
|
||||||
<select
|
<select id="body-font" class="select" v-model="bodyFont">
|
||||||
id="body-font"
|
|
||||||
class="select"
|
|
||||||
v-model="bodyFont"
|
|
||||||
>
|
|
||||||
<option v-for="font in sortedFontFamilies" :key="font" :value="font">
|
<option v-for="font in sortedFontFamilies" :key="font" :value="font">
|
||||||
{{ font }}
|
{{ font }}
|
||||||
</option>
|
</option>
|
||||||
@@ -75,11 +67,7 @@ const fontSize = computed({
|
|||||||
<theme-swap />
|
<theme-swap />
|
||||||
|
|
||||||
<label for="font-size" class="font-label">s</label>
|
<label for="font-size" class="font-label">s</label>
|
||||||
<select
|
<select id="font-size" class="select" v-model="fontSize">
|
||||||
id="font-size"
|
|
||||||
class="select"
|
|
||||||
v-model="fontSize"
|
|
||||||
>
|
|
||||||
<option v-for="size in fontSizes" :key="size" :value="size">
|
<option v-for="size in fontSizes" :key="size" :value="size">
|
||||||
{{ size }}
|
{{ size }}
|
||||||
</option>
|
</option>
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ const goHome = () => router.push({ name: "Home" })
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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" />
|
<img src="/favicon.png" alt="Remanso icon" class="remanso-logo" />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ const isMarkdown = (filename?: string) => filename?.endsWith(".md") ?? false
|
|||||||
|
|
||||||
const yieldToMain = () =>
|
const yieldToMain = () =>
|
||||||
"scheduler" in globalThis
|
"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))
|
: new Promise<void>((r) => setTimeout(r, 0))
|
||||||
|
|
||||||
export const useComputeBacklinks = () => {
|
export const useComputeBacklinks = () => {
|
||||||
@@ -43,9 +45,10 @@ export const useComputeBacklinks = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fileBacklinkId = generateId(DataType.BacklinkNote, file.sha)
|
const fileBacklinkId = generateId(DataType.BacklinkNote, file.sha)
|
||||||
const fileBacklink = await data.get<DataType.BacklinkNote, BacklinkNote>(
|
const fileBacklink = await data.get<
|
||||||
fileBacklinkId
|
DataType.BacklinkNote,
|
||||||
)
|
BacklinkNote
|
||||||
|
>(fileBacklinkId)
|
||||||
if (fileBacklink) {
|
if (fileBacklink) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,11 +37,20 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
|
|||||||
_persistFonts() {
|
_persistFonts() {
|
||||||
if (!this.userSettings) return
|
if (!this.userSettings) return
|
||||||
try {
|
try {
|
||||||
const { chosenTitleFont, chosenBodyFont, chosenFontSize, chosenFontFamily } =
|
const {
|
||||||
this.userSettings
|
chosenTitleFont,
|
||||||
|
chosenBodyFont,
|
||||||
|
chosenFontSize,
|
||||||
|
chosenFontFamily
|
||||||
|
} = this.userSettings
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
`remanso:fonts:${this.user}:${this.repo}`,
|
`remanso:fonts:${this.user}:${this.repo}`,
|
||||||
JSON.stringify({ chosenTitleFont, chosenBodyFont, chosenFontSize, chosenFontFamily })
|
JSON.stringify({
|
||||||
|
chosenTitleFont,
|
||||||
|
chosenBodyFont,
|
||||||
|
chosenFontSize,
|
||||||
|
chosenFontFamily
|
||||||
|
})
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
@@ -61,7 +70,8 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(lsFonts).length) {
|
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)
|
Object.assign(this.userSettings, lsFonts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
src/shims-vue.d.ts
vendored
4
src/shims-vue.d.ts
vendored
@@ -1,5 +1,5 @@
|
|||||||
declare module '*.vue' {
|
declare module "*.vue" {
|
||||||
import type { DefineComponent } from 'vue'
|
import type { DefineComponent } from "vue"
|
||||||
const component: DefineComponent<{}, {}, any>
|
const component: DefineComponent<{}, {}, any>
|
||||||
export default component
|
export default component
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
// Update these values to change the light and dark themes
|
// Update these values to change the light and dark themes
|
||||||
|
|
||||||
export const themeConfig = {
|
export const themeConfig = {
|
||||||
light: 'emerald',
|
light: "emerald",
|
||||||
dark: 'forest'
|
dark: "forest"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,14 @@ import { useRepoList } from "@/modules/repo/hooks/useRepoList.hook"
|
|||||||
|
|
||||||
const { username } = useGitHubLogin()
|
const { username } = useGitHubLogin()
|
||||||
const { isReady } = useRepos()
|
const { isReady } = useRepos()
|
||||||
const { favoriteRepos, otherRepos, favoriteCheckboxes, toggleCheckbox, canLoadMore, loadMore } =
|
const {
|
||||||
useRepoList()
|
favoriteRepos,
|
||||||
|
otherRepos,
|
||||||
|
favoriteCheckboxes,
|
||||||
|
toggleCheckbox,
|
||||||
|
canLoadMore,
|
||||||
|
loadMore
|
||||||
|
} = useRepoList()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
Reference in New Issue
Block a user