An attention orchestration system for budgeting moments toward personal flourishing.
Zenborg is a local-first web application for conscious attention allocation. Not a task manager. Not a habit tracker. Not a calendar.
Core Philosophy:
- Orchestration, not elimination: Accept distractions, budget for them
- Consciousness as currency: Allocate attention, not time
- Presence over outcomes: No "done" buttons, no completion tracking
The Question: "Where will I place my consciousness today?"
Moment - A named intention (1-3 words max)
interface Moment {
id: string // UUID
name: string // "Morning Run", "Deep Work"
areaId: string // FK to Area
phase: Phase | null // morning/afternoon/evening/night
day: string | null // ISO date, null when unallocated
order: number // 0-2 (max 3 per phase)
createdAt: string
updatedAt: string
}Area - Life domain with color (user-extensible)
interface Area {
id: string
name: string // "Wellness", "Craft", "Social"
color: string // hex color
emoji: string // 🟢, 🔵, 🟠
isDefault: boolean
order: number
createdAt: string
updatedAt: string
}
// 5 default areas: Wellness, Craft, Social, Joyful, IntrospectiveCycle - Time container (e.g., "Barcelona Summer")
interface Cycle {
id: string
name: string
startDate: string // ISO date
endDate: string | null // null for ongoing
isActive: boolean // only one active at a time
createdAt: string
updatedAt: string
}PhaseConfig - User-configurable phase settings
interface PhaseConfig {
id: string
phase: Phase // morning/afternoon/evening/night
label: string // "Morning", "Afternoon"
emoji: string // ☕, ☀️, 🌙, 🌃
color: string // hex color
startHour: number // 0-23
endHour: number // 0-23 (wraps for night: 22-6)
isVisible: boolean // can hide Night phase
order: number
createdAt: string
updatedAt: string
}- Max 3 moments per (day, phase) combination
- Only 1 active cycle at a time
- Areas cannot be deleted if they have moments (FK constraint)
- Moment names: 1-3 words enforced in app
Core:
- Next.js 15 (App Router, TypeScript, Turbopack)
- Tailwind CSS 4 (monochromatic design + color accents)
- Shadcn/ui (Radix primitives, no heavy modals)
State & Data:
@legendapp/state- Reactive local-first state management@legendapp/state/persist- IndexedDB persistence@legendapp/state/sync-supabase- Future cloud sync (Phase 2)
Interactions:
@dnd-kit/core+@dnd-kit/sortable- Drag & drop (optional for mouse users)date-fns- Lightweight date handling
Testing:
- Vitest - Unit tests
- Playwright - E2E tests
- @testing-library/react - Component tests
Future (Phase 2):
- Supabase - PostgreSQL database + auth + real-time sync
Hexagonal (Ports & Adapters) with DDD principles:
src/
├── domain/ # Pure TypeScript, no frameworks
│ ├── entities/ # Moment, Area, Cycle (business logic)
│ ├── value-objects/ # Phase, PhaseConfig
│ └── repositories/ # Interfaces (ports)
├── infrastructure/ # Framework-specific implementations
│ ├── persistence/ # IndexedDB + future Supabase adapters
│ └── state/ # Legend State store
├── application/ # Use cases (orchestration)
│ ├── use-cases/ # CreateMoment, AllocateMoment, etc.
│ └── services/ # TimeService (phase detection)
└── presentation/ # React components + hooks
├── components/ # UI components (Vim-aware)
├── hooks/ # Custom React hooks
└── app/ # Next.js pages
Key Principles:
- Domain logic isolated from UI/infrastructure
- Local-first: IndexedDB primary, Supabase secondary (eventual consistency)
- PostgreSQL-ready schema: All entities designed for relational DB migration
- SOLID: Single responsibility, dependency inversion, open/closed
Standardized pattern for entity forms (Habits, Moments):
┌─────────────────────────────────────────────────┐
│ Presenters (Form Dialogs) │
│ - Read UI state from uiStore │
│ - Call onSave/onDelete callbacks for persistence│
│ - Minimal local state (popovers, focus) │
├─────────────────────────────────────────────────┤
│ Infrastructure/State │
│ - uiStore: Ephemeral form state (NOT persisted) │
│ - store: Domain entities (persisted) │
├─────────────────────────────────────────────────┤
│ Application/Services │
│ - HabitService, MomentService, AreaService │
│ - Orchestrate domain operations │
├─────────────────────────────────────────────────┤
│ Domain/Entities │
│ - Habit, Moment, Area (pure business logic) │
└─────────────────────────────────────────────────┘
1. UI Store State (infrastructure/state/ui-store.ts):
- Form field values (name, areaId, emoji, etc.)
- Dialog open/close state
- Mode (create/edit)
- Editing entity ID (for edit mode)
- Convenience defaults (lastUsedAreaId)
2. Helper Functions:
openHabitFormCreate()/openMomentFormCreate()- Initialize form for create modeopenHabitFormEdit()/openMomentFormEdit()- Initialize form for edit modecloseHabitForm()/closeMomentForm()- Close form and reset state
3. Form Dialog Component:
- Props: Only
onSaveandonDeletecallbacks - Reads all form state from
habitFormState$ormomentFormState$ - Updates store directly (e.g.,
habitFormState$.name.set(value)) - Local state ONLY for UI (popover open states, validation errors)
4. Parent Component:
- Calls helper functions to open forms
- Provides persistence callbacks that call application services
- No form state management (delegated to uiStore)
// 1. UI Store (infrastructure/state/ui-store.ts)
export interface HabitFormState {
open: boolean;
mode: "create" | "edit";
name: string;
areaId: string;
emoji: string | null;
attitude: Attitude | null;
phase: Phase | null;
tags: string[];
editingHabitId: string | null;
}
export const habitFormState$ = observable<HabitFormState>({...});
export function openHabitFormCreate(params?: { areaId?: string }) {
habitFormState$.set({
open: true,
mode: "create",
name: "",
areaId: params?.areaId || lastUsedAreaId$.peek() || "",
// ... rest of fields
});
}
// 2. Form Dialog (components/HabitFormDialog.tsx)
interface HabitFormDialogProps {
onSave: (props: CreateHabitProps | UpdateHabitProps) => void;
onDelete?: () => void;
}
export function HabitFormDialog({ onSave, onDelete }: HabitFormDialogProps) {
// Read from store
const formState = use$(habitFormState$);
const { open, mode, name, areaId, emoji } = formState;
// Update store directly
const handleSave = () => {
onSave({ name, areaId, emoji, ... });
closeHabitForm();
};
return (
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && closeHabitForm()}>
<input
value={name}
onChange={(e) => habitFormState$.name.set(e.target.value)}
/>
</Dialog>
);
}
// 3. Parent Component (components/AreaGallery.tsx)
function AreaGallery() {
const handleSaveHabit = (props: CreateHabitProps | UpdateHabitProps) => {
const formState = habitFormState$.peek();
if (formState.mode === "edit") {
habitService.update(formState.editingHabitId, props);
} else {
habitService.create(props);
}
};
return (
<>
<button onClick={() => openHabitFormCreate({ areaId: area.id })}>
New Habit
</button>
<HabitFormDialog onSave={handleSaveHabit} onDelete={handleDeleteHabit} />
</>
);
}| Principle | How It Helps |
|---|---|
| Separation of Concerns | UI state (uiStore) separated from domain state (store) |
| Single Source of Truth | Form state lives in one place, not duplicated in props |
| Type Safety | Store provides typed state, prevents prop drilling |
| Testability | Can test form logic by manipulating store directly |
| Reusability | Helper functions make opening forms trivial |
| Clear Boundaries | Callbacks define persistence boundary (Presenter → Application) |
| Convenience | Preserve defaults (lastUsedAreaId) across sessions |
Areas use inline editing (not dialogs) per design constraint "No modals, flat UI":
- Simple properties (name, emoji, color)
- Contextual to specific card
- Local state in card component (EmptyAreaCard, PlanAreaCard)
- No need for dialog or global state
| Normal Mode | Action |
|---|---|
hjkl |
Navigate grid (left/down/up/right) |
gg / G |
First / Last moment |
w / b |
Next / Previous moment |
i |
Insert mode (create or edit) |
dd |
Delete moment |
yy |
Yank (duplicate) |
p |
Put (paste yanked moment) |
x |
Quick delete (unallocated only) |
: |
Command mode |
Ctrl+/ |
Toggle compass view |
| Command Mode | Action |
|---|---|
:ty1 |
Allocate to Today, phase 1 (Morning) |
:wy3 |
Allocate to Tomorrow, phase 3 (Evening) |
:d |
Unallocate (return to drawing board) |
:area |
Open area management |
:settings |
Open phase settings |
Day shortcuts: y (yesterday), t (today), w (tomorrow/will do)
Phase shortcuts: 1 (morning), 2 (afternoon), 3 (evening), 4 (night)
┌─────────────────────────────────────────────────────────┐
│ Zenborg │
├─────────────────────────────────────────────────────────┤
│ │
│ Timeline (3 days × 3-4 phases grid) │
│ │
│ Yesterday │ Today ★ │ Tomorrow │
│ ☕ [slot] │ [slot] │ [slot] │
│ ☀️ [slot] │ [slot] │ [slot] │
│ 🌙 [slot] │ [slot] │ [slot] │
│ │
├─────────────────────────────────────────────────────────┤
│ Drawing Board │
│ [unallocated moments...] │
└─────────────────────────────────────────────────────────┘
┌───────────────────┐
│ ← Today → │ (swipe or buttons)
├───────────────────┤
│ ☕ Morning │
│ [moment 1] │
│ [moment 2] │
├───────────────────┤
│ ☀️ Afternoon │
│ [moment 1] │
├───────────────────┤
│ 🌙 Evening │
│ [moment 1] │
│ [moment 2] │
├───────────────────┤
│ Drawing Board │
│ [unallocated...] │
└───────────────────┘
Mobile differences:
- Single-day column (not 3-day grid)
- Drawing board below timeline (not sidebar)
- Swipe left/right to navigate days
- Monochromatic base: Off-white (#fafaf9), light gray (#f5f5f4)
- Phase colors as accents: Morning (amber), Afternoon (yellow), Evening (purple), Night (dark slate)
- Area colors on moments: Border or small pill
- Flat design: No modals, inline editing only
- Things 3: Flat hierarchy, inline editing, keyboard-first
- Linear: Clean spacing, subtle borders, fast shortcuts
- Vercel Dashboard: Monochrome with color accents, no clutter
- Claude UI: Generous whitespace, simple interactions
- Moment names: 20-24px, bold, monospace or Inter
- Command line: 14px, monospace (Fira Code, JetBrains Mono)
- Labels: 14px, medium weight
/* Base */
bg-stone-50, text-stone-900, border-stone-200
/* Phase colors */
morning: #f59e0b (amber)
afternoon: #eab308 (yellow)
evening: #8b5cf6 (purple)
night: #1e293b (dark slate)
/* Area colors */
Wellness: #10b981 (green)
Craft: #3b82f6 (blue)
Social: #f97316 (orange)
Joyful: #eab308 (yellow)
Introspective: #6b7280 (gray)- All data stored locally in browser
- Auto-save on every change (500ms debounce)
- Schema matches PostgreSQL structure (UUIDs, FKs, timestamps)
- No backend, no auth, no sync
- Enable
@legendapp/state/sync-supabaseplugin - Real-time bidirectional sync (local ↔ cloud)
- Conflict resolution: last-write-wins
- Offline-first: app works without connection
- Already PostgreSQL-compatible: All entities use proper relational design
- UUIDs for all IDs: Enables distributed creation (no auto-increment)
- Foreign keys enforced: area_id REFERENCES areas(id)
- Timestamps on everything: created_at, updated_at for audit trail
- Indexes for performance: Composite indexes on (day, phase), area_id, etc.
MVP explicitly excludes:
- ❌ Task management (no subtasks, no dependencies)
- ❌ Time tracking (no duration, no timers)
- ❌ Habit tracking (no streaks, no completion percentages)
- ❌ Metrics/analytics (no dashboards, no charts)
- ❌ Notifications/reminders (calm tech, not nagging tech)
- ❌ Collaboration (single-user only)
- ❌ Attachments/URLs (3 words is the interface)
- ❌ Calendar sync (Phase 3+ only)
- ❌ Mobile native apps (PWA is sufficient)
Design Constraints:
- No modals (flat UI, inline editing)
- No outcomes (orchestration, not task completion)
- No quantification (presence, not performance)
- Boring by design (mindful tech is intentionally calm)
# Create Next.js app
npx create-next-app@latest zenborg --typescript --tailwind --app --src-dir
# Install dependencies
npm install @legendapp/state date-fns
npm install @dnd-kit/core @dnd-kit/sortable
npm install -D vitest @testing-library/react @playwright/test
# Run dev server
npm run dev# Unit tests (domain logic)
npm run test
# E2E tests
npx playwright install # First time only
npm run test:e2e# Production build
npm run build
# Start production server
npm run startPrimary Question: "Did I consciously allocate my attention today?"
User Testing Goals:
- Is the learning curve acceptable for power users?
- Does single-day mobile view feel focused?
- Is inline editing better than modals?
- Does the 3-word constraint feel liberating?
Technical Health:
- No data loss on refresh/crash
- Drag & drop smooth (60fps desktop)
- Touch interactions smooth (mobile)
- Loads in <1 second
This project is the digital evolution of a physical whiteboard system using magnets. Key insights:
Orchestration, Not Elimination:
"It is not about eliminating distractions from my life. Accept them, make some room for them to avoid them growing too much."
Consciousness as Currency:
"I'm not budgeting hours; I'm allocating attention. The difference changes everything."
No Metrics, Only Presence:
"The system measures through presence, not performance. Did I consciously allocate my attention today? That's the only metric that matters."
Physical Constraints → Digital Liberation:
"Three items maximum per phase. This isn't limitation; it's liberation."
Mindful Tech is Boring:
"Mindful tech comes at a cost: it's boring. It's not meant to be exciting, intriguing. It's meant to hide the digital tech behind a wall - away from our attention."
Q: Why 3 words maximum for moments? A: Forces clarity. If you can't name it in 3 words, the intention isn't clear enough.
Q: Why no "done" button or completion tracking? A: This isn't task management. It's about committing to the time, not achieving outcomes.
Q: Why no infinite timeline scrolling? A: Presence requires bounded context. Yesterday (reflection), Today (presence), Tomorrow (intention). That's enough.
Q: Why PostgreSQL if it's local-first? A: The schema is designed for eventual cloud sync (Phase 2). Local IndexedDB structure matches PostgreSQL for seamless migration.
Q: Can I use this without learning Vim shortcuts? A: Yes. Drag & drop, inline forms, and click interactions work. Vim mode is for power users who want maximum efficiency.
Q: Why "Zenborg"? A: Zen (mindfulness, presence) + Cyborg (technology augmenting human capability). A mindful cyborg approach to attention management.
Attention Orchestration System (Attend): The physical whiteboard + magnets prototype that inspired Zenborg
Mindful Technology: Technology designed to reduce attention strain, using peripheral interfaces ("feelers") and ambient outputs ("indicators")
Calm Technology: Systems that inform without demanding focal attention (Mark Weiser)
Shape Up: Basecamp's product development methodology (appetite-based, fixed time/variable scope)
Habylon: Future system for habit-building through ambient interaction
Perceive: Knowledge graph system where moments become nodes in a network of intention and action
MIT License - See LICENSE file for details
This is a personal project for conscious attention management. Built with the philosophy that structure should guide our organic growth and that technology should enhance rather than extract human attention.
"Where will I place my consciousness today?"
- do not run the dev OR build the app, I'm running it myself
- IMPORTANT: only use
stonetones (monochrome) unless attributed to an area - all mobile UX should be designed for landscape exprience. Portrait mode won't be considered.