Skip to content

01 - Principles

Why we make the decisions we make. Every rule elsewhere in this spec traces back to one of these.

Three layers:

  1. Craft principles — how we think about the work (Linear-leaning).
  2. Applied principles — how we translate thinking into interfaces (Ant-leaning, with UX-law citations).
  3. Platform principles — our own constraints (multi-app shell, density, permissions).

Layer 1 — Craft principles

C1. Quality is the product

“Craftsmanship was replaced with growth hacks” — Linear, linear.app/readme. We reject that. A sloppy tooltip, a jumping layout, a confusing error message, a 400ms spinner where a skeleton belongs — each one is a real defect. Fix the whole thing, not just the demo path.

Zero-defect is the target. The last 1-5% is not “acceptable” — it’s the part that loses users.

C2. Opinionated defaults over configurable surfaces

“A tool should work for you, not the other way around” — Linear.

Every new setting is a design failure in disguise. If you find yourself adding a toggle, ask: can we pick the right answer and ship it? The answer is almost always yes.

Rule: a new user-facing setting needs a one-paragraph justification in its PR. “We couldn’t decide” is not a justification.

C3. Pick one name per concept

“Flexibility lets everyone invent their own workflows, which eventually creates chaos as teams scale” — Linear.

Agent. Tool. Knowledge Base. Thread. Conversation. Workflow. Assistant. Each has exactly one meaning. Rejected synonyms live in the glossary in 07-voice.md and we enforce them in UI strings, code identifiers, and docs.

C4. Speed is a spec, not a vibe

“Fastest of its kind” is not marketing. It’s a target.

  • Local UI state change: same frame (≤16ms).
  • Route transitions and modal opens: next frame, animate over 150-200ms max.
  • Network-backed mutation: optimistic where safe; otherwise show a skeleton or inline pending state within 500ms.
  • Never a white page, ever, for more than 500ms.

Latency budget is enforced in 02-foundations.md.

C5. Density over decoration

Employees use our admin for 8+ hours a day. Every pixel of chrome must earn its space. Compact rows (h-12), 4/8/16px paddings, no hero sections, no redundant titles, no “welcome” screens.

End-user chat is warmer — density is tuned per surface in 02-foundations.md, not relaxed everywhere.

C6. Keyboard-first

Every view is keyboard-navigable. ⌘K opens the command palette. ? opens the shortcuts overlay. j/k moves in a list. enter opens. esc closes. / focuses search. This is a contract, not a nice-to-have — see 08-accessibility.md.

C7. Momentum over sprints

We don’t “sprint”. We ship, measure, iterate. Cycles, not sprints. Decide and move on. Three half-baked attempts beat one big perfect launch that arrives two quarters late.

C8. Restraint

“Perfection is not when there is nothing more to add, but when there is nothing left to take away” — Saint-Exupéry, quoted by Ant Design.

A new primitive enters @na/ui only if an existing one can’t do the job after thirty seconds of thought. New primitives require a PR that names the existing primitive they replace and justifies why it couldn’t stretch.

Primitive adoption decision tree (run before writing any new UI in apps/admin or apps/chat):

  1. Check the inventory. Open 04-components.md and search for what you need. The Attio-native primitives section at the bottom covers record detail, property lists, filters, inbox, and wizard flows.
  2. Stretch an existing primitive. Can an existing component absorb this with a new prop or variant? A <Button size="xl"> beats a new <HeroButton>. A <Card tone="destructive"> beats a new <DangerZone>.
  3. Propose a net-new primitive only if (1) and (2) fail. PR must include: (a) the existing primitive you considered and why it didn’t work, (b) the callers this primitive will have at landing (≥2 is the rule — one-off components belong in the app, not @na/ui), (c) a Storybook story.

If you’re about to write more than 50 lines of raw JSX with Tailwind, stop. Re-read step 1. Raw <div> + Tailwind is a code smell that means you skipped the primitive inventory.

Design-decision annotations. When a file breaks a spec rule with justification, add a comment at the top of the file or above the offending block:

// design-decision: uses <Card> around <Table> here because this is the agent
// picker surface from the wizard, and the card boundary is the click target.
// See 04-components.md § Card exception 2.

Undocumented exceptions accumulate into tomorrow’s inconsistency. Documented ones become spec amendments when they repeat.

C9. Direct manipulation beats hidden state

If you can edit it, edit it in place. If you can click it, it should look clickable. If a button is disabled, the reason appears in the tooltip. If an error happens, the error appears next to the thing that errored — not in a corner of the screen.

C10. Ship

The review that never ends is the design that never helped anyone. Ship incomplete, iterate. The thing in prod teaches you more than the thing in Figma.


Layer 2 — Applied principles (cite the UX law)

Ten named principles. Each one has a citation because naming the law — “this is a Fitts violation” — is faster than arguing taste.

A1. Proximity (Gestalt)

Related items go together. Unrelated items have space between them. A label sits next to its field. A section header sits above its content. A submit button sits at the end of its form — not floating away from the fields it submits.

Rule: group visually only if the items are semantically related. If they’re not related, add space or a separator.

A2. Alignment (Gestalt)

Everything aligns to one starting edge (usually left). Numbers right-align with tabular-nums. Form colons right-align; field values left-align. Don’t center-align over a left-aligned body. Don’t randomly indent sections.

