| name | design-system |
| description | Aisets UI design system — token usage, CVA component patterns, Tailwind co-location rules, and pre-delivery checklist. Use this skill whenever editing ANY file under ui/src/, including .tsx, .ts, and style files. Also use when creating new UI components, modifying existing ones, changing colors/spacing/typography, adding animations, or touching anything visual. Even simple one-line UI tweaks should consult this skill — the token and cascade rules catch subtle bugs.
|
| globs | ["ui/src/**/*.tsx","ui/src/**/*.ts","ui/src/styles/**"] |
Aisets Design System
Why this skill exists
This project uses a strict token-based design system with CVA (class-variance-authority) for
component variants and Tailwind for co-located styles. The system prevents visual inconsistencies,
dark/light theme breakage, and cascade conflicts that are hard to debug. Every rule below exists
because someone hit the problem it prevents.
1. How styling works
┌─ _tokens.scss ──── CSS custom properties (--g-canvas, --g-ink, --g-accent, etc.)
│ `:root` = light, `[data-theme="dark"]` = canonical dark
│
├─ tailwind.css ──── @theme block maps --g-* → Tailwind classes
│ e.g. --color-g-surface: var(--g-surface) → `bg-g-surface`
│ The `g-` prefix in class names means "uses a design token"
│
├─ _patterns.scss ── @keyframes, .sr-only, .bg-checker (shared utilities)
│
├─ components/ui/ ── CVA primitives (Button, Modal, Select, Badge, etc.)
│ Each exports a `*Variants` function + component
│
└─ components/ ───── Page components use Tailwind classes directly in JSX
The only SCSS files are _tokens.scss, _patterns.scss, and globals.scss.
No component styles live in SCSS. Everything visual is co-located in .tsx.
Token naming convention
Tailwind classes use the g- prefix to reference design tokens:
- Colors:
bg-g-surface, text-g-ink, border-g-line, text-g-red
- Radius:
rounded-g-sm (4px), rounded-g-md (6px), rounded-g-lg (12px)
- Shadows:
shadow-g-sm, shadow-g-md, shadow-g-pop, shadow-g-focus
- Fonts:
font-g (body), font-g-mono, font-g-display
- Text:
text-g-chip (10px), text-g-caption (11px), text-g-ui (12px), text-g-body (13px)
- Easing:
ease-g, ease-g-out, ease-g-spring
- Button heights:
h-g-btn-sm (26px), h-g-btn-md (32px), h-g-btn-lg (36px)
Full token values are in _tokens.scss. Full @theme mapping is in tailwind.css.
Cascade gotcha (important)
SCSS files (_tokens.scss, _patterns.scss) are NOT inside a CSS @layer. Tailwind v4
utilities ARE in a layer. This means SCSS properties always beat Tailwind utilities in
the cascade.
Practical consequence: if an element has both a SCSS class and a Tailwind class for the same
property (e.g., .content-scroll sets padding: 32px and you add p-4), the SCSS wins
silently. The fix: remove the SCSS class entirely and replace with full Tailwind.
Also: twMerge in Tailwind v4 can't always resolve spacing-scale utilities (w-80) against
keyword utilities (w-full). Use arbitrary values (w-[320px]) when combining with CVA bases.
twMerge font-size vs color conflict (critical)
twMerge treats ALL text-* classes as one group. Custom theme tokens like text-g-ui
(font-size) and text-g-ink (color) look identical to twMerge — it keeps only the last one,
silently dropping font-size. This is fixed in cn.ts via extendTailwindMerge registering
font-size tokens in a separate class group.
Before changing font-size tokens: run twMerge('text-g-ui text-g-ink') to verify the
font-size class survives. If it's dropped, fix cn.ts classGroups.font-size first.
When adding new text-* theme tokens: register them in cn.ts extendTailwindMerge → classGroups → font-size or they will be silently eaten by color classes.
2. Component pattern
New UI primitive → CVA
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/cn";
const widgetVariants = cva("base tailwind classes here", {
variants: {
variant: { primary: "...", secondary: "..." },
size: { sm: "...", md: "..." },
},
defaultVariants: { variant: "primary", size: "md" },
});
type WidgetProps = React.HTMLAttributes<HTMLElement> &
VariantProps<typeof widgetVariants>;
function Widget({ variant, size, className, ...props }: WidgetProps) {
return <div className={cn(widgetVariants({ variant, size }), className)} {...props} />;
}
export { Widget, widgetVariants };
Page component → direct Tailwind
Page-level components don't need CVA — just use Tailwind classes in className with cn()
for conditionals. Use the existing UI primitives for buttons, inputs, modals, etc.
Before creating a new component
Read the barrel export at ui/src/components/ui/index.ts to see what already exists. The
project has 21+ primitives including Button, Badge, Card, Modal, Select, Tabs, Tooltip,
DropdownMenu, Notice, Toast, EmptyState, and more. Check before building from scratch.
3. Page layout philosophy
Every view follows the Browse / Duplicates blueprint. Deviating from this structure makes the
product feel inconsistent.
3.1 Content-first — no decorative chrome
- No page titles or hero sections. Content (StatCards, toolbar, grid) starts at the top edge.
- No standalone empty states. If a page has a primary action area (e.g. dropzone), merge the
empty-state message into that area. Never stack two centered visual blocks vertically.
- No description paragraphs. The sidebar nav label is sufficient context. Add help via tooltip.
3.2 Three-layer structure
Every data view follows: StatCards → Sticky Toolbar → Content Grid.
┌──────────────────────────────────────────────┐
│ [StatCard] [StatCard] [StatCard] [StatCard] │ ← summary row
│ [StackedBar ···························] │ ← optional health bar
├──────────────────────────────────────────────┤
│ [Tabs] [Search···] [Sort ▾] [View ▾] [Act] │ ← sticky toolbar (z:4–5)
├──────────────────────────────────────────────┤
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │card│ │card│ │card│ │card│ │card│ │card│ │ ← content grid
│ └────┘ └────┘ └────┘ └────┘ └────┘ └────┘ │
└──────────────────────────────────────────────┘
- Omit layers that don't apply (e.g. PreCheck has no toolbar, Browse has no StatCards).
- Sidebar
FilterRail is a separate column — never embed filters in the main content area.
3.3 StatCard neutrality
StatCard icon and label are always text-g-ink-4 (neutral grey). The large number value provides
emphasis. Semantic color goes only on badge/chip elements in the content area, not on stat labels.
3.4 Information density
- 4px base spacing, compact density. Avoid large padding between functional elements.
- File metadata uses compact
<Badge> chips, not full sentences.
- Details open in drawers/panels on click — not inline-expanded paragraphs.
4. Component rules
These aren't taste preferences — each prevents a specific class of bugs:
| Rule | Why |
|---|
All values from --g-* tokens | Raw hex breaks when themes switch. A hardcoded #ffffff is invisible on light canvas. |
| Single CTA per screen | Multiple --g-cta buttons create visual competition. Users don't know where to click. |
| Color + icon + text for status | ~8% of males are colorblind. Color alone is invisible to them. |
6px default radius (--g-r-md) | Consistency across the product. Only overlays get 12px. |
Tooltips via Radix, not title | Native tooltips can't be styled, have inconsistent delay, and no keyboard trigger. |
| Lucide icons only | Mixing icon sets (or using emoji) creates visual noise. |
cn() for all class merging | It wraps clsx + twMerge — handles conditional classes and deduplicates conflicts. |
| No decorative left-edge bars | Never add purely decorative vertical colored bars on container edges. Exception: functional severity indicators (border-l-[3px] + severity color) are allowed on data rows when severity is a primary dimension (e.g. Optimize list items). Always pair with border-l-transparent fallback for consistent alignment and add extra pl-5 to separate the bar from content. |
| StatCard icon mandatory | Every <StatCard> must have icon={<LucideIcon size={14} />}. Omitting icons on some cards in a grid breaks visual rhythm. |
| i18n code-first | Never render backend strings directly. Use t(\ns.${code}`, { defaultValue: raw })` with the machine code field. |
| AI contracts stay English-only | Every AI-facing prompt, system/follow-up/repair prompt, tool schema, tool arg, label, description, impact, stream status code, and action metadata stays English/structured, even for non-English user messages. Define response formats that separate machine fields from display text, then localize only assistant prose and UI-visible status/reply strings through i18n/settings; hard-coded non-English intent, synonym, count/unit, or fallback phrases in UI/backend logic become hidden business rules. |
5. Pre-delivery checklist
Run through before reporting any UI task as done:
6. Reference
For full token tables (all colors, spacing, radius, shadow values), type scale, surface
hierarchy, component specs, accessibility rules, and view-by-view patterns:
→ Read DESIGN.md (the comprehensive spec). This skill covers the rules you need for every
edit; DESIGN.md has the detailed reference data you need when designing something new.