Avatars, chips, code blocks, stats and badges: ways to show information.
With no colour to lean on, kinds and states are told apart by shape (solid, outline, dashed, hatched), weight and words.
13 parts
Initials on a softened grey square, in five sizes, and a stack that folds extra people into a count.
import { Avatar } from '@rchursin/product/ui'
const sizes = [ ['xs', 16], ['sm', 20], ['md', 24], ['lg', 32], ['xl', 40],] as const
/** Five names that land on the five greys */const team = ['Alex Rivera', 'Jonas Weber', 'Theo Park', 'Hana Sato', 'Mara Quinn']
/** Initials on a softened square. The grey is worked out from the name, so a person always gets the same one. */export default function AvatarSizes() { return ( <div className="flex flex-col items-center gap-10"> <ul className="flex items-end gap-5"> {sizes.map(([size, px]) => ( <li key={size} className="flex flex-col items-center gap-2"> <Avatar name="Mara Quinn" size={size} /> <span className="font-mono text-[11px] text-fg-3 tabular-nums"> {size} {px} </span> </li> ))} </ul> <ul className="flex flex-wrap justify-center gap-x-5 gap-y-3"> {team.map((name) => ( <li key={name} className="flex items-center gap-2 text-[13px] text-fg"> <Avatar name={name} size="md" /> {name} </li> ))} </ul> </div> )}A small label on a thing, in four shapes (outline, solid, muted, dashed), with an optional icon and remove button.
import { Chip } from '@rchursin/product/ui'
/** Four kinds, told apart by shape: a ring, a solid fill, a quiet tint and a dashed edge. Two sizes. */export default function ChipVariants() { return ( <div className="flex flex-col items-center gap-5"> <div className="flex flex-wrap items-center justify-center gap-2"> <Chip icon="tag">telemetry</Chip> <Chip variant="solid" icon="play"> Running </Chip> <Chip variant="muted" icon="pencil"> Draft </Chip> <Chip variant="dashed" icon="clock"> Not saved </Chip> </div> <div className="flex flex-wrap items-center justify-center gap-2"> <Chip size="xs">skill</Chip> <Chip size="xs" variant="solid"> Live </Chip> <Chip size="xs" variant="muted"> Paused </Chip> <Chip size="xs" variant="dashed"> Empty </Chip> </div> </div> )}Highlighted code in five grey roles, with line numbers, picked-out lines, a Wrap toggle, Copy, and long files folded.
import { CodeBlock } from '@rchursin/product/ui'
const source = `import { useState } from 'react'import { Button } from '@rchursin/product/ui'
/** Saves, and keeps focus on the button while it works */export function SaveButton({ onSave }: { onSave: () => Promise<void> }) { const [saving, setSaving] = useState(false)
async function save() { setSaving(true) try { await onSave() } finally { setSaving(false) } }
return ( <Button loading={saving} onClick={save}> {saving ? 'Saving…' : 'Save'} </Button> )}`
/** A file with a title, line numbers and three lines picked out. Highlight only the lines you talk about. */export default function CodeBlockTsx() { return <CodeBlock code={source} lang="tsx" title="SaveButton.tsx" highlightLines={[18, 19, 20]} className="w-full max-w-2xl" />}Task glyphs told apart by shape: a square that fills as work moves on, priority bars, assignee, labels and estimate.
import { useId } from 'react'import { priorities, statuses, type PriorityId, type StatusId } from '@rchursin/product/data/sample'import { PriorityIcon, StatusIcon } from '@rchursin/product/tasks'
/** What each shape is, in words */const statusShape: Record<StatusId, string> = { backlog: 'dashed outline', todo: 'empty square', in_progress: 'half filled', in_review: 'three quarters', done: 'solid, tick cut out', canceled: 'solid, cross cut out',}const priorityShape: Record<PriorityId, string> = { 1: 'solid square with !', 2: 'three bars', 3: 'two bars', 4: 'one bar', 0: 'three dashes' }
/** Every status and priority. The square fills up as work moves on; priority is a meter of bars. */export default function StatusPriorityAll() { const statusId = useId() const priorityId = useId() return (Keep, fix, merge or cut: a mono badge where each verdict has its own shape, and the word is always there.
import { Fragment } from 'react'import { VerdictBadge } from '@rchursin/home/ui'
const verdicts = [ ['keep', 'Used, and it finishes the job.', 'solid'], ['fix', 'Used, but fails too often.', 'outlined'], ['merge', 'Does the same job as another skill.', 'dashed'], ['cut', 'Nobody has run it in weeks.', 'struck through'],] as const
/** Four verdicts, four shapes. The word is always there too, so nothing rests on the shape alone. */export default function VerdictBadges() { return ( <dl className="grid grid-cols-[auto_1fr] items-center gap-x-5 gap-y-3.5 text-[13px]"> {verdicts.map(([verdict, meaning, shape]) => ( <Fragment key={verdict}> <dt> <VerdictBadge verdict={verdict} /> </dt> <dd className="text-fg-2"> {meaning} <span className="text-fg-3">({shape})</span> </dd> </Fragment> ))} </dl> )}A small “Sample data” marker for anything that shows invented records or numbers.
import { SampleBadge } from '@rchursin/product/shell/layout'
const stats = [ ['Cost per task', '$1.84'], ['Finished', '78%'], ['Median session', '14m'],] as const
/** Invented numbers say so, right next to the title, in the same words every time. */export default function SampleBadgeCard() { return ( <div className="lux-card w-full max-w-sm rounded-2xl bg-raised p-5"> <div className="flex items-center justify-between gap-3"> <p className="text-[13px] font-semibold">This week</p> <SampleBadge /> </div> <dl className="mt-4 grid grid-cols-3 gap-3"> {stats.map(([term, value]) => ( <div key={term} className="flex min-w-0 flex-col gap-1"> <dt className="text-xs text-fg-3">{term}</dt> <dd className="text-xl font-semibold tracking-[-0.02em] tabular-nums">{value}</dd> </div> ))} </dl> </div> )}Keyboard shortcuts as a two-column table: key caps on the left, what they do on the right.
import { KeyTable } from '@rchursin/product/shell/layout'
/** A few of the Tasks tool's real shortcuts. "mod" shows as ⌘ on Apple keyboards and Ctrl elsewhere. */const rows = [ { keys: 'j', action: 'Next task' }, { keys: 'k', action: 'Previous task' }, { keys: 'enter', action: 'Open the task' }, { keys: '/', action: 'Search' }, { keys: 'c', action: 'Create a task' }, { keys: 'mod+b', action: 'Switch between list and board' }, { keys: 'mod+z', action: 'Undo the last change' },]
export default function KeyTableShortcuts() { return <KeyTable caption="In the task list" rows={rows} className="w-full max-w-lg" />}A numbered grid of short principles (01, 02, 03), each a title and one or two lines.
import { Rules } from '@rchursin/product/shell/layout'
/** The Tasks tool's rules, most important first. Each is a [title, body] pair; the number comes from the order. */const rules: [string, string][] = [ ['Shape, never colour', 'Status is a square that fills up; priority is a meter of bars. Done and Canceled are both solid, so a tick or a cross tells them apart.'], ['One Tab stop, then arrows', 'The list is a grid: Tab lands once, arrows or J and K move, Enter opens. Fifty rows never cost fifty presses of Tab.'], ['Undo instead of “Are you sure?”', 'Changes happen at once and end in a toast with a single Undo. ⌘Z (Ctrl+Z) keeps working after the toast has gone.'],]
export default function RulesExample() { return <Rules items={rules} className="w-full" />}A do / don’t pair: two cards side by side with a short note each, told apart by icon, word and a dashed edge.
import { DoDont } from '@rchursin/product/iconset/parts'import { Chip } from '@rchursin/product/ui'
/** One rule, shown twice. The verdicts differ by icon, word and a dashed edge, never by colour. */export default function DoDontPair() { return ( <div className="w-full max-w-md"> <DoDont title="Show state with a shape and a word" good={ <span className="flex flex-wrap justify-center gap-2"> <Chip variant="solid" icon="play"> Running </Chip> <Chip variant="dashed" icon="pause"> Paused </Chip> </span> } bad={ <span className="flex gap-3"> <span className="size-3 rounded-full bg-fg" /> <span className="size-3 rounded-full bg-fg-3" /> </span> } goodNote="Solid and dashed, each with its name." badNote="Don’t ask people to tell two greys apart." /> </div> )}Focused time over the last seven days as square-topped bars to scale, with a real table underneath for screen readers.
import type { TimeEntry } from '@rchursin/product/data/sample'import { SampleBadge } from '@rchursin/product/shell/layout'import { WeeklyFocus } from '@rchursin/product/timer'
/** A fixed "now" (Tuesday, October 13, 2026, 5:30 PM), so the page prerenders the same chart every time */const now = new Date(2026, 9, 13, 17, 30)
/** One entry on October `day`, from "HH:MM" to "HH:MM" */function entry(id: string, label: string, day: number, from: string, to: string, kind: TimeEntry['kind'] = 'focus'): TimeEntry { const time = (hm: string) => new Date(2026, 9, day, Number(hm.slice(0, 2)), Number(hm.slice(3))).getTime() return { id, taskId: null, label, start: time(from), end: time(to), kind }}
const log: TimeEntry[] = [ entry('a', 'Review pr-review prompt cost', 7, '13:00', '13:45', 'manual'), entry('b', 'Review pr-review prompt cost', 7, '16:10', '16:35'), entry('c', 'Cut api-docs skill', 8, '14:00', '14:25'), entry('d', 'Write the skill naming guide', 9, '09:30', '09:55'), entry('e', 'Break', 9, '09:55', '10:00', 'break'), entry('f', 'Write the skill naming guide', 9, '10:00', '10:25'), entry('g', 'Write the skill naming guide', 9, '10:30', '10:55'), entry('h', 'Write the skill naming guide', 9, '11:00', '11:25'), entry('i', 'Release-notes agent tone', 12, '10:00', '10:50'), entry('j', 'Weekly skill review', 12, '16:00', '16:25', 'manual'), entry('k', 'Fix test-fixer', 13, '09:05', '09:30'), entry('l', 'Break', 13, '09:30', '09:35', 'break'), entry('m', 'Fix test-fixer', 13, '09:35', '10:00'),]
/** Seven days to scale from a zero baseline. Breaks are left out; today and the best day are labelled. */export default function WeeklyChart() { return ( <div className="flex w-full max-w-md flex-col gap-2"> <div className="flex justify-end"> <SampleBadge /> </div> <WeeklyFocus entries={log} now={now} /> </div> )}The Stopwatch’s table on its own: lap, lap time and total, newest first, with the fastest and slowest named.
import { LapsTable, lapsFrom } from '@rchursin/product/timer'
/** * lapsFrom turns the stopwatch's marks (total time at each Lap press, in ms) into laps, * and names the fastest and slowest once there are two or more. */const laps = lapsFrom([48_210, 95_870, 151_330, 192_480, 241_060])
/** Newest first. Fastest is a solid tag, slowest a dashed one, each with a word and an icon. */export default function LapsTableExample() { return <LapsTable laps={laps} className="w-full max-w-md" />}Events grouped by day under headings that can stick, earliest first, each with its time, calendar pattern and people.
import { AgendaList } from '@rchursin/product/calendar'import type { CalendarEvent } from '@rchursin/product/data/sample'import { SampleBadge } from '@rchursin/product/shell/layout'
/** A fixed "now" (Tuesday, October 13, 2026, 10:40 AM), so the page prerenders the same list every time */const now = new Date(2026, 9, 13, 10, 40)const at = (day: number, h: number, m = 0) => new Date(2026, 9, day, h, m)
const events: CalendarEvent[] = [ { id: 'sprint', title: 'Skill cleanup sprint', start: new Date(2026, 9, 13), end: new Date(2026, 9, 15), allDay: true, calendar: 'team' }, { id: 'standup', title: 'Standup', start: at(13, 9, 30), end: at(13, 9, 45), calendar: 'team', attendees: ['me', 'mq', 'tp', 'iv'] }, { id: 'focus', title: 'Focus: fix test-fixer', start: at(13, 10), end: at(13, 11, 30), calendar: 'focus', taskId: 'RC-128' }, { id: 'lunch', title: 'Lunch with Sam', start: at(13, 12, 30), end: at(13, 13, 30), calendar: 'personal' }, { id: 'review', title: 'pr-review prompt review', start: at(14, 11), end: at(14, 12), calendar: 'team', taskId: 'RC-125', attendees: ['me', 'tp'] }, { id: 'release', title: 'Release window', start: at(15, 15), end: at(15, 17), calendar: 'release', location: 'Room 3' },]
/** Grouped by day, earliest first. Finished events dim, the one under way says Now, and each calendar shows as a pattern. */export default function AgendaWeek() { return ( <div className="flex w-full max-w-lg flex-col gap-2"> <div className="flex justify-end"> <SampleBadge /> </div> <AgendaList events={events} from={now} days={3} now={now} /> </div> )}How an event looks in each calendar view: a block in the week grid, a bar for all-day, a chip in a month cell.
import { useRef, useState } from 'react'import { AllDayBar, EventBlock, MonthChip } from '@rchursin/product/calendar'import { calendarById, eventTime } from '@rchursin/product/calendar/events'import type { CalendarEvent } from '@rchursin/product/data/sample'import { SampleBadge } from '@rchursin/product/shell/layout'import { Popover, usePopover } from '@rchursin/product/ui'
const at = (day: number, h: number, m = 0) => new Date(2026, 9, day, h, m)
const sprint: CalendarEvent = { id: 'sprint', title: 'Skill cleanup sprint', start: new Date(2026, 9, 12), end: new Date(2026, 9, 15), allDay: true, calendar: 'team' }const standup: CalendarEvent = { id: 'standup', title: 'Standup', start: at(14, 9, 30), end: at(14, 9, 45), calendar: 'team' }const review: CalendarEvent = { id: 'review', title: 'pr-review prompt review', start: at(14, 11), end: at(14, 12), calendar: 'team' }const lunch: CalendarEvent = { id: 'lunch', title: 'Lunch with Sam', start: at(14, 12, 30), end: at(14, 13, 30), calendar: 'personal' }const focus: CalendarEvent = { id: 'focus', title: 'Focus: skill naming guide', start: at(14, 13, 30), end: at(14, 15, 30), calendar: 'focus', taskId: 'RC-123' }const release: CalendarEvent = { id: 'release', title: 'Release window', start: at(14, 15, 30), end: at(14, 17), calendar: 'release' }
const label = 'font-mono text-[11px] tracking-[0.12em] text-fg-3 uppercase'
/** * One event in each view. Height decides how much a block shows: under 30px one line, then titleimport { useState } from 'react'import { Button } from '@rchursin/product/ui'
/** Saves, and keeps focus on the button while it works */export function SaveButton({ onSave }: { onSave: () => Promise<void> }) { const [saving, setSaving] = useState(false)
async function save() { setSaving(true) try { await onSave() } finally { setSaving(false) } }
return ( <Button loading={saving} onClick={save}> {saving ? 'Saving…' : 'Save'} </Button> )}Status
Priority
This week
Sample data| J | Next task |
|---|---|
| K | Previous task |
| Enter | Open the task |
| / | Search |
| C | Create a task |
| CtrlB | Switch between list and board |
| CtrlZ | Undo the last change |
Status is a square that fills up; priority is a meter of bars. Done and Canceled are both solid, so a tick or a cross tells them apart.
The list is a grid: Tab lands once, arrows or J and K move, Enter opens. Fifty rows never cost fifty presses of Tab.
Changes happen at once and end in a toast with a single Undo. ⌘Z (Ctrl+Z) keeps working after the toast has gone.
Best day Fri, Oct 9, 1h 40m. Today 50m. Breaks are left out.
| Lap | Lap time | Total |
|---|---|---|
| 5 | 00:48.58 | 04:01.06 |
| 4Fastest | 00:41.15 | 03:12.48 |
| 3Slowest | 00:55.46 | 02:31.33 |
| 2 | 00:47.66 | 01:35.87 |
| 1 | 00:48.21 | 00:48.21 |
Week and day
All day
Month