Skip to content

feat(extractor): follow Tailwind v4 palette, theme tokens, modifiers and composed utilities - #734

Open
owjs3901 wants to merge 2 commits into
mainfrom
feat/tailwind-v4-theme
Open

owjs3901 wants to merge 2 commits into
mainfrom
feat/tailwind-v4-theme

Conversation

@owjs3901

@owjs3901 owjs3901 commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

Refs #687 (B, 권장 항목 1부: TW-05, TW-07, TW-08, TW-09, TW-11, TW-12). #705 (fix/tailwind-classes) 위에 쌓은 PR이며, #705가 먼저 병합되어야 합니다. 이 PR의 base는 main이라 병합 전에는 #705의 커밋도 함께 보입니다.

요약

className의 Tailwind class를 Tailwind CSS v4 기준으로 맞춥니다.

  • TW-08 색상: 하드코딩된 v3 팔레트를 v4 기본 팔레트(oklch)로 교체했습니다. devup.json의 색상은 Tailwind 테마 값처럼 쓸 수 있습니다(bg-primary, text-text → var(--primary), var(--text)). 같은 이름이면 devup 값이 팔레트보다 먼저입니다.
  • TW-11 중요도·불투명도: p-4!, !p-4는 모든 선언에 !important를 붙입니다. bg-red-500/50, /[71.37%], /[0.5], /(--alpha)는 v4처럼 color-mix(in oklab, <색> <알파>, transparent)로 만듭니다.
  • TW-07 필터: blur-*, brightness-*, contrast-*, grayscale, hue-rotate-*, invert, saturate-*, sepia와 backdrop-* 짝이 서로 덮어쓰지 않고 --tw-blur 같은 변수와 filter/backdrop-filter의 합성 값으로 합쳐집니다. -webkit-backdrop-filter도 v4처럼 함께 씁니다.
  • TW-09 그라디언트: bg-linear-to-r, bg-linear-45, bg-conic-*, bg-radial-*(보간 /oklch 등과 임의 값 포함)와 from-*, via-*, to-*(색, % 위치, 임의 값)가 --tw-gradient-* 변수로 합성됩니다. v4에 없는 bg-gradient-to-*는 원문 그대로 둡니다.
  • TW-12 모션: animate-spin/ping/pulse/bounce가 참조하는 @keyframes를 한 번만 내보냅니다. transition, transition-*, duration-*, delay-*, ease-*는 v4의 속성 목록과 --tw-duration/--tw-ease 변수 구성을 따릅니다(duration-250처럼 임의 정수도 ms).
  • TW-05 자식 대상: space-x/y-*, divide-x/y-*, divide-<색>, divide-solid 등이 v4와 같은 :where(& > :not(:last-child)) 선택자로 자식에 적용됩니다(-space-x-4, *-reverse 포함). 변형 선택자와도 합쳐집니다(hover:space-x-4 → :where(&:hover > :not(:last-child))).
  • shadow-*도 v4 구성으로 맞췄습니다: shadow-md는 --tw-shadow와 합성 box-shadow, shadow-<색>은 --tw-shadow-color, shadow-card는 devup shadow 토큰(var(--card)).

@property 규칙은 이제 클래스가 실제로 쓰는 사용자 정의 속성에 대해서만(그리고 키프레임과 함께) 개별 규칙으로 내보냅니다.

동작

확인한 실제 출력(probe.ts, devup.json에 primary/text 색과 card shadow가 있음):

