A Capacitor shell that runs the ZenNotes product core (packages/app-core from
the zennotes monorepo) inside a WKWebView, backed
by a local-first vault on the device filesystem. Implements the architecture in
docs/specs/mobile/ (Phase 0 + the on-device parts of Phase 1).
The zennotes repo is consumed read-only, straight from source, via Vite/TS
path aliases (see vite.config.ts) — nothing in that repo is modified. The
repo is expected at ../../opensource/zennotes relative to this directory.
src/
bootstrap.ts pre-app-core prefs/theme seeding (must import first)
main.tsx install bridge → open vault → renderZenNotesApp()
bridge/
mobile-bridge.ts the third ZenBridge (window.zen) implementation
vault-fs.ts desktop vault.ts semantics over Capacitor Filesystem
vault-core.ts pure helpers ported 1:1 (folder map, meta extraction,
naming, search scoring) — keep in sync with desktop
native-fs.ts Capacitor Filesystem wrapper (atomic writes, file URLs)
events.ts VaultChangeEvent emitter (in-app writes + rescan)
ui-mobile/
MobileShell.tsx bottom nav (capture ⊕ / search / sidebar / palette),
phone drawer behavior via the shared Zustand store
mobile.css safe areas, overlay drawers, keyboard handling
ios/ Capacitor-generated Xcode project (appId md.zennotes)
Key decisions (all forced by "don't modify the zennotes repo"):
runtime: 'web'— the bridge contract has no'mobile'runtime yet. Every desktop-only affordance in app-core gates onruntime === 'desktop', so'web'+ the capability flags produces correct mobile behavior. When the contract gains'mobile'+ the new capability flags (spec 02), flip it here.- Vault location —
Documents/ZenNotes/<vault>in the app container (visible in the Files app viaUIFileSharingEnabled). First run createsMy Vaultseeded with the official demo tour (imported read-only fromapps/desktop/src/main/demo-tour-data.ts). - On-disk contract is byte-compatible with desktop: same folder layout
(
inbox|quick|archive|trash,assets/, legacyattachements/recognized — the misspelling is intentional and load-bearing), same.zennotes/metadata (vault.json, workspace.json, comments/), same naming/collision rules, same NoteMeta extraction regexes. - TikZ is capability-gated off (no WASM TeX on device); blocks show the source with a "renders on ZenNotes desktop" notice, per spec 05.
- Vim mode defaults off on first run (soft keyboard; spec 06) — Settings can re-enable it. The desktop onboarding wizard is skipped; the vault is auto-provisioned.
npm install
npm run sync # vite build + cap sync ios
npx cap open ios # open in Xcode, or:
xcodebuild -workspace ios/App/App.xcworkspace -scheme App \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' buildDev loop against a browser (no simulator): npm run dev — note Capacitor
plugins are absent in a plain browser, so vault I/O won't work; use the
simulator for real testing.
-
Vault CRUD: list/read/write/create/rename (with inbound wikilink rewrite)/ duplicate/move/trash/restore/archive, folders, favorites, comments, templates, vault settings, workspace (session) restore
-
Rendering: KaTeX, Mermaid, JSXGraph, function-plot, highlight.js, images; TikZ graceful fallback
-
Search: fuzzy per-line text search (builtin backend semantics) + title quick switcher; tags view; tasks view (parser shared from shared-domain)
-
Quick capture from the bottom nav (creates in
quick, opens immediately) -
Mobile chrome (iOS-native pass, all phone-width only — iPad keeps the desktop-like layout per spec 07):
- desktop chrome hidden: window title bar, tab strip, editor icon toolbar, word-count status bar, Split mode
- bottom nav: sidebar · back · capture ⊕ · search · ••• (all actions run through the shared command registry, same as the palette)
- ••• opens an iOS action sheet: Edit/Read segment + note actions (outline, rename, move, copy wikilink, archive, trash) + app actions (open notes, vault text search, settings)
- Settings: the desktop two-pane dialog becomes a full-screen paged flow (section list → detail with ‹ back, Done to close; MCP/CLI hidden) via the "mobilizer" in MobileShell — CSS state + two injected buttons, no app-core changes
- dialogs render as bottom sheets; palettes go full-width; side panels (outline/connections/comments) overlay instead of squeezing the editor
- overlay sidebar drawer + backdrop with slide animation, safe areas, keyboard show/hide handling, theme pre-boot (no flash), haptic on capture
- keyboard-hint strips hidden on phones (vim hints in Tasks list/calendar/ kanban and Tags footers, palette "Ctrl+N/P · esc" rows, focused-row key chips, the Home "Sidebar ⌘1" chip); collection headers compacted (Refresh/Close dropped, filter narrowed, header wraps); the Assets table collapses to name + used-in on phone width
-
Databases (
.baseCSV) using the same shared logic as the web client -
The spec-06 editing toolbar docked above the soft keyboard (undo/redo, checkbox, bullet, heading cycle, bold/italic/highlight/code, link, wikilink, tag, indent/outdent, dismiss) — drives the shared editor via the store's
editorViewRef+ app-core'slib/cm-format.ts; auto-hides with a hardware keyboard -
Long-press context menus: a 450ms press on chrome surfaces synthesizes the
contextmenuevent the desktop handlers already listen for (the finger-lift's synthetic mouse burst is suppressed or the menu would close instantly); disabled inside the editor to preserve text selection -
Archive/unarchive round-trip (subpath preserved); the ••• sheet is contextual (Unarchive in archive, Restore in trash)
-
iPad: ≥768px keeps the desktop-like layout per spec 07 (persistent sidebar, no bottom nav) — verified on the iPad Pro 11" simulator
-
Share Extension ("Share to ZenNotes" from any app): the native extension (
ios/App/ShareExtension/) writes shared text/URLs into thegroup.md.zennotesApp Group; the app drains them into quick notes on launch/foreground via the app-localShareInboxCapacitor plugin (ShareInboxPlugin.swift, registered byZNViewController). URL-only shares get a hostname title. The target is wired bytooling/add-share-extension.rb(xcodeproj gem via Homebrew CocoaPods) — re-run it if the Xcode project is ever regenerated. Device builds need the App Group registered with the Apple Developer account (automatic signing handles it with a paid membership); the simulator doesn't enforce it. -
Edge-swipe gestures: swipe right from the left edge opens the drawer, swipe left on the drawer closes it (edge-start only, so editor selection and kanban scrolling are untouched)
-
iCloud Drive vault tier (spec 03): ••• → iCloud Sync moves the vault into the app's ubiquity container (
iCloud Drive › ZenNotes) via Apple'ssetUbiquitousmigration; the OS syncs it across devices, and a Mac can open~/Library/Mobile Documents/iCloud~md~zennotes/Documents/ZenNotes/…as a desktop vault. Never combined with any other sync on the same vault (enable = move, not copy). Placeholder discipline:.name.icloudeviction stubs are mapped to logical entries inNativeFs.readdir, reads request a download + retry, andensureDownloadedsweeps the tree at vault open and on every foreground — evicted files must never read as deleted (the documented Obsidian data-loss class). Graceful fallback to local storage when iCloud is signed out. Native side:ICloudVaultPlugin.swift; entitlements: CloudDocuments +iCloud.md.zennotescontainer;NSUbiquitousContainersmakes the folder visible in the Files app. Simulator/device testing requires a signed-in Apple ID (and a paid developer team on real devices — iCloud isn't in free provisioning).
Prefs seeding + theme attributes live in an inline classic <script> in
index.html, not a module: the app-core store reads
localStorage['zen:prefs:v2'] at module-evaluation time, and Rollup chunk
hoisting (manualChunks) runs the store chunk before any entry-chunk module —
an imported "bootstrap.ts" silently ran too late on fresh installs.
- ZenNotes Sync (spec 04) — the E2E-encrypted first-party sync service;
the largest Phase 1 item, spans
apps/servertoo - Home-screen widget / App Shortcuts capture entry points
- iPad split view (two notes side by side); Android (Phase 2)
- Store distribution work (signing, TestFlight, App Store listing — spec 08)