components: class and style merge with the caller's on an extends host - #414
Merged
Merged
Conversation
…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 parent's binding on a patched child
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A key the host line sets is the line's, and rest never writes it.
classandstylenow merge instead, in two modes decided by whether the body reads the key back through@rest.classwrite ends with the rest view'sclassread and itsstylemerges by key, each as an effect. A wrapper (extends Button) passes its line'sclassas[line, @rest.class]and itsstylethrough the same runtime merge, re-pushed by the updater effect, and the host merges that one value with its own line.@rest.classor@rest.styleanywhere in the body makes that key the author's. No append, no merge.classandclassNameare one key in the rest map; a line passing either spelling owns both.class,className, andstyleon every extends component's props surface in both modes.asChildstays out: a caller's class or style on an adopted host still lands through the runtime's rest writer, which replaces.The demo
Buttondrops itsclassesprop,Choicepassesclass, andIconextendssvg.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,_updatePropfor 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, theOmitsubtraction, the pattern-versus-call distinction in the rest-read scanner.test/spawn/cli/check.test.js(extended tier):rip checkaccepts merged class and disjoint style keys and refuses a shared key or a string on a tag host and through a wrapper.Gates
test:rip3497,test7118,test:spawn264,test:browser19,packages/uiPlaywright 81 (1 pre-existing skip),bin/rip check packages/ui/demo/appclean, corpus and parser zero drift,dist/@ripregenerated.