diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 9dcff203c8..51e761afe3 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -12,14 +12,18 @@ { "name": "ranui", "source": "./plugins/ranui", - "description": "ranui web-component library: import map, element inventory, builder/reactivity, accessibility, conventions.", - "author": { "name": "chaxus" } + "description": "Build, review and polish ranui UI: task-to-rule lookup, public contracts and browser acceptance.", + "author": { + "name": "chaxus" + } }, { "name": "ranuts", "source": "./plugins/ranuts", "description": "ranuts utility library: import map (utils/node/visual/vnode), ranuts/utils inventory, conventions.", - "author": { "name": "chaxus" } + "author": { + "name": "chaxus" + } } ] } diff --git a/AGENTS.md b/AGENTS.md index fa7e389a68..c6298ec04e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,8 @@ Start with the owner of the change; do not load every reference or introduce a s do not create a parallel palette or copy one application's dimensions into library defaults. The index routes decisions; workflow recipes describe host responsibilities, not extra ranui APIs. +For visual changes, use the [UI task lookup](packages/ranui/docs/INTERFACE_GUIDE.md#ui-task-lookup) +to find exact owner sections and an evidence brief. Read only applicable owner sections; reuse existing controls and local token mappings. Stop when the contract, smallest patch and evidence are clear. Optional recipes do not require new infrastructure. diff --git a/README.md b/README.md index 078318f54b..fe461278dd 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,31 @@ document.body.append(button); Components use custom elements and standard browser events, so no React or Vue runtime is required. Import other components through their own entries, such as `ranui/input`. See the [component examples](https://ran.chaxus.com/src/ranui/) for markup, events and customization, or a [standalone script example](./packages/ranui/README.md#iife--cdn) for use without a bundler. +## Use with an agent + +The [ranui skill](./plugins/ranui/skills/ranui/SKILL.md) guides UI implementation, visual +review and browser acceptance. It reads the host's installed ranui references on demand; +it does not install the library or add tools, hooks or external services. + +**Claude Code plugin** — add this repository's marketplace, then install ranui: + +```sh +claude plugin marketplace add chaxus/ran +claude plugin install ranui@ran +``` + +Invoke `/ranui:ranui` in a new session. The same marketplace also offers `ranuts` for utility work. + +**Codex skill** — after cloning this repository, copy `plugins/ranui/skills/ranui/` into +your host project's `.agents/skills/ranui/`. Start a new task and invoke `$ranui`. +This uses the same skill body; the `.claude-plugin` manifest is the Claude installation route. +See [Codex skill discovery](https://learn.chatgpt.com/docs/build-skills#where-codex-loads-local-skills). + +Example task: “Use ranui to polish this settings pane: keep related text together, align +control labels, and verify actual screenshots in light/dark at narrow/wide widths.” +The [UI task lookup](./packages/ranui/docs/INTERFACE_GUIDE.md#ui-task-lookup) maps these +requests to canonical rules and an evidence brief. Source contributors start with [AGENTS.md](./AGENTS.md). + ## Work on the repository Use Node 24+ and pnpm 11.6.0. diff --git a/README.zh-CN.md b/README.zh-CN.md index 965da4b183..09c06f1fe7 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -47,6 +47,30 @@ document.body.append(button); 组件使用浏览器原生的自定义元素和标准事件,无需 React 或 Vue 运行时。其他组件通过各自入口引入,例如 `ranui/input`。在[组件示例](https://ran.chaxus.com/cn/src/ranui/)中查看标签、事件和定制方式;不使用构建工具时,可参考[独立脚本示例](./packages/ranui/README.zh-CN.md#iife--cdn)。 +## 与 agent 一起使用 + +[ranui skill](./plugins/ranui/skills/ranui/SKILL.md) 用于界面实现、视觉审查和浏览器验收。 +它按需读取宿主项目已安装的 ranui 规范,不会安装组件库,也不包含工具、hooks 或外部服务。 + +**Claude Code 插件**:添加本仓库的 marketplace,再安装 ranui: + +```sh +claude plugin marketplace add chaxus/ran +claude plugin install ranui@ran +``` + +新会话中使用 `/ranui:ranui`。同一 marketplace 也提供用于工具函数任务的 `ranuts`。 + +**Codex skill**:克隆本仓库后,将 `plugins/ranui/skills/ranui/` 复制到宿主项目的 +`.agents/skills/ranui/`,新建任务后使用 `$ranui`。两者使用同一份 skill 正文; +`.claude-plugin` 清单用于 Claude 的插件安装。 +参见 [Codex skill 发现规则](https://learn.chatgpt.com/docs/build-skills#where-codex-loads-local-skills)。 + +使用示例:“按 ranui 规范优化这个设置侧栏,收拢相关文案、对齐控件文字, +并用明暗主题、窄宽屏的实际截图验收。” +[UI 任务索引](./packages/ranui/docs/INTERFACE_GUIDE.md#ui-task-lookup) 将这些问题直接对应到 +权威规则和验收简表。修改 ran 源码时先读 [AGENTS.md](./AGENTS.md)。 + ## 参与开发 使用 Node 24+ 和 pnpm 11.6.0。 diff --git a/packages/docs/cn/src/ranui/design-guides/index.md b/packages/docs/cn/src/ranui/design-guides/index.md index 79273dd513..ecfebe8124 100644 --- a/packages/docs/cn/src/ranui/design-guides/index.md +++ b/packages/docs/cn/src/ranui/design-guides/index.md @@ -85,6 +85,12 @@ description: '使用 ranui 构建界面:根据元素的用途选择设计令 [权威组合规范](https://github.com/chaxus/ran/blob/main/packages/ranui/docs/DESIGN.md#11-composition--the-page-not-the-component) 维护例外及验收条件。 +纵向间距按元素关系统一:标题与说明、字段与提示可用 8px,组间 16px,相关子区块间 24px。 +每一处间距只由一层布局负责;先计入子元素 margin、已有内边距和行内控件的基线留白,再决定是否增加间隔。 +换行、提示隐藏和错误出现后,都要在浏览器中实测。 +[权威纵向节奏规范](https://github.com/chaxus/ran/blob/main/packages/ranui/docs/DESIGN.md#vertical-rhythm-across-sibling-views) +维护例外与可运行的反向校验,不改变组件默认值。 + ## 选排版 先问这段文字是什么**角色**(heading、label、copy、button、mono),字体、字号、字重、行高就从 diff --git a/packages/docs/de/src/ranui/design-guides/index.md b/packages/docs/de/src/ranui/design-guides/index.md index 9d9dc193ba..5bfd702413 100644 --- a/packages/docs/de/src/ranui/design-guides/index.md +++ b/packages/docs/de/src/ranui/design-guides/index.md @@ -53,6 +53,14 @@ Nimm jeden Abstand aus der [neunstufigen Skala](/de/src/ranui/design-system/#spa Erfinde kein `20px` oder `28px`. Den Rhythmus einer Seite erzeugt gerade die begrenzte Auswahl; ein einziger Abstand außerhalb der Skala bricht ihn. Halte gemeinsame Achsen über Bereiche hinweg (Kanten, Grundlinien und Spalten, die fluchten) und prüfe die Ausrichtung an den gezeichneten Pixeln statt nach Augenmaß. +Vertikale Abstände folgen der Beziehung: Titel/Beschreibung und Feld/Hinweis können 8px, +getrennte Gruppen 16px und zusammengehörige Unterabschnitte 24px verwenden. Jeder Abstand +hat genau einen Verantwortlichen; berücksichtigen Sie Kind-Margins, Innenabstände und +Grundlinienraum bei Inline-Steuerelementen. Prüfen Sie Zeilenumbrüche sowie ausgeblendete +Hinweise und Fehlerzustände im Browser. Die +[verbindlichen Regeln zum vertikalen Rhythmus](https://github.com/chaxus/ran/blob/main/packages/ranui/docs/DESIGN.md#vertical-rhythm-across-sibling-views) +enthalten Ausnahmen und ausführbare Gegenproben, ohne Komponentenvorgaben zu ändern. + ## Schrift wählen Frag, welche **Rolle** der Text spielt (Überschrift, Beschriftung, Fließtext, Schaltfläche, dicktengleich), und Schriftart, Größe, Stärke und Zeilenhöhe ergeben sich alle aus der [Schriftskala](/de/src/ranui/design-system/#typography). Wähl keine rohen Pixelwerte je Einzelfall. diff --git a/packages/docs/es/src/ranui/design-guides/index.md b/packages/docs/es/src/ranui/design-guides/index.md index 88ea20d054..686ae788e0 100644 --- a/packages/docs/es/src/ranui/design-guides/index.md +++ b/packages/docs/es/src/ranui/design-guides/index.md @@ -53,6 +53,14 @@ Toma cada espacio de la [escala de nueve valores](/es/src/ranui/design-system/#s No inventes `20px` ni `28px`. El conjunto limitado es lo que produce el ritmo de la página; un solo espacio fuera de la escala es lo que lo rompe. Mantén ejes compartidos entre regiones (bordes, líneas base y columnas que se alinean) y comprueba la alineación contra los píxeles dibujados, no a ojo. +El ritmo vertical depende de la relación: título/descripción y campo/ayuda pueden usar 8px, +los grupos separados 16px y las subsecciones relacionadas 24px. Cada separación debe tener +un solo responsable; cuente los márgenes de los hijos, el relleno existente y el espacio de +línea base de los controles en línea. Compruebe los saltos de línea, la ayuda oculta y los +errores en el navegador. Las +[reglas canónicas de ritmo vertical](https://github.com/chaxus/ran/blob/main/packages/ranui/docs/DESIGN.md#vertical-rhythm-across-sibling-views) +incluyen excepciones y pruebas inversas ejecutables, sin cambiar los valores predeterminados de los componentes. + ## Elegir la tipografía Pregunta qué **rol** cumple el texto (título, etiqueta, cuerpo, botón, monoespaciado) y la fuente, el tamaño, el grosor y la altura de línea salen todos de la [escala tipográfica](/es/src/ranui/design-system/#typography). No elijas píxeles crudos caso por caso. diff --git a/packages/docs/fa/src/ranui/design-guides/index.md b/packages/docs/fa/src/ranui/design-guides/index.md index a5a6f2e2ed..5923db2805 100644 --- a/packages/docs/fa/src/ranui/design-guides/index.md +++ b/packages/docs/fa/src/ranui/design-guides/index.md @@ -53,6 +53,13 @@ description: 'قواعد طراحی برای ساختن صفحه با ranui: ن `20px` یا `28px` اختراع نکنید. ریتم صفحه را همان مجموعه محدود می‌سازد و یک فاصله بیرون از مقیاس آن را می‌شکند. ستون فقرات مشترک میان نواحی را حفظ کنید (لبه‌ها، خط‌های مبنا و ستون‌هایی که هم‌راستا می‌شوند) و هم‌ترازی را با پیکسل‌های رسم‌شده بسنجید نه با چشم. +فاصلهٔ عمودی را بر اساس رابطهٔ عناصر یکسان کنید: عنوان و توضیح یا فیلد و راهنما می‌توانند 8px، +گروه‌های جدا 16px و زیربخش‌های مرتبط 24px فاصله داشته باشند. هر فاصله فقط یک مسئول داشته باشد؛ +حاشیهٔ عناصر فرزند، فاصلهٔ داخلی موجود و فضای خط مبنای کنترل‌های درون‌خطی را ابتدا حساب کنید. +شکستن سطر، راهنمای پنهان و حالت خطا را در مرورگر اندازه‌گیری کنید. +[راهنمای مرجع ریتم عمودی](https://github.com/chaxus/ran/blob/main/packages/ranui/docs/DESIGN.md#vertical-rhythm-across-sibling-views) +استثناها و آزمون‌های معکوس قابل اجرا را تعریف می‌کند، بدون تغییر پیش‌فرض اجزا. + ## انتخاب تایپوگرافی بپرسید متن چه **نقشی** دارد (عنوان، برچسب، بدنه، دکمه، تک‌عرض) و آنگاه قلم و اندازه و وزن و ارتفاع خط، همه از [مقیاس تایپوگرافی](/fa/src/ranui/design-system/#typography) درمی‌آیند. برای هر مورد جداگانه پیکسل خام انتخاب نکنید. diff --git a/packages/docs/ja/src/ranui/design-guides/index.md b/packages/docs/ja/src/ranui/design-guides/index.md index 90fce26385..c0744bd385 100644 --- a/packages/docs/ja/src/ranui/design-guides/index.md +++ b/packages/docs/ja/src/ranui/design-guides/index.md @@ -53,6 +53,13 @@ ranui のコンポーネントで組み立てた画面が、部品の寄せ集 `20px` や `28px` を発明しないでください。ページのリズムを生むのは限られた選択肢のほうで、スケールから外れた余白ひとつがそれを壊します。領域をまたいだ共通の背骨(揃った端、ベースライン、カラム)を保ち、揃っているかは目ではなく描画されたピクセルで確かめてください。 +縦方向の間隔は要素の関係でそろえます。タイトルと説明、フィールドと補足には 8px、 +グループ間には 16px、関連する小区画の間には 24px を使えます。各間隔を管理する層は一つにし、 +子要素の margin、既存の内側余白、インラインコントロールのベースライン余白を先に数えます。 +折り返し、補足の非表示、エラー表示をブラウザーで実測してください。 +[縦方向のリズムに関する正式な規則](https://github.com/chaxus/ran/blob/main/packages/ranui/docs/DESIGN.md#vertical-rhythm-across-sibling-views) +に例外と実行可能な逆検証をまとめており、コンポーネントの既定値は変更しません。 + ## 文字の選び方 そのテキストがどんな**役割**を担うか(見出し、ラベル、本文、ボタン、等幅)を問えば、フォント・大きさ・太さ・行間はすべて[タイプスケール](/ja/src/ranui/design-system/#typography)から決まります。個別に生の px を選ばないでください。 diff --git a/packages/docs/ko/src/ranui/design-guides/index.md b/packages/docs/ko/src/ranui/design-guides/index.md index 1b52226c78..3d84d87f31 100644 --- a/packages/docs/ko/src/ranui/design-guides/index.md +++ b/packages/docs/ko/src/ranui/design-guides/index.md @@ -53,6 +53,13 @@ ranui 컴포넌트로 만든 화면이 부품 더미가 아니라 **하나의 `20px`이나 `28px`을 지어내지 마세요. 페이지의 리듬을 만드는 것은 제한된 선택지이고, 그것을 깨뜨리는 것은 스케일을 벗어난 간격 하나입니다. 영역을 가로지르는 공통의 등뼈(맞춰진 가장자리, 기준선, 열)를 지키고, 정렬은 눈이 아니라 그려진 픽셀로 확인하세요. +세로 간격은 요소의 관계에 맞춰 통일합니다. 제목과 설명, 필드와 도움말은 8px, +서로 다른 그룹은 16px, 관련 하위 구역은 24px을 사용할 수 있습니다. 각 간격은 한 계층에서만 +관리하고, 자식 요소의 margin, 기존 안쪽 여백, 인라인 컨트롤의 기준선 여백을 먼저 계산합니다. +줄바꿈, 도움말 숨김, 오류 표시 상태를 브라우저에서 실측하세요. +[세로 리듬의 공식 지침](https://github.com/chaxus/ran/blob/main/packages/ranui/docs/DESIGN.md#vertical-rhythm-across-sibling-views) +에서 예외와 실행 가능한 역검증을 다루며, 컴포넌트 기본값은 변경하지 않습니다. + ## 글자 고르기 그 글자가 어떤 **역할**을 하는지(제목, 레이블, 본문, 버튼, 고정폭) 물으면 글꼴·크기·굵기·행간이 모두 [타입 스케일](/ko/src/ranui/design-system/#typography)에서 따라 나옵니다. 인스턴스마다 날 px을 고르지 마세요. diff --git a/packages/docs/pt/src/ranui/design-guides/index.md b/packages/docs/pt/src/ranui/design-guides/index.md index a6ba1b990f..0c498aa91b 100644 --- a/packages/docs/pt/src/ranui/design-guides/index.md +++ b/packages/docs/pt/src/ranui/design-guides/index.md @@ -53,6 +53,14 @@ Tire cada espaço da [escala de nove valores](/pt/src/ranui/design-system/#spaci Não invente `20px` nem `28px`. É o conjunto limitado que produz o ritmo da página; um único espaço fora da escala é o que o quebra. Mantenha espinhas compartilhadas entre regiões (bordas, linhas de base e colunas que se alinham) e confira o alinhamento contra os pixels desenhados, não a olho. +O ritmo vertical depende da relação: título/descrição e campo/ajuda podem usar 8px, +grupos separados 16px e subseções relacionadas 24px. Cada intervalo deve ter um único +responsável; conte as margens dos filhos, o preenchimento existente e o espaço da linha +de base dos controles em linha. Verifique quebras de linha, ajuda oculta e estados de erro +no navegador. As +[regras canônicas de ritmo vertical](https://github.com/chaxus/ran/blob/main/packages/ranui/docs/DESIGN.md#vertical-rhythm-across-sibling-views) +definem exceções e testes inversos executáveis, sem alterar os padrões dos componentes. + ## Escolher a tipografia Pergunte que **papel** o texto cumpre (título, rótulo, corpo, botão, monoespaçado) e a fonte, o tamanho, o peso e a altura de linha saem todos da [escala tipográfica](/pt/src/ranui/design-system/#typography). Não escolha pixels crus caso a caso. diff --git a/packages/docs/src/ranui/design-guides/index.md b/packages/docs/src/ranui/design-guides/index.md index 7226620236..7a42afdf1b 100644 --- a/packages/docs/src/ranui/design-guides/index.md +++ b/packages/docs/src/ranui/design-guides/index.md @@ -98,6 +98,13 @@ the resting whole pane and explain the distinct purpose of each remaining border the [canonical composition rules](https://github.com/chaxus/ran/blob/main/packages/ranui/docs/DESIGN.md#11-composition--the-page-not-the-component) own the exceptions and acceptance criteria. +Keep vertical rhythm consistent by relationship: title/description and field/hint pairs can +use 8px, separate groups 16px, and related subsections 24px. Give each gap one owner; +count child margins, existing padding and inline baseline space before adding another gap. +Verify wrapped copy and hidden helper/error states in the browser. The +[canonical vertical-rhythm guidance](https://github.com/chaxus/ran/blob/main/packages/ranui/docs/DESIGN.md#vertical-rhythm-across-sibling-views) +owns the exceptions and runnable reverse checks, without changing component defaults. + ## Choosing type Ask what **role** the text plays (heading, label, copy, button, mono) and the font, size, diff --git a/packages/ranui/CLAUDE.md b/packages/ranui/CLAUDE.md index 62598daf26..cce0493002 100644 --- a/packages/ranui/CLAUDE.md +++ b/packages/ranui/CLAUDE.md @@ -19,21 +19,14 @@ For other decisions, use the short **[interface index](docs/INTERFACE_GUIDE.md)* Visual craft stays in DESIGN; engineering stays in CODING. Read only relevant owners and use the generated references below for exact APIs. The index and both references ship in npm. -Nine of the non-negotiables below are machine-checked by `pnpm -F ranui verify:design`, which CI runs on every pull request: dark-unsafe colour fallbacks, raw colour literals, the spacing scale, the sizing scale, mouse-only drag loops, `:host` display rules that disable `hidden`, token fallbacks naming undeclared tokens, components searching their own shadow tree for elements they built, and shadow trees built outside the constructor. Known source-checkout violations are ratcheted in [docs/design-rule-baseline.json](https://github.com/chaxus/ran/blob/main/packages/ranui/docs/design-rule-baseline.json) — you cannot add one, and you cannot silently undo a fix. The remaining rules are no less binding; they just are not mechanically decidable, so verify them by rendering the result. - -For each element's **attributes / properties / events / slots / `::part()`**, consult **[docs/COMPONENTS.md](docs/COMPONENTS.md)** (generated — run `npm run doc:api` after changing any component's API; CI fails if you forget) and **[docs/style-tokens-public.md](docs/style-tokens-public.md)** for its CSS variables. The non-negotiables: - -- **Color is a state ladder, not a palette.** Each scale step 100→1000 has one fixed job: 100 default bg · 200 hover bg · 300 active bg · 400 border · 500 hover border · 600 active border · 700 solid · 800 solid hover · 900 secondary text · 1000 primary text. Use the **semantic tokens** (`--ran-color-*`), never raw hex, in components. -- **Dark-safe fallbacks.** A component token's fallback must point at a token that _flips_ (`var(--ran-color-text, …)`, `var(--ran-gray-alpha-100, …)`, `var(--ran-blue-100, …)`) — never a light-only literal like `rgba(0,0,0,.06)` or `#e6f7ff`, which breaks in dark mode. -- **Spacing:** the `--ran-space-*` scale only (4px base, 9 values). 8 within a group, 16 between groups, 32–40 between sections. -- **Sizing:** element intrinsic dimensions (icon size, control height, small square/rect controls) use the separate `--ran-size-*` scale (16/18/20/24/28/30/32), never `--ran-space-*` — spacing and sizing have different ranges/progressions and consumers need to retune one without perturbing the other. A one-off dimension that isn't shared across components (e.g. a menu's `min-width`) stays a plain component token with its own literal fallback instead of forcing a scale step. -- **Typography:** choose a role (heading / label / copy / button / mono), not a raw px size — each role is backed by real `--ran-text-*` tokens (`--ran-text-heading-1..4`, `-label-1..3`, `-copy-1..2`, `-button-size`, plus matching `-weight` tokens), see DESIGN.md §3. Text that doesn't map to any role (a one-off emphasis state) keeps its own component token rather than being forced in. -- **Elevation = role.** Pick the shadow by what the element _is_: in-flow surface (card/section) → `--ran-shadow-elevated`; floating overlay (dropdown, select, popover, toast/message) → `--ran-shadow-menu`; blocking dialog → `--ran-shadow-modal`. A floating overlay must never fall back to the card tier (`elevated`) — it looks flat. -- **Radius/motion:** use the tokens; prefer no motion (0ms) and keep what remains quick (150/200/300ms); respect `prefers-reduced-motion`. -- **Copy:** [DESIGN §6](docs/DESIGN.md#6-content--copy-is-part-of-the-system) owns contextual action labels, errors and confirmation wording; do not add a second wording rule here. -- **Accessibility:** WCAG AA contrast; never signal state by color alone (pair an icon/label); visible focus ring on every interactive element; icon-only controls need an `aria-label`; full keyboard nav. -- **Mobile and desktop, both.** Any drag/slider/gesture uses Pointer Events (`pointerdown/move/up/cancel`), never mouse-only, matched by `touch-action: none` on the exact drag surface. Any `trigger="hover"` overlay degrades to click/tap on touch (`isMobile()` gate — see `r-select`/`r-popover`). Prefer viewport-relative sizing (`%`, `min()`, `clamp()`, `vw`/`vh`) over inventing a one-off `@media` breakpoint. See DESIGN.md §8. -- **Verify rendered output** for affected surfaces, relevant widths/themes and materially changed input/state paths; code review alone does not prove geometry or interaction. Library CI still runs its complete gates. +For exact attributes, properties, events, slots and parts, use generated +[COMPONENTS.md](docs/COMPONENTS.md); component variables live in +[style-tokens-public.md](docs/style-tokens-public.md). Regenerate these when their source contracts change. + +For visual changes, use the [UI task lookup](docs/INTERFACE_GUIDE.md#ui-task-lookup) and +[verification brief](docs/INTERFACE_GUIDE.md#verification-brief). DESIGN owns visual rules, +[mechanical checks](docs/DESIGN.md#what-is-machine-checked) and rendered acceptance; +the implementation recipes below do not establish a second visual standard. --- @@ -683,6 +676,18 @@ padding: var(--ran-card-padding, var(--ran-space-4, 16px)); ## Component Reference +### Form integration + +`r-input`, `r-checkbox` and `r-select` are form-associated custom elements. Named, enabled +controls inside a native form contribute through `ElementInternals` to `new FormData(form)`; +normal checked/value submission rules still apply despite their internal shadow controls. +`r-colorpicker` is not form-associated: include its `value` explicitly in the host's submitted +data, or track its `change` event's `detail.value`. Confirm event contracts in COMPONENTS. + +`r-button`'s `type` chooses appearance, not native submit behavior. Use a native submit button +or connect the command to the intended form's `requestSubmit()`; do not bypass native validation +with `form.submit()` or add a second keyboard activation handler. + ### r-card A structured content container with header, body, and footer zones. @@ -892,6 +897,7 @@ disclosure language rather than two. What is specific to it: line — a reader watching a model think wants to see where it has got to, and the opening sentence stopped being news several paragraphs ago. Once it stops, the _first_ line, because that is where the block starts and the reader is now deciding whether to open it. + While expanded, the header omits the summary because the body already shows that content. - **It expands while streaming and collapses when that clears — until the reader touches it.** After their own gesture the automatic behaviour stops for good, the same ownership rule `createBottomFollower` applies to scrolling. Setting `open` from script counts as @@ -899,6 +905,17 @@ disclosure language rather than two. What is specific to it: - **`duration` under a second is not shown.** A reader cares that it was fast, not that it was 340ms. +```ts +const reasoning = document.querySelector('r-reasoning')!; +reasoning.streaming = true; // expands +reasoning.content += delta; // grows while visible +reasoning.duration = 4200; // "4.2s" beside the label +reasoning.streaming = false; // collapses, unless the reader intervened +``` + +The default slot replaces the rendered text, for callers using `` in the body. +Feed `ranuts/stream`'s `reasoning-delta` blocks from `snapshot().blocks` into this view. + ### r-tool-card Renders a tool call and its result from a **declared intent** rather than from markup. @@ -939,33 +956,6 @@ assigns `call`, `result` and `status`. --- -### r-reasoning - -A collapsible chain of thought. - -Reasoning is the one part of a response a reader wants to watch while it happens and almost -never wants to keep afterwards, so the element expands while `streaming` is set and -collapses when it clears. - -**Until the reader touches it.** Once they expand or collapse it themselves — or a caller -sets `open` from script — the automatic behaviour stops for good. That is the same -ownership rule `createBottomFollower` applies to scrolling, for the same reason: an -interface that keeps re-deciding something the reader already decided is worse than one -that never decided at all. - -```ts -const reasoning = document.querySelector('r-reasoning')!; -reasoning.streaming = true; // expands -reasoning.content += delta; // grows while visible -reasoning.duration = 4200; // "4.2s" beside the label -reasoning.streaming = false; // collapses, unless the reader intervened -``` - -Sub-second durations render as nothing: a reader cares that it was fast, not that it was -340ms. The default slot replaces the rendered text, for a caller that wants `` -in the body. `ranuts/stream` already separates `reasoning-delta` from `text-delta`, so a -conversation view can feed this directly from `snapshot().blocks`. - ### r-attachments The files staged alongside a message: it holds the list, previews it, validates what diff --git a/packages/ranui/components/reasoning/index.ts b/packages/ranui/components/reasoning/index.ts index 101d564d0d..6627a258ee 100644 --- a/packages/ranui/components/reasoning/index.ts +++ b/packages/ranui/components/reasoning/index.ts @@ -218,7 +218,7 @@ export class Reasoning extends RanElement { // Sub-second thinking is noise; a reader cares that it was fast, not that it was 340ms. const meta = duration === null || duration < 1000 ? '' : `${(duration / 1000).toFixed(1)}s`; this._row.heading = meta === '' ? this.label : `${this.label} · ${meta}`; - this._row.summary = summaryOf(this._content, streaming); + this._row.summary = this.open ? '' : summaryOf(this._content, streaming); } } diff --git a/packages/ranui/docs/DESIGN.md b/packages/ranui/docs/DESIGN.md index 932429096e..a4cf6d322a 100644 --- a/packages/ranui/docs/DESIGN.md +++ b/packages/ranui/docs/DESIGN.md @@ -542,6 +542,55 @@ what the gap _means_: components. **Smaller gap means tighter relationship** — that is the only thing vertical rhythm communicates, so do not undo it with a decorative divider. +### Vertical rhythm across sibling views + +**Repeat the relationship, not every distance.** Sibling settings, list and detail views +share a spacing vocabulary; they do not need equal-height headers, descriptions or empty +states. For a compact application pane, a useful composition is: + +| Relationship | Starting point | +| ------------------------------------------------------------------------ | -------------------------------------------- | +| Title and its description; field label, control and associated hint | `--ran-space-2` (8px) | +| Introductory group to first control/action group; successive form groups | `--ran-space-4` (16px) | +| Related subsections in a compact pane | `--ran-space-6` (24px) | +| Separate major regions | `--ran-space-8` / `--ran-space-10` (32/40px) | + +These are composition choices using the existing scale, not new component defaults. +Keep the preceding table's 12px content-group role where it fits. A heading without a +description leads straight into the next group; it does not need a vacant description row. +Let translated or multiline copy grow naturally while the gap after its final line box +retains the same relationship. Do not fix text heights or add manual breaks to align views. + +**One owner per gap.** Let a parent stack own sibling separation, or let an explicit child +margin own it. Avoid a parent's `gap` plus a child's default paragraph/heading margin at +the same boundary. Reset margins locally in authored composition, not across every library +component. Account for collapsing margins and the line-box space left by inline controls; +an explicit block/flex/grid composition can remove an accidental baseline gap. Do not +compensate with a transform, negative margin or a smaller font. + +Place a hint with the field or action it explains. Count padding already inside an unframed +label/target before adding more outside space: a zero outer margin is not necessarily a +zero visible gap. Conversely, a framed button followed immediately by a hint may still +need separation. Hidden optional content should leave no unexplained row or gap. A reserved +async-status line is legitimate when its stability has a stated purpose and bounds. + +**Measure the composed result in both axes.** For horizontal writing, the frame gap is +`next.top - previous.bottom`, after fonts, custom elements and pending content settle. +Name the measured layers: control frame, line box and text Range answer different questions. +Keep line-height leading, owned padding and hairlines distinct from inter-group space; +16px of padding plus a 1px rule is not an invented 17px spacing token. Inspect actual text +alongside frame measurements rather than forcing glyph edges to match a frame-gap value. + +Use independently declared expected gaps, not only equality between siblings. Exercise +wrapping, resizing, helper/error visibility and relevant input densities. Reverse-check a +doubled child margin while horizontal spines stay correct, a hidden helper that leaves a +phantom gap, and text-only vertical drift inside unchanged frames. Each affected assertion +must reject its mutation and pass after restoration. Confirm that mutated and restored +styles have settled before measuring; reduced motion can still retain a short transition. +The source-only +[browser layout example](https://github.com/chaxus/ran/blob/main/packages/ranui/test/e2e/layout-contract.spec.ts) +demonstrates these checks; human review still judges the complete pane's rhythm. + ### The measure governs text, not the column **Choose a readable measure for the actual script and font.** For Latin prose, 60–75 @@ -749,6 +798,8 @@ These references inform decisions; DESIGN's accessibility, tokens and platform c | Primary reference | Adaptation for ran applications | Evidence to collect | | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| [Linear: calmer interfaces](https://linear.app/now/behind-the-latest-design-refresh) | Let supporting navigation recede; keep headers and action positions predictable | Compare sibling views with real content and their full surrounding chrome | +| [Geist: typography](https://vercel.com/geist/typography) | Apply ran's existing text roles as combinations of size, weight, tracking and leading | Read actual labels, hints and wrapped copy; do not copy another product's pixel sizes | | [Lightspark: limit details](https://design.lightspark.com/limit-the-number-of-details) | Remove redundant controls/states; spend attention on remaining interactions | Complete the task with fewer interruptions and no lost route | | [Butterick: typography](https://practicaltypography.com/typography-in-ten-minutes.html) | Evaluate body type, leading and measure together; retain role tokens and script-appropriate fonts | Read realistic long text in each supported script and zoom level | | [Craft: tabular numbers](https://craft.gustavofior.com/tabular-numbers) | Consider tabular figures for changing counts, timers and aligned numeric columns | Exercise narrow/wide digits and digit-count changes without adjacent controls jumping | @@ -804,7 +855,8 @@ a small patch does not require redesigning unrelated pages. A "no" within scope 4. Could this do less: any entry point, option or state removable without weakening the task? 5. Are named spines exact at the visible text/content layer, including nested and empty states, with intentional navigation columns and no doubled padding? Do adjacent controls share their declared - icon/text columns? Are title hit zones content-hugging? + icon/text columns? Are title hit zones content-hugging? Do repeated vertical relationships use + consistent gaps with one owner, including wrapped copy and hidden helper/error content? 6. Is running text comfortable in its actual scripts, fonts and zoom levels, and is everything that is **not** running text free of that measure cap? 7. Does every element that looks like part of one object agree — border, radius **and** diff --git a/packages/ranui/docs/INTERACTION.md b/packages/ranui/docs/INTERACTION.md index b9ac73c6c0..b644d3c12d 100644 --- a/packages/ranui/docs/INTERACTION.md +++ b/packages/ranui/docs/INTERACTION.md @@ -47,6 +47,8 @@ buttons in its supported slot; do not patch private shadow roots or add misleadi A collapsed region must still identify its contents and state; disclosure must not hide an unresolved actionable error. Collapsing a region containing focus returns focus to its toggle. +Use the collapsed summary to help readers decide whether to expand. When the expanded body +already displays that content, omit the repeated summary and keep the title and state visible. Removing the focused object moves focus to the next meaningful action or a stable parent control. Do not move focus merely because a background request completed. diff --git a/packages/ranui/docs/INTERFACE_GUIDE.md b/packages/ranui/docs/INTERFACE_GUIDE.md index 4a8285126e..c0ef73bb74 100644 --- a/packages/ranui/docs/INTERFACE_GUIDE.md +++ b/packages/ranui/docs/INTERFACE_GUIDE.md @@ -21,6 +21,21 @@ Source contributors edit `theme/tokens.less` and `theme/dark.less`, not the buil Jobs, credentials, persistence and document mutation belong to the host application. A recipe does not add ranui APIs or require every interface to have accounts or queues. +## UI task lookup + +For a visual change, start with the matching row rather than reading all of DESIGN. +These links route to existing rules; they introduce no additional standard or default dimensions. + +| Observed problem or request | Relevant DESIGN sections | Evidence that answers it | +| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| New view, text roles, colors or accessibility | [color](./DESIGN.md#1-color--a-state-ladder-not-a-palette), [type](./DESIGN.md#3-typography--choose-a-role-not-a-size), [copy](./DESIGN.md#6-content--copy-is-part-of-the-system), [accessibility](./DESIGN.md#7-accessibility--the-system-serves-everyone), [input/viewport](./DESIGN.md#8-input--viewport--mobile-and-desktop-both) | State the task and role mapping; inspect real copy, contrast, focus and narrow/touch reflow | +| “Premium”, “like Linear”, busy or visually flat | [craft](./DESIGN.md#evaluate-craft-through-evidence), [emphasis](./DESIGN.md#emphasis-is-a-budget), [containers](./DESIGN.md#structure-before-containers) | Compare the whole resting pane before/after; explain the task served by each remaining frame, fill and effect | +| Uneven vertical spacing, wrapped copy, hidden helper leaving a hole | [vertical rhythm](./DESIGN.md#vertical-rhythm-across-sibling-views) | Independently expected relationships; show/hide and resize; distinguish frame gap from text leading and baseline space | +| Controls line up but labels or icons drift | [alignment](./DESIGN.md#alignment-is-structure-not-polish) | Frame, text Range, icon and hit-area measurements; reverse-check text-only drift | +| Oversized commands, weak hierarchy or cramped touch controls | [command hierarchy](./DESIGN.md#command-hierarchy-in-constrained-panes), [density](./DESIGN.md#density-follows-control-roles) | Full adjacent control group at actual mouse/touch density, with resting affordance and reachable targets | +| Redundant navigation or misplaced actions | [information architecture](./DESIGN.md#12-information-architecture--the-shape-before-the-composition) | Complete the route and return path; verify labels, action placement and focus against INTERACTION | +| Add animation or subtle effects | [motion](./DESIGN.md#5-motion--prefer-none), [craft](./DESIGN.md#evaluate-craft-through-evidence) | Repeat and interrupt the action; inspect reduced motion and theme switching | + ## Before implementing Identify the task, affected object, destination and observable outcome. Read only the relevant @@ -31,17 +46,20 @@ does not require a density abstraction, queue, account flow or revision model. ## Verification brief -> Task and user outcome: … -> Relevant owners and public contracts: … -> State owner, scope, capability limits and applicable recovery behavior: … -> Smallest legible composition and token mapping: … -> Remaining frames/fills/dividers and the distinct purpose of each, reviewed in the full pane: … -> Named alignment spines, frame/content/text/icon/hit geometry, intentional navigation columns: … -> Header hit-zone bounds, nested insets and empty-state continuity: … -> Resting action affordance, shared adjacent control/icon/text columns and owned gutter extensions: … -> Regression evidence at the layer of the defect: … -> Rendered themes, widths, keyboard/touch and relevant failure states: … -> Tested commit, CI results and unverified external prerequisites: … +Fill only fields affected by the task. Keep this as a short work note or PR evidence, not a new +configuration file. Declare expectations before deriving measurements from the implementation. + +> Outcome and affected views/states: … +> Applicable owner sections, public contracts and host capability limits: … +> Composition: task/action hierarchy, distinct frame purposes, token roles and density: … +> Geometry contract: named content/text/icon spines, hit bounds, vertical relationships and independently expected helper/error state: … +> Browser evidence: before/after actual host captures, theme/width/locale/input/state, measurements and rejected/restored mutations relevant to the defect: … +> Validation: tested commit, commands/results, CI and unverified variants or prerequisites: … + +A 12px declaration rendering as 17px calls for inspecting the line box, margins, padding and +borders before changing a token. A hidden error row must be checked against an independently +expected state; reading its actual visibility back as the expected value can validate the defect. +Keep screenshot proposals, browser examples and implemented host changes explicitly distinguished. Static token/link audits cannot prove that CSS won the cascade or an action is reachable. Library contributors use [CODING's checklist](./CODING.md#checklist-before-opening-a-pull-request); diff --git a/packages/ranui/skills/refactoring-ui-design/README.md b/packages/ranui/skills/refactoring-ui-design/README.md deleted file mode 100644 index fa0f8d820f..0000000000 --- a/packages/ranui/skills/refactoring-ui-design/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# refactoring-ui-design - -A cross-platform agent skill for the **ranui** component library: developer-oriented -UI polish tactics (hierarchy, spacing, type, color, depth) applied as concrete refactors. - -The skill runs on **both Claude and OpenAI** from a single source of truth. - -## Layout - -``` -refactoring-ui-design/ -├── SKILL.md # ⭐ the skill: instructions body (shared) + Claude frontmatter -├── agents/ -│ └── openai.yaml # OpenAI-only interface + policy; references SKILL.md -└── README.md # this file -``` - -## Design: one body, many adapters - -`SKILL.md` is the **single source of truth for the instructions**. Its Markdown body is -the guidance both platforms execute; its YAML frontmatter (`name`, `description`, -`metadata`, …) is what the **Claude** skill loader reads directly. - -Each file under `agents/` is a **thin vendor adapter** — interface labels and invocation -policy only. An adapter must **not** copy the instruction text; it points back at -`../SKILL.md` so the two platforms can never drift. - -| Platform | Reads for discovery/UI | Reads for instructions | -| -------- | --------------------------------- | ---------------------- | -| Claude | `SKILL.md` frontmatter | `SKILL.md` body | -| OpenAI | `agents/openai.yaml` → `interface`| `SKILL.md` body (via `instructions.source`) | - -### `agents/openai.yaml` - -| Key | Purpose | -| ------------------------------ | ---------------------------------------------------------- | -| `instructions.source` | Path to the shared body (`../SKILL.md`) — do not inline it | -| `interface.display_name` | Name shown in the OpenAI UI | -| `interface.short_description` | One-line summary | -| `interface.default_prompt` | Suggested invocation prompt | -| `policy.allow_implicit_invocation` | Whether the model may invoke without an explicit call | - -## Editing rules - -- **Change behavior → edit `SKILL.md` only.** Both platforms pick it up. -- **Change how it shows up / when it fires on OpenAI → edit `agents/openai.yaml`.** -- Keep `display_name` / `short_description` in `openai.yaml` consistent with - `SKILL.md`'s `name` / `description`. -- Adding another platform = add one file under `agents/` that references `../SKILL.md`; - never fork the instructions. diff --git a/packages/ranui/skills/refactoring-ui-design/SKILL.md b/packages/ranui/skills/refactoring-ui-design/SKILL.md deleted file mode 100644 index 7d63a27688..0000000000 --- a/packages/ranui/skills/refactoring-ui-design/SKILL.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -name: refactoring-ui-design -description: Use when designing, reviewing, or improving web UI visual quality, especially when a developer-built interface feels cluttered, flat, generic, poorly spaced, weakly hierarchical, or hard to scan. -license: MIT -metadata: - version: 1.0.0 - category: design - tags: [ui, design, refactoring, visual-hierarchy, spacing, color, typography] - homepage: https://www.refactoringui.com/ - # This Markdown body is the single source of truth for the skill instructions. - # Per-vendor adapters live in ./agents/ and reference this file — see README.md. - agents: [claude, openai] ---- - -# Refactoring UI Design - -## Overview - -Apply Refactoring UI's developer-oriented design tactics to make an interface clearer, more polished, and easier to use. Treat visual design as a sequence of concrete refactors, not as decoration or vague taste. - -This skill is distilled from the public Refactoring UI site and its published table of contents. Do not quote or reproduce paid book content. - -## When to Use - -Use this when: - -- Building or redesigning a frontend screen, component, dashboard, form, landing page, or documentation page. -- Reviewing a UI that "works" functionally but looks unpolished. -- Choosing spacing, typography, colors, shadows, imagery, or visual hierarchy. -- Converting a wireframe or plain HTML into a finished interface. - -Do not use this to override explicit product requirements, an existing design system, accessibility requirements, or brand guidelines. Use it to improve execution within those constraints. - -## Workflow - -1. Start with the feature, not the layout. - - Identify the primary job, primary user action, and most important information. - - Design enough real content to solve that job before adding surrounding sections. - - Avoid designing too many states or pages before the core screen works. - -2. Choose a personality, then limit choices. - - Pick a small set of reusable decisions for radius, spacing, type scale, color, shadow, and border strength. - - Reuse tokens and local patterns. Random one-off values usually make the UI look accidental. - -3. Establish hierarchy before styling details. - - Not all elements are equal. Make the primary action, current state, and key content visually dominant. - - Use size, weight, color, contrast, spacing, and position together; do not rely on font size alone. - - De-emphasize supporting labels, metadata, disabled actions, helper text, and secondary controls. - - Replace unnecessary labels with context, grouping, icons, or stronger content structure when the meaning remains clear. - -4. Fix layout and spacing. - - Start with more whitespace than feels natural, then tighten only where relationships need to be clear. - - Use a spacing scale instead of arbitrary margins. - - Do not fill the whole screen just because space exists. - - Avoid ambiguous spacing: related items should be closer to each other than to unrelated items. - - Prefer simple alignment and grouping over complex grids unless the content genuinely needs a grid. - -5. Tune text. - - Use a deliberate type scale with a small number of sizes. - - Keep body copy line length readable. - - Align text for scanning; avoid centered text in dense operational interfaces. - - Make line-height proportional to text size. - - Links and interactive text do not always need bright link color if context already signals interactivity. - - Use letter spacing sparingly, mainly for small uppercase labels. - -6. Build color from roles, not decoration. - - Define neutral, primary, accent, success, warning, danger, and surface shades before scattering colors. - - Use multiple shades per color role so hover, active, border, background, and text states feel related. - - Maintain contrast, but do not solve everything with pure black, pure white, or harsh saturated colors. - - Do not rely on color alone for state; pair it with labels, icons, shape, or position. - -7. Add depth only where it clarifies structure. - - Use shadows to communicate elevation, layering, or focus. - - Replace excessive borders with background contrast, spacing, or subtle shadow. - - If using shadows, keep the light source consistent. - - Overlap elements only when it improves hierarchy or establishes a useful layer. - -8. Handle images and media deliberately. - - Use images at an intended size and crop; do not let arbitrary uploads define the layout. - - Ensure text over images has consistent contrast across likely image content. - - Avoid blurry, dark, generic, or decorative imagery when the user needs to inspect real content. - -9. Finish with small polish passes. - - Improve empty states, default browser controls, focus states, hover states, and loading states. - - Add accent borders, subtle backgrounds, or icon treatments only when they clarify grouping or state. - - Remove visual noise before adding more visual effects. - -## UI Smell Fixes - -| Smell | Refactor | -| --------------------------- | --------------------------------------------------------------------------------------------------- | -| Everything competes equally | Increase hierarchy with weight, contrast, spacing, and position. | -| Too many boxes and lines | Remove borders; use spacing, background contrast, or subtle shadow. | -| Page feels cramped | Increase outer padding and group spacing before changing colors. | -| Page feels sparse | Constrain width, tighten related groups, and add content hierarchy. | -| Content is hard to scan | Add section structure, consistent alignment, and stronger labels or metadata treatment. | -| Colors feel random | Reduce to defined roles and shade scales. | -| UI feels flat | Add elevation, overlap, or background layering only where structure needs it. | -| Typography feels amateur | Reduce font sizes, use fewer weights, improve line-height, and enforce readable line length. | -| Forms feel heavy | Group fields, reduce label emphasis, clarify primary action, and remove unnecessary dividers. | -| Dashboard feels noisy | De-emphasize chrome, emphasize current data, and use tables/cards only where comparison needs them. | - -## Implementation Rules - -- Prefer editing real UI over explaining design theory. -- Make a first pass on structure and hierarchy before colors and shadows. -- Use tokens or CSS variables when the project has them. -- Keep changes consistent with the surrounding component library. -- Verify responsive behavior. Spacing, line length, and hierarchy must work on mobile and desktop. -- Motion is for interaction, not for theme changes. Never let a light↔dark switch animate — no `transition: all` and no `background`/`background-color` in a transition on themed surfaces, or the surface visibly fades (e.g. a white→dark flash) while the rest of the page has flipped instantly. -- If a design system supports dark mode, verify the result in **both** light and dark, and while **toggling** between them (flashes only show mid-switch) — not just in a single static theme. -- For significant frontend changes, inspect the result visually in a browser screenshot before claiming completion. - -## Source Notes - -Public Refactoring UI themes used here include: starting from a feature, limiting choices, hierarchy, layout and spacing, text design, color systems, depth, images, and finishing touches. diff --git a/packages/ranui/skills/refactoring-ui-design/agents/openai.yaml b/packages/ranui/skills/refactoring-ui-design/agents/openai.yaml deleted file mode 100644 index dbb0f7d52c..0000000000 --- a/packages/ranui/skills/refactoring-ui-design/agents/openai.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# OpenAI-side adapter for the shared `refactoring-ui-design` skill. -# -# The skill instructions live ONCE in ../SKILL.md. This file only carries -# OpenAI-specific interface + policy and points at that shared body, so the -# same guidance is loaded on OpenAI and on Claude — no duplicated content. -# See ../README.md for the directory convention. -schema_version: 1 - -# Single source of truth — the same Markdown body Claude loads from SKILL.md. -instructions: - source: ../SKILL.md - -interface: - display_name: 'Refactoring UI Design' - short_description: 'Developer-focused UI polish tactics' - default_prompt: 'Use $refactoring-ui-design to review this interface and suggest concrete visual refactors.' - -policy: - allow_implicit_invocation: true diff --git a/packages/ranui/test/e2e/__screenshots__/linux-arm64/reasoning.spec.ts/Mobile-Chrome/reasoning.png b/packages/ranui/test/e2e/__screenshots__/linux-arm64/reasoning.spec.ts/Mobile-Chrome/reasoning.png index ea45b7d192..25d560433e 100644 Binary files a/packages/ranui/test/e2e/__screenshots__/linux-arm64/reasoning.spec.ts/Mobile-Chrome/reasoning.png and b/packages/ranui/test/e2e/__screenshots__/linux-arm64/reasoning.spec.ts/Mobile-Chrome/reasoning.png differ diff --git a/packages/ranui/test/e2e/__screenshots__/linux-arm64/reasoning.spec.ts/chromium/reasoning.png b/packages/ranui/test/e2e/__screenshots__/linux-arm64/reasoning.spec.ts/chromium/reasoning.png index bdbc5bd31e..f5f5f714ec 100644 Binary files a/packages/ranui/test/e2e/__screenshots__/linux-arm64/reasoning.spec.ts/chromium/reasoning.png and b/packages/ranui/test/e2e/__screenshots__/linux-arm64/reasoning.spec.ts/chromium/reasoning.png differ diff --git a/packages/ranui/test/e2e/__screenshots__/manifest.json b/packages/ranui/test/e2e/__screenshots__/manifest.json index 3e7e3c3dbb..36e67f28b9 100644 --- a/packages/ranui/test/e2e/__screenshots__/manifest.json +++ b/packages/ranui/test/e2e/__screenshots__/manifest.json @@ -12,9 +12,9 @@ "systemFontsSha256": "c339750d748e2a442eec2756219b1e49e9601fe3a6927100deea37040547d226", "systemFontFiles": 48, "localFontsSha256": "a95cd2bae45daa40ee591ea0ed3affc47db95cc32c42eb2f2f6f23c46f7b237e", - "harnessSha256": "07e77a57646cafc5da00e1e39f113f165c672d18fddc492359e5a71a768df315" + "harnessSha256": "fc9fa95baeeb9b38c58ab50a99d2a66bc91ce4c3ba8b01f3f5edda2f8ba83e88" }, - "image": "sha256:bc98b66229174ae71636131e3a7d21063c68d4c965497ad505e5a79871e3c628", + "image": "sha256:307110f3bbc7c3b0ecb32b788bae193a729c2e9226d5cef63d89f662b8e94b9a", "snapshots": { "button.spec.ts/Mobile-Chrome/button.png": "3b4b6ba3280a12f8b1645fc970c864dfe6f0d548ad01281aba96452d5c49ac34", "button.spec.ts/chromium/button.png": "eab068eb0d14e78f141496b02c218e5c0fff686ab301644c69d77b747871d8ef", @@ -84,8 +84,8 @@ "radar.spec.ts/Mobile-Chrome/radar-uniform.png": "c290b416f2d73c076af98e6e3f2c82a206e8ba295382e2b34a38282cbfb048fb", "radar.spec.ts/chromium/radar-default.png": "c05be2be65627ba516f68a19a0b82230ff9e9f518d67ec6f55874fa7ef827810", "radar.spec.ts/chromium/radar-uniform.png": "96b8279c5786934ce6a19998ffc7fd08292147909a181c49111a9f917c318568", - "reasoning.spec.ts/Mobile-Chrome/reasoning.png": "a22e1e4b86a88dd0e743aae2e468669213ec9d1e59d141d6ac4df406d73e3860", - "reasoning.spec.ts/chromium/reasoning.png": "5754404694aa3141171c6b3783996b201e56dc38bda327a264c270350ac8d527", + "reasoning.spec.ts/Mobile-Chrome/reasoning.png": "f83c4e0659dd66f7f620f3de8fe56753ff333d346bed713ea155ffe79a8fb2bf", + "reasoning.spec.ts/chromium/reasoning.png": "dcd2520e6ace181ebe5262cacaf5284cde4d4334ad786ffb654885fabd083fd4", "section.spec.ts/Mobile-Chrome/section.png": "887cbbd31415003cb9613bb4754c2aed4a3316fff17d9b33b3fc6bb546968db8", "section.spec.ts/chromium/section.png": "ee7e2f0109556846cee13e4c038bccae7cd4781438356ae668d35e00a1361912", "select.spec.ts/Mobile-Chrome/select-disabled.png": "c2ad1530f0792e6f90a5c101446e1a20172e5b1bac7ea03a28bf209773f3e966", diff --git a/packages/ranui/test/e2e/layout-contract.md b/packages/ranui/test/e2e/layout-contract.md index 8f3630fed6..e560b963d4 100644 --- a/packages/ranui/test/e2e/layout-contract.md +++ b/packages/ranui/test/e2e/layout-contract.md @@ -32,3 +32,18 @@ compact hit-area bounds use that box, while visible alignment always uses the te Adapt the selectors, named spines, wrapping policy and deliberate padding to the consumer surface. The half-CSS-pixel tolerance handles fractional layout rounding and must not mask a one-pixel drift. + +The vertical fixture adds a host-owned spacing vocabulary: 8px for title/description and +label/control/hint pairs, 16px between groups, and 24px between related subsections. These +values are independent expectations, not values read back from the implementation's own +`gap`. The canonical [vertical-rhythm guidance](../../docs/DESIGN.md#vertical-rhythm-across-sibling-views) +explains which relationships they represent and when a different scale role is appropriate. + +Real text wraps on resize; the description's height changes while the following group's +gap stays constant. Showing a longer error expands its group; hiding it removes the row +and its gap. Reverse checks add an extra child margin, reserve a phantom helper row and +translate only text vertically inside an unchanged paragraph frame. The text mutation +continues to satisfy the frame-gap checks, demonstrating why a separate Range check and +complete rendered inspection remain necessary. All mutations are removed and the original +relationships checked again. Borders, internal padding and line-height leading are measured +separately rather than rounded into an invented spacing value. diff --git a/packages/ranui/test/e2e/layout-contract.spec.ts b/packages/ranui/test/e2e/layout-contract.spec.ts index b873fb8374..a26c4a866a 100644 --- a/packages/ranui/test/e2e/layout-contract.spec.ts +++ b/packages/ranui/test/e2e/layout-contract.spec.ts @@ -107,6 +107,7 @@ test('reverse checks reject text-only drift, filler-sized hit zones and sibling const spine = await geometry(page, '#intro', 'text'); expect(() => sameSpine(drift.left, spine.left)).toThrow(/named spine/); await page.locator('#label').evaluate((node) => node.removeAttribute('style')); + await expect(page.locator('#label')).toHaveCSS('transform', 'none'); await composition(page); await page.locator('#title').evaluate((node) => { (node as HTMLElement).style.flex = '1 1 0%'; @@ -116,6 +117,7 @@ test('reverse checks reject text-only drift, filler-sized hit zones and sibling const label = await geometry(page, '#label', 'text'); expect(() => compactHitZone(expanded.width, label.width, 0)).toThrow(/hug label content/); await page.locator('#title').evaluate((node) => node.removeAttribute('style')); + await expect(page.locator('#title')).toHaveCSS('flex-grow', '0'); await composition(page); await page.locator('#empty-label').evaluate((node) => { (node as HTMLElement).style.paddingInlineStart = '8px'; @@ -124,6 +126,7 @@ test('reverse checks reject text-only drift, filler-sized hit zones and sibling const shifted = await geometry(page, '#empty-label', 'text'); expect(() => sameSpine(shifted.left, spine.left)).toThrow(/named spine/); await page.locator('#empty-label').evaluate((node) => node.removeAttribute('style')); + await expect(page.locator('#empty-label')).toHaveCSS('padding-left', '0px'); await composition(page); }); @@ -166,3 +169,159 @@ test('missing and empty text targets fail instead of silently accepting zero geo await expect(geometry(page, '#missing')).rejects.toThrow(/Missing geometry target/); await expect(geometry(page, '#filler', 'text')).rejects.toThrow(/Empty geometry target/); }); + +// Vertical relationships are host composition too; these are not component defaults. +async function verticalFixture(page: Page) { + await isolatedSetup(page, DEV_SERVER, 'r-button'); + await page.evaluate(() => { + document.body.style.cssText = 'margin: 0; padding: 0; background: var(--ran-color-bg);'; + }); + await mount( + page, + ` +
+
+
+

Connection settings / 接続設定

+

Choose the account used for this workspace. A longer translated description can wrap naturally without reserving the same number of lines in every view.

+
+
+ + +

Stored on this device

+
+
+ + + +
+ Connect account +
+

Advanced settings

+
`, + ); +} + +function declaredVerticalGap(actual: number, expected: number) { + expect(Math.abs(actual - expected), 'vertical gap must match its declared relationship').toBeLessThanOrEqual(0.5); +} + +async function verticalGap(page: Page, before: string, after: string, expected: number) { + const previous = await geometry(page, before); + const next = await geometry(page, after); + declaredVerticalGap(next.top - previous.bottom, expected); +} + +async function verticalComposition(page: Page, errorVisible = false) { + const error = page.locator('#vertical-error'); + expect(await error.isVisible(), 'helper visibility must match the declared state').toBe(errorVisible); + if (errorVisible) expect((await error.textContent())?.trim(), 'a visible error needs its message').toBeTruthy(); + const surface = await geometry(page, '.vertical-surface'); + const title = await geometry(page, '#vertical-title'); + declaredVerticalGap(title.top - surface.top, 16); + for (const [before, after, gap] of [ + ['#vertical-title', '#vertical-description', 8], + ['#vertical-heading', '#vertical-field', 16], + ['#vertical-label', '#vertical-input', 8], + ['#vertical-input', '#vertical-hint', 8], + ['#vertical-field', '#vertical-notes', 16], + ['#vertical-notes', '#vertical-save', 16], + ['#vertical-flow', '#vertical-section', 24], + ] as const) + await verticalGap(page, before, after, gap); + const notes = await geometry(page, '#vertical-notes'); + const last = await geometry(page, errorVisible ? '#vertical-error' : '#vertical-textarea'); + // A hidden hint must not leave a phantom row, margin or flex gap after the control. + declaredVerticalGap(notes.bottom - last.bottom, 0); + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); +} + +for (const variant of [ + { width: 320, theme: 'light' as const }, + { width: 1100, theme: 'dark' as const }, +]) { + test(`vertical rhythm survives wrapping, resizing and helper visibility at ${variant.width}px, ${variant.theme}`, async ({ + page, + }, info) => { + await page.setViewportSize({ width: variant.width, height: 1000 }); + await page.emulateMedia({ colorScheme: variant.theme }); + await page.addInitScript((theme) => localStorage.setItem('ran-theme', theme), variant.theme); + await verticalFixture(page); + await expect(page.locator('html')).toHaveAttribute('data-ran-theme', variant.theme); + await verticalComposition(page); + await page.locator('.vertical-surface').screenshot({ path: info.outputPath('vertical-rhythm.png') }); + const copyHeight = (await geometry(page, '#vertical-description')).height; + await page.setViewportSize({ width: variant.width === 320 ? 1100 : 320, height: 1000 }); + await verticalComposition(page); + const resizedHeight = (await geometry(page, '#vertical-description')).height; + expect(variant.width === 320 ? copyHeight > resizedHeight : copyHeight < resizedHeight).toBe(true); + await page.locator('#vertical-error').evaluate((node) => { + node.textContent = 'A longer validation message grows with its content instead of leaving a fixed empty row.'; + (node as HTMLElement).hidden = false; + }); + await verticalGap(page, '#vertical-textarea', '#vertical-error', 8); + await verticalComposition(page, true); + await page.locator('#vertical-error').evaluate((node) => ((node as HTMLElement).hidden = true)); + await verticalComposition(page); + }); +} + +test('vertical reverse checks reject doubled margins, phantom helper space and text-only drift', async ({ page }) => { + await verticalFixture(page); + await verticalComposition(page); + const title = await geometry(page, '#vertical-title'); + await page.locator('#vertical-description').evaluate((node) => { + (node as HTMLElement).style.marginBlockStart = '8px'; + }); + await expect(page.locator('#vertical-description')).toHaveCSS('margin-top', '8px'); + expect(await geometry(page, '#vertical-title')).toEqual(title); + await expect(verticalGap(page, '#vertical-title', '#vertical-description', 8)).rejects.toThrow(/vertical gap/); + await page.locator('#vertical-description').evaluate((node) => node.removeAttribute('style')); + await verticalComposition(page); + + await page.locator('#vertical-error').evaluate((node) => { + (node as HTMLElement).hidden = false; + (node as HTMLElement).style.minHeight = '24px'; + }); + // The empty node now reserves a row, even though it has no text. + expect((await geometry(page, '#vertical-error')).height).toBe(24); + await expect(verticalComposition(page)).rejects.toThrow(/helper visibility/); + await page.locator('#vertical-error').evaluate((node) => { + (node as HTMLElement).hidden = true; + node.removeAttribute('style'); + }); + await verticalComposition(page); + + const frame = await geometry(page, '#vertical-description'); + const text = await geometry(page, '#vertical-copy', 'text'); + await page.locator('#vertical-copy').evaluate((node) => { + (node as HTMLElement).style.transform = 'translateY(8px)'; + }); + await expect(page.locator('#vertical-copy')).toHaveCSS('transform', 'matrix(1, 0, 0, 1, 0, 8)'); + expect(await geometry(page, '#vertical-description')).toEqual(frame); + // Frame relationships still pass; the Range must reject the separate visible-text defect. + await verticalComposition(page); + const shifted = await geometry(page, '#vertical-copy', 'text'); + expect(() => declaredVerticalGap(shifted.top - text.top, 0)).toThrow(/vertical gap/); + await page.locator('#vertical-copy').evaluate((node) => node.removeAttribute('style')); + await expect(page.locator('#vertical-copy')).toHaveCSS('transform', 'none'); + declaredVerticalGap((await geometry(page, '#vertical-copy', 'text')).top - text.top, 0); + await verticalComposition(page); +}); diff --git a/packages/ranui/test/e2e/reasoning.spec.ts b/packages/ranui/test/e2e/reasoning.spec.ts index 5782516a3a..32bf4332e8 100644 --- a/packages/ranui/test/e2e/reasoning.spec.ts +++ b/packages/ranui/test/e2e/reasoning.spec.ts @@ -50,6 +50,31 @@ test.describe('r-reasoning', () => { await expect(page.locator('r-reasoning')).toHaveAttribute('open', ''); }); + test('keeps the open header quiet and restores the summary on collapse', async ({ page }) => { + await mount(page, '
'); + await page.evaluate(() => { + const block = document.querySelector('r-reasoning') as HTMLElement & { content: string; streaming: boolean }; + block.streaming = true; + block.content = 'Checking the notes.\nSeparating progress and risks.'; + }); + const summary = () => + insideShadow( + page, + 'r-reasoning', + (root) => (root.querySelector('r-disclosure-row') as HTMLElement & { summary: string }).summary, + ); + expect(await summary()).toBe(''); + await page.locator('r-reasoning').click({ position: { x: 32, y: 12 } }); + expect(await summary()).toBe('Separating progress and risks.'); + await page.locator('r-reasoning').click({ position: { x: 32, y: 12 } }); + expect(await summary()).toBe(''); + await page.evaluate(() => { + (document.querySelector('r-reasoning') as HTMLElement & { streaming: boolean }).streaming = false; + }); + await expect(page.locator('r-reasoning')).toHaveAttribute('open', ''); + expect(await summary()).toBe(''); + }); + test('renders', { tag: '@visual' }, async ({ page }) => { await mount(page, `
`); await page.evaluate(() => { diff --git a/packages/ranui/test/unit/reasoning.contract.test.ts b/packages/ranui/test/unit/reasoning.contract.test.ts index ca73f32bbe..5dac91e3ba 100644 --- a/packages/ranui/test/unit/reasoning.contract.test.ts +++ b/packages/ranui/test/unit/reasoning.contract.test.ts @@ -49,6 +49,7 @@ describe('r-reasoning contract', () => { // ago; deciding whether to open it afterwards, the first line is where it starts. const { block, row } = mount(); block.streaming = true; + toggle(row, false); block.content = '先看小数部分\n0.9 比 0.11 大\n所以 9.9 更大'; expect(row.summary).toBe('所以 9.9 更大'); @@ -56,6 +57,26 @@ describe('r-reasoning contract', () => { expect(row.summary).toBe('先看小数部分'); }); + it('shows the summary only while collapsed, without duplicating the open body', () => { + const { block, row, shadow } = mount(); + block.streaming = true; + block.content = 'First line\nLatest line'; + expect(block.open).toBe(true); + expect(row.summary).toBe(''); + expect(shadow.querySelector('.ran-reasoning-text')?.textContent).toBe('First line\nLatest line'); + toggle(row, false); + expect(row.summary).toBe('Latest line'); + block.content += '\nNew line'; + expect(row.summary).toBe('New line'); + toggle(row, true); + expect(row.summary).toBe(''); + block.streaming = false; + expect(block.open).toBe(true); + expect(row.summary).toBe(''); + toggle(row, false); + expect(row.summary).toBe('First line'); + }); + it('says nothing on the collapsed line before any reasoning has arrived', () => { const { block, row } = mount(); block.streaming = true; diff --git a/packages/ranui/test/visual/baseline-review.md b/packages/ranui/test/visual/baseline-review.md index 5de2803ae7..4bc9b3bad5 100644 --- a/packages/ranui/test/visual/baseline-review.md +++ b/packages/ranui/test/visual/baseline-review.md @@ -124,3 +124,21 @@ inert or replace its mounted control; it retains the same Submit button and scre assertion. The Modal functional lifecycle case now observes closing-state completion rather than sleeping 500ms, while preserving its assertion that closing is set immediately. These fixture changes refresh only the harness fingerprint; PNGs and tolerances stay intact. + +### Expanded reasoning header — 2026-10-21 + +Expanded reasoning now keeps its label in the header and its text in the body; the summary +remains visible only while collapsed. Independent review approved the desktop Chromium and +Mobile Chrome reasoning replacements: only the redundant header summary disappears, while +body text, the collapsed second row and geometry remain intact. + +The pinned container generated 142 candidate PNGs with **148/148 tests passing**. The reviewer +also inspected ten unrelated candidate pairs with small edge-pixel differences. Their existing +reviewed PNG bytes are retained; only the two intentional reasoning images change. Snapshot +hashes are computed from that final reviewed tree. The new reasoning regression changes the +owning harness fingerprint; measured browser, platform, fonts and graphics metadata are +unchanged. The derived image ID is informational. + +Two fresh canonical comparisons of the final reviewed tree passed **148/148 each** (2.8 and +2.1 minutes), with zero retries, updates disabled and all existing screenshot tolerances +unchanged. This refresh follows inspection of the actual images, not a relaxed assertion. diff --git a/plugins/ranui/.claude-plugin/plugin.json b/plugins/ranui/.claude-plugin/plugin.json index e98a0b40f6..11e810e671 100644 --- a/plugins/ranui/.claude-plugin/plugin.json +++ b/plugins/ranui/.claude-plugin/plugin.json @@ -1,8 +1,8 @@ { "name": "ranui", "displayName": "ranui", - "version": "1.0.0", - "description": "Reference skill for the ranui web-component library: import map, element inventory, builder/reactivity, accessibility, and pointers to the API docs shipped in the npm package.", + "version": "1.1.0", + "description": "Build, review and polish ranui interfaces using task-focused canonical guidance, public APIs and actual browser acceptance.", "author": { "name": "chaxus", "url": "https://github.com/chaxus/ran" diff --git a/plugins/ranui/skills/ranui/SKILL.md b/plugins/ranui/skills/ranui/SKILL.md index 6a0047eed5..d4f5bf6ca2 100644 --- a/plugins/ranui/skills/ranui/SKILL.md +++ b/plugins/ranui/skills/ranui/SKILL.md @@ -1,209 +1,73 @@ --- name: ranui -description: Use when building UI with the ranui web-component library (elements like ``, ``, ``, ``, ``) or its fluent DOM builder (`ranui/builder` — Div/View/Span/signal/createEffect/EventManager). Covers common import examples, conventions, accessibility, and where the authoritative API and export references live. +description: Use when building, reviewing, or polishing interfaces with ranui web components or ranui/builder, including cluttered hierarchy, uneven spacing, oversized controls, and UI changes that need browser acceptance. --- -# ranui — web component library +# ranui -Framework-agnostic Web Components (Custom Elements + Shadow DOM). Works in React, -Vue, Svelte, Solid, or plain JS. Each element is `` and is used like native HTML. -Styling crosses the shadow boundary via **CSS variables**, **`::part()`**, or the -per-instance **`sheet`** attribute (escape hatch). +Build on the host's existing framework, controls and theme. Keep the patch scoped to the +user's task; a spacing fix does not require a new design system or state layer. -## Authoritative reference (read these for exact APIs — don't guess) +## Find the owner -When `ranui` is installed in a project, its docs ship inside the package: +Read references from the **host project root**, not this skill's installation directory. +Use `node_modules/ranui/` below; in a ran source checkout, use `packages/ranui/` instead. +If the installed version lacks a reference, use the matching source checkout and report the +version gap. Do not assume the plugin cache contains the library or silently install/upgrade it. -- **`node_modules/ranui/docs/COMPONENTS.md`** — generated per-element API: every - element's attributes, typed properties, events (with `detail` shape), slots, and - `::part()` names. This is the source of truth for props/events. Regenerated in the - source repo via `npm run doc:api`. -- **`node_modules/ranui/docs/DESIGN.md`** — design system rules (color states, - spacing, motion, accessibility §7, pre-ship checklist). -- **`node_modules/ranui/docs/INTERFACE_GUIDE.md`** — short decision-to-owner index. -- **`node_modules/ranui/docs/INTERACTION.md`** — observable input, focus, keyboard, - drafts, feedback, progress and recovery; read for behavior changes. -- **`node_modules/ranui/docs/WORKFLOW_PATTERNS.md`** — optional batch/queue, settings, - account/data and revision recipes. Load only for a relevant task; these are host - responsibilities, not additional library APIs. Visual craft remains in DESIGN. -- **`node_modules/ranui/docs/style-tokens-public.md`** — the CSS variable (theming - token) catalog. +| Task | Read only the applicable owner | +| --- | --- | +| Visual polish, spacing, alignment, density or motion | `node_modules/ranui/docs/INTERFACE_GUIDE.md` → **UI task lookup**, then its linked DESIGN sections | +| Input, keyboard, focus, feedback or recovery | `node_modules/ranui/docs/INTERACTION.md` | +| Exact attributes, events, slots or parts | `node_modules/ranui/docs/COMPONENTS.md`; confirm exports/types in the installed package | +| Theme variables or component overrides | `node_modules/ranui/docs/style-tokens-public.md`; foundation declarations in `dist/ranui.css` | +| Library internals, lifecycle or SSR implementation | `node_modules/ranui/docs/CODING.md`, then relevant `CLAUDE.md` sections | +| Batches, settings, account/data or result revisions | `node_modules/ranui/docs/WORKFLOW_PATTERNS.md`, only for a relevant host task | -If a generated contract omits a detail, check the element's declaration through the -installed package's `exports` → `types` target before inspecting repository source. +DESIGN owns visual rules; generated references own APIs. Do not copy their catalogs into this +skill or infer a contract from an example. Stop reading when the relevant contract is clear. -**`node_modules/ranui/docs/CODING.md`** and **`node_modules/ranui/CLAUDE.md`** also -ship with the package: engineering rules and the detailed implementation reference -(architecture, builder, LESS conventions, testing). Source copies live in `packages/ranui`. +## Work on the real surface -## Import map +1. Identify the user outcome, affected views and current defect in the host. Use the index's + **Verification brief** to record a small, independent acceptance contract before editing. +2. Read the applicable owner sections. For requests such as “premium” or “like Linear”, use + DESIGN's **Evaluate craft through evidence** to choose concrete improvements within ran's tokens. +3. Implement the smallest change using supported controls and public contracts. Keep layout, + text/icon alignment and pointer hit areas distinct; inspect the composed pane, not isolated controls. +4. For changed geometry or interaction, render the actual host and inspect screenshots at the + affected widths/themes/states. Follow DESIGN's browser acceptance guidance; static checks and + changed assertions do not establish visual quality. A runnable fixture demonstrates a technique, + not acceptance of the host application. +5. Run the host's relevant checks and deliver the tested commit, observed result, screenshot and + measurement locations, and any unverified state. Update a PR only within the user's authorization. -Start with the host's existing controls, framework and theme authority; a small fix does not -require a new component or state layer. Read only applicable owner sections, then stop when -the supported contract, patch and evidence are clear. Foundation declarations ship in -the installed package’s `dist/ranui.css`; component overrides are in the generated token catalog. +## Minimal public imports -Minimal bootstrap (once in the host entry): +Only bootstrap what the host does not already provide: ```ts -import 'ranui/style'; // foundation tokens and shared styles +import 'ranui/style'; import 'ranui/button'; import 'ranui/input'; ``` -Add `import 'ranui/fonts'` only when the host wants bundled Geist fonts. If the host opts -into ranui's persisted light/dark/system mode, use `import { initTheme } from 'ranui/theme'` -and call `initTheme()` once. Preserve an existing host theme authority; do not add a switch -or another persistence owner merely to use the components. - -Per-component, tree-shakeable side-effect imports register the custom element: - -```ts -import 'ranui/button'; // then use -import 'ranui/modal'; -import 'ranui/select'; -import 'ranui/input'; -import 'ranui/message'; // also exposes an imperative message API -import 'ranui/checkbox'; -``` - -Common element subpaths (examples, not a complete inventory): `button icon image input message skeleton tab tabpane -radar modal select progress player popover dropdown content loading colorpicker math -checkbox card router route link`. For all supported subpaths, read `exports` in the -installed `node_modules/ranui/package.json`; use COMPONENTS.md for element contracts. - -The fluent DOM builder + reactivity (no custom-element registration): - -```ts -import { Div, View, Span, Label, ButtonBuilder, signal, createEffect, EventManager, Style } from 'ranui/builder'; -``` - -Consumer SSR rendering helpers are public exports: +For a host already using the fluent builder: ```ts -import { HTMLElementSSR, RanElement, h, renderToString } from 'ranui/ssr'; -``` - -Streaming rendering uses `ranui/ssr-stream`; types use `ranui/typings`. `defineSSR` -is an internal registration helper at `@/utils/ssr-registry` in the ranui source -checkout. It is not exported from `ranui/ssr` or a consumer registration subpath. - -## Elements at a glance - -- **Forms**: `r-input`, `r-checkbox`, `r-select` (+ `r-option`, `r-dropdown-item` as - options). The three field elements are **form-associated custom elements** - (`attachInternals()` + `ElementInternals.setFormValue()`) — their values are - collected by `new FormData(form)` when they're real descendants of a native - `
`, same as any native field — no wrapper component required. Use - `serializeForm(form)` (from `ranui`) to turn a submit into a plain object. - `r-button`'s `type` chooses visual appearance, not native form submission. Use a native - `