Skip to content

fix(sheet): preserve Tailwind hover and active cascade order - #770

Merged
owjs3901 merged 1 commit into
mainfrom
fix/tailwind-hover-active-order
Oct 14, 2026
Merged

owjs3901 merged 1 commit into
mainfrom
fix/tailwind-hover-active-order

Conversation

@owjs3901

@owjs3901 owjs3901 commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Refs #687 (Tailwind v4 hover/active 정렬, W46). main의 11790bef에서 시작한 독립 수정이며, 선행 병합이 필요한 PR은 없습니다. 팔레트 변경을 포함하는 #734와는 별개입니다.

요약

Tailwind v4 hover:가 만든 (hover:hover) 미디어 규칙이 항상 일반 규칙 뒤에 출력되어, 같은 specificity의 active:, focus:, disabled:보다 우선하던 문제를 수정합니다. hover 가능 장치 가드는 그대로 두고, specificity나 CSS 파일 배치, breakpoint는 바꾸지 않습니다.

sheet는 level 0의 단독 (hover:hover) 가드 안의 로컬 선택자만 기존 선택자 그룹 순서에 참여시킵니다. 다른 미디어 조건, width와 결합한 hover, supports, container, global 규칙은 기존 정렬을 유지합니다. Devup 자체 _hover/_focus/_active/_disabled와 반응형 props의 출력도 그대로 유지합니다.

명시적 Devup _media={{'(hover: hover)': {_hover: ...}}} 역시 같은 가드 형태이므로 같은 정렬 규칙을 적용합니다. 이것은 의도된 동작 변경이며, _active가 단독 hover 가드보다 뒤에 와서 누르는 동안 우선합니다. 이 형태의 회귀도 두 CSS 모드에서 확인합니다.

Tailwind 원본 오라클

저장소 benchmark가 설치한 Tailwind CSS 4.3.3의 실제 compiler에 수정하지 않은 theme.css와 후보 class를 넣어 CSS를 생성했습니다. 실제 Chromium 153에서 640/1024px, hover 가능한 장치, 실제 마우스 hover/down으로 비교합니다. focus/active/disabled 및 hover media 상태도 확인합니다. dark는 Tailwind에서 prefers-dark, Devup에서 기존 data-theme="dark" 경로를 선택합니다.

className Tailwind 실제 규칙 순서 640px 누름 1024px 누름 Devup singleCss=true/false
hover:bg-red-500 active:bg-blue-500 hover → active blue-500 blue-500 두 폭 모두 일치
md:hover:bg-red-500 active:bg-blue-500 active → md:hover blue-500 red-500 두 폭 모두 일치
hover:bg-red-500 focus:bg-green-500 active:bg-blue-500 hover → focus → active blue-500 blue-500 두 폭 모두 일치
hover:bg-red-500 disabled:bg-gray-500 (disabled button) hover → disabled gray-500 gray-500 두 폭 모두 일치
dark:hover:bg-red-500 active:bg-blue-500 (dark) active → dark:hover red-500 red-500 두 폭 모두 일치

md:hover는 Tailwind 원본에서도 1024px에서 hover가 이깁니다. 따라서 active가 항상 이긴다는 추정으로 반응형 Tailwind 정렬을 바꾸지 않습니다. Devup 자체 반응형 _hover는 기존 선택자 그룹 계약대로 _active보다 먼저 출력되며, 이 차이도 유지합니다. dark는 Devup의 기존 root 선택자 specificity가 더 높아 red-500이 우선하며, 기존 선택자 자체는 변경하지 않습니다.

현재 main의 Devup 팔레트는 v3 hex이고 Tailwind 4.3은 oklch입니다. 따라서 이 수정의 비교 기준은 동일 유틸리티의 승리이며, 서로 다른 팔레트의 문자값 동일성이 아닙니다. 실제 값은 blue: Devup rgb(59, 130, 246) / Tailwind oklch(0.623 0.214 259.815), red: rgb(239, 68, 68) / oklch(0.637 0.237 25.331), gray: rgb(107, 114, 128) / oklch(0.551 0.027 264.364)입니다. #734의 실제 diff를 읽어 v4 oklch 팔레트 교체를 포함하는 것을 확인했으며, 이 PR에는 그 변경이나 Tailwind 테마 override를 가져오지 않습니다.

동작

실제 fresh WASM 출력입니다. 선택자와 className 자체는 바뀌지 않고, 가드 블록의 위치만 바뀝니다.

수정 전 (hover:bg-red-500 active:bg-blue-500, singleCss):

