Design principles
Lumen is a workspace tool. It has to feel calm, legible, and fast. These principles are the constraints that make that possible.
1. Tight primitive reuse
Every new screen reuses existing primitives:
<Button>·<Input>·<Select>·<Modal>·<Avatar>·<Badge>·<Tabs>·<Topbar>·<EmptyState>·<StatCard>·<Breadcrumb>·<Card>
If something isn't covered, extend the primitive — don't hand-roll a one-off. The Tabs component has a count prop; that got added when we needed counts on project-detail tabs. Adding it as a prop took 30 seconds and now every tab bar stays consistent.
Exceptions require justification in the PR description.
2. Explicit heights on rows
Every top bar is h-[52px]. Every list row is h-8 or h-10. No py-4 inside fixed-height containers — the content overflows on small screens and breaks alignment.
Layout is predictable when heights are explicit. Spacing below the bar gets all the flex room it needs via flex-1.
3. Admin content is full-width
The admin area already has a 220px sub-sidebar. Adding max-w-5xl on top squeezes the content into a thin middle column and breaks the 4-stat-card grid on normal laptop screens. Admin pages use the full remaining width and rely on <AdminPageShell> for padding.
Regular document-style content (docs site, share page) is centered with max-w-[780px] because it's for reading, not scanning.
4. Never use native <select>
Native selects look different in every browser and don't match our tokens. We have <Select> in components/ui/select.tsx — use it. It supports options with descriptions, disabled states, keyboard nav, and the token-driven chrome.
5. Error messages are actionable
When an API call fails, surface the message. The API returns { error: "code", message: "human text" } — the api.ts client extracts message first, falls back to error, and zod validation errors get formatted like "departmentId: Expected string, received null".
Never let [object Object] reach the user. If it does, it's a bug in the client's error handler — fix it at the source, not in every call site.
6. Empty states suggest next action
<EmptyState> takes an optional action prop. Use it.
- "No projects yet" → "+ Create your first project"
- "No members yet" → "+ Invite member"
- "No files yet" → "+ Upload file"
A bare message ("No items") wastes the moment. If you know what the user should do next, say it.
7. Dev-phase = fast iteration
User is sole tenant. Prod = testing. We push directly to master, Dokploy redeploys, user verifies live.
- No PR gating during dev phase
- Label
deployed-testingon issues after commit lands - User closes issue when they've verified in-browser
- Revert via
git revert <sha>if something breaks
This only works because the user trusts the agent not to run DB-destroying migrations without confirming first.