diff --git a/apps/mobile/src/MobileApp.tsx b/apps/mobile/src/MobileApp.tsx index 6fe16944..a50ab109 100644 --- a/apps/mobile/src/MobileApp.tsx +++ b/apps/mobile/src/MobileApp.tsx @@ -18,7 +18,7 @@ import { SlidersHorizontal, } from 'phosphor-react-native' import { MobileNote, notes as fallbackNotes, sidebarSections } from './demoData' -import { loadDemoVaultNotes, saveDemoVaultDraft } from './mobileDemoVault' +import { createDemoVaultNote, loadDemoVaultNotes, saveDemoVaultDraft } from './mobileDemoVault' import { createMobileAutosaveQueue } from './mobileAutosaveQueue' import type { MobileEditorDraft } from './mobileEditorDraft' import { @@ -88,6 +88,18 @@ export function MobileApp() { setCompactNavigation((state) => transitionCompactNavigation(state, { type: 'selectNote', noteId: note.id })) } const saveDraft = useCallback((draft: MobileEditorDraft) => autosaveQueue.enqueue(draft), [autosaveQueue]) + const createNote = useCallback(() => { + void createDemoVaultNote() + .then((note) => { + if (!note) { + return + } + + setAvailableNotes((notes) => [note, ...notes.filter((item) => item.id !== note.id)]) + setCompactNavigation((state) => transitionCompactNavigation(state, { type: 'selectNote', noteId: note.id })) + }) + .catch(() => {}) + }, []) return ( @@ -98,6 +110,7 @@ export function MobileApp() { @@ -112,6 +125,7 @@ export function MobileApp() { selectedNoteId={compactNavigation.selectedNoteId} onNavigate={(event) => setCompactNavigation((state) => transitionCompactNavigation(state, event))} onDraftChange={saveDraft} + onCreateNote={createNote} onSelectNote={selectNote} /> )} @@ -127,6 +141,7 @@ function CompactShell({ saveState, onNavigate, onDraftChange, + onCreateNote, onSelectNote, selectedNoteId, }: { @@ -136,6 +151,7 @@ function CompactShell({ saveState: MobileEditorSaveState onNavigate: (event: CompactNavigationEvent) => void onDraftChange: (draft: MobileEditorDraft) => void + onCreateNote: () => void onSelectNote: (note: MobileNote) => void selectedNoteId: string }) { @@ -174,6 +190,7 @@ function CompactShell({ onNavigate({ type: 'openSidebar' })} onSelectNote={onSelectNote} /> @@ -216,11 +233,13 @@ function SidebarPanel({ onClose }: { onClose?: () => void }) { function NoteListPanel({ notes, + onCreateNote, onOpenSidebar, onSelectNote, selectedNoteId, }: { notes: MobileNote[] + onCreateNote: () => void onOpenSidebar?: () => void onSelectNote: (note: MobileNote) => void selectedNoteId: string @@ -261,7 +280,7 @@ function NoteListPanel({ )} /> - + diff --git a/apps/mobile/src/mobileDemoVault.ts b/apps/mobile/src/mobileDemoVault.ts index a3d942fe..9db3ac10 100644 --- a/apps/mobile/src/mobileDemoVault.ts +++ b/apps/mobile/src/mobileDemoVault.ts @@ -1,6 +1,7 @@ import { demoNoteSources } from './demoData' import type { MobileEditorDraft } from './mobileEditorDraft' import { saveMobileEditorDraft } from './mobileEditorDraftSave' +import { createMobileNoteFile } from './mobileNoteCreate' import { createMobileVaultConfig } from './mobileVaultConfig' import { createNativeMobileVaultStorage } from './mobileNativeVaultStorage' import { createStoredMobileVaultRepository } from './mobileVaultRepository' @@ -24,6 +25,14 @@ export function saveDemoVaultDraft(draft: MobileEditorDraft) { }) } +export async function createDemoVaultNote() { + const storage = createNativeMobileVaultStorage() + const file = createMobileNoteFile() + await storage.writeMarkdownFile(demoVault, file.path, file.content) + + return createStoredMobileVaultRepository({ storage, vault: demoVault }).readNote(file.path.replace(/\.md$/, '')) +} + function demoVaultFiles(): MobileVaultFile[] { return demoNoteSources.map((source) => ({ path: source.filename, diff --git a/apps/mobile/src/mobileNoteCreate.test.ts b/apps/mobile/src/mobileNoteCreate.test.ts new file mode 100644 index 00000000..285e4669 --- /dev/null +++ b/apps/mobile/src/mobileNoteCreate.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest' +import { createMobileNoteFile } from './mobileNoteCreate' + +describe('mobile note create', () => { + it('creates a deterministic markdown file from the current time', () => { + const file = createMobileNoteFile({ + now: new Date('2026-05-04T12:34:56.000Z'), + }) + + expect(file).toEqual({ + path: 'note-mor6mem8.md', + content: [ + '---', + 'title: Untitled', + 'type: Note', + 'created: 2026-05-04T12:34:56.000Z', + '---', + '', + '# Untitled', + '', + ].join('\n'), + }) + }) + + it('supports a custom title for future create flows', () => { + const file = createMobileNoteFile({ + now: new Date('2026-05-04T12:34:56.000Z'), + title: 'Meeting notes', + }) + + expect(file.content).toContain('title: Meeting notes') + expect(file.content).toContain('# Meeting notes') + }) +}) diff --git a/apps/mobile/src/mobileNoteCreate.ts b/apps/mobile/src/mobileNoteCreate.ts new file mode 100644 index 00000000..c1ad7b07 --- /dev/null +++ b/apps/mobile/src/mobileNoteCreate.ts @@ -0,0 +1,25 @@ +import type { MobileVaultFile } from './mobileVaultStorage' + +export function createMobileNoteFile({ + now = new Date(), + title = 'Untitled', +}: { + now?: Date + title?: string +} = {}): MobileVaultFile { + const id = `note-${now.getTime().toString(36)}` + + return { + path: `${id}.md`, + content: [ + '---', + `title: ${title}`, + 'type: Note', + `created: ${now.toISOString()}`, + '---', + '', + `# ${title}`, + '', + ].join('\n'), + } +} diff --git a/docs/MOBILE_PROGRESS.md b/docs/MOBILE_PROGRESS.md index 0755da7b..1fb71b51 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: Refresh saved mobile note projection +- Active slice: Create local mobile note - Push policy: commit locally; do not push unless explicitly requested - Validation target: iPad/iOS simulator first @@ -62,14 +62,15 @@ This file is the resumable working log for Tolaria mobile. The strategy and road - Split editor save-state styles into a separate style module to keep mobile style files at CodeScene `10`. - Added a debounced mobile autosave queue that coalesces rapid TenTap draft changes, marks edited drafts as queued, and ignores stale save results when newer drafts supersede them. - Refreshed the in-memory mobile note projection after the latest successful editor save so the note list, editor source, properties words, and snippets update from canonical Markdown. +- Added the first app-local mobile note creation path and wired the compose button to write a new Markdown note, prepend it to the current list, and select it through the shared compact navigation reducer. ## Next Action 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. Add the first local note create path against app-local vault storage, then wire it into the compact phone and iPad shells. -3. Add a focused simulator interaction path for editor typing/autosave once Expo Go's overlay no longer blocks clean screenshots. +2. Add a focused simulator interaction path for editor typing/autosave once Expo Go's overlay no longer blocks clean screenshots. +3. Add a durable note-create UX state: disabled/pressed state while creating, create failure feedback, and eventual title-entry flow. ## Verification Log @@ -203,6 +204,11 @@ Continue Phase 2 with the next mobile shell slice: - CodeScene after saved note projection refresh: `apps/mobile/src/MobileApp.tsx`, `apps/mobile/src/mobileAutosaveQueue.ts`, `apps/mobile/src/mobileAutosaveQueue.test.ts`, `apps/mobile/src/mobileSavedDraftProjection.ts`, and `apps/mobile/src/mobileSavedDraftProjection.test.ts` scored `10`. - `pnpm --filter @tolaria/mobile test` passed after saved note projection refresh: 16 files / 51 tests. - `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after saved note projection refresh. +- `pnpm --filter @tolaria/mobile test -- src/mobileNoteCreate.test.ts src/mobileVaultRepository.test.ts` passed after local note creation: 17 files / 53 tests. +- `pnpm --filter @tolaria/mobile typecheck` passed after local note creation. +- CodeScene after local note creation: `apps/mobile/src/MobileApp.tsx`, `apps/mobile/src/mobileDemoVault.ts`, and `apps/mobile/src/mobileNoteCreate.test.ts` scored `10`; `apps/mobile/src/mobileNoteCreate.ts` returned no scorable code and no findings. +- `pnpm --filter @tolaria/mobile test` passed after local note creation: 17 files / 53 tests. +- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after local note creation. ## Risks / Watch Items