Write ui-sdk code
The bar: an expert in the framework reads the file and says "this is how I'd write it", and anyone else reads it top to bottom without stopping. Readable first, then fast, never clever.
1. Find the twin
Every piece of code here has a twin that already does it well. Open it before you write a line, and copy its shape, names and order.
| You're writing | Start from |
|---|
| A core component | packages/core/src/components/tabs/tabs.ts
, or once it passes about 400 lines (, , ) |
| A popup | packages/core/src/components/popover/popover.ts
and its adapters, on in |
| A React, Vue or Solid part | The same component's file in packages/<framework>/src/
, on |
| A Svelte part | packages/svelte/src/tabs/
|
| A web component | , on |
| An example | The same example in another framework: examples/components/<name>/<framework>/
|
| A spec | tests/specs/tabs*.spec.ts
|
If two twins disagree, the shape is wrong in one of them. Say so instead of picking one.
Copy a twin's shape, not its comments. Older files carry more comments than the rules below allow.
2. Load the framework
Read the expert's knowledge file for every framework you touch:
.agents/experts/<framework>/knowledge.md
, part 1 and "Common mistakes to flag", and its
. It's pinned to the versions this repo installs, so trust it over memory. Write in the current idiom of that version:
- React 19: pure render, no , as a prop, no effect where an event handler or a derived value belongs, code the React Compiler compiles without a bailout.
- Vue 3.5: , on parts, when a component you write takes one, typed and , no options API.
- Svelte 5: runes only (, , , ), snippets, attributes, attachments. No , , or . Props typed with an . Outside , declaration tags (
{const x = $derived(...)}
) instead of .
- Solid 2: props read where they're used, never destructured; derive with memos, not effects; Solid 2 APIs only.
- Web components: platform first. Options reflect as attributes the way built-ins do, public events are and don't bubble (listen with ), and the user's markup stays in light DOM.
3. Write it
The rules in
hold everywhere. The ones that make code look generated when missed:
- One shape. root, per slot, to change the element, never . Same names and the same props in every framework.
- Names in full words that say what a thing is. Booleans read as questions.
- Exports listed once at the end. Inline types. No return type annotations.
- No comment by default. Write one only when a reader would get the code wrong without it: a browser bug, an APG rule, a trade-off. One to three lines. Never a comment per declaration, and never one that explains the design or the data flow. Examples have none.
- No defensive code for cases that can't happen. Types already rule them out.
- No helper for one caller, and no wrapper that only renames.
- Behavior lives in core. An adapter that grows a listener, a focus call or an attribute is in the wrong layer.
4. Check it
Run each and check the exit code, not the output:
sh
pnpm lint
pnpm format
pnpm typecheck
pnpm --filter <package> test
pnpm --filter examples build # when you touched examples
pnpm test:pr # the specs your change reaches
Then run each framework's own lint rules on the files you touched:
(
runs them on the whole repo, as CI does). Fix what each one reports and run it again until all pass. Older errors are listed in
; when you fix one, run
pnpm lint:idioms --prune-suppressions
.
5. Read it cold
Read the whole diff as the framework's expert would, using the "Review checklist" in each
you loaded, and as a teammate who has never seen it:
- Would an expert in this framework write it this way? If you can name a more idiomatic version, write that one.
- Is every name clear without its comment?
- Is anything duplicated that exists elsewhere?
- Does any function do two things, or any file hold two ideas?
- Any allocation on a hot path, or a layout read after a write?
6. Unslop
Run
on your diff. The change isn't done until it has. Before merging a change to an adapter, core, the specs, examples or framework docs, run
.