diff --git a/apps/mobile/src/MobileApp.tsx b/apps/mobile/src/MobileApp.tsx
index afc03e68..b09a0edc 100644
--- a/apps/mobile/src/MobileApp.tsx
+++ b/apps/mobile/src/MobileApp.tsx
@@ -1,4 +1,4 @@
-import { useMemo, useState } from 'react'
+import { useEffect, useMemo, useState } from 'react'
import {
FlatList,
Pressable,
@@ -17,7 +17,8 @@ import {
PencilSimple,
SlidersHorizontal,
} from 'phosphor-react-native'
-import { MobileNote, notes, sidebarSections } from './demoData'
+import { MobileNote, notes as fallbackNotes, sidebarSections } from './demoData'
+import { loadDemoVaultNotes } from './mobileDemoVault'
import { MobileEditorAdapter } from './MobileEditorAdapter'
import {
createCompactNavigationState,
@@ -34,12 +35,30 @@ export function MobileApp() {
const { width } = useWindowDimensions()
const isTablet = width >= 820
const showsProperties = width >= 1120
- const [compactNavigation, setCompactNavigation] = useState(() => createCompactNavigationState(notes[0].id))
+ const [availableNotes, setAvailableNotes] = useState(fallbackNotes)
+ const [compactNavigation, setCompactNavigation] = useState(() => createCompactNavigationState(fallbackNotes[0].id))
const selectedNote = useMemo(
- () => notes.find((note) => note.id === compactNavigation.selectedNoteId) ?? notes[0],
- [compactNavigation.selectedNoteId],
+ () => availableNotes.find((note) => note.id === compactNavigation.selectedNoteId) ?? availableNotes[0],
+ [availableNotes, compactNavigation.selectedNoteId],
)
+ useEffect(() => {
+ let isActive = true
+
+ loadDemoVaultNotes()
+ .then((loadedNotes) => {
+ if (isActive && loadedNotes.length > 0) {
+ setAvailableNotes(loadedNotes)
+ setCompactNavigation((state) => selectLoadedNote(state, loadedNotes))
+ }
+ })
+ .catch(() => {})
+
+ return () => {
+ isActive = false
+ }
+ }, [])
+
const selectNote = (note: MobileNote) => {
setCompactNavigation((state) => transitionCompactNavigation(state, { type: 'selectNote', noteId: note.id }))
}
@@ -50,7 +69,11 @@ export function MobileApp() {
{isTablet ? (
-
+
{showsProperties ? : null}
@@ -58,6 +81,7 @@ export function MobileApp() {
setCompactNavigation((state) => transitionCompactNavigation(state, event))}
onSelectNote={selectNote}
@@ -71,12 +95,14 @@ export function MobileApp() {
function CompactShell({
activePanel,
note,
+ notes,
onNavigate,
onSelectNote,
selectedNoteId,
}: {
activePanel: CompactPanel
note: MobileNote
+ notes: MobileNote[]
onNavigate: (event: CompactNavigationEvent) => void
onSelectNote: (note: MobileNote) => void
selectedNoteId: string
@@ -112,6 +138,7 @@ function CompactShell({
return (
onNavigate({ type: 'openSidebar' })}
onSelectNote={onSelectNote}
@@ -154,10 +181,12 @@ function SidebarPanel({ onClose }: { onClose?: () => void }) {
}
function NoteListPanel({
+ notes,
onOpenSidebar,
onSelectNote,
selectedNoteId,
}: {
+ notes: MobileNote[]
onOpenSidebar?: () => void
onSelectNote: (note: MobileNote) => void
selectedNoteId: string
@@ -205,6 +234,12 @@ function NoteListPanel({
)
}
+function selectLoadedNote(state: ReturnType, loadedNotes: MobileNote[]) {
+ return loadedNotes.some((note) => note.id === state.selectedNoteId)
+ ? state
+ : { ...state, selectedNoteId: loadedNotes[0].id }
+}
+
function EditorPanel({
note,
onBack,
diff --git a/apps/mobile/src/demoData.ts b/apps/mobile/src/demoData.ts
index 50ba2ef8..b9718925 100644
--- a/apps/mobile/src/demoData.ts
+++ b/apps/mobile/src/demoData.ts
@@ -3,7 +3,7 @@ import { createFixtureMobileVaultRepository } from './mobileVaultRepository'
export type { MobileNote } from './mobileNoteProjection'
-const noteContent: MobileNoteSource[] = [
+export const demoNoteSources: MobileNoteSource[] = [
{
id: 'workflow',
type: 'Essay',
@@ -66,9 +66,9 @@ const noteContent: MobileNoteSource[] = [
},
]
-export const notes: MobileNote[] = projectMobileNotes(noteContent)
+export const notes: MobileNote[] = projectMobileNotes(demoNoteSources)
-export const demoVaultRepository = createFixtureMobileVaultRepository(noteContent)
+export const demoVaultRepository = createFixtureMobileVaultRepository(demoNoteSources)
export const sidebarSections = [
{
diff --git a/apps/mobile/src/mobileDemoVault.ts b/apps/mobile/src/mobileDemoVault.ts
new file mode 100644
index 00000000..7c8567ba
--- /dev/null
+++ b/apps/mobile/src/mobileDemoVault.ts
@@ -0,0 +1,31 @@
+import { demoNoteSources } from './demoData'
+import { createMobileVaultConfig } from './mobileVaultConfig'
+import { createNativeMobileVaultStorage } from './mobileNativeVaultStorage'
+import { createStoredMobileVaultRepository } from './mobileVaultRepository'
+import { seedMobileVaultIfEmpty } from './mobileVaultSeed'
+import type { MobileVaultFile } from './mobileVaultStorage'
+
+const demoVault = createDemoVaultConfig()
+
+export async function loadDemoVaultNotes() {
+ const storage = createNativeMobileVaultStorage()
+ await seedMobileVaultIfEmpty({ files: demoVaultFiles(), storage, vault: demoVault })
+
+ return createStoredMobileVaultRepository({ storage, vault: demoVault }).listNotes()
+}
+
+function demoVaultFiles(): MobileVaultFile[] {
+ return demoNoteSources.map((source) => ({
+ path: source.filename,
+ content: source.content,
+ }))
+}
+
+function createDemoVaultConfig() {
+ const result = createMobileVaultConfig({ id: 'personal', name: 'Personal Journal' })
+ if (!result.ok) {
+ throw new Error(result.error)
+ }
+
+ return result.config
+}
diff --git a/apps/mobile/src/mobileExpoVaultStorage.ts b/apps/mobile/src/mobileExpoVaultStorage.ts
index f94299c0..138cd269 100644
--- a/apps/mobile/src/mobileExpoVaultStorage.ts
+++ b/apps/mobile/src/mobileExpoVaultStorage.ts
@@ -35,6 +35,11 @@ type VaultFileInput = {
path: string
}
+type DirectoryInput = {
+ fileSystem: ExpoMobileVaultFileSystem
+ uri: string
+}
+
export function createExpoMobileVaultStorage(
fileSystem: ExpoMobileVaultFileSystem,
): MobileVaultStorageDriver {
@@ -120,7 +125,7 @@ async function readVaultFile(input: VaultFileInput): Promise {
async function ensureVaultRoot(fileSystem: ExpoMobileVaultFileSystem, vault: MobileVaultConfig) {
const rootUri = vaultRootUri(fileSystem, vault)
- await fileSystem.makeDirectoryAsync(rootUri, { intermediates: true })
+ await ensureDirectory({ fileSystem, uri: rootUri })
return rootUri
}
@@ -128,10 +133,21 @@ async function ensureVaultRoot(fileSystem: ExpoMobileVaultFileSystem, vault: Mob
async function ensureParentDirectory(input: VaultFileInput) {
const parentPath = input.path.split('/').slice(0, -1).join('/')
if (parentPath) {
- await input.fileSystem.makeDirectoryAsync(
- appendUri({ root: input.rootUri, segments: [parentPath] }),
- { intermediates: true },
- )
+ await ensureDirectory({
+ fileSystem: input.fileSystem,
+ uri: appendUri({ root: input.rootUri, segments: [parentPath] }),
+ })
+ }
+}
+
+async function ensureDirectory(input: DirectoryInput) {
+ const info = await input.fileSystem.getInfoAsync(input.uri)
+ if (info.exists && !info.isDirectory) {
+ throw new Error(`Mobile vault path is not a directory: ${input.uri}`)
+ }
+
+ if (!info.exists) {
+ await input.fileSystem.makeDirectoryAsync(input.uri, { intermediates: true })
}
}
diff --git a/apps/mobile/src/mobileVaultSeed.test.ts b/apps/mobile/src/mobileVaultSeed.test.ts
new file mode 100644
index 00000000..5f296530
--- /dev/null
+++ b/apps/mobile/src/mobileVaultSeed.test.ts
@@ -0,0 +1,45 @@
+import { describe, expect, it } from 'vitest'
+import { createMobileVaultConfig } from './mobileVaultConfig'
+import { seedMobileVaultIfEmpty } from './mobileVaultSeed'
+import { createMemoryMobileVaultStorage } from './mobileVaultStorage'
+
+const vault = createVault()
+
+describe('mobile vault seed', () => {
+ it('writes starter notes when the mobile vault is empty', async () => {
+ const storage = createMemoryMobileVaultStorage([])
+
+ await expect(
+ seedMobileVaultIfEmpty({
+ files: [{ path: 'welcome.md', content: '# Welcome' }],
+ storage,
+ vault,
+ }),
+ ).resolves.toBe('seeded')
+
+ await expect(storage.readMarkdownFile(vault, 'welcome.md')).resolves.toBe('# Welcome')
+ })
+
+ it('keeps existing mobile vault files intact', async () => {
+ const storage = createMemoryMobileVaultStorage([{ path: 'existing.md', content: '# Existing' }])
+
+ await expect(
+ seedMobileVaultIfEmpty({
+ files: [{ path: 'existing.md', content: '# Replacement' }],
+ storage,
+ vault,
+ }),
+ ).resolves.toBe('alreadySeeded')
+
+ await expect(storage.readMarkdownFile(vault, 'existing.md')).resolves.toBe('# Existing')
+ })
+})
+
+function createVault() {
+ const result = createMobileVaultConfig({ id: 'personal', name: 'Personal Journal' })
+ if (!result.ok) {
+ throw new Error(result.error)
+ }
+
+ return result.config
+}
diff --git a/apps/mobile/src/mobileVaultSeed.ts b/apps/mobile/src/mobileVaultSeed.ts
new file mode 100644
index 00000000..4149276f
--- /dev/null
+++ b/apps/mobile/src/mobileVaultSeed.ts
@@ -0,0 +1,23 @@
+import type { MobileVaultConfig } from './mobileVaultConfig'
+import type { MobileVaultFile, MobileVaultStorageDriver } from './mobileVaultStorage'
+
+export type MobileVaultSeedResult = 'alreadySeeded' | 'seeded'
+
+export async function seedMobileVaultIfEmpty({
+ files,
+ storage,
+ vault,
+}: {
+ files: MobileVaultFile[]
+ storage: MobileVaultStorageDriver
+ vault: MobileVaultConfig
+}): Promise {
+ const existingFiles = await storage.listMarkdownFiles(vault)
+ if (existingFiles.length > 0) {
+ return 'alreadySeeded'
+ }
+
+ await Promise.all(files.map((file) => storage.writeMarkdownFile(vault, file.path, file.content)))
+
+ return 'seeded'
+}
diff --git a/docs/MOBILE_PROGRESS.md b/docs/MOBILE_PROGRESS.md
index bae7034b..3ad12d03 100644
--- a/docs/MOBILE_PROGRESS.md
+++ b/docs/MOBILE_PROGRESS.md
@@ -8,7 +8,7 @@ This file is the resumable working log for Tolaria mobile. The strategy and road
- Branch: `codex/mobile`
- Active phase: Phase 2 - Mobile Shell
-- Active slice: Implement Expo FileSystem vault storage
+- Active slice: Seed app-local mobile demo vault
- Push policy: commit locally; do not push unless explicitly requested
- Validation target: iPad/iOS simulator first
@@ -53,6 +53,8 @@ This file is the resumable working log for Tolaria mobile. The strategy and road
- Added a mobile vault storage driver contract plus memory implementation, and connected the mobile vault repository to app-local markdown files behind that storage interface.
- Added Expo FileSystem as the first app-local mobile vault storage implementation behind the storage driver contract.
- Created [ADR-0111](./adr/0111-expo-file-system-vault-storage.md) to record the mobile filesystem dependency and app-local vault storage path.
+- Added a mobile vault seeding boundary that writes starter Markdown files only when the app-local vault is empty.
+- Wired the mobile shell to load its starter notes through the native Expo FileSystem storage driver and stored repository path, with fixture notes retained as the fallback while storage initializes.
## Next Action
@@ -60,7 +62,7 @@ Continue Phase 2 with the next mobile shell slice:
1. Dismiss or suppress Expo Go's first-run tools modal during simulator QA so screenshots capture the app without the overlay.
2. Expand the serializer for additional TenTap output needed by real notes: links, emphasis, code, headings, ordered lists, and task lists.
-3. Wire the app shell to the stored repository once an initial demo vault seed path exists.
+3. Expand the serializer for additional TenTap output needed by real notes: links, emphasis, code, headings, ordered lists, and task lists.
## Verification Log
@@ -161,6 +163,12 @@ Continue Phase 2 with the next mobile shell slice:
- `pnpm --filter @tolaria/mobile typecheck` passed after Expo storage adapter extraction.
- CodeScene after Expo storage adapter extraction: `apps/mobile/src/mobileExpoVaultStorage.ts`, `apps/mobile/src/mobileExpoVaultStorage.test.ts`, and `apps/mobile/src/mobileNativeVaultStorage.ts` scored `10`.
- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after Expo storage adapter extraction.
+- `pnpm --filter @tolaria/mobile test -- src/mobileVaultSeed.test.ts` passed after app-local demo vault seeding: 12 files / 40 tests.
+- `pnpm --filter @tolaria/mobile test` passed after app-local demo vault seeding: 12 files / 40 tests.
+- `pnpm --filter @tolaria/mobile typecheck` passed after app-local demo vault seeding.
+- CodeScene after app-local demo vault seeding: `apps/mobile/src/MobileApp.tsx`, `apps/mobile/src/mobileDemoVault.ts`, `apps/mobile/src/mobileExpoVaultStorage.ts`, `apps/mobile/src/mobileVaultSeed.ts`, and `apps/mobile/src/mobileVaultSeed.test.ts` scored `10`; `apps/mobile/src/demoData.ts` returned no scorable code and no findings.
+- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after app-local demo vault seeding.
+- `pnpm --filter @tolaria/mobile exec expo start --ios --clear --port 8087` launched on `iPad Pro 13-inch (M4)` after app-local demo vault seeding; screenshot captured at `/tmp/tolaria-mobile-seeded-ipad.png`. The app rendered via the stored repository path behind Expo Go's first-run Tools modal with no red runtime error overlay.
## Risks / Watch Items