Skip to content

gavinocarroll/borderland

Repository files navigation

Borderland

Borderland is a desktop-first writing companion (with a web build) that helps you Ease Out at the end of a session—capturing what was alive, what’s next, and how it felt—then Drop In at the start of the next session with that context surfaced as a simple slide flow. North Star holds your through-line, and Journal / Progress views surface history and “written into existence” notes.


Tech stack

Layer Technology
UI React 19, TypeScript
Bundler / dev server Vite 7 (@vitejs/plugin-react)
Desktop shell Tauri v2 (Rust, tauri-plugin-deep-link, tauri-plugin-opener, tray)
Persistence Browser localStorage (no backend)

GitHub repository

https://github.com/gavinocarroll/borderland


Branch structure

Branch Role
main Stable v0.1 line
figma-redesign Current active development (UI/flows, web parity, deployment config)

Default work happens on figma-redesign; merge to main when you cut a stable release.


Vercel deployment (web)

  • The repo is intended to deploy from Vercel (static Vite build).
  • vercel.json skips Vercel builds when VERCEL_GIT_COMMIT_REF is gh-pages (avoids noisy double-builds from that branch).
  • vite.config.ts sets base: process.env.VITE_BASE_URL ?? "/" — set VITE_BASE_URL in Vercel if the app is served from a subpath (include trailing slash if required for your setup).

Production URL (typical): https://borderland.vercel.appconfirm under Vercel → your project → Domains; if the project slug differs, the hostname will differ.

Preview URLs: Each push to a connected branch gets a unique *.vercel.app preview URL on the deployment summary.


Screens, routes, and view keys

Routing is not URL-based in-app (no React Router). src/App.tsx holds activeView and renders one screen. Web uses location.hash on load; macOS app uses borderland:// deep links (Tauri emits a deep-link event).

activeView Component Role / UI label (typical)
welcome WelcomeScreen.tsx First-run / onboarding entry
north-star-entry NorthStarEntry.tsx Capture or edit North Star (entry flow)
north-star-log NorthStarLog.tsx North Star log / history
ease-out-form EaseOutForm.tsx Ease Out multi-step form
drop-in DropInView.tsx Drop In slide replay + “time to write”
progress ProgressView.tsx Journal — session history (nav centre label)
progress-v1 ProgressViewV1.tsx Progress — “things written into existence” (progress_marker)
timer TimerScreen.tsx Timer with instruction passed from Drop In

Hash routing (web only, read once on load):

Hash activeView
#dropin drop-in
#easeout ease-out-form
#northstar north-star-log
#journal progress
#progress progress-v1

Data model (src/types/index.ts)

Session

Field Type Purpose
id string Unique session id
createdAt string ISO timestamp (session created / Ease Out saved)
writingStartedAt string? When user logged “start writing” from Drop In
easeOutSubmittedAt string? Reserved for telemetry
dropInViewedAt string? When Drop In was opened for this session
sessionClosedAt string? Reserved for telemetry
easeOut PromptResponse[] Answers keyed by promptId

PromptResponse

Field Type Purpose
promptId string e.g. lines_alive, continue_from, felt_sense, progress_marker
entries string[] One or more text lines per prompt
timeSpentMs number? Optional per-prompt timing

AppMeta

Field Type Purpose
hasCompletedFirstSession boolean Skips Welcome after first Ease Out completion
userName, userEmail, projectTitle string? Profile / project labelling
northStar string? Current North Star text
northStarHistory NorthStarEntry[]? Append-only style history
dropInCount number? Counts Drop In opens (e.g. North Star slide every 3rd)

NorthStarEntry

Field Type
value string
updatedAt string (ISO)

PromptConfig in the same file drives src/config/prompts.ts (labels, Drop In labels, which prompts appear in Ease Out vs Drop In).


Storage (src/storage/storage.ts)

Keys

Key Contents
borderland_sessions JSON array of Session
borderland_meta JSON object AppMeta

Functions

Function Behaviour
getSessions() Parse borderland_sessions; invalid → []
appendSession(session) Push session, write array back
getLastSession() Last element of array or null
updateLatestSession(patch) Merge patch into last session; no-op if empty
getMeta() Parse borderland_meta; returns defaults if missing/invalid
setMeta(meta) Stringify and save full AppMeta
saveNorthStar(value) Updates northStar and appends NorthStarEntry to northStarHistory

