Skip to content

components: class and style merge with the caller's on an extends host - #413

Closed
philiplindberg wants to merge 5 commits into
mainfrom
ui/demo-workbench
Closed

philiplindberg wants to merge 5 commits into
mainfrom
ui/demo-workbench

Conversation

@philiplindberg

Copy link
Copy Markdown
Collaborator

What

A key the host line sets is the line's, and rest never writes it. class and style now merge instead, in two modes decided by whether the body reads the key back through @rest.

  • Automatic mode (the default): a tag host's class write ends with the rest view's class read and its style merges by key, each as an effect. A wrapper (extends Button) passes its line's class as [line, @rest.class] and its style through the same runtime merge, re-pushed by the updater effect, and the host merges that one value with its own line.
  • A shared style key is refused, never resolved by precedence: a literal line style subtracts its keys on the props surface and admits no string; a computed one is checked at each write by the runtime, which throws naming the part and the key.
  • Manual mode: reading @rest.class or @rest.style anywhere in the body makes that key the author's. No append, no merge.
  • class and className are one key in the rest map; a line passing either spelling owns both.
  • The type face admits class, className, and style on every extends component's props surface in both modes.
  • asChild stays out: a caller's class or style on an adopted host still lands through the runtime's rest writer, which replaces.

The demo Button drops its classes prop, Choice passes class, and Icon extends svg.

Earlier commits on the branch: the demo workbench skeleton, the style rule for component bodies, asChild, and the HMR importer note.

Pins

  • test/rip/components.rip: the compiled shape of the automatic tag merge, a static pair and style, manual class, manual style, and the wrapper merge.
  • test/ui/runtime-components.test.js: static and reactive class, _updateProp for class and style, a key from each side, shared keys against literal and computed line styles, the plain rest road, manual mode, the wrapper merge with both shared-key roads and both class spellings.
  • test/toolchain/dts.test.js: the surfaces, the Omit subtraction, the pattern-versus-call distinction in the rest-read scanner.
  • test/spawn/cli/check.test.js (extended tier): rip check accepts merged class and disjoint style keys and refuses a shared key or a string on a tag host and through a wrapper.

Gates

test:rip 3497, test 7118, test:spawn 264, test:browser 19, packages/ui Playwright 81 (1 pre-existing skip), bin/rip check packages/ui/demo/app clean, corpus and parser zero drift, dist/@rip regenerated.

…ata attributes

Each component page is a header, a stage with the trigger, and one
controls panel with a row per popup prop, from shared Page, Stage,
Controls, Row, and Choice components. The Parent section and the
explanatory paragraphs are gone. Dialog exposes closedby, Drawer
exposes side and closedby with a per-side class table, and Menu
exposes side, align, sideOffset, and the last item chosen. Option lists
are checked with satisfies, since an as assertion let a misspelled
option through.

The specs observe data-popup-open, data-open, and data-closed instead
of text readouts, reach a control through its named group, and no
longer drive the cell from an outside button. The drawer spec gains a
per-side placement test.
A one-line prop, state, offer, accept, or computed runs straight into
the member below it and into render; a method, an effect, or any member
with an indented body is followed by a blank line. AGENTS.md states the
rule, and the fourteen sites that broke it are brought in line.
…s host

A caller of a `component extends <tag>` part passes `asChild` with one
element as the body, and the part renders that element as its host in
place of its own tag: the host line in `_create` forks to the runtime's
`_adoptChild`, the slot's append is skipped, and every other line is
unchanged, so the line's attributes, listeners, `_inheritedOwn`, rest
application, `ref:`, and the setup effects all land on the adopted
element. Text, a fragment, a comment, or no body throws at mount naming
the part. The flag rides the rest map, is fixed at construction, refuses
a container and any update, and is never written as an attribute; a
declared prop named `asChild` on an extends component rejects at
compile, and the props surface of a tag-extending component carries
`asChild?: boolean`.

Under HMR a rebuilt child hands its new root to the part that adopted
it; the part releases keeping the DOM and rebuilds onto the new element,
and a part adopted above rebinds in turn. A patch of an adopting part
keeps the host in place. A `style` object on the part's line replaces
the child's, since the style writer remembers one object per element,
and the docs say so.

The rip/ui demo builds every button on one `Button` component with
`variant` and `size` props and a `classes` prop, the shape medlabs uses:
the nine trigger and close sites become `Part asChild` around it, and
`Choice` extends it. A Playwright spec opens Dialog, Drawer, and Menu
through it and asserts the same platform facts the existing specs do.
The browser bundle is regenerated for the runtime change, and the
corpus moves by one line, the host fork on its one extends component.
A key the host line sets is the line's, and rest never writes it. Two
keys now merge instead, in two modes decided by whether the body reads
the key back through @rest. In automatic mode a tag host's class write
ends with the rest view's class read and its style merges by key, each
as an effect since the caller's value may be reactive or arrive through
_updateProp; a wrapper passes its line's class as [line, @rest.class]
and its style through the same runtime merge, re-pushed by the updater
effect, and the host merges that one value with its own line. A style
key both sides set is refused, never resolved by precedence: a literal
line style subtracts its keys on the props surface and admits no
string, and a computed one is checked at each write by the runtime,
which throws naming the part and the key. Reading @rest.class or
@rest.style puts that key in the author's hands: no append, no merge.

class and className are one key in the rest map, so a caller's
className reaches a @rest.class read and the merge alike, and a line
passing either spelling owns both. The rest-read scanner lives in
render.js, since JS emission depends on it and the browser bundle stubs
src/ts; it reads @rest.<key>, @rest['key'], and a destructuring pattern
from @rest, never an object beside @rest in a call or an array.

asChild stays out: a caller's class or style on an adopted host still
lands through the runtime's rest writer, which replaces.

The demo Button drops its classes prop, Choice passes class, and Icon
extends svg so a caller's class reaches it the same way.
@philiplindberg
philiplindberg deleted the ui/demo-workbench branch September 25, 2026 14:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant