Skip to main content

Buttons

Neo defines four distinct button types with strict usage hierarchy. Only one Primary Button is allowed per view.

Primary Button

The highest-emphasis call to action on any screen.
Navy background, white text, full opacity. Cursor pointer.
Limit to one Primary Button per view. It represents the single most important action the user can take.
Never place two Primary Buttons side by side. Demote the secondary action to a Secondary Button.

Secondary Button

Used for supporting actions alongside a Primary Button or as standalone mid-emphasis CTAs.
Outlined or subtle fill background. Navy text. Cursor pointer.

Optimise Prompts Button

A ghost-style button exclusively for prompt editing contexts. Always paired with a sparkle icon.
This button only appears within prompt configuration views. Do not reuse the sparkle + ghost pattern for other features.

Disconnect Button

A destructive ghost button used for disconnecting integrations or removing linked accounts. Requires a confirmation step.
Always require a confirmation dialog before executing the disconnect action. Never fire destructive operations on a single click.

Temperature Slider

A horizontal slider for controlling AI creativity/precision from 0.0 to 1.0.
Always display the current numeric value alongside the slider. Users need to see the exact setting, not just the position.

Text Inputs

Standard Text Input

Empty input with placeholder text in --color-disabled. Border is --color-border.

Text Input with Image Upload

Same specs as Standard Text Input, with an image upload icon button (20x20) positioned inside the input on the right edge. Clicking it opens a file picker filtered to image types.
A select menu with a list of text options. Same dimensions as Text Input (536x40, 5px radius). Chevron-down icon on the right. Options panel appears below with 8px gap and --radius-md corners.
Use the simplest dropdown variant that communicates the options clearly. Only add body copy or headers when options genuinely need explanation.
Never render more than 8-10 options in a visible dropdown list. Use search/autocomplete for longer lists (e.g., model selection with 300+ items).

Toggle

A binary on/off switch for settings and feature flags.
Toggles take effect immediately — no save button required. If the change needs confirmation, use a checkbox + save button pattern instead.

Upload Cards

Document Upload Card

Dimensions: 347 x 86px. Displays file name, size, and a progress/status indicator.
File icon + name + size on the left. Green check-circle icon on the right. Background: --color-white. Border: --color-border.

Instagram Upload Card

Same 3 states and dimensions as Document Upload Card, but with an image thumbnail (48x48, --radius-sm) on the left instead of a file icon.
Always show a retry action on failed uploads. Never leave the user in a dead-end state.
Do not auto-dismiss failed upload cards. The user must acknowledge the failure or retry.

Usage Credit Card

Displays organization credit consumption. Dimensions: 359 x 199px.
Empty progress bar. Gray fill. Usage text shows “0/0 / X used.” Clean starting state.
The overage state must be visually unmistakable. Use the red OVERAGE badge, red progress bar, and red text together — never omit any of the three indicators.

Data Tables

All tables use --type-body for cell content, --type-label-sm for column headers, and --color-border for row dividers.

Agent Table (4 columns)

User Table (6 columns)

Knowledge Base Table (7 columns)

Paginate tables beyond 20 rows. Provide search and column sorting on all tables.
Never render an unbounded table. Always set a page size and show a “Load More” or pagination control.

TabNav (Sub-Navigation)

The single primitive for in-page tab bars — apps/web/src/components/ui/TabNav.tsx. Segmented-control presentation with two selectable modes (route vs. state); see docs/developer/design-system/navigation.mdx for the full mode/props reference.
White background, --neo-border border, --shadow-sm, 600-weight text in --text-primary.
Use <TabNav> for every in-page tab bar. Never hand-roll a border-b-2 underline row or a row of <Button variant="default|outline"> as a view switcher.
Do not build a bespoke mobile tab dropdown. Pass mobileMode="dropdown" (recommended at 5+ items or long labels) and TabNav renders its own.

The single primitive for every dashboard page title — apps/web/src/components/ui/PageHeader.tsx. Props: title, description?, actions?, tabs?, className?. Anatomy is fixed and owned by the component: Breadcrumb (rendered globally by (dashboard)/layout.tsx, not by PageHeader) → Title/Description/Actions → Tabs → page content. Always plain on the page background — never wrapped in a card or a full-bleed bar.
Use <PageHeader> for every page title. Section layouts (admin, operator, settings, knowledge/[id]/graph) render the single PageHeader for their section; child pages use <h2> and must not render a second one.
Never hand-roll an <h1> — 14 different title classNames existed across the app before this component.

Admin Config List

The pattern for admin/config surfaces presenting many settings grouped into categories — shared by admin/system-config and admin/prompt-config. Reference: apps/web/src/app/(dashboard)/admin/system-config/page.tsx (~L418–610). Editing happens inline within the row, driven by an editingKey-style state — not a nested second disclosure. Status uses <Badge> variants, never hand-rolled colored chips.
Use the admin config list for admin/config surfaces with many settings that group naturally into categories.
Don’t invent a fourth collapsible-list shape. Use a plain Card + divide-y for a simple flat list, or the table conventions above for tabular data.

Badge

The status-chip primitive — apps/web/src/components/ui/Badge.tsx. variant prop selects a token-backed color pairing; default is neutral. default, secondary, destructive, and outline also exist as legacy aliases for backward compatibility — prefer the semantic names above in new code.
Use <Badge variant> for every status chip. Status colors used elsewhere on the page must resolve through the same variant mapping, not a hand-rolled bg-*/text-* pair.