Known gap: getMeta() rebuilds a subset of fields and does not currently merge optional fields like dropInCount from stored JSON (see Known issues).


Keyboard shortcuts (src/App.tsx)

All use e.metaKey ( ⌘ Command on macOS ). Behaviour is global while the window has focus.

Shortcut Action
⌘ J Open Journal (progressProgressView)
⌘ K Open North Star log (north-star-log)
⌘ P Open Progress v1 (progress-v1ProgressViewV1)
⌘ ⇧ X Export sessions + meta + exportedAt as JSON to clipboard (textarea + document.execCommand('copy')), then alert
⌘ ⇧ Backspace localStorage.clear() and return to Welcome

Drop In (DropInView.tsx): ← / → arrow keys step slides (in addition to click-to-advance), with cleanup in the same effect as the click listener.


Deep links

macOS app (Tauri)

Registered scheme: borderland:// (src-tauri/tauri.conf.json, tauri-plugin-deep-link). Rust (src-tauri/src/main.rs) shows/focuses the window and emits payload to the webview; React listens only when __TAURI_INTERNALS__ is on window.

URL prefix Screen
borderland://dropin Drop In
borderland://easeout Ease Out
borderland://northstar North Star log
borderland://journal Journal (progress)

Note: #progress (web) maps to progress-v1; there is no borderland://progress handler in App.tsx today—add one if you want parity.

Web (static / Vercel)

Use the same destinations with hash fragments after your deployed origin, e.g. https://borderland.vercel.app/#dropin.

Copy link in Drop In: Tauri copies borderland://… lines; browser copies origin + pathname + #… (see handleCopyLink in DropInView.tsx).


How to run the dev server

cd borderland
npm install
npm run dev

Vite serves on port 1420 (matches Tauri devUrl in tauri.conf.json). Open the printed local URL in a browser for web-only dev.

With Tauri window:

npm run tauri dev

How to build a macOS DMG (Tauri)

Prerequisites: Rust toolchain, Xcode command line tools, and Tauri prerequisites for macOS.

cd borderland
npm install
npm run tauri build

Artifacts (exact subfolders can vary slightly by Tauri 2 / bundle settings) appear under:

src-tauri/target/release/bundle/ — look for dmg/ (.dmg installer) and macos/ (.app).

tauri.conf.json sets "targets": "all"; DMG is included for macOS when building on macOS.


How to push to GitHub and trigger a Vercel deploy

  1. Connect the repo once in Vercel: New Project → import gavinocarroll/borderland, framework Vite, build command npm run build, output dist.

  2. Set branch: Production can track main or figma-redesign depending on your release process; previews build for other connected branches.

  3. Push:

    git checkout figma-redesign   # or main
    git add -A
    git commit -m "your message"
    git push origin figma-redesign
  4. Vercel runs npm run build on the push; inspect the deployment log for the live Production or Preview URL.

vercel.json: Pushes that only touch gh-pages are ignored by Vercel’s build (script exits 0 → skip), so that branch does not spawn app deploys.


Known issues

  1. getMeta() vs AppMeta: Reconstructed meta omits some stored optional fields (e.g. dropInCount) so behaviour after a full reload may not match in-memory state until getMeta returns a full merge of AppMeta.
  2. Global console.log on every keydown in App.tsx — debugging noise; remove or guard with import.meta.env.DEV.
  3. Telemetry export uses deprecated document.execCommand('copy') — fragile under strict permissions; consider navigator.clipboard with fallback.
  4. progress vs progress-v1: Two different “progress” experiences (Journal vs Signs of progress); naming and shortcuts (⌘J vs ⌘P) take discipline to document for users.
  5. Deep link parity: Web #progress has no matching borderland://… handler in App.tsx yet.
  6. CSP is null in tauri.conf.json — tighten before shipping untrusted remote content.

Project layout (short)

src/App.tsx              # activeView, hash, deep-link listener, shortcuts
src/storage/storage.ts   # localStorage API
src/types/index.ts       # Session, AppMeta, NorthStarEntry, PromptConfig
src/config/prompts.ts    # Prompt catalogue
src/views/*.tsx          # Screens
src-tauri/               # Rust, Tauri config, icons, Info.plist
vercel.json              # Vercel ignore rules for gh-pages
vite.config.ts           # base path, port 1420, Tauri HMR host

License

package.json marks the package as private; add a license file when you open-source or distribute.