diff --git a/.changeset/web-design-system-route.md b/.changeset/web-design-system-route.md new file mode 100644 index 000000000..fa80e48c8 --- /dev/null +++ b/.changeset/web-design-system-route.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Rebuild the web design-system easter egg as an in-app overlay that uses the app's real design tokens, so it stays in sync instead of drifting as a separate copy. diff --git a/apps/kimi-web/AGENTS.md b/apps/kimi-web/AGENTS.md index 8660c78b2..19180fe1c 100644 --- a/apps/kimi-web/AGENTS.md +++ b/apps/kimi-web/AGENTS.md @@ -8,12 +8,12 @@ The browser web UI for Kimi Code — a peer to the TUI in `apps/kimi-code`. It t ## Design system (normative — required when modifying the UI) -- **Before changing any component, style, layout, or theme, read `apps/kimi-web/design/design-system.html`.** It is the canonical design system and visual spec for this app (tokens in §02, primitives in §03, chat in §04, theme rules in §05, style rules in §06). New and modified UI must match it. +- **Before changing any component, style, layout, or theme, read the design system view at `src/views/DesignSystemView.vue` (open it as an overlay: long-press the sidebar logo).** It is the canonical design system and visual spec for this app (tokens §02, primitives §03, chat §04, theme rules §05, style rules §06). It consumes the product tokens from `src/style.css` directly, so it stays in sync with the app. New and modified UI must match it. - **Use the primitives in `src/components/ui/`.** The library covers Button, IconButton, Badge, Pill, Card, Input/Select/Textarea/Field, Dialog, Spinner, MoonSpinner, Link, Menu/MenuItem, SegmentedControl, Tabs, Switch, Checkbox, Avatar, EmptyState, Divider, Tooltip, Banner, Sheet, Skeleton, CommandBar, TopBar. One semantic = one component — do not hand-roll a bespoke button/badge/dialog/input for a single screen. When a primitive replaces an element, **delete the old scoped CSS** (do not append override blocks). -- **Use the tokens, not ad-hoc values.** Colors, fonts, radii, spacing, shadows, z-index, and motion come from the CSS custom properties in `src/style.css` (catalogued in §02). Canonical names are `--color-*` / `--radius-*` / `--space-*` / `--text-*` / `--font-*` / `--z-*` / `--shadow-*` / `--ease-*` / `--duration-*` / `--weight-*` / `--leading-*`. New tokens from §02 use the `--p-` prefix: `--p-focus-ring`, `--p-selection`, `--p-ic-sm/md/lg`, `--p-sidebar-w`, `--p-content-max/-wide`, `--p-bp-sm/-md`. (When §03 examples show `--p-text/--p-line/--p-accent/--p-r-*`, map them to the canonical `--color-text/--color-line/--color-accent/--radius-*`.) +- **Use the tokens, not ad-hoc values.** Colors, fonts, radii, spacing, shadows, z-index, and motion come from the CSS custom properties in `src/style.css` (catalogued in the design-system view §02). Canonical names are `--color-*` / `--radius-*` / `--space-*` / `--text-*` / `--font-*` / `--z-*` / `--shadow-*` / `--ease-*` / `--duration-*` / `--weight-*` / `--leading-*`. A small set of layout/focus tokens keep the `--p-` prefix: `--p-focus-ring`, `--p-selection`, `--p-ic-sm/md/lg`, `--p-sidebar-w`, `--p-content-max/-wide`, `--p-bp-sm/-md`. - **The moon spinner (🌑…🌘) is reserved** for the chat "waiting for the agent's first response" state only, and is rendered solely by `ui/MoonSpinner.vue`; every other loading state uses the plain `Spinner`. - **Run `pnpm --filter @moonshot-ai/kimi-web check:style`** (`scripts/check-style.mjs`) — it enforces the §06 anti-pattern rules (no-gradient, no-glassmorphism except TopBar `frost`, no-emoji-icon except moon, no-hardcoded-hex/font, radius/z/weight from scale). Do not add new violations. -- **Verify visually.** For any UI change, render it in the browser (light + dark, plus hover/focus states) and confirm it matches `design-system.html` and introduces no regression before considering it done. Build/typecheck/check-style are necessary but not sufficient. +- **Verify visually.** For any UI change, render it in the browser (light + dark, plus hover/focus states) and confirm it matches the design-system view and introduces no regression before considering it done. Build/typecheck/check-style are necessary but not sufficient. ## Layout (`src/`) diff --git a/apps/kimi-web/design/design-system.html b/apps/kimi-web/design/design-system.html deleted file mode 100644 index 092897350..000000000 --- a/apps/kimi-web/design/design-system.html +++ /dev/null @@ -1,2360 +0,0 @@ - - - - - -Kimi Web · Design System - - - - - - - - - -
- - - - -
-
- - -
-
- ● Design System · v1.0 -

Kimi Web Design System

-

- This document defines the visual language and component specification for Kimi Web — design tokens, component primitives, the chat interface, theming, and style rules. - All UI work is grounded in it: unified, restrained, token-driven, and themeable. -

-
- Scope apps/kimi-web - Component primitives - Theme 1 set · 4 customizable colors - Light / dark mode -
-
- -
- i -
- This spec is the single reference when changing the web UI. Before adding or modifying a component, style, layout, or theme, read this document first; - color, font, radius, spacing, shadow, z-index, and motion always use the §02 tokens, components reuse the §03 primitives, and the §06 style rules are followed. -
-
-
- - -
-
- 01 -

Design Principles

-
-

- Every UI decision traces back to the following principles. Kimi Web is a local Agent tool for developers: quick scanning, long stretches of staring, often in the dark — the design serves the task, and is restrained, clinical, and density-first. -

- -
    -
  • Consistency —— The same semantics use the same component. The primary button, dialog, input, and badge should each have exactly "one" correct way to be written across the entire site.
  • -
  • Hierarchy —— Build a clear hierarchy through size, weight, color, and whitespace; emphasize through "restraint" rather than "bolder and bigger".
  • -
  • Proximity —— Group related elements, leave whitespace between unrelated ones. A card's padding, line spacing, and group spacing all come from the same spacing scale.
  • -
  • Feedback —— hover / active / focus / loading / success / error all have visible states, and the state language is unified.
  • -
  • Breathing room —— Control density with the spacing scale rather than arbitrary pixels; prefer restrained whitespace over cramming controls together.
  • -
  • Accessibility (A11y) —— Text contrast ≥ 4.5:1, visible focus rings, touch targets ≥ 32px, and states that don't rely on color alone.
  • -
  • Reduction —— The number of colors, radii, shadow levels, and type sizes all converge to a finite set of tokens; delete stray values.
  • -
- -
- -
- Brand tone (the do-not list): calm, clinical, never exaggerated. Reject purple gradients, glassmorphism, glowing shadows, AI purple / blue glows, endlessly looping fussy micro-animations, "Boost your productivity"-style marketing copy, and using emoji as icons — the moon phases 🌑…🌘 are the sole exception, used only in the "waiting for the Agent to respond" chat state, as a brand signature. These are all common tells of AI-generated interfaces (an "AI tell"), deliberately avoided. -
-
- -
i
- Declare design intent first (Design Read): before adding a component / page, write one sentence describing its scenario, audience, and tone (for example, "a lightweight tool card embedded in a conversation, for developers, calm and restrained"), then build. If the intent isn't clear, ask one question first rather than defaulting to the nearest existing style. -
-
- - - -
-
- 02 -

Design Tokens

-
-

- Collapse every visual decision into tokens. Color tokens keep the existing short names and fill out the semantics (lowering migration cost), - while spacing, z-index, motion, and font-weight fill in the scales that are currently missing. Every token has: name, light value, dark value, and usage. -

- -
i
- Naming convention: --<category>-<role>-<state>. For example --color-text-muted, --radius-md, --space-4. - To reduce churn, the existing short names (--bg / --ink / --line / --blue …) are kept as compatibility aliases for one release cycle. -
- -

Color

-

Semantic-first, in three layers: background / text / border + accent + status colors. All colors are defined in light / dark pairs, with contrast ≥ 4.5:1.

-
i
The table below shows the derived semantic tokens. The neutrals and the accent are derived from the 4 color seeds in §05 — for example --color-accent comes from --accent-primary, and --color-bg comes from the current light / dark surface. The semantic status colors (success / warning / danger / info) are independent palettes paired with the seeds, one set each for light / dark; they are not auto-derived from the seeds. Day-to-day reskinning usually only needs the 4 seeds, with the status colors fine-tuned as needed.
-
-
bg
#ffffff / #0d1117
-
surface
#fafbfc / #161b22
-
surface-sunken
#f3f5f8 / #0d1117
-
fg
#14171c / #e8eaed
-
fg-muted
#6b7280 / #9aa0a8
-
line
#e7eaee / #2d333b
-
accent (KMBlue)
#1783ff / #58a6ff
-
accent-soft
#e8f3ff / rgba(88,166,255,.14)
-
- - - - - - - - - - - - - - -
TokenLightDarkUsage
--color-bg#ffffff#0d1117Page background
--color-surface#fafbfc#161b22Panel / sidebar / card head
--color-surface-raised#ffffff#1c2128Raised card / dialog / input
--color-text#14171c#e8eaedBody text / headings
--color-text-muted#6b7280#9aa0a8Secondary text / placeholder
--color-line#e7eaee#2d333bDivider / card border
--color-accent#1783ff#58a6ffPrimary action / link / focus
--color-success#0e7a38#3fb950Success / pass
--color-warning#a9610a#d29922Warning / pending
--color-danger#c0392b#f85149Danger / error / abort
- -

Surface usage

-

The four surface layers each have a role — choose by "raised layer / default flat layer / sunken layer / page background", and avoid treating --p-surface-raised as a universal background.

- - - - - - - - -
TokenLightDarkUsage
--p-surface-raised#ffffff#1c2128Raised card / dialog / input (raised layer)
--p-surface#fafbfc#161b22Panel / sidebar / card head (default flat layer)
--p-surface-sunken#f3f5f8#0d1117Code block / inline input / recessed area (sunken layer)
--p-bg#ffffff#0d1117Page background
- -

Focus ring

-

All focusable controls (button, input, link, menu item, switch, checkbox) use the focus-ring token uniformly; do not hand-write a box-shadow focus ring.

- - - - - - -
TokenValueUsage
--p-focus-ring0 0 0 3px var(--p-accent-soft)Default focus ring (link, menu item, switch, checkbox)
--p-focus-ring-strong0 0 0 3px var(--p-accent-soft), 0 0 0 1px var(--p-accent)Strong focus ring (button, primary action)
- -

Text selection

-

The text-selection color uses --p-selection uniformly (light rgba(23,131,255,.18) / dark rgba(88,166,255,.32)), applied by the global ::selection rule; do not set a separate highlight background.

- -

Disabled state

-

All disabled controls use opacity:.5 + cursor:not-allowed uniformly; do not separately grey out or recolor.

- -

Font families

-

Kimi Web uses two font families: --font-ui (UI and body, system fonts first) and --font-mono (code and monospace). Components always reference the variables; do not hard-code font names.

- -

--font-ui · UI & body (system fonts first)

-

Body and UI use each platform's native UI font — close to the system feel, comfortable for long text and CJK. Fallback chain:

-
--font-ui
--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI",
-  "PingFang SC", "Microsoft YaHei", "Noto Sans SC",
-  "Helvetica Neue", Arial, sans-serif,
-  "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji";
-
    -
  • System UI fonts first: SF Pro on macOS / iOS, Segoe UI on Windows.
  • -
  • CJK next: PingFang SC (macOS) / Microsoft YaHei (Windows) / Noto Sans SC (Linux).
  • -
  • Helvetica Neue / Arial / sans-serif as generic fallbacks; emoji fonts at the end.
  • -
- -

--font-mono · Code & monospace

-

Code, tool names, line numbers, diffs, etc. use JetBrains Mono (a self-hosted variable font), falling back to the system monospace:

-
--font-mono
--font-mono: "JetBrains Mono Variable", "JetBrains Mono",
-  ui-monospace, "SF Mono", Menlo, Consolas, monospace;
- -

Loading strategy

- - - - - - - -
FontSourceBundledUsage
JetBrains Mono@fontsource-variable/jetbrains-mono✓ self-hostedmonospace / code (--font-mono)
Inter@fontsource-variable/inter✓ self-hostedoptional: page titles / brand wordmark (--font-display)
System UI / CJK fontsoperating systembody / UI (--font-ui), not bundled
-
- Self-hosted Inter / JetBrains Mono: no external network requests, no FOUT, works offline; system fonts are not bundled, consistent with the local-first approach. -
- -

Usage rules

-
    -
  • Components always use var(--font-ui) / var(--font-mono); do not hard-code font names like 'Inter' / 'JetBrains Mono'.
  • -
  • Body / UI use --font-ui (system fonts first); code / monospace use --font-mono (JetBrains Mono).
  • -
  • Inter is used only for headings / brand scenarios that need a unified look (optional --font-display); it is no longer the body default.
  • -
- -

Type scale & weight

-

