feat: create local mobile notes
This commit is contained in:
@@ -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 (
|
||||
<SafeAreaProvider>
|
||||
@@ -98,6 +110,7 @@ export function MobileApp() {
|
||||
<NoteListPanel
|
||||
notes={availableNotes}
|
||||
selectedNoteId={compactNavigation.selectedNoteId}
|
||||
onCreateNote={createNote}
|
||||
onSelectNote={selectNote}
|
||||
/>
|
||||
<EditorPanel note={selectedNote} saveState={selectedSaveState} onDraftChange={saveDraft} />
|
||||
@@ -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({
|
||||
<NoteListPanel
|
||||
notes={notes}
|
||||
selectedNoteId={selectedNoteId}
|
||||
onCreateNote={onCreateNote}
|
||||
onOpenSidebar={() => 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({
|
||||
</Pressable>
|
||||
)}
|
||||
/>
|
||||
<Pressable style={styles.composeButton}>
|
||||
<Pressable onPress={onCreateNote} style={styles.composeButton}>
|
||||
<PencilSimple size={28} color="#ffffff" />
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
@@ -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,
|
||||
|
||||
34
apps/mobile/src/mobileNoteCreate.test.ts
Normal file
34
apps/mobile/src/mobileNoteCreate.test.ts
Normal file
@@ -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')
|
||||
})
|
||||
})
|
||||
25
apps/mobile/src/mobileNoteCreate.ts
Normal file
25
apps/mobile/src/mobileNoteCreate.ts
Normal file
@@ -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'),
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user