This document is the single source of truth for how this app should look and feel.
It is extracted directly from the boundless-platform frontend (the main
boundlessfi.xyz app). Anyone building UI in this repo must follow it so the
Builders sub-app is visually indistinguishable from the main platform.
Values here are lifted from the Boundless Figma library ("Boundless UX/UI Design"). Do not invent new colors, fonts, spacing, or component shapes. If you need something that is not here, copy it from boundless-platform first.
House rule: No em dashes in code, comments, or copy. Use periods, commas, or rewrite the sentence.
| Concern | Choice |
|---|---|
| Framework | Next.js (App Router, React Server Components) |
| Styling | Tailwind CSS v4 with CSS-first @theme tokens (no tailwind.config.js) |
| Component library | shadcn/ui, style new-york, base color neutral, CSS variables on |
| Icons | lucide-react |
| Class merging | cn() from @/lib/utils (clsx + tailwind-merge) |
| Variants | class-variance-authority (cva) |
| Default theme | dark (the app boots in dark mode; light is a derived counterpart) |
| Toasts | sonner, positioned top-right, richColors |
Tailwind is configured entirely in globals.css via @theme static { ... }.
There is no JS Tailwind config. The components.json for shadcn should mirror
boundless-platform:
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}The palette is the heart of the brand: a cool charcoal neutral with a
bright teal primary (#2eedaa) and deep green-black surfaces. Every scale
runs 50..900 (10 steps, no 75) and backs the Tailwind utilities
(bg-primary-500, text-error-400, border-neutral-700, and so on).
| Token | Hex |
|---|---|
neutral-50 |
#f3f7f6 |
neutral-100 |
#dde7e5 |
neutral-200 |
#c2d0cd |
neutral-300 |
#9fb3af |
neutral-400 |
#7a8f8b |
neutral-500 |
#5c6f6b |
neutral-600 |
#42534f |
neutral-700 |
#2c3a37 |
neutral-800 |
#1c2625 |
neutral-900 |
#121a1a |
| Token | Hex |
|---|---|
primary-50 |
#eafdf7 |
primary-100 |
#bef9e5 |
primary-200 |
#9ff7d8 |
primary-300 |
#73f3c6 |
primary-400 |
#58f1bb |
primary-500 |
#2eedaa (brand) |
primary-600 |
#2ad89b (hover) |
primary-700 |
#21a879 |
primary-800 |
#19825e |
primary-900 |
#136447 |
| Token | Hex |
|---|---|
secondary-50 |
#e8e9e9 |
secondary-100 |
#b8bbba |
secondary-200 |
#969a99 |
secondary-300 |
#666c6b |
secondary-400 |
#484f4e |
secondary-500 |
#1a2322 |
secondary-600 |
#18201f |
secondary-700 |
#121918 |
secondary-800 |
#0e1313 |
secondary-900 |
#0b0f0e |
Success (green) — 50 #e9f9ef · 100 #baedcd · 200 #99e4b5 · 300 #6bd893 · 400 #4ed17e · 500 #22c55e · 600 #1fb356 · 700 #188c43 · 800 #136c34 · 900 #0e5327
Warning (amber) — 50 #fef7e6 · 100 #fce7b0 · 200 #fadc8a · 300 #f8cc55 · 400 #f7c234 · 500 #f5b301 · 600 #dfa301 · 700 #ae7f01 · 800 #876201 · 900 #674b00
Error (red) — 50 #ffeded · 100 #ffc8c8 · 200 #ffadad · 300 #ff8888 · 400 #ff7171 · 500 #ff4d4d · 600 #e84646 · 700 #b53737 · 800 #8c2a2a · 900 #6b2020
Info (blue) — 50 #ebf6ff · 100 #c2e4ff · 200 #a5d7ff · 300 #7cc4ff · 400 #62b9ff · 500 #3ba7ff · 600 #3698e8 · 700 #2a77b5 · 800 #205c8c · 900 #19466b
| Token | Hex / value | Use |
|---|---|---|
brand |
#2eedaa |
Marketing surfaces (bg-brand, text-brand) |
brand-hover |
#2ad89b |
Hover state for brand |
brand-soft |
#eafdf7 |
Soft brand background |
lime |
#a7f950 |
Bright lime accent (glows, winner rings) |
border-subtle |
#1f2a28 |
Quieter than the default border |
ink |
#0d1111 |
Deepest surface / text on primary |
ink-soft |
#141c1c |
Slightly raised ink surface |
active-bg |
rgba(46,237,170,0.08) |
Active/selected tint |
active-bg2 |
rgba(46,237,170,0.32) |
Stronger active tint |
hero-bg |
#0d1111 |
Hero backgrounds |
text-muted |
#999da2 |
Muted text |
text-muted-brand |
#e8e9e7 |
Muted text on brand surfaces |
surface-subtle |
rgba(250,250,250,0.02) |
Barely-there surface lift |
The dark theme is canonical. Light is a derived, usable counterpart. These
map to Tailwind utilities like bg-background, text-foreground,
bg-card, border-border, ring-ring.
| Token | Hex | Figma meaning |
|---|---|---|
--background |
#0d1111 |
Background/Base |
--foreground |
#f1fff1 |
Text/Primary |
--card |
#141c1c |
Surface/Base |
--card-foreground |
#f1fff1 |
|
--popover |
#1a2322 |
Background/Raised |
--popover-foreground |
#f1fff1 |
|
--primary |
#2eedaa |
Primary 500 |
--primary-foreground |
#0d1111 |
Text on primary button |
--secondary |
#1a2322 |
Secondary 500 |
--secondary-foreground |
#f1fff1 |
|
--muted |
#141c1c |
|
--muted-foreground |
#72736f |
Text/Secondary |
--accent |
#182120 |
Surface/Hover |
--accent-foreground |
#f1fff1 |
|
--destructive |
#ff4d4d |
Error 500 |
--destructive-foreground |
#0d1111 |
|
--border |
#2e3a38 |
Border/Default |
--input |
#2e3a38 |
|
--ring |
#2eedaa |
| Token | Hex |
|---|---|
--background |
#ffffff |
--foreground |
#121a1a |
--card |
#ffffff |
--popover |
#ffffff |
--primary |
#2eedaa |
--primary-foreground |
#0d1111 |
--secondary |
#f3f7f6 |
--muted |
#f3f7f6 |
--muted-foreground |
#5c6f6b |
--accent |
#eafdf7 |
--accent-foreground |
#136447 |
--destructive |
#ff4d4d |
--border |
#dde7e5 |
--input |
#dde7e5 |
--ring |
#2eedaa |
Chart series (both themes): chart-1 #2eedaa, chart-2 #3ba7ff,
chart-3 #f5b301, chart-4 #ff4d4d, chart-5 #22c55e.
Sidebar tokens follow card surfaces: dark uses sidebar #141c1c,
sidebar-accent #182120, sidebar-border #2e3a38, with sidebar-primary #2eedaa.
| Role | Family | Variable | Notes |
|---|---|---|---|
| Body / sans (default) | Plus Jakarta Sans | --font-jakarta |
Loaded via next/font/google |
| Headings (display) | TASA Orbiter | --font-heading |
Brand display face. .woff2 not in repo yet, so it falls back to Plus Jakarta Sans until the real face lands via next/font/local. All h1..h6 use it automatically. |
| Mono | Geist Mono | --font-mono |
Falls back to ui-monospace |
| Display (wordmarks) | Bebas Neue | --font-bebas |
Condensed face for partner wordmarks, loaded via next/font/google weight 400 |
Wire fonts in app/layout.tsx exactly like boundless-platform:
import { Bebas_Neue, Plus_Jakarta_Sans } from 'next/font/google';
const jakarta = Plus_Jakarta_Sans({ variable: '--font-jakarta', subsets: ['latin'] });
const bebasNeue = Bebas_Neue({ variable: '--font-bebas', weight: '400', subsets: ['latin'] });
// <html className={`${jakarta.variable} ${bebasNeue.variable} h-full antialiased`}>In globals.css the base layer sets h1..h6 { font-family: var(--font-heading); }
and button { cursor: pointer; }.
Apply headings with font-heading. Body and captions use the default sans.
Captions are uppercase with wide tracking.
| Utility | Size | Line height | Letter spacing | Weight |
|---|---|---|---|---|
text-display-lg |
56px | 1 | -0.04em | |
text-display-sm |
48px | 1 | -0.04em | |
text-h1 |
40px | 1 | -0.04em | |
text-h2 |
32px | 1 | -0.04em | |
text-h3 |
28px | 1.2 | -0.02em | |
text-h4 |
24px | 1.2 | -0.02em | |
text-h5 |
20px | 1.2 | -0.02em | |
text-h6 |
18px | 1.2 | -0.02em | |
text-body-lg |
18px | 1.45 | ||
text-body |
16px | 1.45 | ||
text-body-sm |
14px | 1.45 | ||
text-body-xs |
12px | 1.45 | ||
text-caption-lg |
14px | 1.2 | 0.12em | 600 |
text-caption-sm |
12px | 1.2 | 0.12em | 600 |
text-caption-xs |
10px | 1.2 | 0.16em | 600 |
Base radius is --radius: 0.5rem (8px, the Figma "Rounded" button radius).
Derived utilities:
| Utility | Value |
|---|---|
rounded-sm |
radius - 4px |
rounded-md |
radius - 2px |
rounded-lg |
radius (8px) |
rounded-xl |
radius + 4px |
rounded-pill / rounded-full |
999px |
The whole app aligns to a single content width: --container-page: 1440px
(max-w-page). Put gutters on the outer element and cap content with an inner
mx-auto max-w-page, so the gutter only bites below the cap and wide screens
land on exactly this width. Nav, page body, and footer all share this cap.
Two reusable utilities exist in globals.css:
scrollbar-hide— hides the scrollbar entirely.custom-scrollbar— thin 6px scrollbar, thumb#4a4a4a, transparent track.
Build all interactive UI from the shadcn primitives in components/ui,
customized to the design system via cva variants and className. Do not
hand-roll a raw <button>, <input>, or <select> when a primitive exists.
The button composes from independent axes. Defaults: intent=primary,
appearance=solid, size=large, shape=rounded.
- intent:
primary|secondary|white|destructive - appearance:
solid|outline|text - size:
small(h-10, px-4, text-sm) |large(h-12, px-6, text-base) - shape:
rounded(rounded-lg) |pill(rounded-full) - iconOnly: square icon button (width tracks height)
- loading: shows a
Loader2spinner and blocks interaction
Anything that reads as an action button uses Button. Bespoke menu/combobox/
disclosure triggers and compact nav icon affordances that do not fit this system
may stay as semantic <button>s with their own layout.
Key state colors to preserve:
- Primary solid:
bg-primary-500 text-ink, hoverbg-primary-400, activebg-primary-600, focus ringprimary-200, disabledbg-[#1a2422] text-[#4a5a57]. - Destructive solid:
bg-error-500 text-white, hoverbg-error-400, activebg-error-600. - Outline/text intents use tinted borders and
/8background washes on hover.
<Button>Primary large</Button>
<Button intent="secondary" appearance="outline" size="small">Secondary</Button>
<Button intent="destructive">Delete</Button>
<Button appearance="text" shape="pill">Text pill</Button>
<Button iconOnly aria-label="Add"><Plus /></Button>
<Button loading>Saving</Button>Bordered frame with optional leading icon, control, and trailing add-on/status
icon, a label above, and helper text below. Compose from inputSize
(small h-9 | large h-11, sm:h-14), shape (rounded | pill), and state
(default | success | error).
- Default state:
border-neutral-600, focusborder-primary-500with a0 0 0 4px rgba(46,237,170,0.12)ring glow andbg-ink-soft. - Success: green border,
CircleCheckstatus icon. - Error: red border,
CircleXstatus icon, helper text in error color. - Caret is
caret-primary-500; placeholder istext-neutral-400/70.
bg-card text-card-foreground, rounded-xl, border, shadow-sm, vertical
gap-6, py-6, with px-6 on header/content/footer. Subcomponents: Card,
CardHeader, CardTitle, CardDescription, CardAction, CardContent,
CardFooter.
From boundless-platform components/ui/: avatar, avatar-group, button,
card, checkbox, command, country-select, dialog, dropdown-menu,
entity-row, form, glow-banner, hover-card, image-cropper, input,
label, option-card, pagination, popover, select, skeleton,
snapshot-card, sonner, sparks-icon, stats-bar, success-dialog, tabs,
textarea, token-amount.
Layout components in components/layout/: app-nav, boundless-logo,
brand-icons, footer-glow, mobile-nav-menu, pill-button, profile-menu,
site-footer, site-header.
When you need one of these, copy it over rather than rebuilding it.
A few named animations live in the theme (marketing surfaces mostly). Reuse these rather than inventing timings:
animate-prize-scroll— 18s linear vertical loop (prize ticker).animate-card-rotate— 10s stepped rotation through four rows.animate-marquee-left/animate-marquee-right— 48s horizontal marquees.animate-glow-in— glow entrance with a springlinear()easing.
Respect motion-reduce: variants (the input status icon animation already does).
The main app is feature-first. Even though this Builders app is display-only, mirror the structure so code stays consistent and portable.
app/ # routes only: thin components that compose features
layout.tsx # root layout, wires <Providers>, fonts
globals.css # Tailwind v4 @theme tokens (copy from boundless-platform)
features/ # one folder per domain (builders, projects, teams, ...)
<name>/
api/ # TanStack Query hooks on the typed API client
components/ # feature-scoped UI
hooks/
types.ts # derived from generated schema where possible
index.ts # the ONLY public surface
components/
ui/ # shadcn primitives (themeable)
layout/ # nav, shells, footer
lib/
api/ # typed REST client (generated types, do not hand-write)
utils.ts # cn()
providers/ # small client provider tree (theme, query, ...)
Rules to keep parity with boundless-platform:
- Import features by their
index.ts, never reach into internals. - One typed API client under
@/lib/api. When the backend (boundless-nestjs) exposes builders/projects endpoints, generate types withnpm run codegenand derive from the generated schema. Never hand-write API types. - Validated env: import
envfrom@/lib/env, never readprocess.envdirectly. - Server state via TanStack Query. Keep UI state local or in feature hooks.
- Since this app only displays builders, projects, and teams, expect mostly
read (
GET) hooks and no create/upload flows. Creation lives in the main app.
Match boundless-platform's .prettierrc.json:
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "avoid",
"endOfLine": "lf",
"quoteProps": "as-needed",
"jsxSingleQuote": true,
"proseWrap": "preserve",
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "./app/globals.css",
"tailwindFunctions": ["clsx", "cn", "cva"]
}- Single quotes, JSX single quotes, semicolons, 2-space indent, 80 col width.
prettier-plugin-tailwindcsssorts class names. It is aware ofclsx,cn, andcva.- No em dashes anywhere: code, comments, or copy.
- Before finishing a change, run
typecheck,lint, andtest(andbuildwhen touching config or Tailwind).
To bring the design system into this app:
- Copy
globals.cssfromboundless-platform/src/app/into this app'sapp/globals.css(it contains the full@theme, tokens, keyframes, and utilities). Adjust the import path if your CSS lives elsewhere. - Add
components.json(section 1) and install shadcn primitives, or copy thecomponents/uifolder directly from boundless-platform. - Install fonts in
app/layout.tsx(Plus Jakarta Sans + Bebas Neue) and setdefaultTheme='dark'in the theme provider. - Add
cn()tolib/utils.ts(clsx + tailwind-merge). - Copy
.prettierrc.jsonand the ESLint setup for consistent formatting. - Build every screen from the primitives and tokens above. When something is missing, port it from boundless-platform rather than inventing it.
Golden rule: if in doubt, open boundless-platform and copy what it does. This app must look like a native part of Boundless, not a cousin.