The type scale keeps and formalizes the existing --ui-font-size (UI, default 14px) / --content-font-size (body, 16px) dual tracks: UI controls / buttons / forms use --text-base (14px); reading body — including chat Markdown, message bubbles, etc. uses --text-lg (16px, relaxed line-height) for readability; the sidebar session list uses calc(var(--ui-font-size) - 1px) (15px, one step below the sidebar's 16px base) for density. - Drop stray font-weight: 650 / 750; converge on two weights, 400 / 500 (regular / emphasis).

-
-
Page Title
--text-2xl · 22 / 500
-
Section Title
--text-xl · 18 / 500
-
Chat body / card title
--text-lg · 16 / 400
-
UI control / button / form
--text-base · 14 / 500
-
Helper text / table
--text-sm · 13 / 400
-
Badge / timestamp / line number
--text-xs · 12 / 500
-
- - - - - - - - -
TokenValueUsage
--font-ui-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC"…UI & body (system fonts first)
--font-monoJetBrains Mono…code, tool names, line numbers, diffs
--leading-tight/normal/relaxed1.25 / 1.5 / 1.7headings / UI / long text
--weight-regular/medium400 / 500body / emphasis
- -

Icon size

-

Icons use three size tokens uniformly. The global .p-ic default is 16px (--p-ic-md); components pick as needed, and random pixel sizes are forbidden.

- - - - - - - -
TokenValueUsage
--p-ic-sm14pxsmall button, badge, menu item, inline link icon
--p-ic-md16pxdefault (button, icon button, toolbar)
--p-ic-lg20pxToast status icon, empty-state illustration
- -

Icon

-

Icons always come from the centralized registry lib/icons.ts: in templates use the <Icon name size /> component (components/ui/Icon.vue); for v-html contexts (such as a tool glyph) use iconSvg(name, size). Do not hand-write <svg> — the scripts/check-style.mjs icon-from-registry rule flags stray SVGs. Icons come from Remix Icon (Apache-2.0), uniformly in a fill style (fill="currentColor", 24×24 source grid), with color following the text; size uses the three tokens below. The registry is generated on demand by scripts/gen-icon-data.mjs from @iconify-json/ri, offline and tree-shaken. The whole site uses only this one icon family; do not mix in other icon libraries, and never hand-write SVG paths. When an icon is missing, add it to the registry (edit the mapping in gen-icon-data.mjs and regenerate) rather than drawing it in a component.

- -

Size scale

-
-
sm · 14
-
md · 16
-
lg · 20
-
- -

Icon library

-

Currently registered icons, grouped by purpose. The icon data is determined by the NAME_TO_REMIX mapping in scripts/gen-icon-data.mjs; running pnpm gen:icons generates lib/icon-data.ts. This catalog is regenerated with node --experimental-strip-types scripts/gen-icon-catalog.mjs so the registry and the document never drift.

-
-
Actions
-
plus
-
chat-new
-
close
-
check
-
search
-
copy
-
link
-
external-link
-
download
-
undo
-
send
-
image
-
settings
-
sliders
-
log-in
-
Navigation & layout
-
chevron-down
-
chevron-right
-
arrow-up
-
arrow-down
-
arrow-right
-
minus
-
panel-collapse
-
panel-expand
-
expand
-
collapse
-
list
-
Files & tools
-
folder
-
folder-closed
-
folder-plus
-
folder-solid
-
file
-
file-text
-
file-plus
-
file-off
-
image-off
-
code
-
terminal
-
pencil
-
glob
-
globe
-
check-list
-
bolt
-
git-pull-request
-
Communication
-
message
-
mail
-
user
-
Status & media
-
info
-
help-circle
-
alert-triangle
-
clock
-
sparkles
-
play
-
stop
-
star
-
star-outline
-
dots-horizontal
-
Other
-
sort
-
grip
-
- -

Do not use emoji as functional icons (the sole exception is the moon phases 🌑…🌘, used only in the "waiting for the Agent to respond" chat state). The Kimi brand mark (the 32×22 eye logo) is a brand asset and is not part of this icon system.

-

A few special graphics are not in the registry; each has a dedicated component maintained in one place, and must not be copied by hand: <ContextRing :pct /> (the Composer context progress ring, data-driven), <AuthStateIcon kind /> (the success / expired / error colored illustrations in the login flow), <Spinner /> (loading state). Status dots (such as in the Provider list) always use CSS dots (border-radius:50%), not SVG. The scripts/check-style.mjs icon-from-registry rule exempts the above and the brand mark; all other hand-written <svg> is flagged.

- -

Spacing

-

A 4px base grid. All spacing, gaps, and padding inside and outside components come from this scale — no arbitrary pixels.

-
-
--space-1 · 4
icon gap, badge padding
-
--space-2 · 8
control gap, small padding
-
--space-3 · 12
button padding, form-item gap
-
--space-4 · 16
card padding, grid gap
-
--space-5 · 20
dialog padding
-
--space-6 · 24
section gap
-
--space-8 · 32
large section gap
-
- -

Dense list (sidebar / file tree)

-

High-density navigation lists like the sidebar share one rhythm, all on the 4px grid: in-row vertical padding --space-1 (4px), no margin between rows (the hover pill provides the separation); section gap (between logo / search / action buttons / group title / list) uniformly --space-2 (8px); between groups --space-2; the brand header is slightly looser at the top (--space-3). When building similar lists, reuse this scale — do not hand-write 1/6/7/10px.

- -

Radius

-

Merge the existing 14 values into the nearest of 7 scale steps. Rule: the component type determines the radius, not the author's feel.

-
-
xs · 4
-
sm · 6
-
md · 8
-
lg · 12
-
xl · 16
-
2xl · 20
-
full · 999
-
- - - - - - - - - - - -
TokenValueUsageMerged from
--radius-xs4pxsmall badge, inline tag2/3/4px →
--radius-sm6pxsmall button, icon button, menu item5/6px →
--radius-md8pxbutton, input, badge, card7/8/9px →
--radius-lg12pxdropdown panel10/12px →
--radius-xl16pxdialog, bottom Sheet, Composer14/16px →
--radius-2xl20pxaccent container / large panel20px
--radius-full999pxpill badge, avatar, send button999px / 50%
- -

Elevation & z-index

-

Shadows express only "elevation", never decoration (no colored glow). z-index is unified into a scale, eradicating 9999-style one-upping.

-
-
-
sm · dropdown menu / sticky
-
md · Toast
-
lg · overlay (reserved)
-
xl · dialog
-
-
- - - - - - - - - - - -
Z-index TokenValueUsage
--z-base0normal flow
--z-sticky100sticky header / sidebar
--z-dropdown200dropdown menu / tooltip
--z-overlay300overlay / bottom Sheet
--z-modal400dialog
--z-toast600toast
--z-max9999reserved: only this tier for extreme fallback
- -

Motion

- - - - - - - - - -
TokenValueUsage
--ease-outcubic-bezier(0.16, 1, 0.3, 1)enter, hover, expand
--ease-in-outcubic-bezier(0.4, 0, 0.2, 1)panel width, layout changes
--duration-fast120mspress, focus
--duration-base160mshover, show/hide
--duration-slow260msdialog, Sheet, layout
- -

Reduced motion

-
i
- Under @media (prefers-reduced-motion: reduce), all animation and transition durations drop to about 0.001ms (effectively off), and the MoonSpinner moon phase pauses on the current frame. Components should not check this individually; it is handled uniformly in the global styles. -
- -

Layout & breakpoints

-

Layout sizes and responsive breakpoints are tokenized too: sidebar width, content reading-column width, and two global breakpoints. Components should not hard-code pixels.

- - - - - - - - - -
TokenValueUsage
--p-sidebar-w264pxleft session sidebar width
--p-content-max760pxchat reading-column max width
--p-content-wide920pxwide content (settings / panel)
--p-bp-sm640pxmobile / desktop boundary
--p-bp-md980pxnarrow / wide screen boundary
-
i
- At ≤640px: dialogs become bottom Sheets, the sidebar collapses into an expandable drawer, and Composer toolbar controls are allowed to wrap. -
-
- - -
-
- 03 -

Primitives

-
-

- Component primitives are the "smallest correct units" of the site UI. - Each primitive exposes variants along only two dimensions — variant / size — with appearance driven by tokens, - so it naturally supports light / dark mode and customizable theme colors. -

- -
i
- For every interactive primitive, the keyboard behavior, focus, and ARIA contract are in §08 Accessibility. New primitives must ship with a keyboard model — mouse-only interaction is not enough. -
- - -

Component selection guide

- - - - - - - - - - - - - - - -
ScenarioUse
Primary action (submit / confirm)Button variant=primary
Secondary action / cancelButton secondary / ghost
Destructive action (delete / abort)Button danger / danger-soft
Status markerBadge
Toolbar filter / model switchPill
2–4 mutually exclusive optionsSegmentedControl
Top tabsTabs
Switch / multi-selectSwitch / Checkbox
Floating content card / list action menuCard / Menu
Inline notice / global toastBanner / Toast
Dialog / confirmation · bottom panel (mobile)Dialog / Sheet
- - -

Button

-

4 semantic variants × 3 sizes. The primary action primary takes its color from the current theme color (§05 can switch between the blue and black families). Radius uses --radius-md uniformly (small size --radius-sm), weight 600, with a visible focus ring.

-
-
Variant matrix lightpreview
-
- medium · default -
- - - - - -
- small -
- - - -
- With icon / state -
- - - -
-
-
-
-
Dark skin dark
-
-
- - - - -
-
-
- -

API

-
-
Button.vue · usage
-
<Button variant="primary" size="md" :loading="submitting">Save</Button>
-// variant: primary | secondary | ghost | danger | danger-soft
-// size:    sm | md | lg
-
-
-
States
-
-
- - - -
-
-
- - -

IconButton

-

Unified into three sizes — 26 / 32 / 44px — with a light-grey hover background and a visible focus ring. Replaces the ad-hoc icon + click areas scattered across components today.

-
-
IconButton
-
- - - - - -
-
-
i
- The desktop IconButton comes in sm 26 / md 32; on touch devices the tap target should be ≥ 44px, so use lg 44px, satisfying the §01 accessibility principle (the mobile three-piece set uses lg). -
- - -

Badge · Chip · Pill

-

Collapsed into two kinds: Badge (status badge, with an optional status dot) and Pill (the clickable pill in the composer toolbar). Radius, font size, and padding are all unified.

-
-
Badge · status badge
-
- Semantic variants -
- pending - running - completed - needs confirmation - failed - KIMI -
- With icon / small size -
- plan - passed - read-only -
-
-
-
-
Pill · toolbar pill (composer)
-
- kimi-k2· thinking - yolo - 12k / 200k -
-
- - -

Card / Surface

-

All cards across the site share one shell: flat, 1px border, --radius-md radius, no shadow. The structure is split into three parts — head / body / foot. Cards differ only in the head — in two tiers by visual weight, while the shell stays consistent:

-
    -
  • Operation card —— "process" content such as tool calls, Agent, Todo. The head is compact mono with no fill, low weight by default, not competing with the conversation.
  • -
  • Attention card —— content that needs a user decision, such as Question / Approval. The head carries a semantic color band (accent / warning) to stand out from the message stream.
  • -
-
-
Operation card · compact mono head (no fill)
-
-
-
- read_file - session.ts -
-
The head uses mono + a neutral background to emphasize its "code / process" nature; the body uses sans for readability. Flat, radius-md, same shape as the tool group and Agent group.
-
-
-
-
-
Attention card · semantic color-band head (accent / warning)
-
-
-
A decision needs your confirmationquestion
-
The head uses a semantic light background (accent-soft / warning-soft) to stand out from the message stream, signaling that the user must step in. The shell is exactly the same as the operation card.
-
-
-
-
-
Group · the container owns the border, rows are separated by hairlines
-
-
-
3 tool calls· completed
-
read_filesession.ts
-
grep"jwt" · 4 hits
-
-
-
-
    -
  • Unified shell: all cards are flat + 1px border + radius-md, casting no shadow.
  • -
  • Differences are intentional: only the head distinguishes the type (compact mono vs semantic color band); the shell stays consistent.
  • -
  • Grouping: the outer container owns the border and radius; inner rows are separated by border-top hairlines, rather than each row being its own card.
  • -
  • Status dots: running (pulsing blue) / done (green) / failed (red), sharing one color vocabulary (see §04 tool calls).
  • -
- - -

Input / Select / Textarea

-

Unified 38px height (32px small), --radius-md radius, --color-surface-raised background, and a unified blue focus ring (0 0 0 3px accent-soft).

-
-
Form primitives
-
-
-
- - - Only letters, numbers, and hyphens are allowed. -
-
- - -
-
-
- - -
-
-
-
-
States
-
-
-
- - - Please enter a valid workspace name -
-
- - - Normal state · validation passed -
-
-
-
- - -

Code / Diff

-

Inline code, code blocks, and diffs all use the monospace font (--p-font-mono). Code blocks have a filename title bar and a copy button. Diffs use + / - row colors to express additions and deletions — additions use a success light background, deletions use a danger light background, with no gradients.

-
-
Code / Diff
-
- inline code -
The server uses jwt.verify(token) to verify the signature, returning 401 on failure.
- code block -
-
- session.ts - -
-
import { verify } from './jwt';
-
-export function auth(token: string) {
-  return verify(token, process.env.JWT_SECRET!);
-}
-
- diff -
-
session.ts · +3 -1
-
import { verify } from './jwt';
-
-const secret = 'dev-secret';
-
+const secret = process.env.JWT_SECRET!;
-
return verify(token, secret);
-
-
-
- - -

Dialog

-

One dialog primitive replaces 6 hand-written implementations: unified --radius-xl radius, --shadow-xl shadow, 20px head padding, right-aligned footer actions, and an IconButton close button.

-
-
Dialog primitive
-
-
-
-
-
New chat
-
Create an independent Agent chat in the current workspace.
-
- -
-
-
- - -
-
-
- - -
-
-
-
- -
i
- Size & height: Dialog offers three widths — md 440 / lg 640 / xl 760 (--p-content-max) — chosen by content weight. Height comes in two kinds: auto (default, grows with content up to max-height) and fixed (constant height min(680px, 100vh - 64px), with overflow scrolled inside the body). Content / multi-tab dialogs (settings, model picker, provider manager, folder browser) always use fixed so the frame size stays constant and doesn't jump when switching tabs or content length; short confirmation dialogs keep auto. -
- - -

Toast

-

Unified information architecture: status icon + title + description. The status color appears only on the icon, avoiding large colored areas that create visual noise.

-
-
Toast
-
-
- -
Connected to server
The local daemon is responding normally; you can start a new chat.
-
-
- -
Context usage 82%
Consider running /compact to free up space.
-
-
-
- - -

Spinner

-

Loaders fall into two categories by scenario — do not mix them:

-
    -
  • Spinner (plain · SVG ring) —— the default loader. Used for button loading, app startup (GlobalLoading), and general inline waits — "everything else".
  • -
  • MoonSpinner (moon phase · brand signature) —— used only for the chat waiting state of "message sent, waiting for the Agent's first response" (the sending placeholder in ChatPane, SideChatPanel, ActivityNotice).
  • -
- -

Spinner · plain loader (default)

-
-
Spinner · common scenarios
-
-
- - Loading… - -
-
-
- -

MoonSpinner · moon phase (only "waiting for the Agent")

-
-
MoonSpinner · chat waiting state only signature
-
- Frame loop (8 frames) -
- 🌑🌒🌓🌔🌕🌖🌗🌘 -
- Usage · only while the chat waits for a response -
- 🌔Thinking… - 🌕Waiting for response… -
-
-
-
i
The moon phase is the sole exception to the emoji-as-icon rule, and is limited to the "waiting for the Agent's first response" scenario. It is currently implemented twice — in MoonSpinner.vue and ActivityNotice.vue — and should be merged into a single MoonSpinner component, sized via tokens and supporting prefers-reduced-motion. All other loading states use the plain Spinner.
- - -

Link

-

Inline text link: the default is the accent color with no underline; on hover it shows an underline and darkens. The .muted variant uses the secondary text color. Used for in-text jumps, external links, "view all", and other lightweight actions.

-
-
Link · inline link
-
-
- Read the full design token docs before building. - View on GitHub - View history -
-
-
- - -

Menu / Dropdown

-

Dropdown menu panel: raised surface + border + light shadow (--shadow-sm, flat-leaning). Menu items support icons, the current (active) state, the danger state, and the disabled state, with separators grouping items. On touch / mobile, use lg (≥44px row height) for menu items.

-
-
Menu · dropdown menu
-
-
-
Open file
-
Selected item
-
Disabled item
-
-
Delete chat
-
-
-
- - -

SegmentedControl

-

Mutually exclusive short option groups, commonly used for 2–4 option switches such as "light / dark / follow system". The current item is highlighted with a raised surface + subtle shadow.

-
-
SegmentedControl
-
-
- Light - Dark - Follow system -
-
-
- - -

Tabs

-

Tabs with a bottom hairline, used for grouping and switching sibling content. The current tab is marked with accent text + an accent underline.

-
-
Tabs
-
-
- General - Agent - Advanced -
-
-
- - -

Switch

-

A two-state switch for settings that take effect immediately. 36×20 track with full radius, 16px knob; when on, the track turns accent and the knob slides right, with the transition driven by tokens.

-
-
Switch
-
- - -
-
- - -

Checkbox

-

A 17×17 checkbox. When checked it fills with the accent color and shows a white tick (inline SVG). Often paired with a text label.

-
-
Checkbox
-
- - - -
-
- - -

Avatar

-

A 32px default avatar with md radius; .sm is 24px. Can hold an initial or an icon; falls back to this placeholder when there is no image.

-
-
Avatar
-
- K - - K -
-
- - -

EmptyState

-

A centered placeholder for empty lists / panels: a 48px faint icon + title + hint, avoiding blank pages.

-
-
EmptyState
-
-
- -
No chats yet
-
Click "New chat" to start a conversation with Kimi
-
-
-
- - -

Divider

-

A 1px horizontal divider (--p-line); .p-divider-v is the vertical divider, used between inline elements.

-
-
Divider
-
-
Content above
-
-
Content below
-
- kimi-k2 - - thinking -
-
-
- - -

Tooltip

-

A CSS-only hover hint, wrapped in .p-tip. Inverted background (--p-text / --p-bg), single line, no wrapping — carries only short notes.

-
-
Tooltip (hover the button)
-
- - - New chat - -
-
- - -

Banner

-

An inline notice bar placed at the top of a content area. Three states — .info / .warning / .danger — each with a matching 18px icon.

-
-
Banner
-
-
Connected to server
-
Currently in yolo mode; tool calls will run automatically
-
-
- - -

Sheet / BottomSheet

-

A mobile bottom slide-up panel: xl top radius + drag handle, xl shadow. At ≤640px, dialogs become bottom-anchored Sheets.

-
-
BottomSheet
-
-
-
-
Choose a model
-
kimi-k2 · thinking
-
kimi-k2 · instant
-
-
-
- - -

Skeleton

-

A placeholder for loading content, using a breathing opacity animation (no gradients), following the no-gradient-text rule. Composed into titles / text lines / avatars.

-
-
Skeleton
-
-
-
-
-
-
-
-
-
- - -

Command Bar

-

An inline combination of "primary action + command text + copy", sitting between a button and a code block — used for install / onboarding / one-click execution. The primary action reuses Button primary; the command area uses a mono light-grey background.

-
-
Command Bar
-
-
- - curl -fsSL https://code.kimi.com/install.sh | bash -
-
-
- - -

TopBar

-

The application top bar. Solid by default; the .frost variant is translucent + background blur, used only for sticky navigation bars, and is the sole exception to the no-glassmorphism rule (see §06).

-
-
TopBar · solid / frosted glass
-
-
- Solid TopBar - -
-
- Frosted-glass TopBar · .frost - -
-
-
- -

SectionLabel

-

A small group title for sidebar lists, used to section the content below (such as Workspaces in the sidebar). Spec: 13px / 700 / uppercase / letter-spacing .08em, color --color-fg-faint; left-aligned to the row's starting padding (--sb-pad-x), keeping the same indent as the group rows below. For scripts without case (such as Chinese), text-transform:uppercase simply has no effect — no special handling needed.

-
-
Sidebar · group title
-
- -
- - kimi-code-web -
-
- - playground -
-
-
-
- - -
-
- 04 -

Chat Interface Overhaul

-
-

- The message stream is the core of Kimi Web. The goal of the overhaul: have the 6 card types (Agent / Tool / Question / Approval / Swarm / Todo) - share one card skeleton, distinguished only by the head icon and semantic color; and collapse the Composer into a single rounded container. -

- -

Unified message stream

-
-
Conversation · 760px reading column
-
-
- - -
Please change the login endpoint to JWT and add the corresponding unit tests.
- - - 🌔Analyzing the auth module… - - -
-
- - - 3 tool calls - · completed · 0.8s - -
- -
- - - read_file - src/auth/session.ts - 0.2s - -
-
-
12 export function verify(token: string) {
13 return jwt.verify(token, getSecret());
14 }
-
- -
- - - read_file - src/auth/middleware.ts - 0.2s - -
- -
- - - grep - "jwt.verify" · 4 matches - 0.1s - -
-
- - -
-

I looked at the structure of src/auth; it is currently based on a session cookie. The scope of the change is below — once you confirm, I'll start.

-
- - -
-
- - A decision needs your confirmation -
-
How long should the JWT expiry be? Default 7 days, refresh token 30 days.
-
- - -
-
- - -
-
- - Write permission required - write_file -
-
About to modify src/auth/middleware.ts, 42 lines changed. Allow?
-
- - - -
-
- - -
-
Replace session with JWT signing
-
Refactor the auth middleware
-
Add unit tests
-
- -
-
-
- -

Tool calls: compact by default, grouped, expand on demand

-

High-frequency calls like read_file / bash / grep are "operational noise" — if each one took a full card, parallel triggers would quickly drown out the conversation. - The new strategy splits tool calls into three tiers by visual weight, pushing them as light as possible:

- -
-
Three visual-weight tiers
-
- ① Tool row · lightest (default) -
- - - read_file - src/auth/session.ts - 0.2s -
- ② Tool group · medium (consecutive / parallel auto-merged; collapsed to one line) -
-
- - - 3 tool calls - · completed · 0.8s - -
-
- ③ Decision card · heavy (only question / approval, needs user input) -
-
Write permission requiredwrite_file
-
About to modify src/auth/middleware.ts, 42 lines changed.
-
-
-
- -
    -
  • Tool calls render as compact rows by default (30px single-line mono + status dot + key argument); no head / body / shadow.
  • -
  • Consecutive or parallel calls auto-merge into one tool group; when collapsed, the whole group takes one line (N tool calls · status).
  • -
  • Clicking a row expands it in place to show details (code / output); click again to collapse — details don't grab attention by default.
  • -
  • Status is expressed with a colored dot: running (pulsing blue) / done (green) / failed (red), taking no extra space.
  • -
  • Only two types keep a full card: Question (needs an answer) and Approval (needs authorization) — they genuinely need the user's attention.
  • -
- -
-
Tool Call · compact row (expand on demand)
-
-
-
3 tool calls· completed
-
read_filesession.ts
-
12 export function verify(…
-
read_filemiddleware.ts
-
grep"jwt" · 4 hits
-
-
-
- -

Composer

-

Unified into a single rounded container: --radius-xl, with the whole border turning blue + a soft focus ring on focus. Toolbar controls all use the Pill / IconButton primitives, and the send button is a 32px circle.

-
-
Composer
-
-
-
Message Kimi, / to run a command, @ to reference a file…
-
-
- - yolo - plan -
-
- kimi-k2· thinking - -
-
-
-
-
-
i
- Site-wide consistency: the composer has only one radius (--radius-xl · 16px) and one height; toolbar controls all use the Pill / IconButton primitives, and the send button is a 32px circle — it no longer drifts with the theme. -
- -

Responsive

-

See §02 --p-bp-sm for the breakpoint. This section only gives mobile-adaptation pointers for the chat interface; a full mobile mockup is out of scope for this spec.

-
i
- At ≤640px: dialogs anchor to the bottom as Sheets (xl top radius, top drag handle), the sidebar collapses into an expandable drawer, the Composer toolbar is allowed to wrap, and the chat reading column drops its max-width to fill the screen. -
-
- - -
-
- 05 -

Theming

-
-

- Kimi Web uses one unified theme: the same components, fonts, radii, shadows, and surfaces — "reskinning" only changes colors. - Colors are collapsed into 4 seed tokens — two theme colors + one light surface + one dark surface; the neutrals and accent are derived from them, - and the semantic status colors (success / warning / danger) ship as independent palettes paired with the seeds, one set each for light / dark. -

- -

Color seeds

-

Day-to-day customization only needs these 4 seeds; the whole site's neutrals and accent change with them:

-
-
-
Theme color · primary
--accent-primary
-
Theme color · secondary
--accent-secondary
-
Light surface
--surface-light
-
Dark surface
--surface-dark
-
-
- -

Accent families

-

Within one theme, the theme color (accent) can switch among several color families. Two parallel families are provided today: blue (default, brand blue, carrying semantic emphasis) and black (neutral black, carrying the most restrained strong action). Both share the same components, fonts, radii, and surfaces — switching families only swaps the accent token set, with zero structural change; more families (green / purple, etc.) can be added later. The two cards below show the same primary button under the two families.

-
-
Family switch · same primary, different theme color
-
-
-
- Blue family · default -
accent
- --accent #1783ff · soft #e8f3ff -
-
- Black family · neutral -
accent
- --accent #14171c · soft #f1f2f4 -
-
-
-
- -

Theme console · change 4 colors, light & dark change together

-
-
Theme Console
-
-
- Primary #1783ff - Secondary #6b7280 - Light surface #ffffff - Dark surface #0d1117 -
-
-
- Light surface preview - - White background + accent button + neutral text -
-
- Dark surface preview - - Dark background + same accent + derived text -
-
-
-
- -

Light / dark mode

-

Driven by the two surfaces --surface-light / --surface-dark: whichever surface is current derives the corresponding foreground, border, shadow, and status colors. Switching light / dark simply swaps between these two sets of derived tokens, with zero structural change.

- -
- Benefits of one theme: components, fonts, radii, and surfaces are consistent site-wide; reskinning only changes 4 color seeds; light / dark mode works out of the box; semantic status colors are independently tunable. -
-
- - - -
-
- 06 -

Style Rules

-
-

- Anti-pattern rules that all UI code must follow. These rules are also the basis of the check-style detection script, one-to-one with a warning. -

- - - - - - - - - - - - - - -
Rule IDWhat it detectsAction
no-gradient-textgradient text / gradient backgroundForbidden
no-glassmorphismbackdrop-filter: blur (TopBar sticky nav bar is the sole exception)TopBar exempt
no-color-glowcolored / large-radius box-shadow glowForbidden
no-emoji-iconusing emoji as a functional icon (the moon phases 🌑…🌘 are the sole exception, and only in the "waiting for the Agent to respond" chat state; all other loading states use the plain Spinner)Moon phase exempt
no-hardcoded-hexunregistered hex color inside a component <style>Warning
no-hardcoded-fonthard-coded font-family in a component (e.g. 'Inter') instead of var(--font-ui)Warning
radius-from-scaleradius value not in {4,6,8,12,16,20,999}Warning
z-from-scalez-index using an unregistered large numberWarning
weight-from-scalefont-weight not in {400,500}Warning
- -

State matrix

-

Every interactive primitive should define the following states where applicable; missing ones are flagged by the style rules. focus-visible always uses --p-focus-ring (appears only on keyboard focus, see §08); disabled is uniformly opacity:.5.

- - - - - - - - - - - - - -
StateButtonInputCardMenu itemSwitch
default
hover
active / pressed
focus-visible
disabled
loading
selected / active
error
readonly
- -

Moon phase exemption

-
- The "🌑…🌘" moon-phase emoji are a brand signature of Kimi Web, used only in the chat state of "message sent, waiting for the Agent's first response", and are rendered uniformly by the MoonSpinner component; waiting states such as ActivityNotice reuse it rather than implementing their own moon phase. - It is the sole exception to the no-emoji-icon rule; all other loading states use the plain Spinner. -
- -

Glassmorphism exemption

-
- backdrop-filter: blur is banned site-wide, with the sole exception of the .frost variant of TopBar — and only in the one place of the "sticky navigation bar", used to stay readable over scrolling content. No other component (card, dialog, Toast, panel) may use glassmorphism; violations are flagged under no-glassmorphism. -
- - -
- - -
-
- 07 -

App Shell & Sidebar

-
-

- The structural spec for the app shell (three-column grid + right preview panel) and the left session sidebar. These are business-agnostic "skeletons" — - components, fonts, radii, and surfaces are reused from §02 / §03, but layout and alignment have their own conventions. -

- -

Layout grid

-

On desktop it is a single-row 5-track grid: the sidebar and the right panel each occupy a permanent track, with the conversation column in the middle; two 0-width tracks are for the ResizeHandles.

-
App.vue · .app
grid-template-columns: var(--side-w) 0 minmax(0, 1fr) 0 auto;
-/*           sidebar ↑      ↑handle  ↑conversation  ↑handle ↑right panel (auto) */
- - - - - - - - -
TokenValueUsage
--side-w248px (adjustable)left conversation column width, changed by dragging the ResizeHandle; should approach §02's --p-sidebar-w (264px)
--preview-w460pxwidth of the right preview panel when open
--panel-head-h48pxunified height for all right panel heads + the conversation column head, so the hairline runs as one line
--p-bp-sm640px≤640 switches to a mobile single column (top bar + conversation), no sidebar / handle / right panel
-
    -
  • The right panel track exists permanently, with its width transitioning between 0 ↔ var(--preview-w) (when open it squeezes the conversation column, rather than switching templates).
  • -
  • When the sidebar is collapsed, track 1 becomes a thin "rail" holding only an expand IconButton, avoiding crushing the conversation column head.
  • -
  • All grid children must have min-height:0; min-width:0, so only the inner scroll containers scroll and the page itself does not scroll.
  • -
- -

Sidebar alignment system (--sb-*)

-

All sidebar rows (group head, session row, New chat button) share 3 custom properties, so the "session title" aligns precisely under the "workspace name".

- - - - - - - -
TokenValueUsage
--sb-pad-x16pxrow horizontal padding
--sb-gutter20pxleading icon slot width (14px icon + 6px whitespace)
--sb-gap6pxgap between the icon slot and the text
-
i
- The session title's starting x = --sb-pad-x + --sb-gutter + --sb-gap. The group head has a folder icon and the session row has a status slot; both icons are the same width and position, so the titles align naturally. -
- -

Sidebar structure

-

The sidebar from top to bottom: brand header → search → New chat → grouped list (workspace head + session rows). Controls reuse the §03 primitives as much as possible.

- - - - - - - - - -
BlockUseNote
Brand headerlogo + name + IconButtoncollapse / settings use IconButton sm; the logo is animated (a blinking eye)
Searchbare search row (custom)no border, hover/focus shows a sunken background; icon + input + clear IconButton. Do not use Input (the 38px bordered version is too heavy)
New chatfull-width left-aligned button (custom)same rhythm as the session rows in the list (left-aligned, hover sunken). Do not use Button (centered, breaks the rhythm)
Section label.p-section-labeluppercase muted small titles like "Workspaces"
Workspace head / session rowsee next two sectionsshare --sb-* alignment
-
!
- Why New chat / search / inline rename don't use Button / Input: they are "list-style" controls (full-width, left-aligned, compact, borderless), while Button is centered and Input is a 38px bordered control — forcing them in would break the sidebar's visual density and alignment. This is an intentional custom exception, not an oversight. -
- -

Session row

-

A session row is an inset rounded pill, structured as: status slot → title → attention Badge → trailing action slot.

- - - - - - - - - - -
PartRule
Containermargin: 1px 6px; padding: 7px 10px; radius-md; hover = surface-sunken; active = accent-soft + inset 0 0 0 1px accent-bd
Status slot (lead)fixed --sb-gutter width; running = Spinner sm, otherwise unread = 7px accent dot
Titleflex:1 with truncation; double-click enters inline rename (compact input, not Input)
Attention BadgeBadge sm: info (needs answer) / warning (needs approval) / danger (aborted); always shown, not toggled on hover
Trailing action slot (trail)fixed slot shared by the time ↔ kebab (see below)
Archive confirmationreplaces the title area, Button sm (danger confirm / secondary cancel)
- -

Trailing action slot

-

The session row's rightmost is a fixed-width action slot: at rest it shows the relative time (mono xs, fg-faint); on hover it swaps in place to the kebab (IconButton sm).

-
    -
  • The time and kebab are stacked with display:inline-grid + grid-area:1/1 and swapped on hover via visibilitynever display:none.
  • -
  • The slot width is always max(time width, IconButton sm 26px), so the right region's total width is unchanged across hover — badges don't shift, the title's visible character count doesn't change, no jitter.
  • -
  • A session awaiting an answer (info Badge) likewise shows no time on hover: the slot holds only the kebab.
  • -
-
SessionRow.vue
.act { display: inline-grid; flex: none; align-items: center; justify-items: center; }
-.act .ts, .act .kebab { grid-area: 1 / 1; }
-.act .kebab { visibility: hidden; }
-.se:hover .act .kebab, .act:has(.kebab.open) .kebab { visibility: visible; }
-.se:hover .act .ts,   .act:has(.kebab.open) .ts   { visibility: hidden; }
-
i
- Why the time and kebab share a slot and use visibility rather than display:none: the time is variable-width (2h / 5m / just now …), the kebab a fixed 26px. If hover used display:none to drop the time and append the kebab, the right region would reflow — badges shift and the title's truncation changes, causing visible jitter. Sharing one slot and swapping via visibility keeps the slot width constant; this is a hard rule for sidebar list-row stability. -
- -

Workspace group

-

The group head and session rows share --sb-*: folder icon (open/closed) → name → path subtitle, with the kebab and "+" revealed on hover.

-
    -
  • The folder icon sits in the --sb-gutter slot, switching icons between open and closed states.
  • -
  • A small fg-muted path line sits below the name.
  • -
  • The kebab (menu) and "+" (new chat in this workspace) both use IconButton sm, shown on hover or keyboard focus (when not hovered they stay in the tab order via opacity:0, keeping them keyboard-reachable).
  • -
  • The group is collapsible; when collapsed its session list is hidden.
  • -
- -

Show more & collapse

-

The "load more / show less" control at the bottom of each workspace group is a session-row-shaped compact list control (same family as search, New chat, inline rename — not a Button). It doubles as the pagination trigger and the in-group expand / collapse toggle.

- - - - - - - - -
PartRule
Containersession-row pill: display:flex; gap:--sb-gap; min-height:26px, same padding as a session row, radius-md; hover = surface-sunken (no text recolor); :focus-visible uses --p-focus-ring
Lead slotempty, --sb-gutter wide, so the label's start x aligns with the session titles (--sb-pad-x + --sb-gutter + --sb-gap)
Labelfont-ui, text-xs, --color-text; flex:1, truncated
Behavior"Load more" fetches the next page and auto-expands; once more than the first page is loaded, "Show less" appears and collapses back to the first page (view-layer trim — data is kept, no refetch); "Show all" re-expands
- -

ResizeHandle

-

A 4px vertical drag bar, layered over the 1px column border (margin: 0 -2px makes the whole 4px grabbable), turning accent on hover / drag.

- - - - - - - - -
RuleValue
Width / cursor4px / col-resize
Normal / activetransparent / accent fill
Layer--z-sticky, over the column border
Behaviorpanel width follows the pointer 1:1 while dragging (the parent disables transitions to avoid lag); on release it is persisted to localStorage
- -

Right panel

-

The right panels (file preview / Diff / thinking / sub-agent / side chat) share one track and one head primitive.

-
    -
  • The panel head uses the PanelHeader primitive (48px = --panel-head-h), the same height as the conversation column head, so the hairline runs as one line.
  • -
  • Panel head: bold mono title + optional muted subtitle + middle slot (Badge / control / path) + close IconButton on the right.
  • -
  • When opened, the panel width goes from 0 → var(--preview-w), smoothly squeezing the conversation column.
  • -
  • At ≤640px the panel becomes a full-screen overlay (position:fixed; inset:0).
  • -
- -
i
- One-sentence principle: the sidebar / shell is a "list + grid" skeleton that reuses the §02 tokens and §03 primitives (Button / IconButton / Badge / Menu / Spinner / PanelHeader); compact list controls that don't fit a primitive (search, New chat, inline rename, show-more) keep their custom form, governed by this section. -
-
- - -
-
- 08 -

Accessibility (pragmatic edition)

-
-

- Kimi Web is a local developer tool; it does not target a specific WCAG conformance level, nor maintain a full screen-reader QA matrix. - This section collects only the rules that are "low-cost, don't hurt the look, and directly benefit keyboard-heavy users", as the baseline contract for each primitive; - the more expensive, lower-ROI parts (such as real-time announcement orchestration for streaming output) are not mandatory for now. -

- -
i
- On the "ugly" focus ring: the focus visibility required below always uses :focus-visible (not :focus). - It appears only on keyboard focus; mouse clicks don't trigger it, so it doesn't pollute the mouse-driven visual; the ring's strength is tuned uniformly with --p-focus-ring, not overridden per place. -
- -

1. Contrast & color

-
    -
  • Body text vs. background contrast ≥ 4.5:1; control borders, icons, and key graphics ≥ 3:1. When changing theme colors / dark mode, verify against §05 together.
  • -
  • Button text vs. button background, and form controls (input, placeholder, helper / error text) vs. their section background must all have contrast ≥ 4.5:1 (large text ≥ 3:1). White-on-white text, a transparent borderless button floating over the page background, and a light placeholder on a near-white background are all flagged by the style rules.
  • -
  • State is not conveyed by color alone. Error, selected, and disabled states also carry text, an icon, or a shape change (for example an error state is not just red, but also carries text or an icon).
  • -
- -

2. Keyboard operable

-

Anything doable with a mouse must also be doable with a keyboard; Tab order follows the DOM, with no invented skipping. Composite controls define their keyboard model per the table below; a missing model is treated as incomplete:

- - - - - - - - -
ControlKeyboard behavior
DialogTab cycles within the dialog (focus trap); Esc closes; focus returns to the trigger element after closing.
Menu / move the highlight, Enter selects, Esc closes.
Tabs / switch tabs (roving tabindex); only the current tab is in the Tab sequence.
Switch / Segmented / or Space / Enter to toggle.
- -

3. Focus visibility

-
    -
  • Every interactive element must have a visible focus indicator on keyboard focus, uniformly via :focus-visible + --p-focus-ring (primary actions may use --p-focus-ring-strong).
  • -
  • Bare outline: none is forbidden. To remove the default outline, you must provide an equivalent replacement style.
  • -
- -

4. Labels & semantics

-
    -
  • Semantic HTML first (button / a / input / dialog…); ARIA is added only when native semantics fall short.
  • -
  • Icon-only buttons must have an aria-labelIconButton already enforces this with a required label prop.
  • -
  • Dialog: role="dialog" + aria-modal="true", with the title as the dialog's accessible name.
  • -
  • Purely decorative SVG / icons get aria-hidden="true" to avoid being read out by screen readers.
  • -
- -

5. Target size

-

Desktop click targets ≥ 32px; touch devices ≥ 44px (consistent with the §01 principle and the IconButton lg tier).

- -

6. Reduced motion

-

Handled uniformly in the global styles per §02's @media (prefers-reduced-motion: reduce); components do not check this individually. The MoonSpinner moon phase pauses on the current frame.

- -

7. Live announcements (non-mandatory)

-

Screen-reader announcements are not a mandatory contract in this product. Short hints like Toast can use role="status" / aria-live; chat streaming output is currently not announced word-by-word, which is an acceptable trade-off, to be added later if a real need arises.

- -
- Explicitly not mandatory for now: a WCAG conformance-level claim, a complete ARIA pattern table, a per-screen-reader QA matrix, and real-time announcement orchestration for streaming output — these are not written into the primitive contract, to avoid becoming slogans no one maintains. -
-
- -
-
-
- - - - diff --git a/apps/kimi-web/public/design-system.html b/apps/kimi-web/public/design-system.html deleted file mode 100644 index 092897350..000000000 --- a/apps/kimi-web/public/design-system.html +++ /dev/null @@ -1,2360 +0,0 @@ - - - - - -Kimi Web · Design System - - - - - - - - - -
- - - - -
-
- - -
-
- ● Design System · v1.0 -

Kimi Web Design System

-

- This document defines the visual language and component specification for Kimi Web — design tokens, component primitives, the chat interface, theming, and style rules. - All UI work is grounded in it: unified, restrained, token-driven, and themeable. -

-
- Scope apps/kimi-web - Component primitives - Theme 1 set · 4 customizable colors - Light / dark mode -
-
- -
- i -
- This spec is the single reference when changing the web UI. Before adding or modifying a component, style, layout, or theme, read this document first; - color, font, radius, spacing, shadow, z-index, and motion always use the §02 tokens, components reuse the §03 primitives, and the §06 style rules are followed. -
-
-
- - -
-
- 01 -

Design Principles

-
-

- Every UI decision traces back to the following principles. Kimi Web is a local Agent tool for developers: quick scanning, long stretches of staring, often in the dark — the design serves the task, and is restrained, clinical, and density-first. -

- -
    -
  • Consistency —— The same semantics use the same component. The primary button, dialog, input, and badge should each have exactly "one" correct way to be written across the entire site.
  • -
  • Hierarchy —— Build a clear hierarchy through size, weight, color, and whitespace; emphasize through "restraint" rather than "bolder and bigger".
  • -
  • Proximity —— Group related elements, leave whitespace between unrelated ones. A card's padding, line spacing, and group spacing all come from the same spacing scale.
  • -
  • Feedback —— hover / active / focus / loading / success / error all have visible states, and the state language is unified.
  • -
  • Breathing room —— Control density with the spacing scale rather than arbitrary pixels; prefer restrained whitespace over cramming controls together.
  • -
  • Accessibility (A11y) —— Text contrast ≥ 4.5:1, visible focus rings, touch targets ≥ 32px, and states that don't rely on color alone.
  • -
  • Reduction —— The number of colors, radii, shadow levels, and type sizes all converge to a finite set of tokens; delete stray values.
  • -
- -
- -
- Brand tone (the do-not list): calm, clinical, never exaggerated. Reject purple gradients, glassmorphism, glowing shadows, AI purple / blue glows, endlessly looping fussy micro-animations, "Boost your productivity"-style marketing copy, and using emoji as icons — the moon phases 🌑…🌘 are the sole exception, used only in the "waiting for the Agent to respond" chat state, as a brand signature. These are all common tells of AI-generated interfaces (an "AI tell"), deliberately avoided. -
-
- -
i
- Declare design intent first (Design Read): before adding a component / page, write one sentence describing its scenario, audience, and tone (for example, "a lightweight tool card embedded in a conversation, for developers, calm and restrained"), then build. If the intent isn't clear, ask one question first rather than defaulting to the nearest existing style. -
-
- - - -
-
- 02 -

Design Tokens

-
-

- Collapse every visual decision into tokens. Color tokens keep the existing short names and fill out the semantics (lowering migration cost), - while spacing, z-index, motion, and font-weight fill in the scales that are currently missing. Every token has: name, light value, dark value, and usage. -

- -
i
- Naming convention: --<category>-<role>-<state>. For example --color-text-muted, --radius-md, --space-4. - To reduce churn, the existing short names (--bg / --ink / --line / --blue …) are kept as compatibility aliases for one release cycle. -
- -

Color

-

Semantic-first, in three layers: background / text / border + accent + status colors. All colors are defined in light / dark pairs, with contrast ≥ 4.5:1.

-
i
The table below shows the derived semantic tokens. The neutrals and the accent are derived from the 4 color seeds in §05 — for example --color-accent comes from --accent-primary, and --color-bg comes from the current light / dark surface. The semantic status colors (success / warning / danger / info) are independent palettes paired with the seeds, one set each for light / dark; they are not auto-derived from the seeds. Day-to-day reskinning usually only needs the 4 seeds, with the status colors fine-tuned as needed.
-
-
bg
#ffffff / #0d1117
-
surface
#fafbfc / #161b22
-
surface-sunken
#f3f5f8 / #0d1117
-
fg
#14171c / #e8eaed
-
fg-muted
#6b7280 / #9aa0a8
-
line
#e7eaee / #2d333b
-
accent (KMBlue)
#1783ff / #58a6ff
-
accent-soft
#e8f3ff / rgba(88,166,255,.14)
-
- - - - - - - - - - - - - - -
TokenLightDarkUsage
--color-bg#ffffff#0d1117Page background
--color-surface#fafbfc#161b22Panel / sidebar / card head
--color-surface-raised#ffffff#1c2128Raised card / dialog / input
--color-text#14171c#e8eaedBody text / headings
--color-text-muted#6b7280#9aa0a8Secondary text / placeholder
--color-line#e7eaee#2d333bDivider / card border
--color-accent#1783ff#58a6ffPrimary action / link / focus
--color-success#0e7a38#3fb950Success / pass
--color-warning#a9610a#d29922Warning / pending
--color-danger#c0392b#f85149Danger / error / abort
- -

Surface usage

-

The four surface layers each have a role — choose by "raised layer / default flat layer / sunken layer / page background", and avoid treating --p-surface-raised as a universal background.

- - - - - - - - -
TokenLightDarkUsage
--p-surface-raised#ffffff#1c2128Raised card / dialog / input (raised layer)
--p-surface#fafbfc#161b22Panel / sidebar / card head (default flat layer)
--p-surface-sunken#f3f5f8#0d1117Code block / inline input / recessed area (sunken layer)
--p-bg#ffffff#0d1117Page background
- -

Focus ring

-

All focusable controls (button, input, link, menu item, switch, checkbox) use the focus-ring token uniformly; do not hand-write a box-shadow focus ring.

- - - - - - -
TokenValueUsage
--p-focus-ring0 0 0 3px var(--p-accent-soft)Default focus ring (link, menu item, switch, checkbox)
--p-focus-ring-strong0 0 0 3px var(--p-accent-soft), 0 0 0 1px var(--p-accent)Strong focus ring (button, primary action)
- -

Text selection

-

The text-selection color uses --p-selection uniformly (light rgba(23,131,255,.18) / dark rgba(88,166,255,.32)), applied by the global ::selection rule; do not set a separate highlight background.

- -

Disabled state

-

All disabled controls use opacity:.5 + cursor:not-allowed uniformly; do not separately grey out or recolor.

- -

Font families

-

Kimi Web uses two font families: --font-ui (UI and body, system fonts first) and --font-mono (code and monospace). Components always reference the variables; do not hard-code font names.

- -

--font-ui · UI & body (system fonts first)

-

Body and UI use each platform's native UI font — close to the system feel, comfortable for long text and CJK. Fallback chain:

-
--font-ui
--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI",
-  "PingFang SC", "Microsoft YaHei", "Noto Sans SC",
-  "Helvetica Neue", Arial, sans-serif,
-  "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji";
-
    -
  • System UI fonts first: SF Pro on macOS / iOS, Segoe UI on Windows.
  • -
  • CJK next: PingFang SC (macOS) / Microsoft YaHei (Windows) / Noto Sans SC (Linux).
  • -
  • Helvetica Neue / Arial / sans-serif as generic fallbacks; emoji fonts at the end.
  • -
- -

--font-mono · Code & monospace

-

Code, tool names, line numbers, diffs, etc. use JetBrains Mono (a self-hosted variable font), falling back to the system monospace:

-
--font-mono
--font-mono: "JetBrains Mono Variable", "JetBrains Mono",
-  ui-monospace, "SF Mono", Menlo, Consolas, monospace;
- -

Loading strategy

- - - - - - - -
FontSourceBundledUsage
JetBrains Mono@fontsource-variable/jetbrains-mono✓ self-hostedmonospace / code (--font-mono)
Inter@fontsource-variable/inter✓ self-hostedoptional: page titles / brand wordmark (--font-display)
System UI / CJK fontsoperating systembody / UI (--font-ui), not bundled
-
- Self-hosted Inter / JetBrains Mono: no external network requests, no FOUT, works offline; system fonts are not bundled, consistent with the local-first approach. -
- -

Usage rules

-
    -
  • Components always use var(--font-ui) / var(--font-mono); do not hard-code font names like 'Inter' / 'JetBrains Mono'.
  • -
  • Body / UI use --font-ui (system fonts first); code / monospace use --font-mono (JetBrains Mono).
  • -
  • Inter is used only for headings / brand scenarios that need a unified look (optional --font-display); it is no longer the body default.
  • -
- -

Type scale & weight

-

The type scale keeps and formalizes the existing --ui-font-size (UI, default 14px) / --content-font-size (body, 16px) dual tracks: UI controls / buttons / forms use --text-base (14px); reading body — including chat Markdown, message bubbles, etc. uses --text-lg (16px, relaxed line-height) for readability; the sidebar session list uses calc(var(--ui-font-size) - 1px) (15px, one step below the sidebar's 16px base) for density. - Drop stray font-weight: 650 / 750; converge on two weights, 400 / 500 (regular / emphasis).

-
-
Page Title
--text-2xl · 22 / 500
-
Section Title
--text-xl · 18 / 500
-
Chat body / card title
--text-lg · 16 / 400
-
UI control / button / form
--text-base · 14 / 500
-
Helper text / table
--text-sm · 13 / 400
-
Badge / timestamp / line number
--text-xs · 12 / 500
-
- - - - - - - - -
TokenValueUsage
--font-ui-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC"…UI & body (system fonts first)
--font-monoJetBrains Mono…code, tool names, line numbers, diffs
--leading-tight/normal/relaxed1.25 / 1.5 / 1.7headings / UI / long text
--weight-regular/medium400 / 500body / emphasis
- -

Icon size

-

Icons use three size tokens uniformly. The global .p-ic default is 16px (--p-ic-md); components pick as needed, and random pixel sizes are forbidden.

- - - - - - - -
TokenValueUsage
--p-ic-sm14pxsmall button, badge, menu item, inline link icon
--p-ic-md16pxdefault (button, icon button, toolbar)
--p-ic-lg20pxToast status icon, empty-state illustration
- -

Icon

-

Icons always come from the centralized registry lib/icons.ts: in templates use the <Icon name size /> component (components/ui/Icon.vue); for v-html contexts (such as a tool glyph) use iconSvg(name, size). Do not hand-write <svg> — the scripts/check-style.mjs icon-from-registry rule flags stray SVGs. Icons come from Remix Icon (Apache-2.0), uniformly in a fill style (fill="currentColor", 24×24 source grid), with color following the text; size uses the three tokens below. The registry is generated on demand by scripts/gen-icon-data.mjs from @iconify-json/ri, offline and tree-shaken. The whole site uses only this one icon family; do not mix in other icon libraries, and never hand-write SVG paths. When an icon is missing, add it to the registry (edit the mapping in gen-icon-data.mjs and regenerate) rather than drawing it in a component.

- -

Size scale

-
-
sm · 14
-
md · 16
-
lg · 20
-
- -

Icon library

-

Currently registered icons, grouped by purpose. The icon data is determined by the NAME_TO_REMIX mapping in scripts/gen-icon-data.mjs; running pnpm gen:icons generates lib/icon-data.ts. This catalog is regenerated with node --experimental-strip-types scripts/gen-icon-catalog.mjs so the registry and the document never drift.

-
-
Actions
-
plus
-
chat-new
-
close
-
check
-
search
-
copy
-
link
-
external-link
-
download
-
undo
-
send
-
image
-
settings
-
sliders
-
log-in
-
Navigation & layout
-
chevron-down
-
chevron-right
-
arrow-up
-
arrow-down
-
arrow-right
-
minus
-
panel-collapse
-
panel-expand
-
expand
-
collapse
-
list
-
Files & tools
-
folder
-
folder-closed
-
folder-plus
-
folder-solid
-
file
-
file-text
-
file-plus
-
file-off
-
image-off
-
code
-
terminal
-
pencil
-
glob
-
globe
-
check-list
-
bolt
-
git-pull-request
-
Communication
-
message
-
mail
-
user
-
Status & media
-
info
-
help-circle
-
alert-triangle
-
clock
-
sparkles
-
play
-
stop
-
star
-
star-outline
-
dots-horizontal
-
Other
-
sort
-
grip
-
- -

Do not use emoji as functional icons (the sole exception is the moon phases 🌑…🌘, used only in the "waiting for the Agent to respond" chat state). The Kimi brand mark (the 32×22 eye logo) is a brand asset and is not part of this icon system.

-

A few special graphics are not in the registry; each has a dedicated component maintained in one place, and must not be copied by hand: <ContextRing :pct /> (the Composer context progress ring, data-driven), <AuthStateIcon kind /> (the success / expired / error colored illustrations in the login flow), <Spinner /> (loading state). Status dots (such as in the Provider list) always use CSS dots (border-radius:50%), not SVG. The scripts/check-style.mjs icon-from-registry rule exempts the above and the brand mark; all other hand-written <svg> is flagged.

- -

Spacing

-

A 4px base grid. All spacing, gaps, and padding inside and outside components come from this scale — no arbitrary pixels.

-
-
--space-1 · 4
icon gap, badge padding
-
--space-2 · 8
control gap, small padding
-
--space-3 · 12
button padding, form-item gap
-
--space-4 · 16
card padding, grid gap
-
--space-5 · 20
dialog padding
-
--space-6 · 24
section gap
-
--space-8 · 32
large section gap
-
- -

Dense list (sidebar / file tree)

-

High-density navigation lists like the sidebar share one rhythm, all on the 4px grid: in-row vertical padding --space-1 (4px), no margin between rows (the hover pill provides the separation); section gap (between logo / search / action buttons / group title / list) uniformly --space-2 (8px); between groups --space-2; the brand header is slightly looser at the top (--space-3). When building similar lists, reuse this scale — do not hand-write 1/6/7/10px.

- -

Radius

-

Merge the existing 14 values into the nearest of 7 scale steps. Rule: the component type determines the radius, not the author's feel.

-
-
xs · 4
-
sm · 6
-
md · 8
-
lg · 12
-
xl · 16
-
2xl · 20
-
full · 999
-
- - - - - - - - - - - -
TokenValueUsageMerged from
--radius-xs4pxsmall badge, inline tag2/3/4px →
--radius-sm6pxsmall button, icon button, menu item5/6px →
--radius-md8pxbutton, input, badge, card7/8/9px →
--radius-lg12pxdropdown panel10/12px →
--radius-xl16pxdialog, bottom Sheet, Composer14/16px →
--radius-2xl20pxaccent container / large panel20px
--radius-full999pxpill badge, avatar, send button999px / 50%
- -

Elevation & z-index

-

Shadows express only "elevation", never decoration (no colored glow). z-index is unified into a scale, eradicating 9999-style one-upping.

-
-
-
sm · dropdown menu / sticky
-
md · Toast
-
lg · overlay (reserved)
-
xl · dialog
-
-
- - - - - - - - - - - -
Z-index TokenValueUsage
--z-base0normal flow
--z-sticky100sticky header / sidebar
--z-dropdown200dropdown menu / tooltip
--z-overlay300overlay / bottom Sheet
--z-modal400dialog
--z-toast600toast
--z-max9999reserved: only this tier for extreme fallback
- -

Motion

- - - - - - - - - -
TokenValueUsage
--ease-outcubic-bezier(0.16, 1, 0.3, 1)enter, hover, expand
--ease-in-outcubic-bezier(0.4, 0, 0.2, 1)panel width, layout changes
--duration-fast120mspress, focus
--duration-base160mshover, show/hide
--duration-slow260msdialog, Sheet, layout
- -

Reduced motion

-
i
- Under @media (prefers-reduced-motion: reduce), all animation and transition durations drop to about 0.001ms (effectively off), and the MoonSpinner moon phase pauses on the current frame. Components should not check this individually; it is handled uniformly in the global styles. -
- -

Layout & breakpoints

-

Layout sizes and responsive breakpoints are tokenized too: sidebar width, content reading-column width, and two global breakpoints. Components should not hard-code pixels.

- - - - - - - - - -
TokenValueUsage
--p-sidebar-w264pxleft session sidebar width
--p-content-max760pxchat reading-column max width
--p-content-wide920pxwide content (settings / panel)
--p-bp-sm640pxmobile / desktop boundary
--p-bp-md980pxnarrow / wide screen boundary
-
i
- At ≤640px: dialogs become bottom Sheets, the sidebar collapses into an expandable drawer, and Composer toolbar controls are allowed to wrap. -
-
- - -
-
- 03 -

Primitives

-
-

- Component primitives are the "smallest correct units" of the site UI. - Each primitive exposes variants along only two dimensions — variant / size — with appearance driven by tokens, - so it naturally supports light / dark mode and customizable theme colors. -

- -
i
- For every interactive primitive, the keyboard behavior, focus, and ARIA contract are in §08 Accessibility. New primitives must ship with a keyboard model — mouse-only interaction is not enough. -
- - -

Component selection guide

- - - - - - - - - - - - - - - -
ScenarioUse
Primary action (submit / confirm)Button variant=primary
Secondary action / cancelButton secondary / ghost
Destructive action (delete / abort)Button danger / danger-soft
Status markerBadge
Toolbar filter / model switchPill
2–4 mutually exclusive optionsSegmentedControl
Top tabsTabs
Switch / multi-selectSwitch / Checkbox
Floating content card / list action menuCard / Menu
Inline notice / global toastBanner / Toast
Dialog / confirmation · bottom panel (mobile)Dialog / Sheet
- - -

Button

-

4 semantic variants × 3 sizes. The primary action primary takes its color from the current theme color (§05 can switch between the blue and black families). Radius uses --radius-md uniformly (small size --radius-sm), weight 600, with a visible focus ring.

-
-
Variant matrix lightpreview
-
- medium · default -
- - - - - -
- small -
- - - -
- With icon / state -
- - - -
-
-
-
-
Dark skin dark
-
-
- - - - -
-
-
- -

API

-
-
Button.vue · usage
-
<Button variant="primary" size="md" :loading="submitting">Save</Button>
-// variant: primary | secondary | ghost | danger | danger-soft
-// size:    sm | md | lg
-
-
-
States
-
-
- - - -
-
-
- - -

IconButton

-

Unified into three sizes — 26 / 32 / 44px — with a light-grey hover background and a visible focus ring. Replaces the ad-hoc icon + click areas scattered across components today.

-
-
IconButton
-
- - - - - -
-
-
i
- The desktop IconButton comes in sm 26 / md 32; on touch devices the tap target should be ≥ 44px, so use lg 44px, satisfying the §01 accessibility principle (the mobile three-piece set uses lg). -
- - -

Badge · Chip · Pill

-

Collapsed into two kinds: Badge (status badge, with an optional status dot) and Pill (the clickable pill in the composer toolbar). Radius, font size, and padding are all unified.

-
-
Badge · status badge
-
- Semantic variants -
- pending - running - completed - needs confirmation - failed - KIMI -
- With icon / small size -
- plan - passed - read-only -
-
-
-
-
Pill · toolbar pill (composer)
-
- kimi-k2· thinking - yolo - 12k / 200k -
-
- - -

Card / Surface

-

All cards across the site share one shell: flat, 1px border, --radius-md radius, no shadow. The structure is split into three parts — head / body / foot. Cards differ only in the head — in two tiers by visual weight, while the shell stays consistent:

-
    -
  • Operation card —— "process" content such as tool calls, Agent, Todo. The head is compact mono with no fill, low weight by default, not competing with the conversation.
  • -
  • Attention card —— content that needs a user decision, such as Question / Approval. The head carries a semantic color band (accent / warning) to stand out from the message stream.
  • -
-
-
Operation card · compact mono head (no fill)
-
-
-
- read_file - session.ts -
-
The head uses mono + a neutral background to emphasize its "code / process" nature; the body uses sans for readability. Flat, radius-md, same shape as the tool group and Agent group.
-
-
-
-
-
Attention card · semantic color-band head (accent / warning)
-
-
-
A decision needs your confirmationquestion
-
The head uses a semantic light background (accent-soft / warning-soft) to stand out from the message stream, signaling that the user must step in. The shell is exactly the same as the operation card.
-
-
-
-
-
Group · the container owns the border, rows are separated by hairlines
-
-
-
3 tool calls· completed
-
read_filesession.ts
-
grep"jwt" · 4 hits
-
-
-
-
    -
  • Unified shell: all cards are flat + 1px border + radius-md, casting no shadow.
  • -
  • Differences are intentional: only the head distinguishes the type (compact mono vs semantic color band); the shell stays consistent.
  • -
  • Grouping: the outer container owns the border and radius; inner rows are separated by border-top hairlines, rather than each row being its own card.
  • -
  • Status dots: running (pulsing blue) / done (green) / failed (red), sharing one color vocabulary (see §04 tool calls).
  • -
- - -

Input / Select / Textarea

-

Unified 38px height (32px small), --radius-md radius, --color-surface-raised background, and a unified blue focus ring (0 0 0 3px accent-soft).

-
-
Form primitives
-
-
-
- - - Only letters, numbers, and hyphens are allowed. -
-
- - -
-
-
- - -
-
-
-
-
States
-
-
-
- - - Please enter a valid workspace name -
-
- - - Normal state · validation passed -
-
-
-
- - -

Code / Diff

-

Inline code, code blocks, and diffs all use the monospace font (--p-font-mono). Code blocks have a filename title bar and a copy button. Diffs use + / - row colors to express additions and deletions — additions use a success light background, deletions use a danger light background, with no gradients.

-
-
Code / Diff
-
- inline code -
The server uses jwt.verify(token) to verify the signature, returning 401 on failure.
- code block -
-
- session.ts - -
-
import { verify } from './jwt';
-
-export function auth(token: string) {
-  return verify(token, process.env.JWT_SECRET!);
-}
-
- diff -
-
session.ts · +3 -1
-
import { verify } from './jwt';
-
-const secret = 'dev-secret';
-
+const secret = process.env.JWT_SECRET!;
-
return verify(token, secret);
-
-
-
- - -

Dialog

-

One dialog primitive replaces 6 hand-written implementations: unified --radius-xl radius, --shadow-xl shadow, 20px head padding, right-aligned footer actions, and an IconButton close button.

-
-
Dialog primitive
-
-
-
-
-
New chat
-
Create an independent Agent chat in the current workspace.
-
- -
-
-
- - -
-
-
- - -
-
-
-
- -
i
- Size & height: Dialog offers three widths — md 440 / lg 640 / xl 760 (--p-content-max) — chosen by content weight. Height comes in two kinds: auto (default, grows with content up to max-height) and fixed (constant height min(680px, 100vh - 64px), with overflow scrolled inside the body). Content / multi-tab dialogs (settings, model picker, provider manager, folder browser) always use fixed so the frame size stays constant and doesn't jump when switching tabs or content length; short confirmation dialogs keep auto. -
- - -

Toast

-

Unified information architecture: status icon + title + description. The status color appears only on the icon, avoiding large colored areas that create visual noise.

-
-
Toast
-
-
- -
Connected to server
The local daemon is responding normally; you can start a new chat.
-
-
- -
Context usage 82%
Consider running /compact to free up space.
-
-
-
- - -

Spinner

-

Loaders fall into two categories by scenario — do not mix them:

-
    -
  • Spinner (plain · SVG ring) —— the default loader. Used for button loading, app startup (GlobalLoading), and general inline waits — "everything else".
  • -
  • MoonSpinner (moon phase · brand signature) —— used only for the chat waiting state of "message sent, waiting for the Agent's first response" (the sending placeholder in ChatPane, SideChatPanel, ActivityNotice).
  • -
- -

Spinner · plain loader (default)

-
-
Spinner · common scenarios
-
-
- - Loading… - -
-
-
- -

MoonSpinner · moon phase (only "waiting for the Agent")

-
-
MoonSpinner · chat waiting state only signature
-
- Frame loop (8 frames) -
- 🌑🌒🌓🌔🌕🌖🌗🌘 -
- Usage · only while the chat waits for a response -
- 🌔Thinking… - 🌕Waiting for response… -
-
-
-
i
The moon phase is the sole exception to the emoji-as-icon rule, and is limited to the "waiting for the Agent's first response" scenario. It is currently implemented twice — in MoonSpinner.vue and ActivityNotice.vue — and should be merged into a single MoonSpinner component, sized via tokens and supporting prefers-reduced-motion. All other loading states use the plain Spinner.
- - -

Link

-

Inline text link: the default is the accent color with no underline; on hover it shows an underline and darkens. The .muted variant uses the secondary text color. Used for in-text jumps, external links, "view all", and other lightweight actions.

-
-
Link · inline link
-
-
- Read the full design token docs before building. - View on GitHub - View history -
-
-
- - -

Menu / Dropdown

-

Dropdown menu panel: raised surface + border + light shadow (--shadow-sm, flat-leaning). Menu items support icons, the current (active) state, the danger state, and the disabled state, with separators grouping items. On touch / mobile, use lg (≥44px row height) for menu items.

-
-
Menu · dropdown menu
-
-
-
Open file
-
Selected item
-
Disabled item
-
-
Delete chat
-
-
-
- - -

SegmentedControl

-

Mutually exclusive short option groups, commonly used for 2–4 option switches such as "light / dark / follow system". The current item is highlighted with a raised surface + subtle shadow.

-
-
SegmentedControl
-
-
- Light - Dark - Follow system -
-
-
- - -

Tabs

-

Tabs with a bottom hairline, used for grouping and switching sibling content. The current tab is marked with accent text + an accent underline.

-
-
Tabs
-
-
- General - Agent - Advanced -
-
-
- - -

Switch

-

A two-state switch for settings that take effect immediately. 36×20 track with full radius, 16px knob; when on, the track turns accent and the knob slides right, with the transition driven by tokens.

-
-
Switch
-
- - -
-
- - -

Checkbox

-

A 17×17 checkbox. When checked it fills with the accent color and shows a white tick (inline SVG). Often paired with a text label.

-
-
Checkbox
-
- - - -
-
- - -

Avatar

-

A 32px default avatar with md radius; .sm is 24px. Can hold an initial or an icon; falls back to this placeholder when there is no image.

-
-
Avatar
-
- K - - K -
-
- - -

EmptyState

-

A centered placeholder for empty lists / panels: a 48px faint icon + title + hint, avoiding blank pages.

-
-
EmptyState
-
-
- -
No chats yet
-
Click "New chat" to start a conversation with Kimi
-
-
-
- - -

Divider

-

A 1px horizontal divider (--p-line); .p-divider-v is the vertical divider, used between inline elements.

-
-
Divider
-
-
Content above
-
-
Content below
-
- kimi-k2 - - thinking -
-
-
- - -

Tooltip

-

A CSS-only hover hint, wrapped in .p-tip. Inverted background (--p-text / --p-bg), single line, no wrapping — carries only short notes.

-
-
Tooltip (hover the button)
-
- - - New chat - -
-
- - -

Banner

-

An inline notice bar placed at the top of a content area. Three states — .info / .warning / .danger — each with a matching 18px icon.

-
-
Banner
-
-
Connected to server
-
Currently in yolo mode; tool calls will run automatically
-
-
- - -

Sheet / BottomSheet

-

A mobile bottom slide-up panel: xl top radius + drag handle, xl shadow. At ≤640px, dialogs become bottom-anchored Sheets.

-
-
BottomSheet
-
-
-
-
Choose a model
-
kimi-k2 · thinking
-
kimi-k2 · instant
-
-
-
- - -

Skeleton

-

A placeholder for loading content, using a breathing opacity animation (no gradients), following the no-gradient-text rule. Composed into titles / text lines / avatars.

-
-
Skeleton
-
-
-
-
-
-
-
-
-
- - -

Command Bar

-

An inline combination of "primary action + command text + copy", sitting between a button and a code block — used for install / onboarding / one-click execution. The primary action reuses Button primary; the command area uses a mono light-grey background.

-
-
Command Bar
-
-
- - curl -fsSL https://code.kimi.com/install.sh | bash -
-
-
- - -

TopBar

-

The application top bar. Solid by default; the .frost variant is translucent + background blur, used only for sticky navigation bars, and is the sole exception to the no-glassmorphism rule (see §06).

-
-
TopBar · solid / frosted glass
-
-
- Solid TopBar - -
-
- Frosted-glass TopBar · .frost - -
-
-
- -

SectionLabel

-

A small group title for sidebar lists, used to section the content below (such as Workspaces in the sidebar). Spec: 13px / 700 / uppercase / letter-spacing .08em, color --color-fg-faint; left-aligned to the row's starting padding (--sb-pad-x), keeping the same indent as the group rows below. For scripts without case (such as Chinese), text-transform:uppercase simply has no effect — no special handling needed.

-
-
Sidebar · group title
-
- -
- - kimi-code-web -
-
- - playground -
-
-
-
- - -
-
- 04 -

Chat Interface Overhaul

-
-

- The message stream is the core of Kimi Web. The goal of the overhaul: have the 6 card types (Agent / Tool / Question / Approval / Swarm / Todo) - share one card skeleton, distinguished only by the head icon and semantic color; and collapse the Composer into a single rounded container. -

- -

Unified message stream

-
-
Conversation · 760px reading column
-
-
- - -
Please change the login endpoint to JWT and add the corresponding unit tests.
- - - 🌔Analyzing the auth module… - - -
-
- - - 3 tool calls - · completed · 0.8s - -
- -
- - - read_file - src/auth/session.ts - 0.2s - -
-
-
12 export function verify(token: string) {
13 return jwt.verify(token, getSecret());
14 }
-
- -
- - - read_file - src/auth/middleware.ts - 0.2s - -
- -
- - - grep - "jwt.verify" · 4 matches - 0.1s - -
-
- - -
-

I looked at the structure of src/auth; it is currently based on a session cookie. The scope of the change is below — once you confirm, I'll start.

-
- - -
-
- - A decision needs your confirmation -
-
How long should the JWT expiry be? Default 7 days, refresh token 30 days.
-
- - -
-
- - -
-
- - Write permission required - write_file -
-
About to modify src/auth/middleware.ts, 42 lines changed. Allow?
-
- - - -
-
- - -
-
Replace session with JWT signing
-
Refactor the auth middleware
-
Add unit tests
-
- -
-
-
- -

Tool calls: compact by default, grouped, expand on demand

-

High-frequency calls like read_file / bash / grep are "operational noise" — if each one took a full card, parallel triggers would quickly drown out the conversation. - The new strategy splits tool calls into three tiers by visual weight, pushing them as light as possible:

- -
-
Three visual-weight tiers
-
- ① Tool row · lightest (default) -
- - - read_file - src/auth/session.ts - 0.2s -
- ② Tool group · medium (consecutive / parallel auto-merged; collapsed to one line) -
-
- - - 3 tool calls - · completed · 0.8s - -
-
- ③ Decision card · heavy (only question / approval, needs user input) -
-
Write permission requiredwrite_file
-
About to modify src/auth/middleware.ts, 42 lines changed.
-
-
-
- -
    -
  • Tool calls render as compact rows by default (30px single-line mono + status dot + key argument); no head / body / shadow.
  • -
  • Consecutive or parallel calls auto-merge into one tool group; when collapsed, the whole group takes one line (N tool calls · status).
  • -
  • Clicking a row expands it in place to show details (code / output); click again to collapse — details don't grab attention by default.
  • -
  • Status is expressed with a colored dot: running (pulsing blue) / done (green) / failed (red), taking no extra space.
  • -
  • Only two types keep a full card: Question (needs an answer) and Approval (needs authorization) — they genuinely need the user's attention.
  • -
- -
-
Tool Call · compact row (expand on demand)
-
-
-
3 tool calls· completed
-
read_filesession.ts
-
12 export function verify(…
-
read_filemiddleware.ts
-
grep"jwt" · 4 hits
-
-
-
- -

Composer

-

Unified into a single rounded container: --radius-xl, with the whole border turning blue + a soft focus ring on focus. Toolbar controls all use the Pill / IconButton primitives, and the send button is a 32px circle.

-
-
Composer
-
-
-
Message Kimi, / to run a command, @ to reference a file…
-
-
- - yolo - plan -
-
- kimi-k2· thinking - -
-
-
-
-
-
i
- Site-wide consistency: the composer has only one radius (--radius-xl · 16px) and one height; toolbar controls all use the Pill / IconButton primitives, and the send button is a 32px circle — it no longer drifts with the theme. -
- -

Responsive

-

See §02 --p-bp-sm for the breakpoint. This section only gives mobile-adaptation pointers for the chat interface; a full mobile mockup is out of scope for this spec.

-
i
- At ≤640px: dialogs anchor to the bottom as Sheets (xl top radius, top drag handle), the sidebar collapses into an expandable drawer, the Composer toolbar is allowed to wrap, and the chat reading column drops its max-width to fill the screen. -
-
- - -
-
- 05 -

Theming

-
-

- Kimi Web uses one unified theme: the same components, fonts, radii, shadows, and surfaces — "reskinning" only changes colors. - Colors are collapsed into 4 seed tokens — two theme colors + one light surface + one dark surface; the neutrals and accent are derived from them, - and the semantic status colors (success / warning / danger) ship as independent palettes paired with the seeds, one set each for light / dark. -

- -

Color seeds

-

Day-to-day customization only needs these 4 seeds; the whole site's neutrals and accent change with them:

-
-
-
Theme color · primary
--accent-primary
-
Theme color · secondary
--accent-secondary
-
Light surface
--surface-light
-
Dark surface
--surface-dark
-
-
- -

Accent families

-

Within one theme, the theme color (accent) can switch among several color families. Two parallel families are provided today: blue (default, brand blue, carrying semantic emphasis) and black (neutral black, carrying the most restrained strong action). Both share the same components, fonts, radii, and surfaces — switching families only swaps the accent token set, with zero structural change; more families (green / purple, etc.) can be added later. The two cards below show the same primary button under the two families.

-
-
Family switch · same primary, different theme color
-
-
-
- Blue family · default -
accent
- --accent #1783ff · soft #e8f3ff -
-
- Black family · neutral -
accent
- --accent #14171c · soft #f1f2f4 -
-
-
-
- -

Theme console · change 4 colors, light & dark change together

-
-
Theme Console
-
-
- Primary #1783ff - Secondary #6b7280 - Light surface #ffffff - Dark surface #0d1117 -
-
-
- Light surface preview - - White background + accent button + neutral text -
-
- Dark surface preview - - Dark background + same accent + derived text -
-
-
-
- -

Light / dark mode

-

Driven by the two surfaces --surface-light / --surface-dark: whichever surface is current derives the corresponding foreground, border, shadow, and status colors. Switching light / dark simply swaps between these two sets of derived tokens, with zero structural change.

- -
- Benefits of one theme: components, fonts, radii, and surfaces are consistent site-wide; reskinning only changes 4 color seeds; light / dark mode works out of the box; semantic status colors are independently tunable. -
-
- - - -
-
- 06 -

Style Rules

-
-

- Anti-pattern rules that all UI code must follow. These rules are also the basis of the check-style detection script, one-to-one with a warning. -

- - - - - - - - - - - - - - -
Rule IDWhat it detectsAction
no-gradient-textgradient text / gradient backgroundForbidden
no-glassmorphismbackdrop-filter: blur (TopBar sticky nav bar is the sole exception)TopBar exempt
no-color-glowcolored / large-radius box-shadow glowForbidden
no-emoji-iconusing emoji as a functional icon (the moon phases 🌑…🌘 are the sole exception, and only in the "waiting for the Agent to respond" chat state; all other loading states use the plain Spinner)Moon phase exempt
no-hardcoded-hexunregistered hex color inside a component <style>Warning
no-hardcoded-fonthard-coded font-family in a component (e.g. 'Inter') instead of var(--font-ui)Warning
radius-from-scaleradius value not in {4,6,8,12,16,20,999}Warning
z-from-scalez-index using an unregistered large numberWarning
weight-from-scalefont-weight not in {400,500}Warning
- -

State matrix

-

Every interactive primitive should define the following states where applicable; missing ones are flagged by the style rules. focus-visible always uses --p-focus-ring (appears only on keyboard focus, see §08); disabled is uniformly opacity:.5.

- - - - - - - - - - - - - -
StateButtonInputCardMenu itemSwitch
default
hover
active / pressed
focus-visible
disabled
loading
selected / active
error
readonly
- -

Moon phase exemption

-
- The "🌑…🌘" moon-phase emoji are a brand signature of Kimi Web, used only in the chat state of "message sent, waiting for the Agent's first response", and are rendered uniformly by the MoonSpinner component; waiting states such as ActivityNotice reuse it rather than implementing their own moon phase. - It is the sole exception to the no-emoji-icon rule; all other loading states use the plain Spinner. -
- -

Glassmorphism exemption

-
- backdrop-filter: blur is banned site-wide, with the sole exception of the .frost variant of TopBar — and only in the one place of the "sticky navigation bar", used to stay readable over scrolling content. No other component (card, dialog, Toast, panel) may use glassmorphism; violations are flagged under no-glassmorphism. -
- - -
- - -
-
- 07 -

App Shell & Sidebar

-
-

- The structural spec for the app shell (three-column grid + right preview panel) and the left session sidebar. These are business-agnostic "skeletons" — - components, fonts, radii, and surfaces are reused from §02 / §03, but layout and alignment have their own conventions. -

- -

Layout grid

-

On desktop it is a single-row 5-track grid: the sidebar and the right panel each occupy a permanent track, with the conversation column in the middle; two 0-width tracks are for the ResizeHandles.

-
App.vue · .app
grid-template-columns: var(--side-w) 0 minmax(0, 1fr) 0 auto;
-/*           sidebar ↑      ↑handle  ↑conversation  ↑handle ↑right panel (auto) */
- - - - - - - - -
TokenValueUsage
--side-w248px (adjustable)left conversation column width, changed by dragging the ResizeHandle; should approach §02's --p-sidebar-w (264px)
--preview-w460pxwidth of the right preview panel when open
--panel-head-h48pxunified height for all right panel heads + the conversation column head, so the hairline runs as one line
--p-bp-sm640px≤640 switches to a mobile single column (top bar + conversation), no sidebar / handle / right panel
-
    -
  • The right panel track exists permanently, with its width transitioning between 0 ↔ var(--preview-w) (when open it squeezes the conversation column, rather than switching templates).
  • -
  • When the sidebar is collapsed, track 1 becomes a thin "rail" holding only an expand IconButton, avoiding crushing the conversation column head.
  • -
  • All grid children must have min-height:0; min-width:0, so only the inner scroll containers scroll and the page itself does not scroll.
  • -
- -

Sidebar alignment system (--sb-*)

-

All sidebar rows (group head, session row, New chat button) share 3 custom properties, so the "session title" aligns precisely under the "workspace name".

- - - - - - - -
TokenValueUsage
--sb-pad-x16pxrow horizontal padding
--sb-gutter20pxleading icon slot width (14px icon + 6px whitespace)
--sb-gap6pxgap between the icon slot and the text
-
i
- The session title's starting x = --sb-pad-x + --sb-gutter + --sb-gap. The group head has a folder icon and the session row has a status slot; both icons are the same width and position, so the titles align naturally. -
- -

Sidebar structure

-

The sidebar from top to bottom: brand header → search → New chat → grouped list (workspace head + session rows). Controls reuse the §03 primitives as much as possible.

- - - - - - - - - -
BlockUseNote
Brand headerlogo + name + IconButtoncollapse / settings use IconButton sm; the logo is animated (a blinking eye)
Searchbare search row (custom)no border, hover/focus shows a sunken background; icon + input + clear IconButton. Do not use Input (the 38px bordered version is too heavy)
New chatfull-width left-aligned button (custom)same rhythm as the session rows in the list (left-aligned, hover sunken). Do not use Button (centered, breaks the rhythm)
Section label.p-section-labeluppercase muted small titles like "Workspaces"
Workspace head / session rowsee next two sectionsshare --sb-* alignment
-
!
- Why New chat / search / inline rename don't use Button / Input: they are "list-style" controls (full-width, left-aligned, compact, borderless), while Button is centered and Input is a 38px bordered control — forcing them in would break the sidebar's visual density and alignment. This is an intentional custom exception, not an oversight. -
- -

Session row

-

A session row is an inset rounded pill, structured as: status slot → title → attention Badge → trailing action slot.

- - - - - - - - - - -
PartRule
Containermargin: 1px 6px; padding: 7px 10px; radius-md; hover = surface-sunken; active = accent-soft + inset 0 0 0 1px accent-bd
Status slot (lead)fixed --sb-gutter width; running = Spinner sm, otherwise unread = 7px accent dot
Titleflex:1 with truncation; double-click enters inline rename (compact input, not Input)
Attention BadgeBadge sm: info (needs answer) / warning (needs approval) / danger (aborted); always shown, not toggled on hover
Trailing action slot (trail)fixed slot shared by the time ↔ kebab (see below)
Archive confirmationreplaces the title area, Button sm (danger confirm / secondary cancel)
- -

Trailing action slot

-

The session row's rightmost is a fixed-width action slot: at rest it shows the relative time (mono xs, fg-faint); on hover it swaps in place to the kebab (IconButton sm).

-
    -
  • The time and kebab are stacked with display:inline-grid + grid-area:1/1 and swapped on hover via visibilitynever display:none.
  • -
  • The slot width is always max(time width, IconButton sm 26px), so the right region's total width is unchanged across hover — badges don't shift, the title's visible character count doesn't change, no jitter.
  • -
  • A session awaiting an answer (info Badge) likewise shows no time on hover: the slot holds only the kebab.
  • -
-
SessionRow.vue
.act { display: inline-grid; flex: none; align-items: center; justify-items: center; }
-.act .ts, .act .kebab { grid-area: 1 / 1; }
-.act .kebab { visibility: hidden; }
-.se:hover .act .kebab, .act:has(.kebab.open) .kebab { visibility: visible; }
-.se:hover .act .ts,   .act:has(.kebab.open) .ts   { visibility: hidden; }
-
i
- Why the time and kebab share a slot and use visibility rather than display:none: the time is variable-width (2h / 5m / just now …), the kebab a fixed 26px. If hover used display:none to drop the time and append the kebab, the right region would reflow — badges shift and the title's truncation changes, causing visible jitter. Sharing one slot and swapping via visibility keeps the slot width constant; this is a hard rule for sidebar list-row stability. -
- -

Workspace group

-

The group head and session rows share --sb-*: folder icon (open/closed) → name → path subtitle, with the kebab and "+" revealed on hover.

-
    -
  • The folder icon sits in the --sb-gutter slot, switching icons between open and closed states.
  • -
  • A small fg-muted path line sits below the name.
  • -
  • The kebab (menu) and "+" (new chat in this workspace) both use IconButton sm, shown on hover or keyboard focus (when not hovered they stay in the tab order via opacity:0, keeping them keyboard-reachable).
  • -
  • The group is collapsible; when collapsed its session list is hidden.
  • -
- -

Show more & collapse

-

The "load more / show less" control at the bottom of each workspace group is a session-row-shaped compact list control (same family as search, New chat, inline rename — not a Button). It doubles as the pagination trigger and the in-group expand / collapse toggle.

- - - - - - - - -
PartRule
Containersession-row pill: display:flex; gap:--sb-gap; min-height:26px, same padding as a session row, radius-md; hover = surface-sunken (no text recolor); :focus-visible uses --p-focus-ring
Lead slotempty, --sb-gutter wide, so the label's start x aligns with the session titles (--sb-pad-x + --sb-gutter + --sb-gap)
Labelfont-ui, text-xs, --color-text; flex:1, truncated
Behavior"Load more" fetches the next page and auto-expands; once more than the first page is loaded, "Show less" appears and collapses back to the first page (view-layer trim — data is kept, no refetch); "Show all" re-expands
- -

ResizeHandle

-

A 4px vertical drag bar, layered over the 1px column border (margin: 0 -2px makes the whole 4px grabbable), turning accent on hover / drag.

- - - - - - - - -
RuleValue
Width / cursor4px / col-resize
Normal / activetransparent / accent fill
Layer--z-sticky, over the column border
Behaviorpanel width follows the pointer 1:1 while dragging (the parent disables transitions to avoid lag); on release it is persisted to localStorage
- -

Right panel

-

The right panels (file preview / Diff / thinking / sub-agent / side chat) share one track and one head primitive.

-
    -
  • The panel head uses the PanelHeader primitive (48px = --panel-head-h), the same height as the conversation column head, so the hairline runs as one line.
  • -
  • Panel head: bold mono title + optional muted subtitle + middle slot (Badge / control / path) + close IconButton on the right.
  • -
  • When opened, the panel width goes from 0 → var(--preview-w), smoothly squeezing the conversation column.
  • -
  • At ≤640px the panel becomes a full-screen overlay (position:fixed; inset:0).
  • -
- -
i
- One-sentence principle: the sidebar / shell is a "list + grid" skeleton that reuses the §02 tokens and §03 primitives (Button / IconButton / Badge / Menu / Spinner / PanelHeader); compact list controls that don't fit a primitive (search, New chat, inline rename, show-more) keep their custom form, governed by this section. -
-
- - -
-
- 08 -

Accessibility (pragmatic edition)

-
-

- Kimi Web is a local developer tool; it does not target a specific WCAG conformance level, nor maintain a full screen-reader QA matrix. - This section collects only the rules that are "low-cost, don't hurt the look, and directly benefit keyboard-heavy users", as the baseline contract for each primitive; - the more expensive, lower-ROI parts (such as real-time announcement orchestration for streaming output) are not mandatory for now. -

- -
i
- On the "ugly" focus ring: the focus visibility required below always uses :focus-visible (not :focus). - It appears only on keyboard focus; mouse clicks don't trigger it, so it doesn't pollute the mouse-driven visual; the ring's strength is tuned uniformly with --p-focus-ring, not overridden per place. -
- -

1. Contrast & color

-
    -
  • Body text vs. background contrast ≥ 4.5:1; control borders, icons, and key graphics ≥ 3:1. When changing theme colors / dark mode, verify against §05 together.
  • -
  • Button text vs. button background, and form controls (input, placeholder, helper / error text) vs. their section background must all have contrast ≥ 4.5:1 (large text ≥ 3:1). White-on-white text, a transparent borderless button floating over the page background, and a light placeholder on a near-white background are all flagged by the style rules.
  • -
  • State is not conveyed by color alone. Error, selected, and disabled states also carry text, an icon, or a shape change (for example an error state is not just red, but also carries text or an icon).
  • -
- -

2. Keyboard operable

-

Anything doable with a mouse must also be doable with a keyboard; Tab order follows the DOM, with no invented skipping. Composite controls define their keyboard model per the table below; a missing model is treated as incomplete:

- - - - - - - - -
ControlKeyboard behavior
DialogTab cycles within the dialog (focus trap); Esc closes; focus returns to the trigger element after closing.
Menu / move the highlight, Enter selects, Esc closes.
Tabs / switch tabs (roving tabindex); only the current tab is in the Tab sequence.
Switch / Segmented / or Space / Enter to toggle.
- -

3. Focus visibility

-
    -
  • Every interactive element must have a visible focus indicator on keyboard focus, uniformly via :focus-visible + --p-focus-ring (primary actions may use --p-focus-ring-strong).
  • -
  • Bare outline: none is forbidden. To remove the default outline, you must provide an equivalent replacement style.
  • -
- -

4. Labels & semantics

-
    -
  • Semantic HTML first (button / a / input / dialog…); ARIA is added only when native semantics fall short.
  • -
  • Icon-only buttons must have an aria-labelIconButton already enforces this with a required label prop.
  • -
  • Dialog: role="dialog" + aria-modal="true", with the title as the dialog's accessible name.
  • -
  • Purely decorative SVG / icons get aria-hidden="true" to avoid being read out by screen readers.
  • -
- -

5. Target size

-

Desktop click targets ≥ 32px; touch devices ≥ 44px (consistent with the §01 principle and the IconButton lg tier).

- -

6. Reduced motion

-

Handled uniformly in the global styles per §02's @media (prefers-reduced-motion: reduce); components do not check this individually. The MoonSpinner moon phase pauses on the current frame.

- -

7. Live announcements (non-mandatory)

-

Screen-reader announcements are not a mandatory contract in this product. Short hints like Toast can use role="status" / aria-live; chat streaming output is currently not announced word-by-word, which is an acceptable trade-off, to be added later if a real need arises.

- -
- Explicitly not mandatory for now: a WCAG conformance-level claim, a complete ARIA pattern table, a per-screen-reader QA matrix, and real-time announcement orchestration for streaming output — these are not written into the primitive contract, to avoid becoming slogans no one maintains. -
-
- -
-
-
- - - - diff --git a/apps/kimi-web/scripts/check-style.mjs b/apps/kimi-web/scripts/check-style.mjs index 243481ddb..e626cfee0 100644 --- a/apps/kimi-web/scripts/check-style.mjs +++ b/apps/kimi-web/scripts/check-style.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node // check-style.mjs — design-system §06 anti-pattern guard for apps/kimi-web. // -// Scans src/** for the rules in design-system.html §06: +// Scans src/** for the rules in the design system (§06 of the DesignSystemView spec): // no-gradient-text, no-glassmorphism (.frost exempt), no-color-glow, // icon-from-registry (hand-written ; Icon/Spinner/MoonSpinner + the // 32x22 brand mark exempt), no-emoji-icon (moon in MoonSpinner exempt), @@ -41,6 +41,12 @@ const ICON_EXEMPT = new Set([ 'components/GlobalLoading.vue', ]); +// Files entirely exempt from the §06 scan. The design-system showcase view is +// documentation/demo CSS (forced-dark previews, syntax-highlighting palettes, +// illustrative mockups) rather than product UI, so the anti-pattern rules do not +// apply to it. +const FILE_EXEMPT = new Set(['views/DesignSystemView.vue']); + const RADIUS_SCALE = new Set([4, 6, 8, 12, 16, 20, 999]); const WEIGHT_OK = new Set([ '400', '500', @@ -96,6 +102,7 @@ function extractStyleBlocks(content) { function checkFile(abs) { const content = fs.readFileSync(abs, 'utf8'); const file = rel(abs); + if (FILE_EXEMPT.has(file)) return; const isCss = abs.endsWith('.css'); const blocks = isCss ? [{ text: content, baseLine: 1 }] : extractStyleBlocks(content); const domainExempt = DOMAIN_HEX_EXEMPT.has(file); diff --git a/apps/kimi-web/src/components/Sidebar.vue b/apps/kimi-web/src/components/Sidebar.vue index b89cf4d95..0a31ae5fc 100644 --- a/apps/kimi-web/src/components/Sidebar.vue +++ b/apps/kimi-web/src/components/Sidebar.vue @@ -3,7 +3,7 @@ The old workspace rail and workspace tabs have been removed; workspace switching, folding and renaming all live in the group header. --> @@ -763,24 +752,10 @@ onBeforeUnmount(() => { @select="onSelectSession" @close="showSearch = false" /> - -
-
-
- Design system - - - -
-