Repository navigation
SELD-R2 PR 1: :hover, :active, :focus and :focus-visible compiled as per-assignment interaction states (R3, R4, R5, R7, R13) - #136
Merged
Conversation
… conflicts resolved beside ANIM-b1 (web-css animations and the interaction state rules both kept) and INL; derived-count pins left at master's values until the regen
…inations per app assignment (R4, R5, R7), the web hover gate (R3) and the fail-closed hit model refusal (R13) selectors.ts lifts :active (focus-within and the rest stay refused); match.ts's InteractionState gains active. analysis/interaction.ts builds, per case, the hover chains, active chains and focus values (pointer focus without focus-visible, keyboard focus with it) from the candidates found to a fixed point, resolves every combination in full and stores each distinct resolution once (R5: a :focus that changes nothing is the none state); more than 256 distinct reachable states in one app assignment, or more than 4096 combinations, are refused naming SELD-R2s (R7); the states never count against the app state table, so seld-r2a's MAX_STATE_ASSIGNMENTS count is gone, and so is combinedStateRefusal (combined states compile). Each state records what Chrome's CSS.forcePseudoState forces to reach it (a real pointer's whole chains and focus), which the parity forced cases use. web-css.ts emits a condition per combination a state stands for, and wraps every condition that needs a :hover candidate in @media (hover: hover); a condition with :hover candidates only off is also emitted without them under not all and (hover: hover), so a tap never leaves a hover style on a phone (R3). project.ts refuses, on native, every interaction rule in a case with a pointer-reachable state and a compiled paint fact outside HIT_MODELLED (R13, fail closed, naming SELD-R2b), after the value checks so a value the profile refuses does not count. Plants: comboStateDropped, webHoverUngated, hitUnmodelledNotRefused (caught in interaction.test.ts). Fixtures: interaction-active and interaction-combo (forced cases), reject-interaction-focus-within and reject-interaction-cap; reject-interaction-active is removed because :active now compiles. docs/decisions.md records R1 to R16 and amends "Hover and focus". Test retargets keeping intent: selectors.test and compile.test refusal pins move from :active to :checked and :focus-within; css-escapes judges div:\68over against Chrome and pins the escaped :focus-within refusal (selectors 59 -> 60); interaction-forced.test lists the four forced fixtures and the new state keys.
… 2028 -> 2060 in device-vectors and translate tests (one hit vector per layout case per DPR: 4 interaction fixtures x 2 directions x 4 DPRs); glyph-clearance pins gain the new cases' lines (DRAGON_PIN_WRITE=1 vitest run packages/parity/test/pixel-reference.test.ts; additive, no existing line changes); the web output's state-member list fails loudly on a state that stands for no combination
…iOS 26.5 runtime, host-arch Android image, CI memory reserve, outcome merge)
…candidate 26.5 resolves to
…vice-run.ts pins the AVDs
…-case captures of the interaction fixtures, emitted CSS, vectors, hit vectors, profiles, north-star check, tw:sweep and the rest of the chain
…on the Mac (device-ci.ts vectors); merge needs both halves; lane records name their hosts (producedOn)
…ns in interaction rules (no silent drop on web); keep color: inherit under an animated colour in state rules; refuse interaction rules on ios and android outside the parity lanes until the native runtime (PR 3, PR 4); R13 counts an overflow value computed from a refused partner as refused and runs only in the lanes; isFocusable follows HTML for invalid tabindex and input type=hidden
…pares each set's model; LAND_ARCH_REBASELINE for the PR that switches, exact failure sets); cli/device-ci.ts in the evidence stamp; a refused merge exits 3 and a blocked device fails its job; CI checks Xcode and the host toolchains; the compare step fails on a difference
…tionLanes; a user's compile refuses its interaction rules there) proves no ios or android profile row: the record lists those targets (laneOnlyNative) and the parity pipeline gives such a case no iOS keys, as the web-only fonts cases
…rth-star check, profiles and the rest of the chain after the native refusal, the interaction-rule animation refusal and no native rows from lane-only cases
Contributor
Author
|
Landing stopped at step Fix the cause, then hand #136 back to the landing queue with the new clean head. |
…138): SELD-R2's faults move to faults/seld-r2.ts (one FAULT_GROUPS line), the interaction group is one sorted GROUPS line, master's side of the central files and tests; project.ts keeps both nativeLanes and interactionLanes; p1/hit pins carried at 2060 until the regen re-derives them
Commands: pnpm regen --skip lanes-host --skip tw-sweep (macos-26) and pnpm regen --only lanes-host (xcode-27), alternated to a fixed point, then pnpm regen --only tw-sweep (macos-26). Base: 6877d7c Run: https://github.com/compiled-run/dragoncss/actions/runs/37275532666
…c-only android vectors lane; merge takes the CI outcomes alone
…ors): master's translate.test and device-vectors.test check hit against p1-floor.json, so the exact 2060 hit pins are dropped; project.ts keeps both REPL-a's images and SELD-R2's laneOnlyNative in the internal record
…irst and only reports the comparison (its tree's lanes are "not run"; the driver judges against the previous position); compare in device-ci.ts with a test on a not-run tree; the android vectors lane records and binds its ABI
Commands: pnpm regen
Floor and pin files merge structurally (git merge driver dragon-floor)
…p + rename) and restored before a text merge on refusal; two insertions at one place of an ordered list are refused
Commands: pnpm regen
…two runs never blame each other's writes on their own steps
Commands: pnpm regen --skip lanes-host --skip tw-sweep (macos-26) and pnpm regen --only lanes-host (xcode-27), alternated to a fixed point, then pnpm regen --only tw-sweep (macos-26). Base: cbfa388 Run: https://github.com/compiled-run/dragoncss/actions/runs/37341041399
floor-merge: never-lower for pins, whole-file replace, no guessing in ordered lists
Commands: pnpm regen
Commands: pnpm regen
regen: one regen per worktree (the MQ-R1 'outside its declared outputs' failure was two regens at once)
tw-sweep capture: the fonts wait fails after 10 s instead of hanging
Commands: pnpm regen; pnpm run parity:devices; pnpm regen
Commands: pnpm regen; pnpm run parity:devices; pnpm regen
thejackshelton
added a commit
that referenced
this pull request
Oct 5, 2026
…'s lowering includes the interaction variants; both sides' record fields, exports and imports; p1-floor.json takes master's until regen re-derives it
thejackshelton
added a commit
that referenced
this pull request
Oct 5, 2026
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.
SELD-R2 PR 1, the compiler part (T064, binding spec notes/T064-seld-r2-spec.md). Built on origin/master, independent of FORM-a; no stack.
What changed
css/selectors.ts,analysis/match.ts).:hover,:active,:focusand:focus-visibleparse in any compound and position, including inside:is(),:where(),:not(),:has()and:nth-*(… of S). They match against anInteractionState(none by default, so every existing match is unchanged).:focus-within,:visited,:checkedand the rest stay refused.analysis/interaction.ts). For each case, the candidates are found to a fixed point. From them come four dimensions: hover chains, active chains, and focus values (pointer focus without focus-visible, keyboard focus with it). Every combination is resolved in full, and each distinct resolution is stored once: a:focusthat changes nothing is the none state (R5). The states are a second level under the case's app assignment and never count against the app state table. More than 256 distinct reachable states in one assignment, or more than 4096 combinations, are refused withDRAGON_UNSUPPORTED_SELECTORnaming SELD-R2s. seld-r2a'scombinedStateRefusaland itsMAX_STATE_ASSIGNMENTScount are deleted, because combined states now compile. Each state records what Chrome'sCSS.forcePseudoStateforces to reach it (a real pointer's whole chains, plus focus), and the parity forced cases use that.emit/web-css.ts). One condition is emitted per combination a state stands for. Every condition that needs a:hovercandidate goes inside@media (hover: hover), Tailwind's shape. A condition whose:hovercandidates are all off is also emitted without them undernot all and (hover: hover). So a tap on a phone never leaves a hover style, and desktop is unchanged.project.ts,HIT_MODELLED). This is fail-closed. In the parity lanes' native compile, every interaction rule is refused in a case that has a pointer-reachable state and a compiled paint fact the hit test does not model. The refusal is[ios]/[android]and names SELD-R2b. It runs after the value checks, so a value the profile already refuses does not count, and an overflow value computed from a refused partner (overflow-y: autobeside a refusedoverflow-x: hidden, computeOverflowPair) does not count either. Outside the lanes it does not run, because every interaction rule is already refused on native (below). (An earlier version of this body said R13 was dormant in enforce mode. That was wrong: it added 10 music-player errors through the computedoverflow-yonhtml, naming SELD-R2b, which would not have cleared them. Both causes are fixed, see the review round below.)nativeInteractionRefusals). iOS and Android have no interaction runtime until PR 3 and PR 4, and no profile row. So every rule that tests:hover,:active,:focusor:focus-visibleis refused on each configured native target withDRAGON_UNSUPPORTED_SELECTOR [ios|android] :<pseudo> is not supported on <target> yet: the native interaction runtime arrives with SELD-R2 PR 3 and PR 4. Web compiles them. Only the parity lanes' compiles (compileFixture,nativeCompile) set the internal optioninteractionLanes, which keeps the states on native so the forced cases and hit vectors prove each state's resolution ahead of the runtime.interactionLanes(a user's compile refuses its interaction rules there) proves no iOS or Android row: the internal record lists those targets (laneOnlyNative), and the parity pipeline gives such a case no iOS keys, as it does for the web-only fonts cases. So the interaction fixtures prove web rows only, and the native profiles, support counts and north star claim nothing for them until PR 3 and PR 4 lift the refusal. The lanes still compile and compare the forced cases and hit vectors on native. Caught byandroid-profile.test(every case proving a promoted android row compiles unblocked for a user), which failed in the previous job run before this fix.comboStateDropped,webHoverUngatedandhitUnmodelledNotRefusedare new. WithinteractionRuleDropped, all four are caught inpackages/dragon/test/interaction.test.ts.<case>~ix<k>, run beside the counted cases throughforced-pseudo.ts) are kept. New fixtures:interaction-activeandinteraction-combo(forced cases, ltr and rtl),reject-interaction-focus-withinandreject-interaction-cap.reject-interaction-activeis removed because:activenow compiles.docs/decisions.mdrecords R1 to R16 under "Runtime styles and animation" and amends "Hover and focus" to "a tap never hovers (principle 3); focus and activation follow Chrome traces".Left for later PRs (not in this one)
rt-interaction.ts, the R7 table instate.ts, R16) and the native glue PR lift the refusal. The lanes compile the states on native throughinteractionLanes.button, range,a[href]),tabindexand the R9display/touchConsumesTapclauses wait for FORM-a, so pointer- and keyboard-reachable focus is empty today and focus is reached only through forced cases. Theinteraction-focusfixture keeps seld-r2a's forced-only shape.@media (hover: hover)fold (R3's authored shape), the hover-only-reveal warning, the UA focus-visible ring refusal, and theinteraction-relayoutfixture. The relayout fixture needs app state and the runtime.Evidence
runFixture). All four layout fixtures pass in both directions, forced cases included: hover 12 cases, focus 8, active 16, combo 10. All three rejects pass.interaction-hoverwas loaded in a touch context (hasTouch,isMobile:(hover: hover)false). Forced:hoverona's chain and a real tap ontallleave both at their none-state styles. In the mouse context the hover style applies.Review round (precomputed review of ee1ae75)
Each fix has a test in
packages/dragon/test/interaction.test.tsthat fails without it (checked by running the new tests with the source fixes reverted: all five failed).interactionRefusals), as before this PR. Test:refuses transition and animation declarations in an interaction rule on every targetpins.a:hover { width: 60px; transition: width 1s linear },.a:hover { animation-play-state: paused },:is(.a:focus) { animation: k 1s }and.a:active { transition-duration: 1s }as refused, and.a { transition: width 1s } .a:hover { width: 60px }as clean.color: inherita child under an animated colour gets (T065 R9). Fixed: the state-rule diff passes the sameunderflag as the base rule, so an inherited colour staysinheritin every state, and a colour the child declares in a state is still emitted. Test:keep an inherited colour inherit under an animated colour in every statepins the reviewer's document.refuses every interaction rule on native outside the lanespins the four[ios]/[android]errors for.a:activeand.card:hover .title, blocked native targets, a ready web output, and no errors for web alone, for the lanes and for a document without interaction rules.laneOnlyNative). Tests:android-profile.testafter the regen, and thelaneOnlyNativepins inrefuses every interaction rule on native outside the lanes.counts an overflow value computed from a refused partner as refused, and leaves R13 to the lanes.input type=hiddennever is. Test:focusability (R9). R9's generaldisplay: noneclause still waits for FORM-a (PR 2): known follow-up.not all and (hover: hover)branch. Known follow-up, not fixed here: PR 2 brings seld-r2c's exclusivity and identity checks over the emitted CSS, and the touch-context Chrome run belongs with the R2 trace lane (PR 3). Today these are covered by the structural tests in interaction.test.ts and the manual touch-context check under Evidence.Outside the spec
packages/parity/test/glyph-clearance-pins.json: new case lines only (DRAGON_PIN_WRITE=1), additive.packages/dragon/test/compile.test.ts,selectors.test.ts,packages/parity/test/css-escapes.test.ts,device-vectors.test.tsandpackages/translate/test/translate.test.ts: retargets listed below.packages/parity/src/pipeline.ts(compileFixture) andpackages/parity/src/native-host.ts(nativeCompile): setinteractionLanes, so the lanes keep the interaction states on native.Changed tests, each with its reason
interaction.test.ts: itscompilehelper compiles as the lanes do (interactionLanes) by default, because those tests prove the states on native as the lanes use them; the new tests passlanes = falseto pin the user-facing native refusal. No existing expectation changed.selectors.test,compile.test: the refusal pins move from:active,:hoverand:focusto:checked,:visitedand:focus-within, which stay refused. Intent kept: interaction and state pseudo-classes outside SELD-R2 are refused.css-escapes.test:div:\68overis now judged against Chrome (it compiles), and the escaped:focus-withinrefusal is pinned instead (selectors 59 to 60).interaction-forced.test: the registry lists the four forced fixtures, and the state keys are the new match-set keys. It adds a pin that a hover-and-press combination forces both chains.device-vectors.test,translate.test: p1/hit goes from 2028 to 2060, one hit vector per layout case per DPR (4 fixtures × 2 directions × 4 DPRs). The floor in p1-floor.json is unchanged.parity.test,platform.test,values.test,fixture-reader.test,android-profile.test,pixel-reference.test: seld-r2a's retargets. Forced cases are counted beside the reachable cases and pass the same prepare hook to both renderings; the values group order ends with interaction;reject-selector-hoverrefuses:focus-within.reject-interaction-activefixture: removed, because:activecompiles (R4).reject-interaction-focus-withintakes its place as the interaction-pseudo refusal.What passed (job seld-r2-c2, at 3991a30)
pnpm regenreached its fixed point; its outputs are in their own commit.pnpm typecheck: pass.pnpm test: Tests 15 failed | 4812 passed | 2 skipped (4829). Failing files: interaction.test.ts device-failures.test.ts land.test.ts lanes-concurrent.test.ts lanes-records.test.ts lanes.test.ts p6a-promotion.test.ts . The device-record files are device step pending (land.test only its evidence:stamp case); rerun alone and passed: interaction.test.ts.vitest run packages/parity/test/android-profile.test.tsalone after the regen: pass.vitest run packages/dragon/test/interaction.test.ts compile.test.ts selectors.test.ts packages/parity/test/interaction-forced.test.ts: 154 passed. The five new review tests fail with the source fixes reverted.Music player north star (examples/music-player/dragon/north-star-check.json, against master)
The native
DRAGON_UNSUPPORTED_SELECTORcounts are the native refusal of the interaction rules until PR 3 and PR 4 (owned rises, cleared by those PRs); R13 adds none outside the lanes.Reviewed size: 188438 bytes (Macroscope ignore patterns excluded), over the ~150 KB guide by the seld-r2a carry-over and the interaction tests; one theme, so it is not split.
🤖 Generated with Claude Code