Apply a dense, token-driven design system for SaaS admin panels built with React + TypeScript + Tailwind CSS + shadcn/ui. Use when building, restyling, or reviewing panel UI — list/table screens, detail modals, dashboards with KPIs and charts, report screens, hub pages, forms, kanban boards, the navigation shell, or the color/typography/spacing foundations. Triggers include "design system", "apply the DS", "list screen", "data table screen", "detail modal", "dashboard cards", "report screen", "hub page", "admin panel UI", "shadcn theme", "dark mode tokens", "make this look consistent".
license
MIT
compatibility
Requires a React 18+ project with TypeScript and Tailwind CSS 3. Assumes shadcn/ui primitives; React Router and TanStack Query are used by the examples but are not required for the tokens and visual patterns.
metadata
{"version":"1.0.0","body-language":"en","reference-language":"pt-BR","source":"extracted from a production multi-tenant lead-generation SaaS panel"}
SaaS Panel Design System
A complete design system for dense operational panels — the kind of product
people keep open all day: CRMs, admin backoffices, analytics consoles.
Extracted from a production SaaS and packaged so an agent can apply it end to end.
It is opinionated on purpose: one solution per problem, no "it depends".
Reference docs are in Brazilian Portuguese (references/), and UI strings
in the examples are pt-BR. The system itself is language-agnostic — when
applying it to a project in another language, translate the UI strings and keep
the structure. Code identifiers are always English.
0. First: is the system already installed here?
Check the target project before writing any component:
Tokens missing → run the bootstrap first: read ../ds-install/SKILL.md
(or tell the user to run /ds-install). Do not hand-write tokens.
Tokens present, component missing → copy it from assets/components/
rather than reinventing it. These files are production code, not sketches.
Everything present → go straight to the pattern you need (§4).
Never partially apply the system. A screen with the right colors but a fixed
PAGE_SIZE and a <Sheet> for details is not "mostly compliant" — it breaks
the two rules users notice most.
1. Non-negotiable decisions
Each row is a rule, not a preference. Violating one is a review blocker.
Problem
The only answer
Never
Sub-navigation inside a page
<PageSubnav> (left rail)
<Tabs>
Show a record's details
<DetailModal> (modal + section sidebar)
<Sheet> / drawer
Edit a record's fields
<InlineField> where the value is read
an "Edit" tab or a second modal
Tabular listing screen
PageHeader → StatsGrid → filter Card → ListCard
fixed PAGE_SIZE, a "Project/Tenant" column
Card portal to other routes
<HubPage>
ad-hoc <Card> grid
Select from a list that can grow
searchable combo (Popover + Command)
<Select> with dozens of items
Go back up the hierarchy
the shell's breadcrumb
a "Back" button on the page
Destructive action
AlertDialog naming the record
window.confirm, silent delete
Blocked action
disabled button + Tooltip with the reason
hiding the button
Chart / KPI
<ChartCard> / <KpiCard> from DashboardKit
bare Recharts in a <Card>
2. Foundations you can apply without opening any file
Color. Every color is an HSL triplet in a CSS variable — no hsl(), no
commas — because that is what makes bg-primary/10 and text-primary/60 work.
--primary-glow exists for dark mode. A 31%-lightness green disappears on
a dark background, so every solid accent in dark uses
dark:text-primary-glow / dark:bg-primary-glow. Translucent fills
(bg-primary/10) keep using primary.
Non-semantic accents (categories, hub cards, dashboard views) come from a
closed vocabulary: emerald, sky, violet, amber, rose, primary.
Light uses solid 50/600/700; dark uses /10 + 300/400. Never a new hex.
Full detail: references/01-fundacoes-cores.md.
Type. Inter only, weights 400/500/600/700. Headings differ by weight and
size, not family. Numbers the user compares vertically always get
tabular-nums, formatted through Intl in the app locale. Missing value renders
—, never R$ 0,00 or 0%.
There is one trick worth knowing before you touch any size:
so text-2xl renders ~21.6px inside the panel. Values in px
(text-[11px], h-9) do not shrink — which is why micro-labels use px.
Detail: references/02-tipografia.md.
Density. This is what makes it feel like a tool and not a landing page:
Separation comes from borders, not shadows (hover:shadow-sm is as far as a
card goes). Detail: references/03-espacamento-layout-elevacao.md.
3. The primitives are patched — do not use stock shadcn
assets/primitives/ holds shadcn components with the system's diffs already
applied. Copy those over the stock ones after shadcn add. The five that
matter most:
button — h-9 (not h-10), shadow-none primary, focus ring
ring-primary/20 ring-offset-0, ghost starts as text-muted-foreground.
card — rounded-xl border-border/60, p-5, hover:shadow-sm, CardTitle
is text-base font-medium.
table — TableHead is uppercase text-xs tracking-wide text-muted-foreground;
rows hover:bg-muted/40, selection bg-primary/5; borders at /40–/50.
dialog — p-5 gap-3.
badge and the rest are stock. Full diff list: references/04-primitivos-ui.md.
4. Screen patterns — read the reference, then copy the example
Each pattern has a doc (the rules and the why) and a runnable example (the
skeleton). Read the doc before writing the screen; skipping it is how the
subtle rules get lost.
It uses a callback ref (not useRef + effect) because the container mounts
only after loading resolves, and it re-measures on frames, timers and
document.fonts.ready because the first layout after a refresh is raw.
Pipeline order.filter → sort → paginate, always. Sorting must run over the
whole list, before pagination — sorting the visible page is a lie:
5. Required states — a screen without them is unfinished
loading → error → empty → content, every time.
// loading — the system's spinner, no library
<div className="animate-spin w-6 h-6 border-2 border-primary border-t-transparent rounded-full" />
// error — always with a retry<CardclassName="border-destructive">…{error}…<Buttonvariant="outline"onClick={refetch}>Tentar Novamente</Button></Card>// empty — INSIDE the table body, message tells WHY it is empty<TableRow><TableCellcolSpan={n}className="text-center py-8 text-muted-foreground">
{search || filter !== 'all' ? 'Nenhum item encontrado' : 'Nenhum item cadastrado ainda'}
</TableCell></TableRow>
Never early-return an empty state that replaces the whole screen: it hides the
header, KPIs and filters, and traps the user with no way to change the filter.
Detail: references/13-estados-feedback.md.
6. Conventions that keep the system coherent
Carry these into the target project's CLAUDE.md / AGENTS.md:
One solution per problem (§1) — no local variations.
Details = modal with sections; never a drawer; never an "Edit" tab.
Toasts live in the data hook, not in the component (one action, one toast).
User-chosen state lives in the URL (?tab=, ?view=, ?period=).
Zero loose hex; accents only from the closed vocabulary.
tabular-nums on comparable numbers; Intl for formatting; — for absent.
7. Antipatterns worth memorizing
Never
Why it broke
Instead
Fixed PAGE_SIZE = 10
Half-empty screen on a big monitor
usePagination(items, { auto: true })
Sorting only the current page
Sorting becomes a lie
useSortedRows before paginating
Early-return empty state
Hides filters; user gets stuck
Empty row inside <TableBody>
useRef + effect to measure
Measures a null node, sticks at minimum
callback ref
Fixed modal height
Tall, empty modal
max-h-[88vh] + scrolling body
Solid primary accent in dark
Unreadable dark smear
dark:*-primary-glow
Hex in a component
Kills dark mode and /10 variants
token or accent vocabulary
Autosave without invalidating the detail query
Field saves, UI reverts
invalidate the detail's own query key
Text without truncate in flex
Layout blows out
min-w-0 on parent + truncate
Editing components/ui/** by hand
Lost on the next shadcn update
wrapper in components/
Full catalog plus the historical bug behind each rule:
references/16-checklists-e-antipadroes.md.
8. File map
SKILL.md this file
references/ 17 docs, pt-BR — read on demand (see references/README.md)
assets/
tokens.css all tokens, light + dark — the starting point
tailwind.config.example.ts config that reads the tokens
primitives/ shadcn WITH the system's diffs applied
components/ the system's own components + hooks (production code)
shell/ AppShell, AppSidebar, Breadcrumbs, SidebarContext (brand-neutral)
examples/ 6 complete reference screens
Companion skills in this package:
ds-install — bootstrap the system into a project (deps, tokens, config,
primitives, components). Run this before the first screen.
ds-audit — audit existing code against the system and report violations.
9. Rebranding
Three lines in assets/tokens.css change the whole product:
If the brand's lightness is above ~55%, revisit --primary-foreground (it may
need dark text). The sidebar gradient in assets/shell/AppSidebar.tsx is brand,
not system — replace it. Everything else derives.