.b:active{background-color:#3B82F6}@media(hover:hover){.a:hover{background-color:#EF4444}}

명시적 Devup 가드 입력:

<Box
  _media={{ '(hover: hover)': { _hover: { bgColor: '#EF4444' } } }}
  _active={{ bgColor: '#3B82F6' }}
/>

수정 후 (위 Tailwind 입력 및 명시적 Devup 가드 입력 모두 같은 출력):

@media(hover:hover){.a:hover{background-color:#EF4444}}.b:active{background-color:#3B82F6}

명시적 Devup 가드의 수정 전 출력도 첫 CSS와 같습니다. per-file CSS에서도 같은 순서이며 class 이름만 파일 접두사를 유지합니다.

/* per-file, before */
.a-b:active{background-color:#3B82F6}@media(hover:hover){.a-a:hover{background-color:#EF4444}}
/* per-file, after */
@media(hover:hover){.a-a:hover{background-color:#EF4444}}.a-b:active{background-color:#3B82F6}

md:hover의 출력은 바이트 단위로 그대로입니다.

.b:active{background-color:#3B82F6}@media(min-width:768px)and (hover:hover){.a:hover{background-color:#EF4444}}

새로 생기는 오류

없음. 새로운 build-time 오류, runtime styling, 의존성을 추가하지 않습니다.

남는 한계

기존 v3/v4 팔레트 차이(#734), Devup의 기존 dark 선택자 경로, 서로 다른 CSS 파일 사이의 cascade/import 순서는 이 PR의 변경 범위가 아닙니다. CSS 파일 이동이나 specificity 증가로 이를 숨기지 않습니다.

이 수정은 출처별 분기가 아니라 같은 가드 형태에 대한 sheet 규칙입니다. css(), styled, StyleX, vanilla-extract 등도 동일한 level-0 로컬 가드를 만들면 같은 그룹 정렬을 적용합니다(기존 StyleX nested-hover snapshot에서 이 형태를 확인했습니다). 이들 라이브러리의 모든 복합 variant에 대한 native 우선순위 일치까지 검증했다는 뜻은 아닙니다. 같은 선택자 그룹 안에서 guarded/unguarded 선언을 혼합하면 기존 property/value/class 정렬 및 breakpoint 순서를 따르며, 조건이라는 이유만으로 가드가 항상 마지막에 오지는 않습니다. 혼합된 sheet에서는 가드 블록을 나누어 다시 열 수 있지만 올바른 cascade와 괄호/layer 구조를 회귀로 검증했습니다.

공개 selector 문서의 “conditions always win over breakpoint values” 문장은 단독 hover 가드에 대해서는 이제 예외가 있습니다. 문서 파일은 이 PR에서 수정하지 않았으며, 이 명시적 예외와 후속 문서 정리 필요성을 보고합니다.

검증

  • 수정 전 freshly built WASM의 실제 Playwright 오라클 비교: 20개 중 12 실패 / 8 통과. plain/focus/disabled 실패를 재현하고 md/dark는 보존 대조군으로 통과했습니다.
  • 수정 전 Devup 자체 선택자/반응형 props의 exact CSS 회귀: 두 CSS 모드 총 4개 통과.
  • Rust emitted-order 새 회귀 46개를 포함해 cargo test --workspace 2441개 통과 / 0 실패. 모든 기존 snapshot은 byte-identical이며 .snap.new도 없습니다. 이 PR에서 의도적으로 재정렬하여 갱신한 기존 snapshot은 0개입니다.
  • cargo fmt --all, Rust 1.99 clippy (--workspace --all-targets -- -D warnings), fresh WASM 및 root build 통과. bun test 5498 pass / 0 fail / coverage 100%. bun lint 0 error, 기존 경고 2개만 유지합니다(styled-components migration 문서의 forwardRef, use-styled-theme.ts의 use 접두사).
  • fresh WASM Playwright 30/30 통과: 요청된 20개 오라클 조합, 명시적 Devup guard 4개, 기존 선택자 props의 exact CSS 4개, 실제 touch/hover:none 가드 2개. focus만 된 상태에서 green, 누르는 동안 blue인 것도 확인합니다.
  • 전후 실제 WASM 비교: 16개 compiled JSX 전체 byte-identical, 기존 선택자/반응형 대조군 CSS 6개 전체 byte-identical. 기존 snapshot 변경은 0개로 확인했습니다.
  • 정상 커밋 훅(bun lint, Rust tarpaulin LLVM all-targets, bun test) 통과. Windows tarpaulin은 **98.07% (13566/13833)**이며 이를 Rust 100%라고 주장하지 않습니다. 필수 **Linux CI 100%**는 아래 CI 게이트에서 확인합니다. 커버리지 제외나 workflow 변경은 추가하지 않습니다.
  • Linux CI 37850423579: success, head 01d53e21e3fdae82610ccaf31089b1a59f26bdc2. publish, landing-next-e2e, vinext-rsc-css-e2e, benchmark 네 작업 모두 성공했습니다. Codecov 업로드와 changepacks 검증도 성공했습니다.
  • Linux Rust 100.00% (9894/9894 lines), Bun 5498 pass / 0 fail / 100%. Landing vinext 및 Next의 per-file/singleCss E2E는 각각 160개씩 통과하여 새 hover 회귀도 실제 Linux CI에서 실행했습니다. 자동 병합하지 않습니다.
  • 최종 소스/QA 검토 APPROVE / blocking issue 없음. 이 검토 호출은 원래 GPT-6.1 Sol로 시작했으나 “servers overloaded” 오류 후 harness가 요청 없이 Claude Opus로 자동 재시도했습니다. 완료된 검토는 read-only였고 파일 변경이나 compiler/browser 실행을 하지 않았습니다. 이 quota 예외를 기록하고 코디네이터 승인 하에 완료된 검토를 수용했으며, 추가 재검토를 반복하지 않았습니다.

모든 로컬 build/browser는 whole-process-tree JobObject watchdog을 사용하며 1초마다 메모리를 관찰하고, fixture/browser는 6GiB, commit hook은 12GiB 제한으로 실행합니다. 실제 gate의 메모리 제한 stop은 없으며, 각 종료 후 job의 ownedActive=0/rootSignaled=true를 확인합니다. 복사한 watchdog의 소유권/메모리/타임아웃/identity 테스트는 통과했으나, MSVC vctip 예외 self-test는 해당 실행에서 linker가 vctip을 생성하지 않아 미실행으로 기록했으며, 코디네이터 승인 하에 production의 fail-closed 규칙은 변경하지 않았습니다. TypeScript LSP는 미설치이고 Rust LSP daemon은 timeout이므로 진단 성공으로 주장하지 않고, 실제 clippy/compiler/tests와 별도 strict 타입검사 및 ESLint로 검증합니다.

Refs #687

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Changepacks

@devup-ui/wasm@1.0.83 → 1.0.84 - bindings/devup-ui-wasm/package.json

Patch

  • style-order-range only checks styleOrder where the build reads it: a prop of a Devup UI component and a top-level key of a style object passed to css/globalCss/keyframes. A styleOrder key in nested data, in a prop value such as an element passed as a prop, or in a call of a function sharing a name with an Object.prototype member is no longer reported. The array, typography and media rules already skip non-style data, and css-utils-literal-only accepts a css()/keyframes() result held in a const inside a function. No new build errors.
  • Tailwind classes in a className: each class compiles on its own and every class Tailwind does not define (custom, plugin and analytics classes) stays as written, where any recognized class used to rebuild the whole className without them and could rewrite part of another class; a class touching a template interpolation (icon-${name}) is left as written, and template classNames no longer get spaces inserted around interpolations. A class whose variant is not understood stays as written instead of applying unconditionally; variants follow Tailwind v4: stacked variants compose (hover:focus: matches the element itself), hover is guarded by @media (hover: hover), group-/peer- use the group/peer marker classes (and group-hover/name), and data-, aria-, has-, nth-, , **, supports-[…] and [&…]/[@media …] variants compile. Utilities emit all their declarations (size-, rounded-t-, truncate, sr-only, not-sr-only, text-sm with its line height, text-sm/6, leading-), translate/rotate/scale/skew compose through Tailwind's individual properties and registered variables (@Property) with negative values kept negative, and arbitrary values follow Tailwind's grammar: spaces for underscores except in url() and var() names, _ for an underscore, spaces around math operators, type hints, [property:value] and (--variable) shorthands. Words that only share a utility's prefix (select-wrapper, order-summary, cursor-foo) are no longer compiled
  • Order standalone nonresponsive hover-capability guards within their selector group so active, focus and disabled styles can override hover. Keep responsive Tailwind hover and Devup selector prop ordering unchanged. No new build errors.

@devup-ui/bun-plugin@1.0.22 → 1.0.23 - packages/bun-plugin/package.json

Patch

  • Bun plugin works with Bun.build: DevupUI() (from @devup-ui/bun-plugin/register) serves the generated stylesheet to the bundler's CSS loader once every other module is loaded, so the build emits a CSS output with the styles of the whole bundle instead of an empty module; class names are short there and readable under the runtime (debug option). Files importing the packages Devup UI takes the place of (@emotion/react, @emotion/styled, styled-components, @vanilla-extract/css), @stylexjs/stylex, or a subpath or re-export of these or @devup-ui/react, are compiled instead of skipped

@devup-ui/components@0.1.60 → 0.1.61 - packages/components/package.json

Patch

  • Auto-update: depends on '@devup-ui/vite-plugin' via a local workspace dependency

@devup-ui/eslint-plugin@1.0.22 → 1.0.23 - packages/eslint-plugin/package.json

Patch

  • style-order-range only checks styleOrder where the build reads it: a prop of a Devup UI component and a top-level key of a style object passed to css/globalCss/keyframes. A styleOrder key in nested data, in a prop value such as an element passed as a prop, or in a call of a function sharing a name with an Object.prototype member is no longer reported. The array, typography and media rules already skip non-style data, and css-utils-literal-only accepts a css()/keyframes() result held in a const inside a function. No new build errors.

@devup-ui/next-plugin@1.0.90 → 1.0.91 - packages/next-plugin/package.json

Patch

  • Auto-update: depends on '@devup-ui/plugin-utils' via a local workspace dependency

@devup-ui/plugin-utils@1.0.17 → 1.0.18 - packages/plugin-utils/package.json

Patch

  • Webpack and Rsbuild builds hold every style in their stylesheets: a stylesheet module is built on its first import, so styles extracted later (the global styles of files webpack or Rsbuild reached late, often outside src) were missing from the output. The files the entries reach under src are now extracted before the build, the shared base on disk is kept current, and when a stylesheet still changed during the build it is compiled once more, writing only the complete result. Rsbuild serves each stylesheet from the current sheet instead of a module-wide string kept only when it grew, which left edits that shortened the CSS and later builds in the same process stale. plugin-utils adds computeReachableFiles

@devup-ui/rsbuild-plugin@1.0.67 → 1.0.68 - packages/rsbuild-plugin/package.json

Patch

  • Webpack and Rsbuild builds hold every style in their stylesheets: a stylesheet module is built on its first import, so styles extracted later (the global styles of files webpack or Rsbuild reached late, often outside src) were missing from the output. The files the entries reach under src are now extracted before the build, the shared base on disk is kept current, and when a stylesheet still changed during the build it is compiled once more, writing only the complete result. Rsbuild serves each stylesheet from the current sheet instead of a module-wide string kept only when it grew, which left edits that shortened the CSS and later builds in the same process stale. plugin-utils adds computeReachableFiles

@devup-ui/vite-plugin@1.0.73 → 1.0.74 - packages/vite-plugin/package.json

Patch

  • Auto-update: depends on '@devup-ui/plugin-utils' via a local workspace dependency

@devup-ui/webpack-plugin@1.0.71 → 1.0.72 - packages/webpack-plugin/package.json

Patch

  • Webpack and Rsbuild builds hold every style in their stylesheets: a stylesheet module is built on its first import, so styles extracted later (the global styles of files webpack or Rsbuild reached late, often outside src) were missing from the output. The files the entries reach under src are now extracted before the build, the shared base on disk is kept current, and when a stylesheet still changed during the build it is compiled once more, writing only the complete result. Rsbuild serves each stylesheet from the current sheet instead of a module-wide string kept only when it grew, which left edits that shortened the CSS and later builds in the same process stale. plugin-utils adds computeReachableFiles

@owjs3901
owjs3901 marked this pull request as ready for review October 8, 2026 22:19
@codecov

codecov Bot commented Oct 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
libs/sheet/src/lib.rs 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@owjs3901
owjs3901 merged commit b621716 into main Oct 14, 2026
5 checks passed
@owjs3901
owjs3901 deleted the fix/tailwind-hover-active-order branch October 14, 2026 06:03
owjs3901 added a commit that referenced this pull request Oct 14, 2026
Refs #770, #734

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
owjs3901 added a commit that referenced this pull request Oct 14, 2026
Refs #770, #734, #735, #753, #739

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
(cherry picked from commit 7960b48)
owjs3901 added a commit that referenced this pull request Oct 14, 2026
Refs #770, #734

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
(cherry picked from commit b71ca4e)
owjs3901 added a commit that referenced this pull request Oct 14, 2026
Refs #770, #734

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
(cherry picked from commit b71ca4e)
owjs3901 added a commit that referenced this pull request Oct 14, 2026
Refs #770, #734, #735, #753, #739

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
(cherry picked from commit 7960b48)
owjs3901 added a commit that referenced this pull request Oct 14, 2026
Refs #770, #734

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
(cherry picked from commit b71ca4e)
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