A priority-ranking web app based on the Crystal Triangle method. The user lists tasks, the app runs them through a pairwise comparison matrix (every item vs. every other item), tallies the votes, and surfaces the top 3 priorities underlined. Everything below the top 3 is marked deferred. The app is built to be used daily as a personal task prioritization tool, with a roadmap toward team/B2B use and PM app integrations.
- User enters 3–20 items
- Each item is assigned a letter A–Z
- App generates every unique pair: A vs B, A vs C... B vs C, B vs D... etc.
- Total pairs for N items = N * (N - 1) / 2
- User picks one item from each pair — whichever matters more right now
- Tally wins per item
- Sort by win count descending
- Top 3 = priorities (displayed underlined)
- Everything ranked 4th and below = deferred
The core logic lives in pure utility functions completely separate from UI components. This is non-negotiable for future API and plugin use.
| Layer | Choice | Why |
|---|---|---|
| Framework | Next.js 14 (App Router) | Best AI tooling support, good defaults |
| Language | TypeScript | Catch errors early, better AI code gen |
| Styling | Tailwind CSS | Fast iteration, easy to override |
| Persistence (phase 1) | localStorage | Zero backend, works immediately |
| Persistence (phase 2) | Supabase | Auth + DB + API, free tier, AI knows it well |
| Deployment | Vercel | One-command deploy, free tier, GitHub integration |
| Package manager | npm | Keep it simple |
crystal-triangle/
├── app/
│ ├── layout.tsx # Root layout, fonts, metadata
│ ├── page.tsx # Home — renders the app shell
│ └── globals.css # Tailwind base + custom CSS vars
├── components/
│ ├── InputPhase.tsx # Step 1: enter and manage items
│ ├── ComparePhase.tsx # Step 2: pairwise comparison cards
│ └── ResultsPhase.tsx # Step 3: ranked results display
├── lib/
│ └── crystalTriangle.ts # ALL business logic — pure functions only, no React
├── hooks/
│ └── useCrystalTriangle.ts # State management, bridges lib → components
├── types/
│ └── index.ts # Shared TypeScript types
└── public/
└── manifest.json # PWA manifest
export type Item = {
id: string
label: string
letter: string
}
export type Pair = {
a: number // index into items array
b: number
}
export type Session = {
items: Item[]
pairs: Pair[]
choices: Record<number, number> // pairIndex → winning item index
phase: 'input' | 'compare' | 'results'
}
export type RankedResult = {
item: Item
votes: number
rank: number
isPriority: boolean // true for top 3
isDeferred: boolean // true for rank 4+
}Implement these pure functions. No React, no imports from components, no side effects.
// Assign letters A-Z to items
export function assignLetters(items: string[]): Item[]
// Generate all unique pairs
export function buildPairs(itemCount: number): Pair[]
// Given choices map, tally votes per item index
export function tallyVotes(pairs: Pair[], choices: Record<number, number>): Record<number, number>
// Sort by votes, assign ranks, flag top 3 vs deferred
export function rankResults(items: Item[], votes: Record<number, number>): RankedResult[]
// Convenience: run full ranking from raw inputs
export function runTriangle(items: string[], choices: Record<number, number>): RankedResult[]Single hook that owns all app state and exposes clean actions to components.
// Exposes:
const {
session, // current Session object
addItem, // (label: string) => void
removeItem, // (index: number) => void
startComparing, // () => void — moves to compare phase
vote, // (pairIndex: number, winnerIndex: number) => void
goBack, // () => void — back one pair
showResults, // () => void
restart, // () => void — full reset
results, // RankedResult[] — computed from current session
progress, // { current: number, total: number }
} = useCrystalTriangle()State persists to localStorage on every change. Key: ct_session.
- Text input + Add button, Enter key submits
- Items render as chips with their assigned letter and a remove button
- Show count: "X items — Y more allowed" (max 20)
- "Start comparing →" button disabled until 3+ items
- No textarea — one item at a time for clarity
- Progress bar at top (current pair / total pairs)
- One card at a time showing both options side by side
- Each option shows: large letter, item text
- Tap/click to select — immediately advances to next pair
- Previously chosen option shows as selected if user navigates back
- "← back" button visible after first pair
- Pair counter: "3 of 21"
- Ranked list, all items shown
- Top 3: rank label (1st/2nd/3rd), underlined item text, vote bar filled dark
- Rank 4+: muted rank, normal text, lighter vote bar
- "Deferred" section label above rank 4+ items
- Vote count shown on each row
- Buttons: "Start over" and "← revise comparisons"
Add to app/layout.tsx metadata:
export const metadata = {
title: 'Crystal Triangle',
manifest: '/manifest.json',
themeColor: '#000000',
appleWebApp: { capable: true, statusBarStyle: 'default' }
}public/manifest.json:
{
"name": "Crystal Triangle",
"short_name": "CT",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}Clean, minimal, utilitarian. This is a daily-use tool — zero decoration, maximum clarity. No gradients, no shadows, no color splashes. Black and white with one neutral gray for secondary elements.
- Font: system-ui or a clean monospace for letters/counts
- Background: white
- Primary text: black
- Secondary text: #666
- Borders: 1px solid #e5e5e5
- Selected/active state: black background, white text
- Progress bar: black fill on light gray track
- Priority items: underlined, font-weight 500
- Deferred items: color #999, normal weight
- Mobile-first sizing: touch targets minimum 44px height
The designer (me) will handle all visual polish after the scaffold is working. Build it functional and clean — do not add decorative elements.
- Scaffold the Next.js project with TypeScript and Tailwind
- Create the folder structure above
- Implement
lib/crystalTriangle.ts— all pure functions with TypeScript types - Implement
hooks/useCrystalTriangle.tswith localStorage persistence - Build all three components (InputPhase, ComparePhase, ResultsPhase)
- Wire everything together in
app/page.tsx - Add PWA manifest and metadata
- Confirm it runs locally with
npm run dev
Do not add Supabase, authentication, or API routes in this session. localStorage only for now.
- Phase 2: Supabase auth + cloud sync — sessions persist across devices
- Phase 3: REST API —
POST /api/rankaccepts items array, returns ranked results - Phase 4: Team mode — multiple users vote on shared item set, app surfaces agreement/divergence
- Phase 5: PM app plugins — Jira, Linear, Notion integrations via OAuth
The pure function architecture in lib/crystalTriangle.ts is the foundation for all of these. Protect it.
Scaffold a new Next.js 14 project with TypeScript and Tailwind CSS using the App Router. Create the folder structure in this brief exactly. Implement all pure functions in lib/crystalTriangle.ts, the state hook in hooks/useCrystalTriangle.ts with localStorage persistence, and all three components. Wire them into app/page.tsx. Add PWA manifest. Design should be clean and minimal — black, white, and gray only. Do not add any backend, auth, or database. Confirm it runs with npm run dev.