<Box className="bg-red-500/50 text-primary border-text/20 shadow-card p-4!" />
<Box className="blur-sm grayscale backdrop-blur-md backdrop-opacity-50" />
<Box className="bg-linear-to-r from-red-500 via-10% to-primary" />
<Box className="animate-spin transition duration-300 ease-out" />
<Box className="space-x-4 divide-y-2 divide-primary md:space-y-2" />
<Box className="prose card bg-gradient-to-r drop-shadow-md bg-nothing/50" />
.a{background-color:color-mix(in oklab,oklch(63.7% .237 25.331) 50%,transparent)}
.b{color:var(--primary)}
.c{border-color:color-mix(in oklab,var(--text) 20%,transparent)}
.d{--tw-shadow:var(--card)}
.e{box-shadow:var(--tw-inset-shadow,0 0 #0000),var(--tw-inset-ring-shadow,0 0 #0000),var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}
.f{padding:1rem !important}
.g{--tw-blur:blur(8px)}  .i{--tw-grayscale:grayscale(100%)}  .j{--tw-backdrop-blur:blur(12px)}  .m{--tw-backdrop-opacity:opacity(50%)}
.h{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}
.n{--tw-gradient-position:to right in oklab}  .o{background-image:linear-gradient(var(--tw-gradient-stops))}
.p{--tw-gradient-from:oklch(63.7% .237 25.331)}  .r{--tw-gradient-via-position:10%}  .s{--tw-gradient-to:var(--primary)}
.q{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from) var(--tw-gradient-from-position),var(--tw-gradient-to) var(--tw-gradient-to-position))}
@keyframes spin{to{transform:rotate(360deg)}}  .t{animation:spin 1s linear infinite}
.v{transition-timing-function:var(--tw-ease,cubic-bezier(.4,0,.2,1))}  ._{transition-timing-function:cubic-bezier(0,0,.2,1)}  .w{transition-duration:var(--tw-duration,150ms)}  .y{transition-duration:300ms}
:where(.aa > :not(:last-child)){margin-inline-start:calc(1rem * var(--tw-space-x-reverse))}
:where(.ab > :not(:last-child)){margin-inline-end:calc(1rem * calc(1 - var(--tw-space-x-reverse)))}
:where(.ae > :not(:last-child)){border-top-width:calc(2px * var(--tw-divide-y-reverse))}
:where(.ag > :not(:last-child)){border-color:var(--primary)}
@media(min-width:768px){:where(.ah > :not(:last-child)){margin-block-start:calc(.5rem * var(--tw-space-y-reverse))}}

prose card bg-gradient-to-r drop-shadow-md bg-nothing/50는 코드에 그대로 남습니다.

