Skip to content

Style Guide

Code standards and conventions for On Book Pro development.


Code Principles

1. Primitive Conformity

All UI elements should use design system primitives from src/components/common/:

  • Button — All clickable actions
  • Input, Select, Textarea — All form elements
  • Modal, ModalHeader, ModalFooter — All dialogs
  • Card — Content containers with consistent shadows
  • TabNavigation, SubtabSwitcher, SidebarTabs — All navigation

Don't: Use raw <button>, <input>, or <select> elements
Do: Import and use the primitive component

Button Icon Support (January 2026)

Buttons with icons should use the leftIcon or rightIcon props:

tsx
// ✅ Good: Use icon props
<Button leftIcon="add" onClick={handleAdd}>Add Item</Button>
<Button rightIcon="arrow_forward">Continue</Button>

// ❌ Bad: Manual icon composition
<Button><Icon name="add" /> Add Item</Button>

Decorator Patterns

FloatingToolbar automatically decorates child buttons with floating-specific styles:

tsx
// Child buttons get pill styling + shadows automatically
<FloatingToolbar position="bottom-right">
  <Button leftIcon="edit" onClick={handleEdit}>Edit</Button>
  <Button leftIcon="delete" variant="danger">Delete</Button>
</FloatingToolbar>

2. Branded Types

Use nominal types for semantic ID safety:

typescript
// ❌ Don't
function getNote(id: string) { ... }

// ✅ Do
import { NoteId } from '@/types/branded';
function getNote(id: NoteId) { ... }

Full Branded Types Guide →


3. File Organization

Features: Domain-specific logic lives in src/features/<feature>/

  • Each feature is self-contained
  • Export a single public API from index.ts

Components: Reusable UI primitives live in src/components/common/

  • Fully generic (no domain logic)
  • Compose from Tailwind utilities

4. Naming Conventions

TypeConventionExample
ComponentsPascalCaseBlockingTracker.tsx
HookscamelCase with use prefixuseBlockingState.ts
TypesPascalCaseBlockingEvent
ConstantsSCREAMING_SNAKE_CASEMAX_UPLOAD_SIZE
FunctionscamelCasecalculateDistance()

5. TypeScript Standards

  • Strict mode enabled: No implicit any
  • Explicit return types for public functions
  • Branded types for IDs and timestamps
  • Zod schemas for external data validation

6. State Management

Use Zustand slices with Immer:

typescript
// ✅ Good: Mutation via Immer
set((state) => {
  state.notes[id].content = newContent;
});

// ❌ Bad: Manual spreading
set({ notes: { ...state.notes, [id]: { ...state.notes[id], content: newContent }}});

7. Accessibility

  • All interactive elements must be keyboard accessible
  • Images require alt attributes
  • Color contrast ratio ≥ 4.5:1
  • Semantic HTML (<nav>, <main>, <section>)

8. Animation Conventions

Use the motion package (Framer Motion) for all enter/exit animations, layout transitions, and spring physics. Do not add new CSS @keyframes for UI transitions.

tsx
// ✅ Good: Use useMotionConfig presets
const { overlayTransition } = useMotionConfig();
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={overlayTransition} />

// ❌ Bad: Raw CSS keyframes for enter/exit
<div className="animate-fade-in" />

When to use which mechanism:

MechanismUse Case
AnimatePresence + motion.divComponents that mount/unmount (modals, toasts, dropdowns)
layoutIdShared element transitions between two states (Barry FAB → panel)
layout propAuto-animating size/position changes (sidebar collapse, accordion)
useMotionConfig presetsAlways — never hardcode spring/tween values

Exit animations: Keep exits fast (≤ 200ms). If a component uses layoutId, exit its children instantly to avoid competing animations.


Git Workflow

Commit Messages

Follow conventional commits:

feat: add blocking path visibility toggle
fix: resolve sync race condition in presence
docs: update setup guide with Node 20 requirement

Branch Strategy

  • main — Production
  • refactor/react-migration — Active development
  • feature/your-feature — Feature branches

For more technical patterns, see the Developer Documentation.