# ran — full documentation corpus # https://ran.chaxus.com • auto-generated at build time ================================================================ # https://ran.chaxus.com/design/guide ================================================================ # Ran Design A one-page checklist for anyone, person or coding agent, building the interface of a web product. Use it before you start, while you build, and before you hand a change back. It assumes the product already has tokens and components; it is about using them well. **MUST** marks a defect a user can observe, and nothing else is a MUST. **SHOULD** is a starting point, not a limit: depart from it whenever the work is better for it, and say why. Items marked **(checked)** are enforced by a test in this repository; the table at the end says which one. Everything else needs your eyes in a real browser. When `ranui` is installed, prefer its shipped guide at `docs/DESIGN.md#ranui-implementation-adapter` inside that package (`node_modules/ranui/docs/DESIGN.md#ranui-implementation-adapter`). Otherwise use the source [implementation adapter](https://github.com/chaxus/ran/blob/main/packages/ranui/docs/adapters/DESIGN.md). The adapter maps these items to ranui's tokens, stacking tiers and component specifics. ## Before you start Answer three questions before you change what a page shows; for a page or region you are building or restructuring, write the answers down: 1. What does the reader need to see on arrival? 2. What else must be visible for that to make sense? 3. What do they do next? A page has one primary model and one primary action per region. Choose the layout from the reader's question: compare → table, recognise and open → list, one object's state → sectioned detail, stages → step flow, what happened → timeline, continuous text → document. Put identity and status first, the primary action in the title region, and feedback beside the action that caused it. Count how hard each choice is, not how many clicks it takes: several steps that each need no thought beat one crowded screen where every option does. For a new page or region, compare two or three directions side by side in one place (a toggle, a canvas) before committing to one, and vary what is actually undecided: the layout, the interaction, the copy or the look. Name the surface's mode, because it decides how much expression the page can spend. A tool (dashboards, settings, editors) and a document (docs, articles, changelogs) spend it on precision and restraint, in patterns the reader already knows; a page that persuades (a landing or pricing page) can commit to a bold point of view, carried by one signature idea it will be remembered for rather than several effects competing for the same attention. The mode comes from the surface, not the product: a tool's landing page persuades, its documentation reads. ## The five-second test From Steve Krug's _Don't Make Me Think_: show the first screen to someone for five seconds, hide it, and ask what the page is, who it is for and what they would do next. If they cannot answer, the hierarchy has failed, whatever the details look like. - The page name is a single, visible heading on the first screen at 320px and at desktop width. **(checked)** - One thing leads. Squint: the screen should reduce to two or three masses of clearly different size. - The primary action is a real button or link on the first screen, named for what it does. ## The trunk test Also from Krug: drop a reader on any page, as if they had been carried there in a car boot. They MUST be able to answer, without scrolling: - **Which site is this?** A site identity that links home. **(checked)** - **Which page am I on?** One page heading, matching the link they followed. **(checked: one heading)** - **What are the main sections?** The primary navigation, in the header or one tap away in a drawer. **(checked)** - **Where am I in them?** The current section and page are marked, not only by colour. **(checked: `aria-current`)** - **How can I search?** Search is visible or one tap away where the site has it. **(checked)** ## Visual language - Emphasis is a budget: colour, weight, badges and elevation are rare, so they mean something. - Reach for spacing, alignment and weight before a border, fill or shadow. Every remaining boundary has a job you can name. - Spacing comes from a 4px scale, and a smaller gap means a closer relationship: 4px inside one control, 8px between related controls, 16px between groups, 24–32px between regions. Hidden content leaves no gap. **(checked: off-scale spacing)** - Colours come from tokens and work in both themes; state is never shown by colour alone. **(checked: raw colours and light-only fallbacks)** - MUST: text meets WCAG AA contrast (4.5:1, large text 3:1) on the real composition, in both themes. The first paint already follows the reader's theme; nothing flashes light before going dark. - Nested corners use **inner radius = outer radius − inset**. When that leaves 2px or less, give the inner shape its own radius, or remove the inner box. ![A card with radius 16, an inset panel 8px inside it with radius 8, and a button 4px inside that with radius 4, their corner arcs sharing one centre; beside it, an inner panel that keeps radius 16 makes the band swell from 8px on the edges to about 11px at the corner](./assets/radius-nesting.svg) - Name the alignment spines, and put headings, rows, actions and empty states on them. Follow the left edges of the text blocks down the page: two or three positions, reused on purpose. ![Three vertical alignment spines through a page: navigation, content and nested form labels, with headings, rows, actions and the empty state all sitting on their declared spine](./assets/alignment-spines.svg) - Use few type sizes, far apart. Body text keeps a measure of about 45–75 characters and a leading of at least 1.5; text a reader needs is never below 12px. **(checked)** - A heading has more space above it than below, so it binds to what it introduces. **(checked)** Headings wrap with `text-wrap: balance`, paragraphs with `text-wrap: pretty`. - The surfaces the browser draws still carry the design: text selection, the caret, focus rings, scrollbars and the numerals in tables (`tabular-nums`). Theme them from the tokens. - Mixed scripts: put the Latin face first and the CJK or Arabic fallback after it, mark each block's `lang`, and give Chinese, Japanese or Korean running text more leading than Latin (about 1.7–1.8). A Latin serif has no Han glyphs; pair it with a CJK serif. - Decoration is a decision, not a default. When an eyebrow label, a gradient, a glow or a card around everything appears, check that it does a job on this page; remove it if it only fills space. ## Layout - MUST: no horizontal page scroll at 320px; wide tables and code scroll inside their own box. **(checked)** - MUST: nothing shifts on load, on a status change, or when a late component registers. Reserve the box for anything that loads or appears later. **(checked: shift on load, Chromium)** - MUST: text, headings, values and navigation are readable on first paint, without script and without animation. **(checked on the home page)** - A component's box contains everything it paints. Rotated, fanned or offset parts stay inside it. - MUST: text and values are never clipped. If something must truncate, the full value stays reachable (a tooltip, an expanded row, the detail page). - Actions stay reachable without hover, and at every width. Anything clickable looks clickable at rest: touch has no hover to reveal it. ## Interaction - MUST: every action works with keyboard, touch and pointer. Focus is always visible, follows reading order, and returns to the trigger when an overlay closes. **(checked: visible focus on the first tab stops of key pages)** - MUST: touch and pointer targets are at least 24×24px, or spaced so a 24px circle around each does not touch its neighbours (WCAG 2.5.8). Links inside running text are exempt. **(checked)** - MUST: input is never lost. Drafts, focus, caret and reading position survive updates, failures and animation. Do not replace an input someone is typing in. - Accept input the way people type it (spaces in a phone number, a pasted value with a trailing space) rather than rejecting it, and ask only for what the task needs. - Validation sits on the field; one activation sends once; success appears only after it is confirmed; a failure keeps every entry and says how to recover. - Each async result is tied to the request that produced it, so a late answer never overwrites a newer one, and a background update never moves focus. - A modal is for a short confirmation or one answer the task cannot continue without, not a place to navigate; transient context belongs in a popover, peer views of one object in tabs, and a second task on its own page. - A modal dialog keeps focus inside it until it closes. Enter never submits while an input method is still composing text. - Selection looks different from hover. A link navigates; a button changes state. ## Motion ![Three position-over-time curves: entering decelerates over 250–400ms and settles on the target without passing it; leaving accelerates over 150–200ms; a third curve, marked as one to avoid, overshoots the target and comes back](./assets/motion-curves.svg) - Feedback (hover, focus, press) is 0–100ms. Entering is 250–400ms and decelerates without overshoot; leaving is 150–200ms and accelerates. Theme switches are not animated. - Controls and content settle without bounce or elastic easing; a page that persuades may spend one expressive moment. - Prefer `transform` and `opacity`; neighbours make room by transform (FLIP). Never animate layout (`height`, `padding`, `max-height`, `top`, `left`), which reflows the page every frame, and never use `transition: all`. - MUST: under reduced motion nothing travels or keeps running; at most a short fade. **(checked)** - Motion is interruptible and reverses cleanly. Watch it as a recording, not a still. - A sliding **selection / hover wash** (menus, tabs, sidebar rows) is one fill that moves, not a per-row background flash. Travel between rows may ease; the wash on the **already selected** row MUST be present on the first visible frame when a menu or panel opens — never fade in after the list appears. Hopping a shared panel between peers may morph the shell; settle the wash only after that shell's geometry is final, then snap (no opacity fade) onto the selected row. - First paint MUST already show chrome labels and control chrome that SSR can provide; an empty host that pops into place after upgrade is a flash, not an entrance. ## Words From Krug again: remove half the words, then half of what is left. - Make things self-evident before making them self-explanatory: name them the way readers already do, and follow conventions unless the new way is clearly better. A clever name costs a moment of thought every time it is read. - Name actions for what they do ("Send invite", not "Submit"). No welcome text or instructions that restate the layout. - Keep what adds a decision, constraint or consequence: accessible names, field labels, units, errors and risk disclosures. - Errors say what happened and what to do next, beside the thing that failed. - Don't fill space with invented content: sections, stats, icons or copy nobody asked for. An area that looks empty is a layout problem, not a content one. - Say the product's own thing. Generic marketing words (empower, seamless, next-generation) and dashes used for rhythm read as copy nobody wrote. - Sentence case. Try long text, large numbers and every locale the product ships. ## Verifying a change - Open the change in a real browser, in both themes, at 320px and at desktop width, and walk it with the keyboard. Exercise the states it touched: hover, focus, disabled, loading, empty, error. - Judge from screenshots, not from the code you wrote: you will read your intentions into the code. - Run the five-second and trunk tests on any page whose structure changed. - Report what you observed, what you only inferred and what you could not check. Do not call your own review acceptance. - Watching three people try the task finds the worst problems faster than any review. Fix the worst first, with the smallest change that removes it. - Rank findings so the reader fixes the right thing first: **P0** blocks use (lost input, unreadable text, an unreachable action), **P1** breaks a MUST, **P2** departs from a SHOULD without a reason, **P3** is polish. - Write each fix as values, not adjectives: "48px to `clamp(40px, 6vw, 72px)`", not "bigger". ## What the checks cover | Item | Check | | -------------------------------------------------------- | ----------------------------------------------------- | | Raw colours, light-only fallbacks, off-scale spacing | `verify:design` (static, configured packages) | | Measure, leading, text size, heading spacing | `packages/docs/test/browser/design-checklist.spec.ts` | | No horizontal scroll; navigation reachable; still motion | `packages/docs/test/browser/site-layout.spec.ts` | | Five-second heading, trunk test, targets, focus, shift | `packages/docs/test/browser/design-checklist.spec.ts` | | Readable without script or entrance motion | `packages/docs/test/browser/layout-audit.spec.ts` | ================================================================ # https://ran.chaxus.com/design/surface ================================================================ # Surface language How a product chooses **solid** versus **liquid glass**, independent of colour theme (light / dark / system). Use it with the [design checklist](./DESIGN.md); ranui's mapping lives in the implementation adapter (installed `ranui` → `docs/DESIGN.md#ranui-implementation-adapter`, or the source [packages/ranui/docs/adapters/DESIGN.md](https://github.com/chaxus/ran/blob/main/packages/ranui/docs/adapters/DESIGN.md)). **MUST** marks a defect a user can observe. **SHOULD** is a starting point. ## Two axes | Axis | Choices | Stamps | Switch | | ---------------- | ----------------------- | ------------------------------------------- | ----------------------------------- | | Colour theme | system / light / dark | `data-ran-theme` | `` / `setTheme` | | Surface language | solid (default) / glass | `data-ran-surface="glass"` (absent = solid) | `` / `setSurface` | - **MUST** Keep the axes orthogonal. Glass still follows light and dark; a colour theme never implies a surface, and glass never implies a palette. - **MUST** Solid is the default. Enable glass on purpose (`setSurface('glass')` or a host stamp). - **SHOULD** Persist the reader's surface choice when the document root owns it (ranui: `localStorage` key `ran-surface`). ## When glass earns its place Align with Apple's Liquid Glass (WWDC25 / HIG Materials): a **functional layer** that floats above a **content layer**. Glass is Regular chrome — navigation, side rails, transient panels, floating toolbars — not a second skin painted over every surface. - **SHOULD** Put glass on **chrome** that floats above content (header bar, sidebar rail, menus, search, toasts that are overlays). - **MUST** Keep **reading and editing content** solid enough to read: body copy, tables, cards, forms the reader fills for longer than a glance, long code. - **MUST** Do not stack glass on glass. One frosted plate per layer; controls on that plate use vibrancy fills, not a second `backdrop-filter`. - **SHOULD** Prefer Clear / Overlay glass only for short-lived layers (menus, search, sheets) that sit above Regular chrome — and only over media-rich content, with dimming when legibility needs it. Do not mix Regular and Clear on the same surface. A docs site, a console and a marketing page spend glass differently. A document surface stays quieter; a persuasive landing may let one glass moment carry the brand. ## Materials - **SHOULD** Give glass a quiet atmosphere behind it (mist, soft wash, product imagery). A flat white page makes frost look like a pale box. - **SHOULD** Use one shared recipe for fill, specular highlight, cool rim and soft depth so header, rail and overlays read as one language. - **MUST** Honour `prefers-reduced-transparency: reduce`: drop blur, use an elevated solid fallback, and keep the same layout. - **MUST** Text and controls on glass still meet WCAG AA on the real composition, in light and dark. ## Interactive state on glass Frosted plates wash out faint fills. State that was clear on Graphite can disappear on glass. - **MUST** Hover and selected remain distinguishable from each other and from rest. - **MUST** State is never colour alone (weight, rail or chip together with colour). - **SHOULD** Hover uses a quiet ink wash; selected uses a brighter frost lift plus weight on light glass (denser white vibrancy on dark), and a rail where the solid Graphite pattern already had one. Do not darken the selected chip with a rim or drop shadow — that reads as solid chrome on a frosted plate. - **SHOULD** Prefer library tokens (`--ran-surface-glass-hover` / `--ran-surface-glass-selected`) and the pilot defaults under document glass (`r-dropdown-item`, `r-select` trigger, `r-tabs` header fill). Hosts should not invent a second hover grey for menus — that is how agents drift from the surface. Overlay panels should read `--ran-surface-glass-overlay-*`, not a parallel host fill. - **SHOULD** Unify glass **menu chrome** (sliding wash cursor, weight for selected), not the host tag: language and other URL menus stay `
` + ``; theme / surface and other value menus stay `r-select`. - **MUST** When a glass menu opens, the wash already sits on the selected row in that first painted frame. Delayed highlight after open reads as broken selection, not motion. - **SHOULD** Reuse the same wash recipe everywhere a list cursor moves under glass: `r-select` lists, `r-menubar` shared viewport, `r-tabs` header fill, and host sidebar / nav row washes. One fill, full row width, weight (not a second fill) for selected. ## Host responsibilities Map Apple's control styles onto ranui pilots: | Role | Apple idea | ranui | | --------------------------- | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | Secondary / toolbar control | `glass` | Default `r-button`, under document glass or `surface="glass"` | | Emphasized action | `glassProminent` / solid fill | `type="primary"` and `type="warning"` stay **solid** | | Content surfaces | Standard materials | `r-card`, `r-input` stay **solid** under document glass; opt in with `surface="glass"` when the element itself is the plate | | Transient overlays | Clear / sheet glass | `r-modal`, non-toast `r-message`, `r-dropdown` (select menus) under document glass or `surface="glass"` | | Toast pills | Opaque emphasis | Stay **solid** | Opt out of document glass on a control with `surface="solid"`. Page chrome (docs headers, app shells) is the **host's** job: the library does not paint your site header. Reuse the same principles above; do not invent a second glass vocabulary. ## Five-second check 1. Can you tell solid from glass without reading the switch label? 2. Is content still readable, or did frost cover the article? 3. Are hover and current page obvious on the frosted chrome? 4. Does reduced-transparency still look intentional? 5. Do light and dark glass both keep contrast? ================================================================ # https://ran.chaxus.com/cn/agents/ ================================================================ --- title: Agents --- # Agents 一个 ran 插件提供三个 skill:design 指导产品设计,ranui 指导组件使用,ranuts 指导工具选择。 按眼前的决策选 skill: - **design** — 布局、交互、动效,或界面能否通过验收 - **ranui** — 用哪个 ``、它的属性,或主题令牌 - **ranuts** — 该 import 哪个工具,或问题落在哪个入口 [design](../design/) · [ranui](../ranui/) · [ranuts](../ranuts/) 从 GitHub 安装插件。 ## Codex ```sh codex plugin marketplace add chaxus/ran codex plugin add ran@ran ``` ## Claude Code ```sh claude plugin marketplace add chaxus/ran claude plugin install ran@ran ``` 新建会话后,Codex 使用 `$design`、`$ranui` 或 `$ranuts`;Claude Code 使用 `/ran:design`、`/ran:ranui` 或 `/ran:ranuts`。 想试用本地克隆的版本,就在仓库根目录执行市场命令,把 `chaxus/ran` 换成 `.`。 [plugins/ran](https://github.com/chaxus/ran/tree/main/plugins/ran) ================================================================ # https://ran.chaxus.com/cn/design/design-guides/ ================================================================ --- description: '在任意产品界面上应用设计方法:选定用途,让令牌给出取值;设计每个可达状态;检查实际渲染出来的效果。使用 时再看 ranui 适配。' --- # Design guidelines 设计规范 在真实产品界面上应用设计方法:先确定元素的用途和状态,再让设计令牌(token)给出具体取值。界面使用 `` 时,再通过 ranui 适配把这些用途映射到 ranui 的令牌与组件。 规则本身放在两个地方,本页只给链接,不再复述:[设计指南](/cn/design/guide)讲适用于任何产品的方法, [ranui 实现适配](https://github.com/chaxus/ran/blob/main/packages/ranui/docs/adapters/DESIGN.md) 讲 ranui 的令牌、层级和组件规则。两者都随包发布在 `node_modules/ranui/docs/DESIGN.md`。 [设计系统](/cn/design/design-system/)列出每个令牌及其取值, [主题系统](/cn/ranui/theme/)说明运行时覆盖, [信息架构](/cn/design/information-architecture/)负责页面结构。 > **适用场景**:为任意产品界面选择颜色、间距、排版、纵深或动效。该界面用 `` 搭建时,再看 ranui 适配。 ## 原则 1. **清晰优先于个性。** 先让主要任务和主操作一目了然。 2. **复用已有控件。** `r-button`、`r-input`、`r-select` 和 `r-modal` 已经具备表单、键盘和无障碍行为, 改样式即可,不要重新实现。 3. **按用途和状态决定。** “这段文字是什么?”有明确答案,“多大看起来合适?”没有。用途对应的令牌给出取值, 并且会跟着主题变化。 4. **设计每个可达状态,并亲眼看结果。** 默认态只是八种状态之一,而一层谁都看不见的投影,只有在浏览器里才会暴露。 这些只是起点。偏离某一条能让界面更好时可以偏离,但要说明理由。 ## 选颜色 [状态阶梯](/cn/design/design-system/#the-ladder)已经决定了悬停和按下的样子,你要做的只是确定用途。 | 这个元素是…… | 用 | | ------------------- | -------------------------------------------------------------- | | 页面或表面的背景 | `--ran-color-bg` / `-bg-subtle` / `-bg-elevated` / `-bg-muted` | | 指针悬停 / 正在按下 | `--ran-color-bg-hover` / `-bg-active` | | 文字 | `--ran-color-text` / `-text-secondary` / `-text-disabled` | | 边框 | `--ran-color-border` / `-hover` / `-active` | | 页面主操作 | `--ran-color-primary`(其上文字用 `--ran-color-primary-text`) | | 状态 | `--ran-color-success` / `-warning` / `-danger` | | 链接 | `--ran-color-link` | **每种强调色只表达一种含义。** 主色是单色,蓝色因此留给链接和聚焦环;绿色表示成功,琥珀色表示警告, 红色表示危险。拿红色做强调,以后就没有颜色能表达“危险”了。 裸 hex 值、不随主题切换的兜底值,或者兜底里引用了不存在的令牌,都会在暗色模式下悄悄出错, 或者让整条声明失效。适配文档的[颜色规则](https://github.com/chaxus/ran/blob/main/packages/ranui/docs/DESIGN.md#ranui-color) 逐条解释了原因,`verify:design` 会在 ranui 自身源码里拦下这些写法。 ## 间距与节奏 间距从 [4px 尺度](/cn/design/design-system/#spacing)里取,并让距离表达含义: - 组内元素之间用 **8px**。 - 组与组之间用 **16px**。 - 区域之间用 **24–32px**。 间距越小,关系越近,所以只要有一处间距不在尺度上,节奏就乱了。每段间距只交给一处负责: 在两个元素之间再加一段空白前,先把子元素的 margin 和父元素的 padding 算进去。 ## 选排版 先问这段文字承担什么**角色**(标题、标签、正文、按钮、等宽),字体、字号、字重和行高都由 [排版尺度](/cn/design/design-system/#typography)决定。真正只出现一次的文字,比如视频上的手势提示, 单独给它一个组件令牌,比硬套最接近的角色更合适。 ## 纵深:投影与层级 按元素的性质选投影层级:文档流里的表面、浮层,还是会挡住操作的对话框。浮层如果退回卡片层级的投影, 看上去就像钉在页面上。 **在自己的页面框架里嵌入 ranui 浮层。** [z-index 阶梯](/cn/design/design-system/#stacking)从 1000 起, 所以 portal 出去的浮层自己就能越过普通的页面骨架。但留在自身 shadow DOM 里的 `position: fixed` 浮层 (`r-modal` 的对话框)最多只能逃到最近的祖先**层叠上下文**。如果你用会创建层叠上下文的东西 (`isolation`、`opacity < 1`、`transform`、`filter`、`will-change`)包住嵌入内容,只在浮层真正打开时 才抬高这个包裹层: ```css .embed { isolation: isolate; /* cheap: no z-index of its own, so nothing is promoted */ } /* Escalate only while a real overlay is open — never "just in case" */ .embed:has(r-modal[open]), .embed:has(r-modal[closing]) { position: relative; z-index: 100; } ``` 给包裹层常驻一个 `z-index`,会让里面所有内容(包括静态内容)在整个滚动过程中都压在吸顶页头上面; 本站就曾经这样上线过。除了 `open` 也要匹配 `closing`:遮罩在关闭过渡期间还在绘制。 ## 动效 {#motion} 高频反馈(悬停、聚焦、按下)即时响应。用动效解释变化时,反馈控制在 0–100ms;进入用 250–400ms, 减速收尾;离开用 150–200ms,逐渐加速。动效要能被打断,并尊重 `prefers-reduced-motion`。 **绝不要让颜色属性参与过渡。** CSS 分不清颜色**为什么**变了,所以加在 `background-color`、`color`、 `border-color`、`box-shadow`、`fill` 或 `stroke` 上的 `transition`,在**主题**切换时同样会触发: 页面已经切换完毕,各个元素却还按自己的节奏慢慢淡变。改用 `transform` 和 `opacity` 做动画。 `transition: all` 和只写时长的 `transition: 0.2s` 同样包含颜色属性。 ## 状态与文案 每个可达状态都是设计的一部分:悬停、激活、聚焦、禁用、加载、空、错误。悬停对应 `bg-hover` / `border-hover`,激活对应 `bg-active`,聚焦对应聚焦环。不能点击的东西不要看起来能点:`r-card` 只有加上 `hoverable` 才响应悬停。 文案同样是系统的一部分: - **按钮**说明操作结果,上下文没交代对象时把对象写上,比如“删除成员”。对象已经明确的对话框可以只写“删除”; 避免含糊的“确定”。 - **错误**先说发生了什么,再说怎么解决。✅“构建失败:产物超过体积上限。请精简产物或调高上限。” ❌“操作失败,请重试。” - **确认提示**说明发生了什么变化,而不是只报成功,并且要等变化确认完成后再出现。✅“项目已删除” ❌“操作成功”。 - 让上下文消除冗余:标题已经是“删除项目”的对话框,按钮不需要再写“永久删除该项目,无法恢复”。 ## 无障碍 {#accessibility} - 文字与背景的对比度满足 **WCAG AA**。 - **绝不只用颜色表达状态**,要配上图标、标签或文字。 - 所有可交互元素都保留**可见的聚焦环**(`--ran-focus-ring`,或 `outline: 2px solid var(--ran-color-focus); outline-offset: 2px`)。 - **一切都能用键盘操作。** - 尊重 `prefers-reduced-motion` 和 `prefers-color-scheme`。 ## 鼠标与触摸、窄屏与宽屏 输入方式和视口都不分主次。 - 拖拽、滑块和手势使用 **Pointer Events**,并在真正的拖拽面上设置 `touch-action: none`。 设置了 `touch-action: none` 却没有对应的指针处理,就是一个坏掉的控件。 - **只在悬停时出现的功能需要点按兜底。** `r-select` 或 `r-popover` 的 `trigger="hover"` 在触摸设备上 会变成点击,你自己写的组件也应该这样。 - **优先使用相对视口的尺寸**(`min(560px, calc(100vw - 32px))`、`clamp()`),不要新造断点:ranui 没有断点令牌,每个硬断点都是一个得有人维护的数字。 - **绝不在移动端藏起完成某件事的唯一入口。** 改为重排。 - **量出来的位置只在下一次重排之前有效。** 基于 `getBoundingClientRect()` 算出的结果,在窗口缩放、 容器重排时都会失效,portal 出去的面板在滚动时也会失效。直接在窄宽度下加载页面,测不到**从宽缩到窄**的过程, 而问题恰恰出在这里。 ## 组件库机器校验了哪些 `pnpm -F ranui verify:design` 在 ranui 自身源码里检查九条规则:不随主题切换的颜色兜底、裸颜色字面量、 间距和尺寸尺度、只支持鼠标的拖拽、会让 `hidden` 失效的 `:host` 规则、引用未声明令牌的兜底、 组件在自己的 shadow 树里查找元素,以及在构造函数之外构建 shadow 树。有意违反规则的那一行要用 `design-ok: ` 注明。这些检查只覆盖配置好的源码目录;使用 ranui 的应用要运行自己的检查, 并亲自查看自己的页面。 ## 设计图解 {#design-examples} ## 证据与验收 {#evidence-and-acceptance} 设计指南里的[验证一次改动](/cn/design/guide#验证一次改动)说明了在真实浏览器里检查什么、报告什么: 哪些是观察到的,哪些只是推断,哪些没能检查。agent 的审查不等于用户验收。 ================================================================ # https://ran.chaxus.com/cn/design/design-system/ ================================================================ --- description: 'ranui 的设计语言与完整令牌参考,覆盖全部全局 `--ran-*` 令牌:Geist 颜色阶梯(含明暗两套值)、语义令牌、间距、尺寸、排版、圆角、投影、层级、动效、聚焦与皮肤基元。' --- # Design system 设计系统 ranui 构建于其上的**设计语言**,以及表达这套语言的**完整**令牌清单:组件库声明的每一个全局 `--ran-*` 自定义属性,都附上它在明暗两套主题下的取值。组件读的是这些令牌而不是写死的数值,所以覆盖 一个令牌就能改变所有用到它的地方。 四个页面回答四个不同的问题,刻意拆开: | 页面 | 回答 | | ------------------------------------------------ | ------------------------ | | **设计系统**(本页) | 令牌**是什么**,即词汇表 | | [设计规范](/cn/design/design-guides/) | 做界面时**如何取舍** | | [信息架构](/cn/design/information-architecture/) | 页面本身该是**什么形状** | | [主题系统](/cn/ranui/theme/) | 运行时**如何切换与覆盖** | > **适用场景**:需要查某个令牌的名字或取值(颜色角色、间距档位、图标尺寸、投影层级、缓动曲线), > 或想理解这些阶梯为什么长这样。 ## 设计语言:Geist ranui 的令牌基于 [Geist](https://vercel.com/geist),也就是 Vercel 的开源设计体系。每条色阶都是 一条由固定职责组成的阶梯,每一档对应一个职责,而不是供人随意挑选的深浅色块:200 不是「稍深一点 的灰」,它就是「悬停背景」。每一档的职责一旦固定,给某个交互状态选颜色就是查表,不用临场判断。 ranui 把这套阶梯落成 `--ran-*` 色阶,在其上叠加语义令牌,并以 **Geist Sans / Geist Mono** 作为 默认字体。 ## 两层令牌 {#two-layers} **第一层,基础色板**:下面这些原始色阶,很少直接消费。 **第二层,语义令牌**:`--ran-color-*` 等,映射到第一层。**请使用这一层。** 暗色模式重定义第一 层,语义令牌里只改那几个无法跟随单一色阶档位的(见本页末尾),其余语义令牌都通过 `var()` 自动翻转, 整个组件库不存在任何组件级的暗色覆盖。 ``` --ran-gray-1000 → #111113(浅色) / #ededef(暗色) ← 第一层,会翻转 --ran-color-text → var(--ran-gray-1000) ← 第二层,跟随 --ran-btn-color → var(--ran-color-text, …) ← 组件令牌 ``` 这条链条就是整个架构:改基础档位则全局传导,改语义令牌则改一个角色,改组件令牌则只改一个元素。 ## 颜色 ### 状态阶梯 {#the-ladder} 每条色阶从 `100` 走到 `1000`,每一档职责固定: | 档位 | 职责 | 档位 | 职责 | | ---- | ---------------- | ---- | --------------------- | | 100 | 默认背景 | 600 | 激活边框 | | 200 | 悬停背景 | 700 | 实心填充(按钮/徽标) | | 300 | 激活(按下)背景 | 800 | 实心填充(悬停) | | 400 | 默认边框 | 900 | 次要文字与图标 | | 500 | 悬停边框 | 1000 | 主要文字与图标 | ### 背景 | 令牌 | 浅色 | 暗色 | 用于 | | ---------------------- | --------------------------------------------------------------- | --------------------------------------------------------------- | ------------ | | `--ran-background-100` | `#ffffff` | `#0a0a0b` | 页面背景 | | `--ran-background-200` | `#f7f7f8` | `#111113` | 轻微区分区域 | ### 灰阶 —— `--ran-gray-100..1000` 文字、边框与表面背后的色阶。 | 档位 | 浅色 | 暗色 | | ---- | --------------------------------------------------------------- | --------------------------------------------------------------- | | 100 | `#f0f0f2` | `#1c1c20` | | 200 | `#ebebee` | `#232328` | | 300 | `#e2e2e6` | `#2c2c32` | | 400 | `#e4e4e8` | `#26262b` | | 500 | `#b8b8c0` | `#3f3f46` | | 600 | `#9d9da6` | `#52525b` | | 700 | `#8a8a94` | `#71717a` | | 800 | `#6e6e78` | `#8b8b94` | | 900 | `#55555e` | `#a1a1aa` | | 1000 | `#111113` | `#ededef` | ### 半透明灰 —— `--ran-gray-alpha-100..1000` 半透明,可以叠在任意表面上。遮罩、悬停蒙层,或者必须压在未知内容之上的分隔线,都该用它。 | 档位 | 浅色 | 暗色 | | ---- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | 100 | `#0000000d` | `#ffffff12` | | 200 | `#00000015` | `#ffffff17` | | 300 | `#0000001a` | `#ffffff21` | | 400 | `#00000014` | `#ffffff24` | | 500 | `#00000036` | `#ffffff3d` | | 600 | `#0000003d` | `#ffffff82` | | 700 | `#00000070` | `#ffffff8a` | | 800 | `#00000082` | `#ffffff78` | | 900 | `#000000b3` | `#ffffff9c` | | 1000 | `#000000e8` | `#ffffffeb` | ### 蓝 —— `--ran-blue-100..1000` 只保留给链接与聚焦环。 | 档位 | 浅色 | 暗色 | | ---- | --------------------------------------------------------------- | --------------------------------------------------------------- | | 100 | `#f0f7ff` | `#06193a` | | 200 | `#e9f4ff` | `#022248` | | 300 | `#dfefff` | `#002f62` | | 400 | `#cae7ff` | `#003674` | | 500 | `#94ccff` | `#00418b` | | 600 | `#48aeff` | `#0090ff` | | 700 | `#0a6cff` | `#3b82f6` | | 800 | `#0a5bd8` | `#005be7` | | 900 | `#005ff2` | `#47a8ff` | | 1000 | `#002359` | `#eaf6ff` | ### 红 —— `--ran-red-100..1000` 危险与错误。 | 档位 | 浅色 | 暗色 | | ---- | --------------------------------------------------------------- | --------------------------------------------------------------- | | 100 | `#ffeeef` | `#330a11` | | 200 | `#ffe8ea` | `#440d13` | | 300 | `#ffe3e4` | `#5d0e17` | | 400 | `#ffd7d6` | `#6f101b` | | 500 | `#ffb1b3` | `#88151f` | | 600 | `#ff676d` | `#f32e40` | | 700 | `#d70f2f` | `#d70f2f` | | 800 | `#ea001d` | `#e2162a` | | 900 | `#d8001b` | `#ff565f` | | 1000 | `#47000c` | `#ffe9ed` | ### 琥珀 —— `--ran-amber-100..1000` 警告。 | 档位 | 浅色 | 暗色 | | ---- | --------------------------------------------------------------- | --------------------------------------------------------------- | | 100 | `#fff6de` | `#2a1700` | | 200 | `#fff4cf` | `#361900` | | 300 | `#fff1c1` | `#502800` | | 400 | `#ffdc73` | `#5b3000` | | 500 | `#ffc543` | `#703e00` | | 600 | `#ffa600` | `#ed9a00` | | 700 | `#ffae00` | `#ffae00` | | 800 | `#ff9300` | `#ff9300` | | 900 | `#aa4d00` | `#ff9300` | | 1000 | `#561900` | `#fff3d5` | ### 绿 —— `--ran-green-100..1000` 成功。 | 档位 | 浅色 | 暗色 | | ---- | --------------------------------------------------------------- | --------------------------------------------------------------- | | 100 | `#ecfdec` | `#002608` | | 200 | `#e5fce7` | `#00320b` | | 300 | `#d3fad1` | `#003a0e` | | 400 | `#b9f5bc` | `#004615` | | 500 | `#82eb8d` | `#006717` | | 600 | `#4ce15e` | `#00952d` | | 700 | `#28a948` | `#00ac3a` | | 800 | `#279141` | `#009432` | | 900 | `#107d32` | `#00ca50` | | 1000 | `#003a00` | `#d8ffe4` | ### 语义颜色令牌 组件真正读取的那一层。除了下文提到的几个字面量,这里的一切都通过上面的色阶解析,因此会自己跟着主题翻转。 | 令牌 | 解析到 | 职责 | | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | | `--ran-color-bg` | `--ran-background-100` | 页面背景 | | `--ran-color-bg-subtle` | `--ran-background-200` | 轻微区分的区域 | | `--ran-color-bg-elevated` | `--ran-background-100` · 暗色 `#18181b` | 卡片、表面 | | `--ran-color-bg-muted` | `--ran-gray-100` | 内凹 / 弱化填充 | | `--ran-color-bg-hover` | `--ran-gray-200` | 悬停表面 | | `--ran-color-bg-active` | `--ran-gray-300` | 激活(按下)表面 | | `--ran-color-text` | `--ran-gray-1000` | 主要文字 | | `--ran-color-text-secondary` | `--ran-gray-900` | 次要文字 | | `--ran-color-text-disabled` | `--ran-gray-700` | 禁用文字 | | `--ran-color-border` | `--ran-gray-400` | 默认边框 | | `--ran-color-border-secondary` | `--ran-gray-300` | 更弱的边框 | | `--ran-color-border-hover` | `--ran-gray-500` | 悬停边框 | | `--ran-color-border-active` | `--ran-gray-600` | 激活边框 | | `--ran-color-border-control` | `--ran-gray-700` | 控件边框:输入框、复选框、选择器(3:1) | | `--ran-color-border-control-hover` | `--ran-gray-900` | 控件边框(悬停) | | `--ran-color-text-placeholder` | `--ran-gray-800` | 占位文字(≥4.5:1) | | `--ran-color-primary` | `--ran-gray-1000` | 主操作(无彩色) | | `--ran-color-primary-hover` | `#2a2a2e` · 暗色 `#d4d4d8` | 主操作悬停 | | `--ran-color-primary-active` | `#3f3f46` · 暗色 `#b4b4bb` | 主操作按下 | | `--ran-color-primary-text` | `--ran-background-100` | 主操作表面**之上**的文字 | | `--ran-color-success` | `--ran-green-700` | 成功 | | `--ran-color-warning` | `--ran-amber-700` | 警告 | | `--ran-color-danger` | `--ran-red-700` | 危险 / 错误 | | `--ran-color-warning-foreground` | `--ran-amber-900` | 警告文字 | | `--ran-color-danger-foreground` | `--ran-red-900` | 危险 / 错误文字 | | `--ran-color-link` | `--ran-blue-800` · 暗色 `#5b9dff` | 链接 | | `--ran-color-focus` | `--ran-blue-700` | 焦点色:焦点环与焦点描边 | `--ran-color-primary-hover` / `-active` 在两套主题下都是字面量:它们是朝页面背景方向走的,而不 是沿着某条色阶走,所以暗色模式直接重定义了它们。暗色模式还把 `--ran-color-bg-elevated` 和 `--ran-color-link` 设成了字面量,因为在暗色下它们都对应不上某一个色阶档位。 `--ran-media-background` 在两种主题下均为 `#000`。视频画布需要保持深色,以承托固定白色的播放控件;Player 的 `--ran-player-background` 派生自此令牌。这个媒体角色不会随页面配色反转。 backdrop 系列颜色用于叠在其他内容上方的表面,比如玻璃。这类表面上的文字是否清晰,取决于下面的内容,与页面主题无关,所以暗色模式下同样不重新定义:`--ran-backdrop-ink-dark`(`#fff`)和 `--ran-backdrop-ink-light`(`#18181b`)分别是深色和浅色背景上的文字颜色,`--ran-backdrop-highlight`、`--ran-backdrop-edge-light` 和 `--ran-backdrop-shadow-light` 负责边缘的高光和轮廓。 ### 每个色彩语义只有一个含义 - **主操作是无彩色的**:浅色下黑底白字,暗色下白底黑字(Geist 的品牌调性, ``)。其上的文字与图标用 `--ran-color-primary-text`,会一起翻转。 这里没有单独的「contrast」令牌:主操作**本身**就是对比度最高的那一个。 - **蓝色是保留色**,只用于链接(`--ran-color-link`)与聚焦环,不是备选的主色。 - **绿色=成功 · 琥珀=警告 · 红色=危险**,一色一义。 不存在 `--ran-color-error`,危险色叫 `--ran-color-danger`。`var()` 引用一个从未声明过的属性会解析 为「空」,整条声明会被丢弃,而且不会有任何报错,所以名字宁可对着表查,也别猜。 ## 间距 {#spacing} 元素之间的距离:`padding`、`margin`、`gap`。以 4px 为基数,**只有九档**: | 令牌 | 值 | 令牌 | 值 | | --------------- | ---- | ---------------- | ---- | | `--ran-space-1` | 4px | `--ran-space-8` | 32px | | `--ran-space-2` | 8px | `--ran-space-10` | 40px | | `--ran-space-3` | 12px | `--ran-space-16` | 64px | | `--ran-space-4` | 16px | `--ran-space-24` | 96px | | `--ran-space-6` | 24px | | | 数字是 4px 的倍数,所以档位是跳着的,没有 `--ran-space-5`。这正是重点:档位有限,页面才有节奏。 ## 尺寸 元素自身的尺寸:图标大小、控件高度、小的方形/矩形控件。 | 令牌 | 值 | 典型用途 | | -------------- | ---- | ------------------------ | | `--ran-size-1` | 16px | 多选框方块、小号内联图标 | | `--ran-size-2` | 18px | — | | `--ran-size-3` | 20px | 控件内部的图标 | | `--ran-size-4` | 24px | 工具栏图标按钮 | | `--ran-size-5` | 28px | 紧凑控件高度 | | `--ran-size-6` | 30px | — | | `--ran-size-7` | 32px | 默认控件高度 | **这是刻意与间距分开的另一条尺度**,混用会被机器校验拦下(`sizing-scale` 规则)。两者的取值范围和 递进方式不同:4px 翻倍式的间距尺度用在图标和控件尺寸上会得出别扭的数值;而且使用者必须能在不动另 一个的前提下单独调整其中一个:图标变大,不应该顺带把每一个恰好同值的间隙也撑开。某一档在数值上与 间距档位重合(`--ran-size-4` 和 `--ran-space-6` 都是 24px)只是巧合,不是别名。 真正一次性、没有别的组件共享的尺寸(比如某个菜单的 `min-width`),就保持为带自己字面量兜底的组件 令牌,不要硬塞进某一档。 ## 排版 {#typography} | 令牌 | 值 | | ------------------- | ------------------------------------------------------------ | | `--ran-font-family` | Geist / Geist Sans,其后是系统 UI 字体栈 | | `--ran-font-mono` | Geist Mono,其后是 `ui-monospace`、SF Mono、Menlo、Consolas… | | `--ran-font-size` | `14px`,基准字号 | | `--ran-line-height` | `1.5715` | 排版按**角色**组织,角色一旦确定,字体、字号、字重、行高就一起定了: | 角色 | 用于 | 字重令牌 | 字号令牌 | | ----------- | ------------ | ------------------------------------------------------------------------------- | ------------------------------------------ | | **heading** | 标题 | `--ran-text-heading-weight`(600) | `--ran-text-heading-1..4`(32/24/20/16px) | | **label** | 单行、可扫读 | `--ran-text-label-weight`(500) | `--ran-text-label-1..3`(14/13/12px) | | **copy** | 多行正文 | `--ran-text-copy-weight`(400) | `--ran-text-copy-1..2`(16/14px) | | **button** | 按钮文字 | `--ran-text-button-weight`(500) | `--ran-text-button-size`(14px) | | **mono** | 代码、数据 | `--ran-text-mono-weight-regular`(400)/ `--ran-text-mono-weight-medium`(500) | 复用 label / copy 档位 | 另有两个令牌只是为了让角色落地正确: | 令牌 | 值 | 原因 | | ------------------------------- | --------- | -------------------------------- | | `--ran-text-heading-tracking` | `-0.03em` | 大字号标题需要更紧的字距。 | | `--ran-text-button-line-height` | `1` | 定高控件内的文字才能锐利地居中。 | Geist 的字重最高只到 600(semibold),强调靠字号和留白,而不是更粗的字重。没有 `--ran-text-copy-3`:12px 那一档叫 `--ran-text-label-3`。 ### 字体 ranui 自托管这两套字体(可变字重 100–900,SIL OFL 1.1 许可),一次引入即可,不依赖 CDN: ```js import 'ranui/fonts'; // 打包器 ``` ```html ``` 不引入也能正常工作,只是回退到系统字体栈。 ## 圆角 | 令牌 | 值 | 用于 | | ------------------- | -------- | ---------------------------- | | `--ran-radius-xs` | `4px` | 小标记:标签、徽标 | | `--ran-radius-sm` | `8px` | 控件(按钮、输入框、选择器) | | `--ran-radius-md` | `12px` | 卡片、对话框 | | `--ran-radius-lg` | `16px` | 大面积表面 | | `--ran-radius-xl` | `24px` | 页面级外框 | | `--ran-radius-full` | `9999px` | 胶囊、头像 | ## 投影 投影是**角色**,不是装饰,按元素「是什么」来选层级。暗色模式会把三档全部替换,因为为白色页面调过 的投影放到黑色页面上就看不见了。 | 令牌 | 用于 | 浅色 | 暗色 | | ----------------------- | ------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | `--ran-shadow-elevated` | 文档流内、同时带边框的表面,如 `r-card`、`r-section` | `0 1px 2px rgba(0,0,0,.04)` | `inset 0 1px 0 rgba(255,255,255,.06), 0 1px 2px rgba(0,0,0,.16)` | | `--ran-shadow-menu` | 浮在内容之上的临时层,如下拉、选择面板、气泡卡片、toast | `0 1px 2px rgba(0,0,0,.06), 0 12px 32px -8px rgba(0,0,0,.16)` | `0 1px 1px rgba(0,0,0,.2), 0 4px 8px -4px rgba(0,0,0,.4), 0 16px 24px -8px rgba(0,0,0,.5)` | | `--ran-shadow-modal` | 阻塞式对话框,如 `r-modal` | `0 2px 4px rgba(0,0,0,.06), 0 24px 64px -12px rgba(0,0,0,.24)` | `0 1px 1px rgba(0,0,0,.2), 0 8px 16px -4px rgba(0,0,0,.4), 0 24px 32px -8px rgba(0,0,0,.5)` | 无边框的浮层只靠投影与页面拉开距离,所以浮层层级的投影必须有真实重量;浮层若回退到「抬起」层级, 看起来就像贴在页面上。 ## 层级 {#stacking} 浮层会 portal 到 ``,因此需要明确的层级: | 令牌 | 默认值 | 用于 | | ------------------ | ------ | ------------------------------------------------------------------------ | | `--ran-z-modal` | `1000` | 阻塞式对话框及其遮罩 | | `--ran-z-dropdown` | `1100` | 下拉 / 选择面板 / 气泡卡片,**高于** modal,弹窗内的选择面板才不会被盖住 | | `--ran-z-message` | `1200` | toast 与通知,永远在最上层 | 阶梯从 1000 起,是为了越过常规页面骨架(导航栏、遮罩通常在几十的量级)。可以在 `:root` 上整体覆盖, 也可以按组件覆盖(`--ran-dropdown-host-z-index`、`--ran-modal-root-z-index`、 `--ran-message-z-index`),但不要用 `!important`。 ## 动效 | 令牌 | 值 | 用于 | | ---------------------------- | ------- | ---------------------------------------------------- | | `--ran-motion-duration-fast` | `0.14s` | 短暂的透明度渐变和图标切换,以及减弱动效时的淡入淡出 | | `--ran-motion-duration-base` | `0.2s` | 气泡、菜单 | | `--ran-motion-duration-slow` | `0.28s` | 较大的展开 | | 缓动令牌 | 曲线 | 性格 | | ---------------------------- | -------------------------------- | ------------------------------------------------------------------------------ | | `--ran-motion-ease-standard` | `cubic-bezier(0.2,0,0,1)` | 减速曲线,通用 | | `--ran-motion-ease-snappy` | `cubic-bezier(0.33,0,0.15,1)` | 干脆、无回弹,用于开关等小状态 | | `--ran-motion-ease-spring` | `cubic-bezier(0.3,1.1,0.4,1)` | 轻微回弹,只留给宿主在需要打动人的页面上自选的一个表现性时刻,组件默认从不使用 | | `--ran-motion-ease-bouncy` | `cubic-bezier(0.34,1.56,0.64,1)` | 明显回弹,同样由宿主按需选用,没有组件使用它 | | `--ran-motion-ease-smooth` | `cubic-bezier(0.4,0,0.2,1)` | 平缓、无回弹,用于展开与布局 | spring 这一族是把调好的 SwiftUI 弹簧参数(response / damping)折算成的单次回弹贝塞尔曲线。 **只把它们用在运动属性上**,即 `transform`、`opacity` 和盒模型几何属性。调色属性 (`background-color`、`color`、`border-color`、`box-shadow`、`fill`、`stroke`)刻意不带默认过渡: CSS 分不清「交互」和「主题翻转」,你给颜色加的淡入淡出,在明暗切换时同样会触发。每个组件仍然保留 `--ran-*-transition` 钩子,需要时可以自行开启。 ## 聚焦 | 令牌 | 值 | 用于 | | -------------------------------- | ----------------------------------------------------------------------- | -------------------------------------- | | `--ran-focus-ring` | `0 0 0 2px var(--ran-background-100), 0 0 0 4px var(--ran-color-focus)` | 标准聚焦环,形式是 `box-shadow` | | `--ran-focus-ring-inverse-color` | `#fff` | **两套主题下都是深色**的表面上的环颜色 | 聚焦环是双层的(内层用背景色,外层用 `--ran-color-focus`,即蓝色),所以在任何表面上都清晰可见;而且它保持蓝色,不跟随已经 变成无彩色的主操作色。 `--ran-focus-ring-inverse-color` **刻意没有在暗色模式里重定义**:它是给那种「无论页面主题如何、自身 表面始终是深色」的组件用的(`r-player` 覆盖在任意视频之上的控制条),而那种表面并不随页面主题变化。 ## 皮肤基元 组件共享的、既不属于颜色也不属于尺寸和排版的少数结构性取值。刻意保持精简:这一层以前大得多, 大部分已经随主题包一起移除了。 | 令牌 | 值 | 用于 | | ------------------------------- | ---------------------------- | --------------------------------------------------------------------- | | `--ran-skin-border-width` | `1px` | 组件绘制的边框宽度 | | `--ran-skin-border-style` | `solid` | 组件绘制的边框样式 | | `--ran-skin-border-image-width` | `4px` | `border-image-slice` 的内缩,button/checkbox/input/modal/message 共享 | | `--ran-skin-raised-shadow` | `var(--ran-shadow-elevated)` | 抬起表面的投影,做一层间接以便皮肤替换 | | `--ran-skin-font-family` | `var(--ran-font-family)` | 组件使用的字体族,同样做了一层间接 | ## 表面语言 Orthogonal to light/dark. Default is **solid** Graphite. **Glass** is explicit (`data-ran-surface="glass"` or `surface="glass"` on a pilot control). Optics lean on [`r-glass`](/cn/ranui/glass/) thin/regular without pulling WebGL into every control. See [Theming](/cn/ranui/theme/). | Token | Light | Use for | | -------------------------------------------- | ------------------------------------ | ------------------------------------------------------------- | | `--ran-surface-glass-blur` | `12px` | Control-scale frost blur | | `--ran-surface-glass-saturate` | `180%` | Frost saturation (iOS vibrancy) | | `--ran-surface-glass-brightness` | `1.08` | Frost brightness | | `--ran-surface-glass-fill` | `rgba(255, 255, 255, 0.55)` | Control tint (dark: `rgba(26, 26, 26, 0.55)`) | | `--ran-surface-glass-specular` | top-lit wash + crown glow | Specular layer over the tint | | `--ran-surface-glass-border` | `rgba(255, 255, 255, 0.72)` | Control frost border (dark: `rgba(166, 166, 166, 0.55)`) | | `--ran-surface-glass-shadow` | lift + bright edge + soft inner glow | Control frost elevation | | `--ran-surface-glass-overlay-blur` | `18px` | Modal/message frost blur | | `--ran-surface-glass-overlay-fill` | `rgba(255, 255, 255, 0.48)` | Overlay tint (dark: `rgba(26, 26, 26, 0.62)`) | | `--ran-surface-glass-overlay-specular` | stronger top-lit wash | Overlay specular layer | | `--ran-surface-glass-overlay-border` | `rgba(255, 255, 255, 0.72)` | Overlay frost border (dark: `rgba(166, 166, 166, 0.55)`) | | `--ran-surface-glass-overlay-shadow` | deeper lift + bright edge | Overlay frost elevation | | `--ran-surface-glass-solid-fallback` | `var(--ran-color-bg-elevated)` | Control fill under `prefers-reduced-transparency` | | `--ran-surface-glass-solid-shadow` | `var(--ran-shadow-elevated)` | Control shadow under reduced transparency | | `--ran-surface-glass-overlay-solid-fallback` | `var(--ran-color-bg-elevated)` | Overlay fill under reduced transparency | | `--ran-surface-glass-overlay-solid-shadow` | `var(--ran-shadow-modal)` | Overlay shadow under reduced transparency | | `--ran-surface-glass-hover` | ink wash 8% | Interactive wash on frosted chrome / overlay rows (dark: 18%) | | `--ran-surface-glass-selected` | light wash 58% | Selected chrome on frost (dark: ink wash 28%) | ## 暗色模式重定义了哪些 `` 上的 `data-ran-theme="dark"`(也可以只作用于某棵子树,见[主题系统](/cn/ranui/theme/)) **只重定义基础色板**,外加四处无法通过色阶解析的例外: - 第一层的全部:gray、gray-alpha、blue、red、amber、green 的每一档,以及两个背景; - `--ran-color-bg-elevated`,暗色下是字面量 `#18181b`,这样卡片才能从黑色页面上浮起来而不是融进去; - `--ran-color-link`,暗色下是字面量 `#5b9dff`,让链接在深色页面上保持足够的对比度; - `--ran-color-primary-hover` / `-active`,它们是字面量而不是色阶引用; - 三档投影,为深色底重新调过。 其余的一切(所有其他语义令牌、所有尺寸、所有时长)都只定义一次。 ## 组件令牌 语义层之下,每个组件还暴露自己的钩子,命名为: ``` --ran-{component}-{element}[-{state}]-{property} ``` 例如 `--ran-btn-hover-background`、`--ran-select-search-active-border-width`。它们默认指向语义令牌, 例如 `var(--ran-btn-background, var(--ran-color-primary, #111113))`,所以覆盖语义令牌能一次影响 全部,覆盖组件令牌则只改一个元素。 完整清单见仓库中的 [style-tokens-public.md](https://github.com/chaxus/ran/blob/main/packages/ranui/docs/style-tokens-public.md), 逐元素接口见[元素 API](/cn/ranui/api)。怎么覆盖见[主题系统](/cn/ranui/theme/#customizing-tokens)。 ## 在自己的 CSS 里使用令牌 {#using-tokens-in-your-own-css} ```css .panel { background: var(--ran-color-bg-elevated); color: var(--ran-color-text); border: var(--ran-skin-border-width) var(--ran-skin-border-style) var(--ran-color-border); border-radius: var(--ran-radius-md); padding: var(--ran-space-4); box-shadow: var(--ran-shadow-elevated); } ``` 三条规则保证它暗色安全: 1. 该跟随主题的值,**不要写死 hex**。 2. **兜底值必须是会翻转的令牌**,写 `var(--ran-color-text, var(--ran-gray-1000))`,不要写 `var(--ran-color-text, #111113)`。 3. **兜底值引用的令牌必须存在**,否则整条声明被丢弃,元素静默沿用继承来的样式。 > 组件库声明的每一个全局令牌都在本页列出;新增令牌若没有在这里记录,单元测试会失败。组件级令牌另行 > 生成,见 > [style-tokens-public.md](https://github.com/chaxus/ran/blob/main/packages/ranui/docs/style-tokens-public.md)。 ================================================================ # https://ran.chaxus.com/cn/design/ ================================================================ --- title: Ran Design --- # Ran Design 布局、交互、动效与证据化验收的方法 — 不依赖特定组件库。ranui 的令牌与适配器把这些方法落到具体取值。 - [设计检查清单](/cn/design/guide) — 一页验收指南 - [表面语言](/cn/design/surface) — 实心与液态玻璃,与颜色主题正交 - [设计系统](/cn/design/design-system/) — 令牌词汇表(ranui 的 `--ran-*`) - [设计规范](/cn/design/design-guides/) — 做界面时如何取舍令牌 - [信息架构](/cn/design/information-architecture/) — 页面该取什么形状 [ranui](../ranui/) · [ranuts](../ranuts/) · [Agents](../agents/) ================================================================ # https://ran.chaxus.com/cn/design/information-architecture/ ================================================================ --- description: '在挑第一个令牌之前,先把高密度页面的形状定下来:定义页面的三个问题、回答主问题的表达骨架,以及每一类信息该放在哪里。' --- # Information architecture 信息架构 页面该长成什么**形状**。这件事要在挑颜色、定间距之前决定。 这一节的其他几页回答的是零件的问题。本页回答的是排在它们前面的那个问题:这块屏幕要装的东西 这么多,用户到底是来做什么的,什么样的排布能让他做成。 | 页面 | 回答 | | ------------------------------------- | ------------------------------ | | **信息架构**(本页) | 页面该是**什么形状** | | [设计系统](/cn/design/design-system/) | 令牌**是什么**:词汇表 | | [设计规范](/cn/design/design-guides/) | 做界面时**怎么在它们之间取舍** | | [主题系统](/cn/ranui/theme/) | 运行时**怎么切换和覆盖** | > **适用场景**:要做一块同时装着多个对象、多种状态以及它们之间关系的屏幕,比如控制台、仪表盘、 > 后台、工作台、监控页。落地页和单一转化的表单不算,那类页面的成败取决于说服力,而不是用户能不能 > 在密集信息里判断准确。 数据齐了不等于页面设计好了。接口返回的字段一个不落,筛选、状态标签、批量操作也都在,用户照样不 知道该先看哪儿。缺的不是信息,是顺序。 ## 动组件之前先回答三个问题 {#three-questions} 1. **用户打开页面,最该先看到的是哪一件事?** 这是页面的主信息。 2. **为了看懂它,还必须同时看到什么?** 关联资源、关联模型、上下文。 3. **看完之后他要做什么?** 作判断、执行操作,或者接着往下想。 这三个问题要在打开组件清单之前答完。三个答案清楚的页面很少选错形状;跳过它们的页面,最后都是照 着接口返回的结构长出来的。 **一个页面只有一个主模型。** 辅助模型可以帮着理解和操作主模型,但不能来抢首屏。 ## 形状由任务决定,不由返回值决定 {#shape} 大部分形状不对的页面,都出自这两个顺手的选择: - 接口返回了数组,于是做成表格。 - 路由里带了一个 ID,于是做成详情页。 这两件事都不构成理由。同一个对象在不同任务下是不同的形状:查找时 issue 是一个**集合**,处理时 是一条**状态流**,协作时是一条**讨论线索**,审计时是一串**事件序列**。记录里有日期,只能说明数 据里有日期,不能说明这一页就该是日历。 ## 怎么选表达骨架 {#skeletons} 选那个能让用户最少转几道弯就回答出主问题的排布。 | 用户眼前要回答的问题 | 必须放在一起的信息 | 表达骨架 | | -------------------------------- | -------------------------------------- | --------------- | | 这些之间差在哪儿? | 参与比较的字段,列的位置固定 | 二维对照表 | | 是哪一个,我要点进去 | 名称、标识、状态 | 列表 / 资源目录 | | 是哪一个,看图就认得出来 | 图放在最显眼处,名称和字段围着它排 | 卡片网格 | | 这个对象是什么,现在什么状态? | 身份、状态、主操作,然后才是属性 | 分区详情 | | 它属于哪里? | 路径、父级、同级 | 层级树 | | 谁依赖它,改了会影响到谁? | 上下游、影响范围 | 关系列表 | | 我走到第几步了,接下来是什么? | 阶段、当前输入、后面的步骤 | 分步向导 | | 每一条在哪个阶段,推动它就是干活 | 列代表阶段,卡片带身份和阻塞点 | 看板 | | 为什么卡住了? | 阶段概要,再到单步结果,再到原始日志 | 追踪下钻 | | 现在健康吗,影响面有多大? | 对象名、状态,以及把状态改掉的那个事件 | 状态墙 | | 发生了什么,先后顺序,谁干的? | 时间、操作者、事件类型 | 事件时间线 | | 谁提了什么,别人怎么回的? | 作者、发言、回复结构 | 讨论线程 | | 改之前和改之后差在哪儿? | 两个版本并排 | 并排比较 | | 趋势怎么样,异常在哪儿? | 指标、它的比较基准、进明细的入口 | 仪表盘 | | 这段时间被谁占着,会不会撞车? | 开始、结束、时长放在同一根轴上 | 日历排期 | | 我接下来该处理哪一条? | 一侧是队列,另一侧是这一条的详情 | 主从工作台 | | 哪些规则生效,会影响到什么? | 配置项、生效范围、造成的后果 | 配置表单 | | 这段正文说了什么? | 正文按顺序读,旁边给一份大纲 | 连续文档 | | 它在哪儿? | 位置、边界、分布 | 地图与画布 | ### 最容易选反的几对 {#swapped-pairs} - **时间线还是分步向导。** 时间线讲已经发生过什么,按先后排;分步向导讲你现在在哪一步、下一步是 什么。两者长得像,指向的时间方向却相反。 - **看板还是筛选。** 只有当拖动卡片本身就是那个操作时,看板才成立。列如果只是几组存下来的筛选条 件,那就是做了个要拖一下才能用的筛选器。 - **日历还是时间线。** 日历回答的是占用和冲突,时间线回答的是先后。记录里的日期不负责在这两者之 间做选择,问题才负责。 - **卡片网格还是表格。** 图要么是识别的锚点,要么不是。如果选择是靠比数字做出来的,把预览缩成第 一列的小图,反而压掉了真正决定结果的字段。 - **关系图还是关系列表。** 只有当路径本身或影响怎么扩散就是判断依据时,才值得画图。否则一份分好 组的上下游列表读起来更快。 - **正文还是字段格子。** 需要顺着读下来的文字就让它保持文字。把每一段都切成卡片或者键值对,正好 毁掉了它原本好读的地方。 不要因为做得出来就把三种视图都做一遍。每多一种视图,就多一套筛选、多一份状态映射、多一组要同步 维护的操作。等到第二种用法确实高频了再加,而不是为了以防万一先加上。 ## 每一类信息该放在哪里 {#placement} | 信息 | 回答的问题 | 该放在 | 不该落到 | | -------- | ------------------ | -------------------------------------------- | -------------------------------- | | **身份** | 这是什么? | 标题、对象概要 | 表格最后一列,或者某个标签页后面 | | **状态** | 现在怎么样? | 标题区或概要区 | 只能在详情字段里翻到 | | **属性** | 它是什么样的? | 详情正文,按人理解的方式分组 | 照着接口字段顺序摊平 | | **关联** | 它和谁有关系? | 自己的区块或标签页,归属、依赖、引用分开表达 | 混进属性表里 | | **变更** | 和之前差在哪儿? | 差异区块、时间线 | 只显示改完之后的值 | | **证据** | 凭什么这么判断? | 紧挨着这个判断,可以展开 | 另开一个日志页 | | **操作** | 现在我能做什么? | 主操作在标题区,其余的挨着各自作用的对象 | 塞进「更多」里 | | **反馈** | 刚才那下做成了吗? | 紧挨着操作,保住任务上下文 | 一个和上下文断开的全局提示 | **每条信息只有一个权威位置。** 别处只放摘要或入口,并且指回那个权威位置。 ## 阅读顺序 {#reading-order} ```text 页面身份 → 当前状态或异常 → 主任务和主操作 → 判断所需的信息 → 关联、变更、证据 → 次要信息和低频操作 ``` - **一页只有一个视觉上的主标题。** 分节标题靠语义推进,不要用字号假装层级。 - **一个任务区里最多一个主操作。** 主按钮代表最可能、价值最高的下一步,不是最危险的那一步。危险 操作用危险语义表达,不该长期占着最重的视觉分量。 - 警告色和危险色留给真正需要用户注意的状态。整页都是绿的,信号就已经花掉了。 - 徽标、标签、横幅共用同一份注意力预算。只强调那些会改变判断和动作的信息。 ## 信息密度 {#density} 密度不是单位面积里塞了多少控件,而是用户一眼能拿走多少**可用来判断**的信息。把间距收紧只提高了视 觉密度,有效密度原地不动;删掉不相关的字段、把需要对比的东西摆到一起,才是真的提高。 | 档位 | 适用场景 | 换来什么 | | -------- | -------------------------------- | -------------------------------------------- | | **宽松** | 首次使用、低频配置、高风险确认 | 解释的空间、更大的分组间距、看得见的影响预览 | | **标准** | 大多数列表、详情和表单 | 可扫读性和每屏信息量之间的默认平衡 | | **紧凑** | 高频专家工作台:监控、运维、审计 | 稳定的列宽、短文案、键盘效率、可保存的视图 | - 一个页面里**最多用相邻的两档**。标准页面里嵌一张紧凑的表格没问题,每个区块各发明一套标尺不行。 - 紧凑不等于「字号和点击区域一起缩小」。容器留白和行高可以收紧,正文的可读性、看得见的焦点圈和指 针目标尺寸不能。 - 对专家用户来说,列管理、保存视图、批量操作和快捷键,都比一次显示更多内容更有用。 ## 怎么维持任务上下文 {#context} 骨架定下来之后,再决定辅助内容放在哪儿: | 用户正在… | 就给他 | | ------------------------------------ | ----------------------------------------------------- | | 在对象或证据之间反复切换 | 主从分栏:一侧队列,一侧当前这条 | | 瞥一眼很轻、看完就走的内容 | 可展开行(`r-disclosure-row`)或气泡卡(`r-popover`) | | 处理可以分享出去、或者需要宽度的内容 | 一条独立路由 | | 做一次确认,或者填一个字段 | 弹窗(`r-modal`) | **弹窗不是一层导航。** 只要需要可复制的链接、浏览历史、并排比较,或者刷新之后工作还得在,就给它一 条路由。 ## 这一层 ranui 能给你什么 {#with-ranui} ranui 在页面形状上是刻意不表态的:它提供基础件和令牌,不提供页面模板。这一层它能给的是: - `r-section` 划出骨架的各个内容带,`r-card` 承载真正独立的重复条目。卡片里不要再套卡片;表单字 段用小标题或分隔线分组就够了。 - `r-tabs` 表达**同一个对象的平级视图**(它的讨论、它的检查、它的差异),不要拿来放互不相干的模 块,那是导航的事。 - `r-disclosure-row` 做渐进展开,`r-popover` 和 `r-dropdown` 承载临时上下文,`r-modal` 只做上面 那张表允许它做的事。 - `r-state-dot` 表达状态,并且永远带上文字标签:[不能只靠颜色](/cn/design/design-guides/#accessibility)。 - 首屏加载时用 `r-skeleton`,长到会让人犯嘀咕的操作配 `r-progress`,结果用 `r-message` 告诉他。 ranui **没有表格、树、日历、看板和时间线**。自己实现时,用[令牌](/cn/design/design-system/)和 [设计规范](/cn/design/design-guides/)去搭,不要另起一套视觉体系:间距取自标尺,字体按角色定, 颜色来自语义令牌,每一个可达状态都要设计到。 ## 典型做坏的方式 {#anti-patterns} - 接口返回的每个字段都变成一行详情,身份、状态、关联和证据以同样的分量一起涌出来。 - 用卡片、颜色和装饰性间距堆出层次感,却没说清该先看哪儿、要据此作出什么判断。 - 几个操作同时用主按钮样式,或者把一个低频操作摆进标题区。 - 状态、属性、关联和变更混进同一张表,结果哪儿都比不出来。 - 用户只是想查个名字和状态,却给了他一张关系图。 - 用弹窗装一条长流程、一次比较,或者别人一定会想发链接出去的内容。 - 同一句话在标题、概要、标签页和表格里各出现一遍,哪一次都没带来新信息。 ## 页面上线前的检查清单 - [ ] 主信息、关联信息、用户的下一步动作,三样都写下来了。 - [ ] 表达骨架是从问题选出来的,不是从返回值的形状推出来的。 - [ ] 页面只有一个主模型,辅助视图服务于它而不是和它抢。 - [ ] 不看需求文档,五秒内能说出这一页的对象、当前状态和主操作。 - [ ] 比较发生在同一个地方,不需要跨标签页或跨页面记住某个值。 - [ ] 每条信息只有一个权威位置,别处都指回去。 - [ ] 密度与使用频次匹配,且没有出现超过相邻两档的情况。 - [ ] 长文本、大数字和窄屏都不会把信息顺序打乱。 - [ ] 对判断没有帮助的区块、字段、标签和按钮,都删掉了。 ================================================================ # https://ran.chaxus.com/cn/examples/chat-composer ================================================================ --- title: 聊天输入区 description: 带 sticky 底部输入区的会话:文本输入、附件条、语音按钮与上下文计量。 --- # 聊天输入区 聊天页分为可滚动会话区与 sticky 输入区。[`r-conversation`](../ranui/conversation/) 负责会话(贴底跟随、行 reconciliation 与 `footer` 插槽);输入区是你放进插槽的普通 markup。 ## 布局骨架
Send
流式行与发送逻辑在脚本中接入;此演示仅展示结构。 ## 组成 | 部分 | 作用 | | -------------------------------------------- | ------------------------------------------------- | | [`r-conversation`](../ranui/conversation/) | 渲染事件日志;`footer` 保持在键盘安全区之上 | | [`r-markdown`](../ranui/markdown/) | 助手正文常用行(流式阶段用 `mode="streaming"`) | | [`r-input`](../ranui/input/) 或 ` Send ``` ```css .composer { display: flex; flex-direction: column; gap: 12px; width: 100%; } .composer-row { display: flex; gap: 8px; align-items: flex-end; } #input { flex: 1; min-height: 2.5rem; resize: vertical; } ``` 按需导入元素: ```js import 'ranui/conversation'; import 'ranui/markdown'; import 'ranui/attachments'; import 'ranui/voice-button'; import 'ranui/token-meter'; import 'ranui/button'; ``` ## 2. 注册 conversation view 在第一次 `push` 前注册全部 view。最简的用户/助手 markdown 行: ```js const chat = document.getElementById('chat'); chat.register({ kind: 'message', match: (e) => e.type === 'message/start' ? { id: e.id, role: 'start' } : e.type === 'message/delta' ? { id: e.id, role: 'update' } : null, start: (e) => ({ role: e.role, text: '' }), update: (state, e) => ({ ...state, text: state.text + e.text }), publication: (e) => (e.type === 'message/delta' ? 'animation-frame' : 'immediate'), mount: () => document.createElement('r-markdown'), patch: (el, node) => { el.content = node.state.text; }, }); function pushMessage(role, text) { const id = crypto.randomUUID(); chat.push({ type: 'message/start', id, role }); if (text) chat.push({ type: 'message/delta', id, text }); } ``` ## 3. 流式模型响应 把 SSE 折叠成 chunk,再把文本 delta 推到当前助手行: ```js import { mapEventStream, createStreamAccumulator } from 'ranuts/stream'; async function streamAssistant(response) { const accumulator = createStreamAccumulator(); const id = crypto.randomUUID(); chat.push({ type: 'message/start', id, role: 'assistant' }); for await (const chunk of mapEventStream(response.body, mapProviderSseToChunks)) { accumulator.push(chunk); if (chunk.type === 'text-delta') { chat.push({ type: 'message/delta', id, text: chunk.text }); } if (chunk.type === 'usage') { document.getElementById('meter').used = chunk.usage.inputTokens + chunk.usage.outputTokens; } } } ``` 为你的 API 实现 `mapProviderSseToChunks`;这是唯一与厂商绑定的模块。[`ranuts/stream`](../../ranuts/stream/) 说明中性的 `StreamChunk` 词汇。 ## 4. 从输入区发送 ```js const input = document.getElementById('input'); const files = document.getElementById('files'); document.getElementById('send').addEventListener('click', async () => { const text = input.value.trim(); if (!text) return; pushMessage('user', text); input.value = ''; files.clear(); const body = buildRequestBody(text, files); // your API shape const response = await fetch('/api/chat', { method: 'POST', body }); await streamAssistant(response); }); ``` 把 [`r-voice-button`](../voice-button/) 接到识别结果写入 `#input`。在 textarea 的 paste 或 picker 里调用 `files.add()` staging 文件——[`r-attachments`](../attachments/) 不会自行拦截手势。 ## 另见 - [Conversation](../conversation/) — view、贴底、`footer` 插槽 - [Markdown](../markdown/) — 流式 `content` 属性 - [Attachments](../attachments/) · [VoiceButton](../voice-button/) · [TokenMeter](../token-meter/) - [ranuts/stream](../../ranuts/stream/) · [ranuts/conversation](../../ranuts/conversation/) - [聊天输入区示例](../../examples/chat-composer) ================================================================ # https://ran.chaxus.com/cn/ranui/api ================================================================ --- title: ranui 元素 API description: 'ranui 的全部自定义元素 —— 43 个元素的属性、属性值、事件、插槽与 ::part() 名称,均从源码提取。' --- # ranui 元素 API(自动生成) 由 `pnpm -F ranui doc:api` 从组件源码自动生成,因此不会与实际发布的代码脱节:逐个元素 列出属性(attribute)、带类型的属性值(property)、事件(含 `detail` 结构与派发选项)、 插槽与 `::part()` 名称。描述直接提取自源码 JSDoc,因此保持英文。 每个元素暴露的 CSS 变量见 [style-tokens-public.md](https://github.com/chaxus/ran/blob/main/packages/ranui/docs/style-tokens-public.md);如何在其中取舍见 [设计系统](/cn/src/design/design-system/)与[设计规范](/cn/src/design/design-guides/)。 单个元素的用法说明在侧边栏各自的页面里,这里是一次性列全的完整接口。 每个事件都标注了它的派发选项:`bubbles`(冒泡)、`composed`(可穿过 Shadow 边界)、 `cancelable`(`preventDefault()` 可否决)。**`element-only`** 表示三者皆无——在祖先节点上 做事件委托永远收不到它,请把监听绑在元素本身上。 **共 43 个自定义元素。** ## `` 源码: `components/attachments/index.ts` - **属性(attribute)**: `sheet: string` - **属性值(property)**: - `accept: string` — Comma-separated types or extensions, in the form `` takes. - `attachments: readonly Attachment[]` — The staged files, in the order they arrived. - `files: File[]` — Just the files, for building a request body. - `maxCount: number` — Most files that may be staged at once; unlimited when unset. - `maxSize: number` — Largest file accepted, in bytes. - `sheet: string` - **事件**: - `attachmentrejected` · bubbles, composed · detail `{ file, reason }` - `attachmentschange` · bubbles, composed · detail `{ attachments }` - **插槽**: — - **Part**: `attachment`, `icon`, `list`, `name`, `remove`, `size`, `thumb` ## `` 源码: `components/button/index.ts` - **属性(attribute)**: `aria-label`, `disabled: boolean | string`, `effect: boolean`, `icon: string`, `iconSize: string`, `sheet: string`, `surface: string`, `type: string` - **属性值(property)**: - `disabled: boolean | string` — Blocks clicks and keyboard activation and removes the button from the tab order. Off by default; `disabled="false"` counts as enabled. - `effect: boolean` — Whether the click ripple is drawn. On by default; opt out with `effect="false"`. - `icon: string` — Name of an `` drawn before the label, for example `'search'`. No icon when empty. - `iconSize: string` — Icon size passed to the inner ``. Defaults to about 1.15× the button's font size; set it only when the icon needs a fixed size. - `sheet: string` - `surface: string` — Surface language for this control (`glass` | `solid`). Paint is CSS-only via - `type: string` — Visual variant: `''` (default) | `'primary'` (monochrome) | `'warning'` | `'text'`. Drives the `:host([type=...])` styles. - **事件**: — - **插槽**: `默认插槽` - **Part**: `button`, `content` ## `` 源码: `components/card/index.ts` - **属性(attribute)**: `description: string`, `heading: string`, `hoverable: boolean`, `sheet: string`, `surface: string` - **属性值(property)**: - `description: string` — Secondary text shown under the heading. Empty by default; the header is hidden when the heading, description and `extra` slot are all empty. - `heading: string` — Heading text. - `hoverable: boolean` — Interactive card (Geist): hover darkens the border one step. Purely presentational — gate it to cards that are actually clickable. - `sheet: string` - `surface: string` — Surface language for this control (`glass` | `solid`). Paint is CSS-only via - **事件**: — - **插槽**: `默认插槽`, `extra(具名)`, `footer(具名)` - **Part**: `body`, `card`, `description`, `extra`, `footer`, `header`, `title` ## `` 源码: `components/checkbox/index.ts` - **属性(attribute)**: `checked: boolean`, `disabled: boolean`, `required: boolean`, `sheet: string`, `value: string` - **属性值(property)**: `checked: boolean`, `disabled: boolean`, `required: boolean`, `sheet: string`, `validationMessage: string`, `validity: ValidityState | undefined`, `value: string` - **事件**: - `change` · element-only · detail `{ checked }` - **插槽**: `默认插槽` - **Part**: `checkbox`, `inner`, `input`, `label`, `wrapper` ## `` 源码: `components/colorpicker/index.ts` - **属性(attribute)**: `alpha-label`, `disabled: boolean`, `hue-label`, `label: string`, `sheet: string`, `value: string` - **属性值(property)**: - `alphaLabel: string` — Accessible name of the opacity slider. - `disabled: boolean` — Blocks opening the picker and emitting `change`, and removes the swatch from the tab order. Off by default; `disabled="false"` counts as enabled. - `hueLabel: string` — Accessible name of the hue slider. - `label: string` — Accessible name of the swatch that opens the picker. Defaults to `'Choose color'`. - `sheet: string` - `value: string` — Selected colour as `#rrggbb` (or `#rgb`), `rgb(...)` or `rgba(...)`; picking a colour writes a hex value, or `rgba(...)` when opacity is below 1. Unparseable values are ignored. Empty until set; the swatch starts white. - **事件**: - `change` · bubbles, composed - **插槽**: — - **Part**: `block`, `swatch` ## `` 源码: `components/popover/content/index.ts` - **属性(attribute)**: — - **属性值(property)**: — - **事件**: - `change` · element-only - **插槽**: `默认插槽` - **Part**: — ## `` 源码: `components/conversation/index.ts` - **属性(attribute)**: `empty: string`, `follow: boolean`, `loading-older`, `older: string`, `sheet: string` - **属性值(property)**: - `empty: string` — Text shown while the projection has produced no rows. - `follow: boolean` — Whether new content is followed until the reader scrolls away from the floor. - `loadingOlder: boolean` — Whether a page is in flight; the affordance stays visible and goes inert. - `older: string` — Label for the paging affordance above the first row. Empty hides it. - `pinned: boolean` — Whether the view is currently following new content. - `sheet: string` - **事件**: - `olderrequest` · bubbles, composed - `pinnedchange` · bubbles, composed · detail `{ pinned }` - **插槽**: `footer(具名)` - **Part**: `conversation`, `empty`, `footer`, `list`, `older` ## `` 源码: `components/disclosure-row/index.ts` - **属性(attribute)**: `busy: boolean`, `expandable: boolean`, `heading: string`, `name: string`, `open: boolean`, `sheet: string`, `summary: string`, `tone: string` - **属性值(property)**: - `busy: boolean` — Whether the work this row stands for is still running. - `expandable: boolean` — Whether the row has a body worth opening. - `heading: string` — The fixed-width left half of the line. - `name: string` — Groups rows so that opening one closes the rest. - `open: boolean` — Whether the body is shown. - `sheet: string` - `summary: string` — The truncating right half. Empty drops the separator with it. - `tone: string` — `error` colours the summary; anything else is the ordinary tone. - **事件**: - `disclosurebeforetoggle` · bubbles, composed, cancelable · detail `{ open }` - `disclosuretoggle` · bubbles, composed · detail `{ open }` - **插槽**: `默认插槽`, `leading(具名)` - **Part**: `body`, `disclosure`, `leading`, `row`, `separator`, `summary`, `title` ## `` 源码: `components/dropdown/index.ts` - **属性(attribute)**: `arrow: string`, `sheet: string`, `surface: string`, `transit: string` - **属性值(property)**: - `arrow: string` - `sheet: string` - `surface: string` — Surface language for this panel (`glass` | `solid`). Paint is CSS-only via - `transit: string` - **事件**: — - **插槽**: `默认插槽` - **Part**: `dropdown` ## `` 源码: `components/select/dropdown-item/index.ts` - **属性(attribute)**: `active: string`, `selected: boolean`, `sheet: string`, `title: string`, `value: string` - **属性值(property)**: - `active: string` — The transient cursor that pointer hover and arrow keys move; r-select sets it. It shares the hover fill and carries no selection meaning. - `selected: boolean` — Marks the committed choice with a check mark. Unlike `active` (the transient cursor), it stays put while the cursor moves. - `sheet: string` - `title: string` - `value: string` - **事件**: — - **插槽**: `默认插槽` - **Part**: `content`, `item` ## `` 源码: `components/fan-gallery/index.ts` - **属性(attribute)**: `sheet: string`, `spread: string` - **属性值(property)**: - `sheet: string` - `spread: string` — Angle in degrees between the first and last child once open. Defaults to `24`; `0` lines them up. - **事件**: — - **插槽**: `默认插槽` - **Part**: `stage` ## `` 源码: `components/glass/index.ts` - **属性(attribute)**: `backdrop: string`, `bevel: string`, `blur`, `dispersion: string`, `displace: string`, `interactive: boolean`, `material: string`, `radius: string`, `rim: boolean`, `saturate: string`, `specular: string`, `tint: string`, `tone: string` - **属性值(property)**: - `backdrop: string` — An `` or `
``` ## CSS Parts 通过 `::part()` 选择器定位内部结构: | Part | 对应元素 | | ---------- | -------------------------------------- | | `wrapper` | 包裹方框与标签的外层 flex 容器 | | `checkbox` | 方框容器 | | `input` | 视觉隐藏的 `` | | `inner` | 实际渲染的方框(边框、填充、勾选标记) | | `label` | 包裹默认插槽的标签 | ```css r-checkbox::part(inner) { border-radius: 50%; } r-checkbox::part(label) { font-weight: 600; } ``` ## 自定义样式 `` 自身暴露了 **32 个 CSS 自定义属性**,另外还会读取主题里的语义令牌。令牌设在任何能继承到的 地方都有效,比如 `:root`、外层容器,或元素本身: ```css r-checkbox { --ran-checkbox-color: var(--ran-color-text-secondary); } ``` Part:`checkbox` · `inner` · `input` · `label` · `wrapper` 完整清单见[样式令牌](/cn/ranui/style-tokens#checkbox);该选哪个令牌见[设计系统](/cn/design/design-system/)。 ================================================================ # https://ran.chaxus.com/cn/ranui/coding-guides/ ================================================================ --- description: '用 ranui 开发时的工程规范:入口选择、attribute / property / 事件契约、跨 Shadow 边界的样式覆盖、状态归属、SSR、测试,以及该避开的反模式。' --- # Coding guidelines 编码规范 这一页讲怎么**用** ranui 开发:组件的契约是什么,Shadow DOM 边界在哪些地方改变了你习惯的规则,还有 哪些坑最好在踩到之前就知道。 视觉方面的规范见[设计规范](/cn/design/design-guides/),令牌清单见 [设计系统](/cn/design/design-system/)。 > **适用场景**:把 ranui 接入应用的各个环节:选择引入方式、绑定事件、给选择器够不到的地方加样式、 > 做服务端渲染、写测试。 ## 原则 1. **元素就是 API。** 属性(attribute)、属性值(property)、事件、插槽和 `::part()` 构成全部契约。 除此之外从外面能看到的一切都是实现细节,随时可能变。 2. **状态只归属一处。** 要么由你的应用持有并推给组件,要么由组件持有并在变化时通知你。双向镜像正是 数值漂移的根源。 3. **用自定义属性、`::part()`、`sheet` 和插槽跨越 Shadow DOM 边界设置样式。** 普通选择器跨不过去, 优先级再高也没用。 4. **用多少引多少。** 每个组件都有独立入口,主入口只是便利,不是必须。 5. **优先用平台能力。** 它们就是标准的自定义元素:`addEventListener`、`setAttribute`、`hidden` 都按 规范工作,框架封装只是可选项。 ## 入口 每个入口只注册它名字所指的那部分。页面如果只需要主题,就不会把整个组件库一起打进去。 | 引入 | 内容 | | ------------------------------- | ---------------------------------------------------------- | | `ranui` | 全部组件(副作用注册所有 `` 元素) | | `ranui/` | 单个组件,如 `ranui/button`、`ranui/select`、`ranui/modal` | | `ranui/theme` | `initTheme` / `setTheme` / `getTheme` 与令牌覆盖;不含元素 | | `ranui/i18n` | 翻译引擎;不含元素 | | `ranui/fonts` | 自托管的 Geist Sans + Geist Mono(仅 `@font-face` CSS) | | `ranui/style` | 样式表,供构建工具没有自动引入时使用 | | `ranui/builder` | 组件自身所用的链式 DOM builder | | `ranui/ssr`、`ranui/ssr-stream` | 服务端渲染 | | `ranui/testing` | 在测试中进入 closed shadow root 的助手 | | `ranui/typings` | 环境类型声明(JSX / TS 元素类型) | ```js import 'ranui/button'; // 单个元素 import 'ranui'; // 全部 ``` **引入是为了副作用。** `import 'ranui/button'` 会注册 ``,导出的类你很少会用到。例外是 服务端渲染,那时需要你自己实例化。 ## 组件契约 每个元素确切的 attribute、property、事件(含 `detail` 结构)、插槽和 part 都从源码生成到 [`COMPONENTS.md`](https://github.com/chaxus/ran/blob/main/packages/ranui/docs/COMPONENTS.md)。 下面讲的是那张表里**看不出来**的部分。 ### attribute 是字符串,property 是类型化的值 HTML 属性小写、只能是字符串;对应的 property 是小驼峰、接受真实的值。二者是同一份状态的两个入口: ```html ``` ```js select.showSearch = true; // property:小驼峰 select.setAttribute('showsearch', ''); // attribute:全小写 ``` - **布尔属性看「有没有」**,和原生 ` ```html 1 2 3 4 ``` 子元素就是你自己的元素。链接、按钮和图片保留原有的角色、名称和 Tab 顺序, 画廊只负责移动它们。 ### 展开角度 `spread` 是第一个和最后一个子元素之间的夹角,单位为度。设为 `0` 时所有子元素叠在一起。
```html … ``` ## 动效 - **展开。** 画廊第一次滚入视口之前,子元素叠成一摞,最前面那张从一开始就能看清。 进入视口后,扇面以弹簧动效依次展开。每个页面只展开一次,滚走再滚回来不会重放。 - **指针与焦点。** 指针下方或持有焦点的子元素会抬起并移到最前,相邻的子元素稍稍向两侧让开。 响应是即时的,新的手势会从上一个手势停下的位置接着走。 - **减少动态效果。** 在 `prefers-reduced-motion: reduce` 下,扇面一开始就是展开的, 所有变化都直接生效,没有过渡。 ## API 参考 ### 属性 | 属性值 | 属性 | 类型 | 默认值 | 说明 | | -------- | -------- | -------- | ------ | ---------------------------------------------- | | `spread` | `spread` | `string` | `'24'` | 展开后第一个与最后一个子元素之间的角度(度)。 | | `sheet` | `sheet` | `string` | `''` | 注入 shadow root 的 CSS。 | ### Part | Part | 元素 | | ------- | -------------------------- | | `stage` | 所有子元素叠放其中的网格。 | ### 样式 | 令牌 | 默认值 | 作用 | | --------------------------- | ---------------- | ------------------------------------------ | | `--ran-fan-gallery-padding` | `--ran-space-10` | stage 四周留给旋转后卡角摆出去的空间。 | | `--ran-fan-gallery-pivot` | `320px` | 旋转中心在行下方多远。越短,扇面张得越开。 | | `--ran-fan-gallery-lift` | `--ran-space-4` | 指针下方或持有焦点的子元素向外移出的距离。 | 画廊的高度等于最高的子元素。旋转后的卡角会超出 stage,所以要在它四周留出空间, 或者调大 `--ran-fan-gallery-padding`。 ================================================================ # https://ran.chaxus.com/cn/ranui/form/ ================================================================ --- description: '如何用 ranui 搭建表单:r-input、r-checkbox、r-select 可以直接放进一个普通的原生
,不需要任何包装组件。' --- # 表单 ranui 不提供一个专门包裹 `` 的组件。`r-input`、`r-checkbox`、`r-select` 本身就是 [Form-Associated Custom Elements](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_form-associated_custom_elements)(各自调用了 `attachInternals()`,通过 `ElementInternals.setFormValue()` 转发自己的值),所以它们本来就能在一个普通的原生 `` 里工作:`new FormData(form)` 能收集到它们,`form.reset()` 能把它们恢复到交互前的状态,`required` 字段会阻止提交并显示浏览器原生的校验提示(锚定在该字段上)。这些都不需要任何 ranui 专属的标记。 > **适用场景**:用 `r-input`/`r-checkbox`/`r-select` 拼一个表单时,直接用一个普通的 `` 就行;如果想把提交结果转成一个普通对象,而不是自己手写 `FormData` 遍历逻辑,可以用下面的 `serializeForm()`。 ## 快速开始 三种字段类型放在同一个普通 `` 里提交,改一下字段的值再提交,就能在下面看到完整的实时结果。这个 demo 用浏览器自带的 `FormData`/`Object.fromEntries` 拼对象,不需要任何 import;下面马上会介绍的 `serializeForm()` 做的是同一件事,只多做了一件 `Object.fromEntries` 做不到的事:同名字段重复出现时会返回数组,而不是悄悄只保留最后一个值。 Member Admin Subscribe to newsletter
> 就像下面「布局」一节说的:字段没有自带的表单级布局,所以这个页面上的每个例子(包括这一个)都给自己的 `
` 设置了 CSS(`display: flex; flex-direction: column; gap: …`)。不设置的话,字段会按普通文档流依次排列、彼此没有间距,看起来会像是重叠/错位,而不是一个表单。 ```html Member Admin Subscribe to newsletter
``` ## `serializeForm(form)` 通过 `FormData` 把一个 `
` 的具名字段收集成一个普通对象。这段样板代码本来每个使用者都要自己手写一遍,才能把一次提交变成能 `JSON.stringify` 或作为 fetch body 发出去的东西。它是一个纯函数,不依赖 ranui 的字段组件,任何真正的 `` 都能用。 ```ts function serializeForm(form: HTMLFormElement): Record; ``` 同一个 `name` 下有多个值时(比如多个 checkbox 共用一个 name),返回的是数组;其它情况返回单个值。 ```ts import { serializeForm } from 'ranui'; const data = serializeForm(document.querySelector('form')); // { username: 'alice', tags: ['a', 'b'] } fetch('/api/signup', { method: 'POST', body: JSON.stringify(data) }); ``` ## 布局 {#layout} 字段没有默认的表单级布局,用普通 CSS 给你自己的 `` 设置样式即可: ```html
``` ## 校验与重置 `r-input`、`r-checkbox`、`r-select` 都支持 `required`(会阻止提交并触发浏览器原生的校验提示,和原生字段一模一样),以及 `checkValidity()`、`reportValidity()`、`validity`、`validationMessage`。原生的 `form.reset()`(或 ` ```html
``` ## 为什么没有 `` 包装组件 一个普通的原生 `
` 已经够用,ranui 的字段组件能直接在里面工作,不需要再包一层组件。`serializeForm()` 补上唯一还缺的一点:把提交结果转成一个普通对象。 ================================================================ # https://ran.chaxus.com/cn/ranui/glass/ ================================================================ --- description: '液态玻璃表面:选一种材质,它就会把背后的内容磨砂化,并在透镜般的边缘处弯折光线。叠在图片或视频上时,会用 WebGL2 直接折射像素本身。' --- # Glass 毛玻璃 液态玻璃表面。选好 `material`,一块玻璃就成型了:背后的内容被磨砂,到了弧形边缘像透镜一样发生弯折,边缘还带一点色散,并被照亮。任何单项参数都可以用同名属性单独覆盖。内容放进默认插槽,文字颜色会自动适配玻璃,保证看得清。 > **适用场景**:面板叠在色彩丰富的内容之上,比如 hero 卡片、悬浮工具条、媒体浮层。背景如果是纯色,就没有东西可弯折,效果也就看不出来了。 ## 演练场 拖动玻璃,切换材质和背景,然后复制代码。代码片段只包含和材质默认值不同的部分。 ```html
…
``` ## 材质 四种材质覆盖了大多数场景,默认是 `regular`。
thin
regular
thick
clear
```html … …regular… … … ``` | 材质 | `blur` | `saturate` | `displace` | `bevel` | `dispersion` | `specular` | | --------- | ------ | ---------- | ---------- | ------- | ------------ | ---------- | | `thin` | 6 | 160 | 48 | 14 | 20 | 100 | | `regular` | 16 | 170 | 78 | 24 | 20 | 100 | | `thick` | 22 | 180 | 56 | 28 | 20 | 90 | | `clear` | 4 | 150 | 90 | 20 | 25 | 110 | ## 叠在图片上 玻璃叠在 `` 或 `