Skip to content
Back to skills

Webui Design

ASecurity

Design system for the AI Edit Video web dashboard (noti.vn) - the three-step type scale, light/dark color tokens, the component primitives every page must use, Inter font, SVG icons, Shopify Admin style layout. MUST read before writing or modifying any UI in apps/web.

  • 108 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added August 31, 2026
designgoshellreactnodegitbackend

Works with

  • cli

Security analysis

A100/100

Scanned August 31, 2026

npx -y skills add notivn/AIEV --skill webui-design --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Webui Design?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Webui Design
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/notivn-webui-design/badge)](https://www.skillsdirectory.com/skills/notivn-webui-design)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: webui-design
description: Design system for the AI Edit Video web dashboard (noti.vn) - the three-step type scale, light/dark color tokens, the component primitives every page must use, Inter font, SVG icons, Shopify Admin style layout. MUST read before writing or modifying any UI in apps/web.
---

# Web UI Design System - AI Edit Video by noti.vn

The web UI is a **monitoring and management dashboard**, not a video editor. Aesthetic standard: Shopify Admin - minimal, high information density but still airy, every element has a reason to exist.

**Enforcement:** `cd apps/web && node scripts/check-design-system.mjs`. It flags off-scale type sizes, off-rhythm spacing, raw colors, and hand-rolled copies of existing primitives. Run it before you claim a UI change is done.

## 1. The type scale - three steps, no fourth

This is the single rule that decides whether the dashboard reads clean or noisy, so it comes first.

| Class | Size | Role |
|---|---|---|
| `text-sm` | 14px | **Content.** The default. Descriptions, values, field labels, item names in a list, messages. When in doubt, this one. |
| `text-meta` | 13px | **Metadata.** Timestamps, ids, counts, the hint under a field, file paths. |
| `text-xs` | 12px | **Chrome detail.** Only inside badges/chips, table `<th>`, and `.t-eyebrow`. Never for a sentence. |

Plus exactly two headings - `text-xl font-semibold` (20px) for the page title, `text-sm font-semibold` (14px) for a card title - and `text-[28px] font-bold` for the big figure inside `<StatTile>`. Never more than 2 heading levels on one page.

**Banned:** `text-[10px]`, `text-[11px]`, `text-[13px]`, `text-[15px]`, `text-base`, `text-lg`, and every other arbitrary size. Need 13px? That is what `text-meta` is for - a hand-typed arbitrary size is exactly how a fourth step sneaks back in. Below 12px there is no step at all: either the text matters enough to be readable, or it should not be on screen.

`text-meta` is defined in `globals.css` via `@theme { --text-meta }`, so it is a real Tailwind utility, not a convention.

**Why this is written down so forcefully:** before the August 2026 overhaul the codebase had **416 uses of `text-xs` against 136 of `text-sm`**. 12px had silently become the body size, with a long tail of 10/11/13/15px underneath. Everything looked the same, so nothing read as more important than anything else. The spec said 14px body the whole time - what was missing was a name for 13px and something that counted.

## 2. Spacing rhythm

Gaps and padding come from one scale: `gap-1` (4px), `gap-2` (8px), `gap-3` (12px), `gap-4` (16px), `gap-6` (24px); `p-2`, `p-3`, `p-4`. **Banned:** `gap-0.5`, `gap-2.5`, `gap-5`, `p-0.5`, `p-1.5`, `p-2.5`. Icon buttons do not need their own padding - use `<IconButton>`.

Page shell: content gutter 20px (`Shell.tsx`), cards 16px inside, 16px between cards. `<PageHeader>` deliberately carries **no bottom margin** - the page's own `gap-4` provides it. Adding `mb-4` back doubles it to 32px and detaches the title from its page.

## 3. Primitives - use them, never re-derive them

Every one of these exists because the same thing had been hand-written many times over. Reaching past a primitive to raw Tailwind is the failure mode this system is built to prevent.

| Need | Use | Replaced |
|---|---|---|
| Button | `<Button variant="primary\|secondary\|destructive" small>` | raw `className="btn btn-primary"` |
| Icon-only button | `<IconButton label size="sm\|md" tone="default\|danger">` | 10+ copies at 4 sizes, 3 hover colors |
| Card | `<Card title hint actions>` | `<div className="card p-0">` variants |
| Box nested inside a card | `<Panel title actions>` | 8 verbatim copies of the same class string |
| Label + input + hint + error | `<Field>`, `<SwitchField>`, `<CheckboxField>` | 7 variants at 3 different label sizes |
| Pick one of a few short options | `<Segmented>` | 6 variants, 3 radii, 3 paddings |
| Pick one option needing a description | `<OptionCard>` in `<OptionCardGroup>` | 5 variants, 2 missing `role="radio"` |
| Status / category label | `<Badge tone label dot>` | 13 hand-rolled pills at 10px and 11px |
| Page or card level message | `<Banner tone="danger\|success\|info\|muted">` | 4 error styles + 4 invented success boxes |
| Loading | `<Skeleton>`, `<TableSkeleton>`, `<CardGridSkeleton>` | 3 loading patterns incl. a primitive defined inside a page |
| Dashboard figure | `<StatTile label value sub icon href>` | defined inside `app/page.tsx`, plus a copy |
| Copy to clipboard | `<CopyButton>` | 2 unrelated-looking implementations |
| List toolbar: search / filter / bulk | `<Toolbar>` + `<FilterChip>` | in-card, outside-card, and absent |
| Empty list | `<EmptyState icon title description action>` | bare `<p>` in several places |
| Progress | `<ProgressBar>` / `.progress-indeterminate` | 4 implementations at 2 heights |

**Nesting depth is capped at Card → Panel.** Three nested borders and the eye can no longer tell what contains what - this happened for real on the translate-video page (Card → bordered box → bordered `<li>`).

**Errors:** a single field's error goes in `<Field error>`, right under the offending input. A card- or page-level error goes in `<Banner tone="danger">`. Never a bare `<p className="text-[var(--danger)]">`, and never swallow the raw log - pass it to `detail`.

## 4. Color tokens

Every color goes through a CSS custom property declared in `:root` and `[data-theme="dark"]`. **Components never contain hex codes**, and never Tailwind's palette (`bg-gray-100`, `text-white`) - map those to tokens. Do not apply alpha modifiers to tokens (`bg-[var(--primary-soft)]/40`) - that produces a color outside the table.

```css
:root {
  /* Brand */
  --primary: #ed3c47;
  --primary-hover: #d62e3a;
  --primary-soft: #fdedef;
  --secondary: #ff7849;

  /* Surface */
  --bg: #ffffff;          /* page background */
  --bg-subtle: #f6f6f7;   /* sidebar, nested blocks, zebra rows */
  --surface: #ffffff;     /* card */
  --border: #e7e7ea;

  /* Text */
  --text: #101113;
  --text-muted: #5f6470;

  /* Semantic */
  --success: #16a34a;
  --success-bg: #e7f6ec;
  --danger: #e8590c;
  --danger-bg: #fbeee5;

  /* Chart series - multi-series dataviz only, validated against the dataviz checker */
  --chart-2: #2f6fed;
  --chart-3: #0d9488;

  /* Shape & motion */
  --radius: 8px;
  --radius-lg: 12px;
  --shadow-card: 0 1px 2px rgba(16, 17, 19, 0.06);
  --transition: 150ms ease;
}

[data-theme="dark"] {
  --primary: #ed3c47;          /* brand color stays the same */
  --primary-hover: #f25560;    /* in dark, hover goes LIGHTER, not darker */
  --primary-soft: #3a1d20;
  --secondary: #ff7849;

  --bg: #131417;
  --bg-subtle: #1a1b1f;
  --surface: #1e1f24;
  --border: #2c2d33;

  --text: #f2f3f5;
  --text-muted: #9a9ea9;

  --success: #2ebd6b;
  --success-bg: #17301f;
  --danger: #f0742e;
  --danger-bg: #33231a;

  --chart-2: #5b8def;
  --chart-3: #27ab92;

  --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.4);
}
```

Dark mode rule: only change token **values**, NEVER add dark-only tokens. Write the component once, it runs in both themes.

The handful of legitimate exceptions (a QR code needing a real white background, an overlay on top of arbitrary video) are listed with their reasons in the `ALLOW` map of `scripts/check-design-system.mjs`. Adding an entry there requires a reason written next to it.

## 5. General principles

1. **Light is the default**, dark is opt-in. The toggle persists to `localStorage` and is applied via the `data-theme` attribute on `<html>`.
2. **Icons are 100% inline SVG** - Lucide set, stroke 1.5-2px, `currentColor` so they inherit text color. No icon fonts, no PNGs, no emoji as functional icons (this includes emoji inside `<option>` labels).
3. **Inter font** via the `@fontsource/inter` package, imported in `apps/web/src/app/layout.tsx` (weights 400/500/600/700). Bundled at build time, so self-hosted - never load fonts from a CDN at runtime.
4. Fixed metadata: title `AI Edit Video by: noti.vn`, description `Edit video tự động bằng AI`, favicon from `public/brand/favicon.png`.

## 6. Brand assets

| File in `apps/web/public/brand/` | Download source | Used when |
|---|---|---|
| `logo-duong-ban.png` | https://noti.vn/image/new/logo-duong-ban.png | Header in light theme |
| `logo-am-ban.png` | https://noti.vn/image/new/logo-am-ban.png | Header in dark theme |
| `favicon.png` | https://noti.vn/image/new/favicon.png | `<link rel="icon">` |

The logo swaps with the theme at the same moment as the tokens (same `data-theme` listener).

## 7. Frame layout

```
┌────────────────────────────────────────────────────────┐
│ Topbar 56px: logo · page name · hardware · backend ·   │
│ language · theme                                       │
├─────────┬────────────────────────────────┬─────────────┤
│ Rail    │  Content: FULL WIDTH, 20px     │ AI panel    │
│ 220px ↔ │  gutter, cards 16px apart      │ 440px ↔ 40px│
│ 56px    │  NO max-width                  │ (a slot of  │
│         │                                │  the shell) │
└─────────┴────────────────────────────────┴─────────────┘
```

Rail and panel widths are decided by **CSS custom properties driven by `data-nav`/`data-panel` on `<html>`**, never by React state - read the long comment block at the top of `globals.css` before changing anything there. Pages declare their right panel with `<ShellRightPanel title="…">`; they never compute padding for it themselves.

**Space rule:** never leave dead whitespace. Lists and tables stretch with the screen. Only standalone forms (create project, edit skill) may cap their width for readability (~640px).

Sidebar - **16 items**, source of truth is `NAV` in `apps/web/src/components/Shell.tsx` (do not re-list them here; this doc has drifted from that array before):
Dashboard, Videos Project, Images Project, Auto cut, Text to video, Dịch video, Voices, Style Design, Phong cách dựng, Render Queue, Assets, Sound Effects, Prompts, Skills, Cấu hình, Kết nối.

`/accelerate` and `/chat` are legacy `redirect()` stubs and correctly absent from NAV. `/m/[id]` is the phone-upload page and bypasses the shell entirely.

## 8. Page skeletons - same job, same shape

**List page** (projects, images, queue, assets, sfx, prompts, skills, styles, video-styles, voices):

```tsx
<div className="flex flex-col gap-4">
  <PageHeader title={…} hint={{titleKey, bodyKey}} actions={<Button>…</Button>} />
  {error && <ErrorBanner message={…} detail={…} />}
  <Card>
    <Toolbar search={…} selectedCount={…} bulkActions={…} onClearSelection={…}>
      <FilterChip … />
    </Toolbar>
    {loading ? <TableSkeleton /> : rows.length ? <table className="table">…</table> : <EmptyState … />}
  </Card>
</div>
```

**Detail page** (projects/[id], text-to-video/[id], translate-video/[id], auto-cut/[id], images/[id], styles/[id]): the 3-column `<Workspace>` from `components/Workspace.tsx`, in work-rhythm order **source → setup → output**, with `<OutputBlock>` first in column 3. Column widths come from a **container query**, not a media query, because the real width depends on whether the rail and panel are collapsed. Hand-rolling `xl:grid-cols-5` here is a bug, not a style choice - it ignores the rail/panel state.

Destructive and save actions live in `<PageHeader actions>` on every detail page. Not at the bottom of the page, not inside a card, not as a bare text button.

## 9. Standard components (CSS contracts in `globals.css`)

- **Button**: 36px tall, 16px horizontal padding, radius `--radius`. `.btn-sm` is 30px/13px. There is no third size - do not override `<Button>` with `h-6`/`h-12`. **One exception:** `/m/[id]`, the phone upload page, uses 64px/48px primary buttons. That page is thumb-driven on a real phone rather than mouse-driven on a desktop, and 36px is below the touch-target minimum. It is the only route outside the dashboard shell, and the exception does not travel.
- **Segmented**: 32px by default; pass `size="md"` for 36px when it sits on the same row as an `.input` or a `<Button>`. A 4px height difference between adjacent controls is immediately visible even to someone who can't name it.
- **Card**: `--surface`, 1px `--border`, radius `--radius-lg`, `--shadow-card`, **padding 16px**. Title 14px/600.
- **Panel**: `--bg-subtle`, 1px `--border`, radius `--radius`, padding 12px. Title 13px/600 - deliberately smaller than a card title so nesting reads correctly.
- **Badge**: 12px, full radius, 6px dot. `dot={false}` for *category* labels - the dot is the convention for *status*, and putting it on a category reads as "running/done".
- **Table**: header 12px uppercase muted, row hover `--bg-subtle`, horizontal rules only, no vertical borders.
- **Progress bar**: 6px, track `--bg-subtle`, fill `--primary`. `.progress-indeterminate` is the same 6px so swapping between them doesn't nudge the layout.
- **Input**: 36px/14px. Never shrink the surface the user types into - `input h-8 text-[13px]` was the single most common violation before the overhaul.

## 10. Realtime & state

- Job/agent state streams over SSE - the UI updates live, and never polls more than once per 5s for static data.
- Every list needs a real empty state: muted SVG icon + one sentence + the primary action.
- Loading is a `<Skeleton>` holding the space the content will occupy, never a centered "Đang tải…" that vanishes and shoves everything upward.
- NEVER swallow errors - the raw log goes in `<Banner detail={…}>`, collapsed.

## 11. What NOT to do

- No flashy gradients, no glassmorphism, no decorative animation - the only motion allowed is the 150ms transition, the progress bar, and the skeleton pulse.
- Never a color outside the token table (including Tailwind grays and alpha-modified tokens).
- Never a hex value in JSX/TSX.
- Never re-implement something in the primitive table above. If a primitive nearly fits, extend the primitive - do not fork it locally.
- Never add video editor features to the web UI - all video processing lives in the backend/engine.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.