test: cover mobile compact navigation
This commit is contained in:
@@ -18,26 +18,28 @@ import {
|
||||
SlidersHorizontal,
|
||||
} from 'phosphor-react-native'
|
||||
import { MobileNote, notes, sidebarSections } from './demoData'
|
||||
import {
|
||||
createCompactNavigationState,
|
||||
transitionCompactNavigation,
|
||||
type CompactNavigationEvent,
|
||||
type CompactPanel,
|
||||
} from './compactNavigation'
|
||||
import { NamedIcon, type IconName } from './NamedIcon'
|
||||
import { styles } from './styles'
|
||||
import { colors } from './theme'
|
||||
|
||||
type CompactPanel = 'sidebar' | 'list' | 'note' | 'properties'
|
||||
|
||||
export function MobileApp() {
|
||||
const { width } = useWindowDimensions()
|
||||
const isTablet = width >= 820
|
||||
const showsProperties = width >= 1120
|
||||
const [selectedNoteId, setSelectedNoteId] = useState(notes[0].id)
|
||||
const [compactPanel, setCompactPanel] = useState<CompactPanel>('list')
|
||||
const [compactNavigation, setCompactNavigation] = useState(() => createCompactNavigationState(notes[0].id))
|
||||
const selectedNote = useMemo(
|
||||
() => notes.find((note) => note.id === selectedNoteId) ?? notes[0],
|
||||
[selectedNoteId],
|
||||
() => notes.find((note) => note.id === compactNavigation.selectedNoteId) ?? notes[0],
|
||||
[compactNavigation.selectedNoteId],
|
||||
)
|
||||
|
||||
const selectNote = (note: MobileNote) => {
|
||||
setSelectedNoteId(note.id)
|
||||
setCompactPanel('note')
|
||||
setCompactNavigation((state) => transitionCompactNavigation(state, { type: 'selectNote', noteId: note.id }))
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -46,16 +48,16 @@ export function MobileApp() {
|
||||
{isTablet ? (
|
||||
<View style={styles.tabletShell}>
|
||||
<SidebarPanel />
|
||||
<NoteListPanel selectedNoteId={selectedNoteId} onSelectNote={selectNote} />
|
||||
<NoteListPanel selectedNoteId={compactNavigation.selectedNoteId} onSelectNote={selectNote} />
|
||||
<EditorPanel note={selectedNote} />
|
||||
{showsProperties ? <PropertiesPanel note={selectedNote} /> : null}
|
||||
</View>
|
||||
) : (
|
||||
<CompactShell
|
||||
activePanel={compactPanel}
|
||||
activePanel={compactNavigation.panel}
|
||||
note={selectedNote}
|
||||
selectedNoteId={selectedNoteId}
|
||||
onPanelChange={setCompactPanel}
|
||||
selectedNoteId={compactNavigation.selectedNoteId}
|
||||
onNavigate={(event) => setCompactNavigation((state) => transitionCompactNavigation(state, event))}
|
||||
onSelectNote={selectNote}
|
||||
/>
|
||||
)}
|
||||
@@ -67,38 +69,38 @@ export function MobileApp() {
|
||||
function CompactShell({
|
||||
activePanel,
|
||||
note,
|
||||
onPanelChange,
|
||||
onNavigate,
|
||||
onSelectNote,
|
||||
selectedNoteId,
|
||||
}: {
|
||||
activePanel: CompactPanel
|
||||
note: MobileNote
|
||||
onPanelChange: (panel: CompactPanel) => void
|
||||
onNavigate: (event: CompactNavigationEvent) => void
|
||||
onSelectNote: (note: MobileNote) => void
|
||||
selectedNoteId: string
|
||||
}) {
|
||||
if (activePanel === 'sidebar') {
|
||||
return <SidebarPanel onClose={() => onPanelChange('list')} />
|
||||
return <SidebarPanel onClose={() => onNavigate({ type: 'closeSidebar' })} />
|
||||
}
|
||||
|
||||
if (activePanel === 'note') {
|
||||
return (
|
||||
<EditorPanel
|
||||
note={note}
|
||||
onBack={() => onPanelChange('list')}
|
||||
onOpenProperties={() => onPanelChange('properties')}
|
||||
onBack={() => onNavigate({ type: 'backToList' })}
|
||||
onOpenProperties={() => onNavigate({ type: 'openProperties' })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (activePanel === 'properties') {
|
||||
return <PropertiesPanel note={note} onClose={() => onPanelChange('note')} />
|
||||
return <PropertiesPanel note={note} onClose={() => onNavigate({ type: 'closeProperties' })} />
|
||||
}
|
||||
|
||||
return (
|
||||
<NoteListPanel
|
||||
selectedNoteId={selectedNoteId}
|
||||
onOpenSidebar={() => onPanelChange('sidebar')}
|
||||
onOpenSidebar={() => onNavigate({ type: 'openSidebar' })}
|
||||
onSelectNote={onSelectNote}
|
||||
/>
|
||||
)
|
||||
|
||||
60
apps/mobile/src/compactNavigation.test.ts
Normal file
60
apps/mobile/src/compactNavigation.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
createCompactNavigationState,
|
||||
transitionCompactNavigation,
|
||||
type CompactNavigationState,
|
||||
} from './compactNavigation'
|
||||
|
||||
describe('compact mobile navigation', () => {
|
||||
it('starts on the note list with the first note selected', () => {
|
||||
expect(createCompactNavigationState('workflow')).toEqual({
|
||||
panel: 'list',
|
||||
selectedNoteId: 'workflow',
|
||||
})
|
||||
})
|
||||
|
||||
it('opens a selected note from the list', () => {
|
||||
const next = transitionCompactNavigation(createCompactNavigationState('workflow'), {
|
||||
type: 'selectNote',
|
||||
noteId: 'release',
|
||||
})
|
||||
|
||||
expect(next).toEqual({
|
||||
panel: 'note',
|
||||
selectedNoteId: 'release',
|
||||
})
|
||||
})
|
||||
|
||||
it('returns from sidebar and note surfaces to the list', () => {
|
||||
expect(panelAfter({ type: 'openSidebar' })).toBe('sidebar')
|
||||
expect(panelAfter({ type: 'closeSidebar' }, 'sidebar')).toBe('list')
|
||||
expect(panelAfter({ type: 'backToList' }, 'note')).toBe('list')
|
||||
})
|
||||
|
||||
it('opens and closes note properties without changing the selected note', () => {
|
||||
const noteState: CompactNavigationState = {
|
||||
panel: 'note',
|
||||
selectedNoteId: 'workflow',
|
||||
}
|
||||
|
||||
const propertiesState = transitionCompactNavigation(noteState, { type: 'openProperties' })
|
||||
const closedState = transitionCompactNavigation(propertiesState, { type: 'closeProperties' })
|
||||
|
||||
expect(propertiesState).toEqual({
|
||||
panel: 'properties',
|
||||
selectedNoteId: 'workflow',
|
||||
})
|
||||
expect(closedState).toEqual({
|
||||
panel: 'note',
|
||||
selectedNoteId: 'workflow',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function panelAfter(
|
||||
event: Parameters<typeof transitionCompactNavigation>[1],
|
||||
panel: CompactNavigationState['panel'] = 'list',
|
||||
) {
|
||||
const state = transitionCompactNavigation({ panel, selectedNoteId: 'workflow' }, event)
|
||||
return state.panel
|
||||
}
|
||||
55
apps/mobile/src/compactNavigation.ts
Normal file
55
apps/mobile/src/compactNavigation.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
export type CompactPanel = 'sidebar' | 'list' | 'note' | 'properties'
|
||||
|
||||
export type CompactNavigationState = {
|
||||
panel: CompactPanel
|
||||
selectedNoteId: string
|
||||
}
|
||||
|
||||
export type CompactNavigationEvent =
|
||||
| { type: 'backToList' }
|
||||
| { type: 'closeProperties' }
|
||||
| { type: 'closeSidebar' }
|
||||
| { type: 'openProperties' }
|
||||
| { type: 'openSidebar' }
|
||||
| { type: 'selectNote'; noteId: string }
|
||||
|
||||
export function createCompactNavigationState(initialNoteId: string): CompactNavigationState {
|
||||
return {
|
||||
panel: 'list',
|
||||
selectedNoteId: initialNoteId,
|
||||
}
|
||||
}
|
||||
|
||||
export function transitionCompactNavigation(
|
||||
state: CompactNavigationState,
|
||||
event: CompactNavigationEvent,
|
||||
): CompactNavigationState {
|
||||
if (event.type === 'selectNote') {
|
||||
return { panel: 'note', selectedNoteId: event.noteId }
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
panel: nextPanel(state.panel, event),
|
||||
}
|
||||
}
|
||||
|
||||
function nextPanel(currentPanel: CompactPanel, event: Exclude<CompactNavigationEvent, { type: 'selectNote' }>) {
|
||||
if (event.type === 'openSidebar') {
|
||||
return 'sidebar'
|
||||
}
|
||||
|
||||
if (event.type === 'closeSidebar' || event.type === 'backToList') {
|
||||
return 'list'
|
||||
}
|
||||
|
||||
if (event.type === 'openProperties') {
|
||||
return 'properties'
|
||||
}
|
||||
|
||||
if (event.type === 'closeProperties') {
|
||||
return 'note'
|
||||
}
|
||||
|
||||
return currentPanel
|
||||
}
|
||||
@@ -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: Scaffold iPad-first Expo shell with fixture data
|
||||
- Active slice: Prepare compact navigation for gesture-driven phone shell
|
||||
- Push policy: commit locally; do not push unless explicitly requested
|
||||
- Validation target: iPad/iOS simulator first
|
||||
|
||||
@@ -34,15 +34,15 @@ This file is the resumable working log for Tolaria mobile. The strategy and road
|
||||
- Added the first iPad-first/phone-ready shell using fixture notes, shared Markdown title/snippet helpers, Phosphor React Native icons, and responsive panels for sidebar, note list, editor, and properties.
|
||||
- Split mobile styles into small panel-specific StyleSheet modules so new mobile code starts at CodeScene `10.0`.
|
||||
- Pinned Expo runtime dependencies to the versions expected by Expo 55 after the initial generated versions failed iOS bundle export (`react-native@0.83.6`, `react@19.2.0`, matching safe-area/svg versions).
|
||||
- Extracted compact phone navigation into a pure tested state machine so future swipe gestures can dispatch explicit events instead of mutating panels ad hoc.
|
||||
|
||||
## Next Action
|
||||
|
||||
Continue Phase 2 with the next mobile shell slice:
|
||||
|
||||
1. Resolve the local CoreSimulator hang so `pnpm mobile:ios` can launch the app in an iPad simulator.
|
||||
2. Add a navigation-state test around the compact panel flow before introducing gestures.
|
||||
3. Add native gesture support for phone/tablet panel transitions.
|
||||
4. Start the storage/auth abstraction skeleton only after the shell has a reliable simulator loop.
|
||||
2. Add native gesture support for phone/tablet panel transitions once the simulator path is usable.
|
||||
3. Start the storage/auth abstraction skeleton after the shell has a reliable simulator loop.
|
||||
|
||||
## Verification Log
|
||||
|
||||
@@ -81,6 +81,10 @@ Continue Phase 2 with the next mobile shell slice:
|
||||
- CodeScene pre-commit safeguard passed for the current change set.
|
||||
- Codacy MCP can read repository analysis and security items for `refactoringhq/tolaria`; local Codacy CLI analysis is currently blocked because the Codacy CLI binary is not installed in this environment.
|
||||
- iOS simulator validation is blocked locally: Xcode is installed (`Xcode 26.3`), but `xcrun simctl list ...` hangs indefinitely even after opening `/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app`.
|
||||
- `pnpm --filter @tolaria/mobile test` passed after compact navigation extraction: 2 files / 6 tests.
|
||||
- `pnpm --filter @tolaria/mobile typecheck` passed after compact navigation extraction.
|
||||
- CodeScene after compact navigation extraction: `apps/mobile/src/compactNavigation.ts`, `apps/mobile/src/compactNavigation.test.ts`, and the touched `apps/mobile/src/MobileApp.tsx` scored `10`.
|
||||
- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after compact navigation extraction.
|
||||
|
||||
## Risks / Watch Items
|
||||
|
||||
|
||||
Reference in New Issue
Block a user