v4 CSS와 다르게 쓰는 부분 (같은 결과)

  • 팔레트 값은 :root의 --color-*가 아니라 값 그대로 씁니다(빌드가 테마 변수를 내보내지 않으므로). devup 색상은 devup이 내보내는 var(--이름)입니다.
  • 그라디언트 보간은 v4의 @supports 중첩 대신 to right in oklab를 바로 씁니다(Baseline 2024 기준).
  • space-*/divide-*의 --tw-*-reverse: 0은 규칙 안에 쓰지 않고 @property의 초기값으로 둡니다. *-reverse class가 어느 순서로 와도 맞습니다.
  • shadow-*는 --tw-inset-shadow 등 ring 쪽 변수에 var(--x, 0 0 #0000) 대체값을 둡니다.

새로 생기는 오류

없음. 이 PR의 모든 변경은 해석 가능한 class의 출력을 바꾸거나, 해석하지 못하는 class를 원문 그대로 두는 것입니다.

남는 한계

  • drop-shadow-*: v4는 테마 변수를 거쳐 색을 바꾸는 구성이라 정확히 재현하지 못해 원문 그대로 남깁니다(이전의 v3 값은 틀려서 지웠습니다).
  • ring-*는 이전 구현 그대로입니다(--tw-ring-color가 없으면 효과가 없고, shadow-*와 합성되지 않음). TW-14 범위로 보고합니다.
  • bg-red-500/[0.5]처럼 숫자 임의 불투명도는 v4처럼 50%로 바꾸지만, inherit/50처럼 v4가 쓸모없는 값을 만드는 경우는 원문 그대로 둡니다. from-(--x)처럼 종류를 알 수 없는 변수 정지점도 원문 그대로 둡니다.
  • Tailwind v4의 CSS @theme/@utility/@custom-variant는 이 PR 범위가 아닙니다(devup.json 테마만 연결).

검증

  • 유틸리티 군마다 v4 소스(utilities.ts, theme.css)와 문서의 CSS를 기준으로 한 테스트: 팔레트·불투명도·!, 필터 17개 함수와 임의 값, 그라디언트(방향·각도·보간·conic·radial·임의 값·정지점), shadow, transition/duration/delay/ease/animate와 키프레임, space/divide와 변형 선택자. 해석하지 않는 class(잘못된 숫자, 음수, 알 수 없는 값)가 원문으로 남는지도 확인합니다.
  • cargo fmt --all, cargo +1.99 clippy --workspace --all-targets -- -D warnings, cargo test --workspace 통과. WASM 재빌드 후 bun run build, bun test(5474 pass, 0 fail, 커버리지 100%), bun lint(기존 경고 2개 외 추가 없음).
  • 추가 발견(범위 밖): ring-*가 정의되지 않은 --tw-ring-color를 읽는 문제(위 남는 한계).

owjs3901 and others added 2 commits October 1, 2026 18:23
…and declarations

Classes Tailwind does not define stay in the className as written, a class with a variant the compiler does not understand is left unchanged instead of applying unconditionally, and utilities emit every declaration Tailwind v4 writes: multi-declaration utilities in full, translate/rotate/scale/skew through their own properties so they compose, and arbitrary values read with Tailwind's grammar. Template classNames keep text that runs into an interpolation attached to it.

Refs #687

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…and composed utilities

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 3, 2026

Copy link
Copy Markdown
Contributor

Changepacks

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

Patch

  • globalCss: theme tokens ($text) resolve to var(--text) in global rules instead of being written as invalid $text; imports given as { url, query } objects with unquoted keys are emitted instead of dropped; an '@layer name' key puts the rules it holds in that cascade layer (layers nest as base.reset) instead of flattening them into invalid declarations, and a layer name that is not one identifier is a build error
  • 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
  • Tailwind classes in a className follow Tailwind CSS v4: colors are the v4 default palette (oklch) and the colors and shadows of devup.json theme are usable as Tailwind theme values (bg-primary, text-text, shadow-card, as var(--token)); the opacity modifier (bg-red-500/50, /[71.37%], /(--alpha)) mixes with color-mix as v4 does; a trailing or leading ! makes every declaration important; blur, brightness, contrast, grayscale, hue-rotate, invert, saturate, sepia and their backdrop-* forms compose through Tailwind's custom properties instead of overwriting filter and backdrop-filter; gradients use bg-linear-, bg-conic- and bg-radial-* with from-, via- and to-* composing through the gradient variables (bg-gradient-to-* is not defined by v4 and stays as written); animate-* brings the @Keyframes it names and transition, duration, delay and ease follow v4; space-* and divide-* apply to the children like v4 (:where(& > :not(:last-child))); @Property rules are emitted only for the custom properties a class uses. Anything the build cannot reproduce exactly (drop-shadow-*) stays as written in the className. No new build errors
  • Theme CSS: a color only one variant defines is defined in that variant; a variant renders dark only when it is named dark or listed as dark in the new theme.colorScheme, others light, and light-dark() only encodes a light default with one dark variant; theme names that are not CSS identifiers are quoted in [data-theme]; typography emits fontStyle and textTransform; color variables come out in a fixed order. $tokens with dashes (text-primary) resolve instead of breaking into var(--text)-primary, and dotted length and shadow names are declared with dashes like their references. registerTheme reports, with the theme path, a token name the $token syntax cannot reach and two tokens of a variant that become the same CSS variable (a-b and a.b)

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

Patch

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

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

Patch

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

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

Patch

  • no-duplicate-value, no-useless-responsive, no-useless-tailing-nulls, no-typography-token-prefix and prefer-media-shorthand only report and fix values the build reads as styles: style props of Box, Flex and the other style components and the arguments of css, globalCss and keyframes, through style objects, responsive arrays, conditions and spreads. Arrays and keys in props the component passes through (data-, aria-, event handlers, HTML attributes, props, styleVars), in arguments of other functions and under imports/fontFaces/params are left alone, where autofix used to rewrite them; styles of a component nested in another's prop are checked too. css-utils-literal-only reads a css() or keyframes() result held in a const of any scope as static, as the build does

@devup-ui/next-plugin@1.0.89 → 1.0.90 - 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.16 → 1.0.17 - packages/plugin-utils/package.json

Patch

  • Theme CSS: a color only one variant defines is defined in that variant; a variant renders dark only when it is named dark or listed as dark in the new theme.colorScheme, others light, and light-dark() only encodes a light default with one dark variant; theme names that are not CSS identifiers are quoted in [data-theme]; typography emits fontStyle and textTransform; color variables come out in a fixed order. $tokens with dashes (text-primary) resolve instead of breaking into var(--text)-primary, and dotted length and shadow names are declared with dashes like their references. registerTheme reports, with the theme path, a token name the $token syntax cannot reach and two tokens of a variant that become the same CSS variable (a-b and a.b)

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

Patch

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

@devup-ui/vite-plugin@1.0.72 → 1.0.73 - 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.70 → 1.0.71 - packages/webpack-plugin/package.json

Patch

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

@codecov

codecov Bot commented Oct 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
bindings/devup-ui-wasm/src/lib.rs 100.00% <ø> (ø)
libs/css/src/theme_tokens.rs 100.00% <100.00%> (ø)
libs/extractor/src/lib.rs 100.00% <ø> (ø)
libs/extractor/src/prop_modify_utils.rs 100.00% <100.00%> (ø)
libs/extractor/src/tailwind.rs 100.00% <ø> (ø)
libs/extractor/src/tailwind_children.rs 100.00% <100.00%> (ø)
libs/extractor/src/tailwind_color.rs 100.00% <100.00%> (ø)
libs/extractor/src/tailwind_effects.rs 100.00% <100.00%> (ø)
libs/extractor/src/tailwind_motion.rs 100.00% <100.00%> (ø)
libs/sheet/src/lib.rs 100.00% <ø> (ø)
... and 1 more
🚀 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.

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