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. -
- -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. -
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. -
- ---<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.
---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.| Token | Light | Dark | Usage |
|---|---|---|---|
| --color-bg | #ffffff | #0d1117 | Page background |
| --color-surface | #fafbfc | #161b22 | Panel / sidebar / card head |
| --color-surface-raised | #ffffff | #1c2128 | Raised card / dialog / input |
| --color-text | #14171c | #e8eaed | Body text / headings |
| --color-text-muted | #6b7280 | #9aa0a8 | Secondary text / placeholder |
| --color-line | #e7eaee | #2d333b | Divider / card border |
| --color-accent | #1783ff | #58a6ff | Primary action / link / focus |
| --color-success | #0e7a38 | #3fb950 | Success / pass |
| --color-warning | #a9610a | #d29922 | Warning / pending |
| --color-danger | #c0392b | #f85149 | Danger / 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.
| Token | Light | Dark | Usage |
|---|---|---|---|
| --p-surface-raised | #ffffff | #1c2128 | Raised card / dialog / input (raised layer) |
| --p-surface | #fafbfc | #161b22 | Panel / sidebar / card head (default flat layer) |
| --p-surface-sunken | #f3f5f8 | #0d1117 | Code block / inline input / recessed area (sunken layer) |
| --p-bg | #ffffff | #0d1117 | Page 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.
| Token | Value | Usage |
|---|---|---|
| --p-focus-ring | 0 0 0 3px var(--p-accent-soft) | Default focus ring (link, menu item, switch, checkbox) |
| --p-focus-ring-strong | 0 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: -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: "JetBrains Mono Variable", "JetBrains Mono", - ui-monospace, "SF Mono", Menlo, Consolas, monospace;
Loading strategy
-| Font | Source | Bundled | Usage |
|---|---|---|---|
| JetBrains Mono | @fontsource-variable/jetbrains-mono | ✓ self-hosted | monospace / code (--font-mono) |
| Inter | @fontsource-variable/inter | ✓ self-hosted | optional: page titles / brand wordmark (--font-display) |
| System UI / CJK fonts | operating system | — | body / UI (--font-ui), not bundled |
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).
| Token | Value | Usage |
|---|---|---|
| --font-ui | -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC"… | UI & body (system fonts first) |
| --font-mono | JetBrains Mono… | code, tool names, line numbers, diffs |
| --leading-tight/normal/relaxed | 1.25 / 1.5 / 1.7 | headings / UI / long text |
| --weight-regular/medium | 400 / 500 | body / 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.
| Token | Value | Usage |
|---|---|---|
| --p-ic-sm | 14px | small button, badge, menu item, inline link icon |
| --p-ic-md | 16px | default (button, icon button, toolbar) |
| --p-ic-lg | 20px | Toast 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
-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.
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.
-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.
-| Token | Value | Usage | Merged from |
|---|---|---|---|
| --radius-xs | 4px | small badge, inline tag | 2/3/4px → |
| --radius-sm | 6px | small button, icon button, menu item | 5/6px → |
| --radius-md | 8px | button, input, badge, card | 7/8/9px → |
| --radius-lg | 12px | dropdown panel | 10/12px → |
| --radius-xl | 16px | dialog, bottom Sheet, Composer | 14/16px → |
| --radius-2xl | 20px | accent container / large panel | 20px |
| --radius-full | 999px | pill badge, avatar, send button | 999px / 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.
| Z-index Token | Value | Usage |
|---|---|---|
| --z-base | 0 | normal flow |
| --z-sticky | 100 | sticky header / sidebar |
| --z-dropdown | 200 | dropdown menu / tooltip |
| --z-overlay | 300 | overlay / bottom Sheet |
| --z-modal | 400 | dialog |
| --z-toast | 600 | toast |
| --z-max | 9999 | reserved: only this tier for extreme fallback |
Motion
-| Token | Value | Usage |
|---|---|---|
| --ease-out | cubic-bezier(0.16, 1, 0.3, 1) | enter, hover, expand |
| --ease-in-out | cubic-bezier(0.4, 0, 0.2, 1) | panel width, layout changes |
| --duration-fast | 120ms | press, focus |
| --duration-base | 160ms | hover, show/hide |
| --duration-slow | 260ms | dialog, Sheet, layout |
Reduced motion
-@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.
-| Token | Value | Usage |
|---|---|---|
| --p-sidebar-w | 264px | left session sidebar width |
| --p-content-max | 760px | chat reading-column max width |
| --p-content-wide | 920px | wide content (settings / panel) |
| --p-bp-sm | 640px | mobile / desktop boundary |
| --p-bp-md | 980px | narrow / wide screen boundary |
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.
-
Component selection guide
-| Scenario | Use |
|---|---|
| Primary action (submit / confirm) | Button variant=primary |
| Secondary action / cancel | Button secondary / ghost |
| Destructive action (delete / abort) | Button danger / danger-soft |
| Status marker | Badge |
| Toolbar filter / model switch | Pill |
| 2–4 mutually exclusive options | SegmentedControl |
| Top tabs | Tabs |
| Switch / multi-select | Switch / Checkbox |
| Floating content card / list action menu | Card / Menu |
| Inline notice / global toast | Banner / 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.
API
-<Button variant="primary" size="md" :loading="submitting">Save</Button> -// variant: primary | secondary | ghost | danger | danger-soft -// size: sm | md | lg-
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.
-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.
-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. -
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.-
-
- 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-tophairlines, 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).
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.
jwt.verify(token) to verify the signature, returning 401 on failure.import { verify } from './jwt';
-
-export function auth(token: string) {
- return verify(token, process.env.JWT_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.
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.
-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)
-MoonSpinner · moon phase (only "waiting for the Agent")
-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.
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.
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.
-Tabs
-Tabs with a bottom hairline, used for grouping and switching sibling content. The current tab is marked with accent text + an accent underline.
-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.
-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.
-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.
EmptyState
-A centered placeholder for empty lists / panels: a 48px faint icon + title + hint, avoiding blank pages.
-Divider
-A 1px horizontal divider (--p-line); .p-divider-v is the vertical divider, used between inline elements.
-
Tooltip
-A CSS-only hover hint, wrapped in .p-tip. Inverted background (--p-text / --p-bg), single line, no wrapping — carries only short notes.
Banner
-An inline notice bar placed at the top of a content area. Three states — .info / .warning / .danger — each with a matching 18px icon.
Sheet / BottomSheet
-A mobile bottom slide-up panel: xl top radius + drag handle, xl shadow. At ≤640px, dialogs become bottom-anchored Sheets.
-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.
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.
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).
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.
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
-13 return jwt.verify(token, getSecret());
14 }
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.
src/auth/middleware.ts, 42 lines changed. Allow?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:
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) andApproval(needs authorization) — they genuinely need the user's attention.
-
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.
--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.
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:
-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.
Theme console · change 4 colors, light & dark change together
-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.
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 ID | What it detects | Action |
|---|---|---|
| no-gradient-text | gradient text / gradient background | Forbidden |
| no-glassmorphism | backdrop-filter: blur (TopBar sticky nav bar is the sole exception) | TopBar exempt |
| no-color-glow | colored / large-radius box-shadow glow | Forbidden |
| no-emoji-icon | using 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-hex | unregistered hex color inside a component <style> | Warning |
| no-hardcoded-font | hard-coded font-family in a component (e.g. 'Inter') instead of var(--font-ui) | Warning |
| radius-from-scale | radius value not in {4,6,8,12,16,20,999} | Warning |
| z-from-scale | z-index using an unregistered large number | Warning |
| weight-from-scale | font-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.
| State | Button | Input | Card | Menu item | Switch |
|---|---|---|---|---|---|
| default | ✓ | ✓ | ✓ | ✓ | ✓ |
| hover | ✓ | ✓ | ✓ | ✓ | — |
| active / pressed | ✓ | — | — | — | — |
| focus-visible | ✓ | ✓ | — | — | ✓ |
| disabled | ✓ | ✓ | — | ✓ | — |
| loading | ✓ | — | — | — | — |
| selected / active | — | — | — | ✓ | ✓ |
| error | — | ✓ | — | — | — |
| readonly | — | ✓ | — | — | — |
Moon phase exemption
-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.
- 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.
-grid-template-columns: var(--side-w) 0 minmax(0, 1fr) 0 auto; -/* sidebar ↑ ↑handle ↑conversation ↑handle ↑right panel (auto) */
| Token | Value | Usage |
|---|---|---|
| --side-w | 248px (adjustable) | left conversation column width, changed by dragging the ResizeHandle; should approach §02's --p-sidebar-w (264px) |
| --preview-w | 460px | width of the right preview panel when open |
| --panel-head-h | 48px | unified height for all right panel heads + the conversation column head, so the hairline runs as one line |
| --p-bp-sm | 640px | ≤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".
-| Token | Value | Usage |
|---|---|---|
| --sb-pad-x | 16px | row horizontal padding |
| --sb-gutter | 20px | leading icon slot width (14px icon + 6px whitespace) |
| --sb-gap | 6px | gap between the icon slot and the text |
--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.
-| Block | Use | Note |
|---|---|---|
| Brand header | logo + name + IconButton | collapse / settings use IconButton sm; the logo is animated (a blinking eye) |
| Search | bare 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 chat | full-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-label | uppercase muted small titles like "Workspaces" |
| Workspace head / session row | see next two sections | share --sb-* alignment |
Session row
-A session row is an inset rounded pill, structured as: status slot → title → attention Badge → trailing action slot.
| Part | Rule |
|---|---|
| Container | margin: 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 |
| Title | flex:1 with truncation; double-click enters inline rename (compact input, not Input) |
| Attention Badge | Badge 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 confirmation | replaces 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/1and swapped on hover viavisibility— neverdisplay: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. -
.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; }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-gutterslot, switching icons between open and closed states.
- - A small
fg-mutedpath line sits below the name.
- - The kebab (menu) and "+" (new chat in this workspace) both use
IconButtonsm, shown on hover or keyboard focus (when not hovered they stay in the tab order viaopacity: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.
-| Part | Rule |
|---|---|
| Container | session-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 slot | empty, --sb-gutter wide, so the label's start x aligns with the session titles (--sb-pad-x + --sb-gutter + --sb-gap) |
| Label | font-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.
| Rule | Value |
|---|---|
| Width / cursor | 4px / col-resize |
| Normal / active | transparent / accent fill |
| Layer | --z-sticky, over the column border |
| Behavior | panel 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
PanelHeaderprimitive (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).
-
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. -
- -: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:
-| Control | Keyboard behavior |
|---|---|
| Dialog | Tab 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: noneis 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-label—IconButtonalready enforces this with a requiredlabelprop.
- - 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.