Rule: one invisible vertical line per container. Labels, headers, body text, and actions all snap to it.

A3. Contrast

30% major / 70% minor per row. One focal point per surface. Not everything can be bold; not everything can be text-foreground. Major = font-semibold text-foreground. Minor = font-normal text-muted-foreground. See 02-foundations.md for the typography scale.

A4. Repetition

Same pattern, same place, every time. Every table looks like every other table. Every card has the same inner padding. Every action bar has the same height. Originality in layout is a bug.

A5. Make it Direct (Cooper)

Edit where the thing lives. Inline edit for single-field tweaks. Click-to-edit for readable→editable transitions. Only escalate to a modal when the edit touches many fields or needs confirmation.

Three tiers (from 05-patterns.md):

  • Click to edit — readability-first surfaces (titles, names).
  • Icon edit — both states matter (editable fields in a detail panel).
  • Multi-field inline — complex edits without a modal (workflow node config).

A6. Stay on the Page

A link that pops a modal is better than a link that navigates away. A confirm popover anchored to the trigger is better than a confirm dialog. Users who lose context lose their train of thought.

Rule: navigate only when the task genuinely belongs on a different page. Everything else = dialog, sheet, popover, or inline.

A7. Keep it Lightweight (Fitts)

Clickable hotspot ≠ visual size. A 16px icon can have a 40px hit target. Hover-reveal low-priority actions. Toggle-reveal rare actions. Always-visible only for the critical ones.

Three-tier disclosure (from 05-patterns.md):

  • Always visible — the primary action of the row/screen.
  • Hover-reveal — secondary actions (edit row, copy row).
  • Toggle-reveal — rare actions (show advanced fields, open debug panel).

A8. Provide an Invitation (Norman signifiers)

Affordances must be visible. Empty states tell the user what to do next. Hover states hint at interactivity. A cursor changes on clickable elements (our Button does this automatically).

Static invitations: blank-slate empty states, unfinished-state hints, one-time tours. Dynamic invitations: hover highlights, “more below” indicators, autocomplete hints.

A9. Use Transition

Animation has intent. Every motion declares its category:

  • Adding (0-200ms ease-out) — element enters the scene.
  • Receding (0-100ms ease-in) — element leaves; faster than adding.
  • Normal (150-250ms) — element moves or morphs within the scene.

Perceived performance matters. When real performance is capped, a well-chosen transition makes the wait feel shorter.

A10. React Immediately (Newton’s 3rd Law)

Every action triggers a visible reaction within 100ms. Click → press state. Submit → pending state. Error → error state (inline, see ActionButton patterns). Silence is a bug.

Rule: if a user action takes more than 500ms to produce feedback, add a skeleton, progress bar, or optimistic update. Never a blank screen.


Layer 3 — Platform principles (our own constraints)

These are specific to Nexus Agentic’s multi-app shell. They don’t generalize to other products.

P1. Shell is infrastructure, not an app

The shell provides layout scaffolding (icon rail, sidebar slot, page bar slot, panel slot). It never owns business logic. Apps plug in — the shell doesn’t know what CRM or Admin means.

P2. One-app users should never know the multi-app system exists

No empty rails, no disabled icons, no “you only have one app” messaging. The shell collapses to a simple sidebar. Zero wasted pixels.

P3. Apps are deploy-independent

Each app must work standalone (its own URL, own build) or composed (unified deploy). The shell adapts — switching apps is a URL redirect in separate deploys, a route change in unified deploys. Apps never import each other.

P4. Two sidebar-like tiers max

Icon rail (which app) + app sidebar (where in the app). Inline tabs in the page bar are a lightweight third level for sub-sections. Never a fourth level.

P5. The user avatar lives bottom-left

Multi-app: rail footer. Single-app: sidebar footer. Always the same corner.

P6. Chat is a panel, not a page

Chat is accessible from any app as a slide-out, split, full-view, or minimized panel. It does not require navigating away.

P7. Consistent page anatomy

Every page: [PageBar] + [PageContent]. List pages use ListPageBar. Detail pages use DetailPageBar with inline tabs. No app invents its own chrome.

P8. Shared packages, not shared state

Apps share @na/shell, @na/ui, @na/auth, @na/api-client. They do NOT share runtime state. Cross-app communication = URL params or a lightweight event bus.

P9. Permissions drive visibility

Icon rail shows only permitted apps. Sidebar shows only permitted sections. No “forbidden” screens — if you see it, you can use it.

P10. Admin is dense, Chat is warm

Admin defaults to compact rows, small text, keyboard-first. Chat defaults to generous spacing, legible type, touch-friendly. Same tokens, different density tier — see 02-foundations.md.


Tie-breakers

When two principles conflict:

  1. Accessibility always wins. A keyboard-unreachable button is a broken button, period.
  2. Clarity beats density. If compressing hides meaning, give the pixels.
  3. Opinion beats configurability. If you can’t decide, decide.
  4. Direct beats modal. If it can happen inline, it should.
  5. Speed beats polish. Ship the thing that works, iterate on the thing that dazzles.

What this spec is not

  • Not a reason to bikeshed. If the rule exists, apply it.
  • Not a replacement for judgment. Edge cases exist — when in doubt, pick the option a thoughtful designer would recognize as “obviously right” and ship it.
  • Not static. Rules change as we learn. Changes land as commits with justification.

Next: 02-foundations.md.