██╗ ██╗██╗███╗ ██╗██╗ █████╗ ██████╗ ██╗ ██╗██╗
██║ ██║██║████╗ ██║██║ ██╔══██╗██╔══██╗ ██║ ██║██║
██║ █╗ ██║██║██╔██╗ ██║██║ ███████║██████╔╝ ██║ ██║██║
██║███╗██║██║██║╚██╗██║██║ ██╔══██║██╔══██╗ ██║ ██║██║
╚███╔███╔╝██║██║ ╚████║███████╗██║ ██║██████╔╝ ╚██████╔╝██║
╚══╝╚══╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═╝
One design system for every WinLab app, installed with the shadcn CLI.
WinLab apps used to pick their own colors, spacing and component variants. This registry is the single source for all of them: a lightweight set of tokens and components, each with only the variants our apps actually use.
Create a Next.js app, then init it from the WinLab base:
npx create-next-app@latest my-app
cd my-app
npx shadcn@latest init https://ui.winlab.tw/r/base.jsonThe base sets the style to winlab, so every component comes from this registry: a bare npx shadcn@latest add button fails on purpose. (init -t next also fails, because the template adds shadcn's own button.)
This writes the WinLab tokens into app/globals.css and registers the @winlab namespace in components.json, so later components install with:
npx shadcn@latest add @winlab/button| Item | Type | What it gives you |
|---|---|---|
base |
registry:base |
Ultramarine brand tokens (Pantone 286 C, #0033A0) on a neutral scale, light and dark; success and warning status colors; --radius: 1rem; the font stacks below |
font-inter |
registry:font |
Inter for Latin text and numbers |
font-noto-sans-jp |
registry:font |
Noto Sans JP for kanji and punctuation (Japanese forms: ,。 sit in the lower left) |
font-noto-sans-tc |
registry:font |
Noto Sans TC for the Traditional Chinese characters Noto Sans JP lacks |
font-jetbrains-mono |
registry:font |
JetBrains Mono for code and IDs, ligatures on |
button |
registry:ui |
40px tall; variants default, outline, ghost, destructive; sizes default, icon |
label |
registry:ui |
Form label |
input |
registry:ui |
Text input, same height as button |
textarea |
registry:ui |
Multi-line input that grows with its content |
skeleton |
registry:ui |
Loading placeholder |
dialog |
registry:ui |
Modal; sizes default (512px) and wide (672px), scrolls past 92% of the viewport height |
alert-dialog |
registry:ui |
Confirmation with Cancel and an action |
select |
registry:ui |
Single-choice dropdown, same height as input |
table |
registry:ui |
Data table with muted headers; add tabular-nums and text-right to number columns |
popover |
registry:ui |
Frosted menu surface anchored to a trigger; w-(--anchor-width) matches the trigger |
command |
registry:ui |
Searchable list for a combobox inside a popover |
combobox |
registry:ui |
ComboboxTrigger (same field look as select) and ComboboxContent (as wide as the trigger) around a command |
dropdown-menu |
registry:ui |
Action menu: items, labels, separators and submenus |
calendar |
registry:ui |
One month of 40px days to pick a single date; weeks start on Monday, captions read 2026 年 10 月 |
attachment |
registry:ui |
One file per row: 40px icon or thumbnail, name, a detail line and actions; states idle (dashed), uploading, error, done |
checkbox |
registry:ui |
Round 20px checkbox |
switch |
registry:ui |
On / off toggle, 40 x 24 |
tabs |
registry:ui |
Segmented tabs |
avatar |
registry:ui |
Round 40px avatar with initials fallback |
tooltip |
registry:ui |
Short inverted label on hover or focus |
separator |
registry:ui |
Hairline divider |
collapsible |
registry:ui |
Unstyled show / hide region |
sonner |
registry:ui |
Toasts: frosted surface, status-colored icons, action and cancel buttons |
app-shell |
registry:block |
The four corners of every WinLab app and the page layouts column, spotlight and wide; set NEXT_PUBLIC_BUILD_SHA, _TIME and _URL at build for the version corner |
confirm-dialog |
registry:block |
Asks before an action runs; the confirm button names the verb, both buttons lock while it runs, and it closes only when the action finishes |
form-dialog |
registry:block |
A short form in a dialog; FormField puts a label over its control, fields and buttons lock while it submits, and it closes only when the submit finishes |
empty-state |
registry:block |
What a list shows with no rows: "還沒有{noun}" or "找不到符合「{query}」的{noun}", with the next action; TableEmpty is the same sentence as a table row |
page-header |
registry:block |
The top of a page: text-title title, and the page's own actions on the right (under the title on a phone); SectionHeader is the same one level down |
list-skeleton |
registry:block |
Placeholder rows for a list (ListSkeleton) or a table body (TableSkeleton) on first load only, split by the same dividers as the real rows |
member-combobox |
registry:block |
Pick lab members by name or email; one member closes on choice, multiple keeps the menu open and toggles |
field-list |
registry:block |
One record's fields as a dl: muted names in a left column, values beside them (under them on a phone), split by dividers |
status-page |
registry:block |
A spotlight page that says what happened and the way out: not found, an error, no access; the site's own 404 uses it |
date-picker |
registry:block |
A date field with the same closed look as select; opens a calendar, closes on choice, can block days, and posts yyyy-mm-dd under name for form-dialog |
action-panel |
registry:block |
A page whose job is one action, such as opening the door: one 192px round button that locks while it runs, turns success for a moment, and says why when it cannot be pressed |
file-upload |
registry:block |
Drop, paste or pick files; chosen files list as attachment rows to remove, files outside accept or maxSize show as errors with the reason, and name posts them inside form-dialog |
badge |
registry:ui |
Status label; variants default, muted, outline, destructive, success, warning |
Text falls back in that order: font-sans is Inter, then Noto Sans JP, then Noto Sans TC. font-mono puts JetBrains Mono in front of the same CJK fonts. Use Inter's tabular-nums for amounts and table figures, not font-mono.
Two sizes, named by role:
| Utility | Size / line height | Use |
|---|---|---|
text-title |
24px / 32px | Page and container titles |
text-body |
16px / 24px | Everything else; secondary text uses text-muted-foreground |
The base clears Tailwind's default scale (--text-*: initial), so text-sm, text-2xl and the rest generate nothing. Components added to this registry use text-title and text-body only. Hierarchy below a title comes from weight, the muted color and layout patterns, not size.
bun run tokens:check rejects anything that bypasses the tokens in this repo: the old scale names, arbitrary values (rounded-[10px], text-[13px]), Tailwind palette colors (bg-blue-500), color literals, inline styles and raw font-size.
Note
A project created with -t next keeps the template's Geist imports in app/layout.tsx. They are unused by the WinLab stacks; delete them to skip the extra download.
The two-layer model, type, radius and color rules are in DESIGN.md.
| Layer | Choice |
|---|---|
| Framework | Next.js 16 (App Router, static export) |
| Registry | shadcn CLI (shadcn build) |
| Styling | Tailwind CSS v4 + Base UI |
| Package manager | Bun |
git clone https://github.com/NYCU-WinLab/ui && cd ui
bun install
bun devRegistry items are declared in registry.json. bun run build runs shadcn build into public/r/ and then builds the site.
The site's own app/globals.css must match the base item's cssVars; bun run theme:check fails when they drift.
The site is a static export served by nginx. Each push to main publishes ghcr.io/nycu-winlab/ui:main and ghcr.io/nycu-winlab/ui:sha-<commit>.
cp .env.example .env # pin UI_VERSION to a sha- tag in production
docker compose pull && docker compose up -dIssues and PRs welcome: start with CONTRIBUTING.md.
MIT · Ultramarine, all the way down.