Skip to content

fix: comprehensive audit — core correctness, 30-100x smaller PNGs, react stability, packaging & CI fixes#6

Merged
Todari merged 2 commits into
mainfrom
fix/audit-improvements
Jun 12, 2026
Merged

fix: comprehensive audit — core correctness, 30-100x smaller PNGs, react stability, packaging & CI fixes#6
Todari merged 2 commits into
mainfrom
fix/audit-improvements

Conversation

@Todari

@Todari Todari commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Summary

Full audit of both published packages (53-agent multi-dimensional review across packaging, core logic, react layer, performance, repo hygiene, and consumer API behavior), with all 40+ confirmed findings fixed.

@react-pixel-ui/core (minor)

Performance

  • PNG encoder now uses a real dependency-free DEFLATE (fixed Huffman + LZ77) instead of uncompressed stored blocks — data URLs are 30–100× smaller (600×400 card: 78 KB → 2.4 KB), verified byte-exact via zlib.inflateSync round-trips
  • generatePixelArt gains options.want: 'styles' | 'composite' | 'both' — consumers stop generating the PNG they discard (~2× faster), plus a small LRU result cache (hover/theme churn becomes free)

Correctness

  • Full 148-entry CSS named-color table (was 14 — lime, navy, tomato… failed to parse); new color(srgb | srgb-linear | display-p3) support (Chromium's computed color-mix() output); anchored rgb() patterns (no more conic-gradient(rgb(...)) misparsed as solid red)
  • Gradient parser: Tailwind v4 to right in oklab hints, turn/rad/grad units, aspect-ratio-correct corner keywords (new resolveGradientAngle), double-position stops, px-position stops, -1 sentinel corruption fix
  • generatePixelShadow: zero offsets stay zero (box-shadow: 0 4px no longer renders diagonal)
  • Composite image: transparent interior for border-only elements (was filled with border color); returns null for unparseable backgrounds so consumers degrade gracefully instead of erasing the element
  • parseComputedStyles: percentage border-radius (50% avatars) resolved against element size; elliptical radii
  • parseBoxShadow: detects trailing inset (real browsers serialize it last — inset shadows were converted to outer shadows); handles shadow lists
  • All exported generators sanitize pixelSize (0 used to hang the tab or throw)

@react-pixel-ui/react (minor)

  • <Pixel>: shadow wrapper no longer remounts the child DOM node on re-measures (input state/focus survive); ResizeObserver re-measures on size changes and zero-size mounts (display:none tabs now get art when shown); unparseable backgrounds left untouched
  • usePixelRef: observer lifecycle moved into an effect — React 18 StrictMode no longer permanently disconnects it after mount; borderless elements keep their (pixelated) shadow instead of losing it
  • <PixelBox>: responsive actually works (no more self-pinned 200×100); style/className consistently land on the root element
  • <PixelButton>: unknown variant falls back to primary (no TypeError); type="button" default; proper displayNames
  • PixelConfigProvider: config prop optional
  • usePixelArt: inline borderRadius arrays no longer defeat the memo

Packaging / Infra

  • Per-condition exports types (.d.mts for ESM) — attw now reports no problems (was "Masquerading as CJS", which let TS approve code that crashed at runtime under nodenext)
  • "use client" via esbuild banner — source maps no longer off by one line
  • release.yml: removed the template placeholder my-slack-bot step that made every successful npm publish run show as failed; release script now runs tests before changeset publish; frozen lockfile
  • actions/checkout, setup-node, cache → v5 (Node 20 runtime deprecation)
  • eslint-config / typescript-config marked private (both had leaked to npm before)
  • changesets → changelog-github (linked changelogs; validated against this repo)
  • Removed stale scripts/update-configs.js (wrong paths, destructive), debug-test.js; fixed turbo outputs warnings; root CHANGELOG → stub + docs/MIGRATION.md

Test plan

  • Core: 125 tests (27 new — deflate round-trips incl. random/large data, gradient CSS Color 4 cases, shadow snapping, composite transparency/bail-out, want/cache)
  • React: 33 tests (8 new — DOM stability across re-measures, responsive unpinning, variant fallback, optional provider config)
  • attw clean on both packed tarballs (node10 / node16-CJS / node16-ESM / bundler)
  • Packed-tarball smoke test: ESM import, CJS require, SSR renderToString
  • changeset version dry-run with changelog-github validated
  • Full turbo build / lint / type-check green

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • 핵심 엔진의 DEFLATE 기반 PNG 압축으로 파일 크기 감소
    • generatePixelArt 옵션 추가로 불필요한 PNG 생성 스킵 가능
    • 입력 기반 LRU 캐싱으로 동일 결과 재생성 감소
    • 전체 CSS named colors 및 color(), color-mix() 함수 지원
    • Tailwind v4 보간 힌트 및 다양한 각도 단위 변환 지원
  • Bug Fixes

    • 색상 파싱 정확도 개선 (정규식 앵커링)
    • 그라디언트 스탑 처리 견고성 강화
    • 섀도우 오프셋 그리드 스냅 처리 개선
    • PixelBox 반응형 모드의 크기 추종 개선
    • PixelButton 미지정 variant 값 처리 개선
  • Documentation

    • v1→v2 마이그레이션 가이드 추가
    • API 및 제약 사항 설명 더욱 상세화
  • Chores

    • GitHub Actions 워크플로우 업데이트

…ity, packaging, CI

Core (@react-pixel-ui/core):
- PNG encoder: real DEFLATE (fixed Huffman + LZ77), 30-100x smaller data URLs
- generatePixelArt: options.want to skip unused PNG output + LRU result cache
- Colors: full 148 CSS named colors, color(srgb/srgb-linear/display-p3),
  anchored rgb()/rgba() patterns
- Gradients: 'in <colorspace>' interpolation hints (Tailwind v4),
  turn/rad/grad units, aspect-ratio magic corners, double-position stops,
  px-position stops, -1 sentinel fix
- pixel-shadow: zero offsets stay zero; nearest-grid rounding
- composite image: transparent interior for border-only elements; null for
  unparseable backgrounds (graceful degradation)
- parseComputedStyles: % border-radius resolution, elliptical radii
- parseBoxShadow: detect trailing 'inset' (computed-style order), shadow lists
- sanitizePixelSize guards in every generator (no more hangs on pixelSize 0)

React (@react-pixel-ui/react):
- Pixel: sticky shadow wrapper (no child DOM remount), ResizeObserver
  re-measure + zero-size mount retry, graceful degradation
- usePixelRef: observer lifecycle owned by effect (StrictMode-safe),
  borderless shadow fix, graceful degradation
- PixelBox: responsive mode unpinned (actually follows container),
  style/className land on root, displayName
- PixelButton: unknown-variant fallback, type=button default, displayName
- PixelConfigProvider: optional config prop
- usePixelArt: per-corner radius memo by value; skips composite output

Packaging & repo:
- exports: per-condition types (.d.mts for ESM) — fixes attw FalseCJS
- react tsup: sourcemap-aware 'use client' banner (maps no longer off by one)
- release.yml: remove placeholder my-slack-bot step that failed every
  publish run; frozen lockfile; release script now runs tests
- actions: checkout/setup-node/cache bumped to v5 (node24 runtime)
- eslint-config/typescript-config: private (no more accidental npm publish)
- changesets: changelog-github with repo links
- remove stale scripts/update-configs.js, debug-test.js; turbo outputs fixed
- root CHANGELOG → stub + docs/MIGRATION.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-pixel-ui Ready Ready Preview, Comment Jun 11, 2026 3:08pm

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Todari, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 39 minutes and 18 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8de7dc97-b364-4767-89cd-820d7ad04c34

📥 Commits

Reviewing files that changed from the base of the PR and between 2da26d1 and b0ef12c.

📒 Files selected for processing (2)
  • .github/workflows/ci.yml
  • .github/workflows/release.yml

Walkthrough

이 PR은 react-pixel-ui v2 릴리스를 위한 중대한 개선사항들을 담고 있습니다. Changesets 기반 자동 버저닝 설정, PNG DEFLATE 압축, CSS Color 4 기반 색상/그라디언트 파싱 강화, React 컴포넌트의 ResizeObserver 기반 라이프사이클 안정화, 그리고 responsive 크기 조정 개선이 핵심입니다.

Changes

Release Infrastructure & Configuration

Layer / File(s) Summary
Changesets and GitHub Actions upgrades
.changeset/config.json, .github/workflows/ci.yml, .github/workflows/release.yml
Changesets를 GitHub 기반 changelog 생성으로 설정하고, GitHub Actions 워크플로우를 v4에서 v5로 업그레이드하며 release에서 frozen-lockfile 강제 적용.
Build scripts and package configuration updates
package.json, scripts/, packages/eslint-config/, packages/typescript-config/, turbo.json
기존 config sync 스크립트 제거, npm release 스크립트에 테스트 단계 추가, 내부 패키지를 private으로 표시, turbo 캐시 범위 정리.
Package.json exports for ESM/CJS
packages/core/package.json, packages/react/package.json
ESM과 CJS용 타입 정의 파일 분리 (.mts vs .ts)와 조건부 entry point 맵핑.

Core Package - Utilities & Fundamentals

Layer / File(s) Summary
Color parsing with CSS Color 4 support
packages/core/src/utils/color.ts, packages/core/src/utils/color.test.ts
CSS named-color 전체 지원 (packed hex 룩업), color() 함수 파싱 (srgb/srgb-linear/display-p3), rgb/rgba 정규식 앵커링으로 그라디언트 내부 오탐 방지.
Gradient parsing with corner angles and robustness
packages/core/src/utils/gradient-parser.ts, packages/core/src/utils/gradient-parser.test.ts
요소 aspect ratio 기반 코너 각도 해석, Tailwind v4 보간 메서드 제거, turn/rad/grad 단위 변환, double-position stop 확장, px 위치 처리 개선.
Box-shadow parsing with multiple shadows
packages/core/src/utils/box-shadow-parser.ts
괄호 중첩을 고려한 여러 섀도우 분리, inset 키워드 위치 유연한 탐지, 임의 위치 색상 추출, zero-offset 섀도우 필터링.
Pixel size sanitization utility
packages/core/src/utils/math.ts, 제너레이터들 (pixel-gradient.ts, pixel-shadow.ts, staircase-polygon.ts, stepped-gradient.ts)
Positive integer (최소 1)로 강제하는 sanitizePixelSize 유틸, 모든 제너레이터에서 0으로 인한 division/infinite loop 방지.
Computed style parsing with border-radius improvements
packages/core/src/computed-style-parser.ts
요소 border-box 크기 기준 퍼센트 border-radius 해석, per-corner 반지름 지원, 레거시 폴백.

Core Package - Generators & Composition

Layer / File(s) Summary
Composite pixel image generation
packages/core/src/generators/composite-pixel-image.ts, packages/core/src/generators/composite-pixel-image.test.ts
정규화된 pixelSize로 그리드 계산, 파싱 불가 배경 시 null 반환으로 원본 CSS 보존, border-only 모드에서 투명 interior, graceful degradation 테스트 추가.
Pixel gradient and shadow generation
packages/core/src/generators/pixel-gradient.ts, packages/core/src/generators/pixel-shadow.ts, packages/core/src/generators/pixel-shadow.test.ts
sanitizePixelSize 적용, resolveGradientAngle 기반 선형 그라디언트 각도, sampleGradient2D export, zero-offset 섀도우 필터 제거.
Staircase polygon and stepped gradient
packages/core/src/generators/staircase-polygon.ts, packages/core/src/generators/stepped-gradient.ts
sanitizePixelSize 통합, resolveGradientAngle 기반 각도 계산, 밴드 수 정규화.
Pixel art composition with want option and LRU caching
packages/core/src/composer.ts, packages/core/src/composer.test.ts
`want: 'styles'
PNG encoding with DEFLATE compression
packages/core/src/utils/png.ts, packages/core/src/utils/png.test.ts
Fixed Huffman DEFLATE (hash chain LZ77) + zlib 래퍼 (ADLER32), stored 블록 폴백, Uint8Array 중심 구조, chunk별 CRC32, 압축 검증 테스트.

Core Package - Public API

Layer / File(s) Summary
Core package public exports
packages/core/src/index.ts, packages/core/src/types.ts
GeneratePixelArtOptions, ElementSize, GradientCorner 타입 re-export, resolveGradientAngle, sanitizePixelSize, sampleGradient2D 유틸 export, corner 필드 추가.

React Components

Layer / File(s) Summary
Pixel component ResizeObserver and DOM stability
packages/react/src/components/Pixel.tsx, packages/react/src/components/Pixel.test.tsx
ResizeObserver로 child 크기 변화 감지, 재측정 시 DOM 리마운트 방지 (wrapper stickiness), shadow wrapper 이전 렌더 시 display:contents 유지.
PixelBox responsive sizing
packages/react/src/components/PixelBox.tsx, packages/react/src/components/PixelBox.test.tsx
Responsive 모드에서 사용자 CSS 크기 추종 (px 강제 제거), user style 병합, displayName 설정, 루트 요소로 props 적용.
PixelButton robustness
packages/react/src/components/PixelButton.tsx, packages/react/src/components/PixelButton.test.tsx
Unknown variant fallback (primary + dev warning), 기본 type="button" (폼 제출 방지), displayName 설정.
PixelConfigProvider optional config
packages/react/src/context/PixelConfigProvider.tsx, packages/react/src/context/PixelConfigProvider.test.tsx
Config 선택사항으로 변경, 기본값 {} 설정, built-in 기본값 사용 가능.

React Hooks

Layer / File(s) Summary
usePixelRef lifecycle refactoring
packages/react/src/hooks/usePixelRef.ts
Ref 콜백 → useIsomorphicLayoutEffect 기반으로 전환 (StrictMode freezing 방지), 단일 effect로 capture/restore/cleanup 통합, padding 스냅샷 제거, composite 모드 지원.
usePixelArt memoization and caching
packages/react/src/hooks/usePixelArt.ts
BorderRadius per-corner 정규화, 반지름 값 기반 memoization, want:'styles' 호출, 조건부 shadow 객체 포함.
useResponsiveSize border-box measurement
packages/react/src/hooks/useResponsiveSize.ts
BorderBoxSize 우선, offsetWidth/offsetHeight 폴백, measure() 헬퍼, update() 배칭으로 중복 상태 업데이트 방지.
tsup banner configuration
packages/react/tsup.config.ts
"use client" 지시문을 번들 stage에서 삽입 (post-build 파일 조작 제거), source map 오프셋 개선.

Documentation

Layer / File(s) Summary
v1→v2 migration guide
docs/MIGRATION.md
Breaking changes, 신규 기능, usePixelCSS → PixelBox/usePixelArt 전환 코드 예제.
README updates
README.md
PixelBox responsive 동작, PixelButton 기본 type, 지원 CSS 값 상세화, graceful degradation 설명.
Changelog and release notes
CHANGELOG.md, .changeset/core-audit-improvements.md, .changeset/react-audit-improvements.md
Changesets 기반 per-package 관리, 패키지별 상세 변경 사항.

🎯 4 (Complex) | ⏱️ ~60 minutes

🐰 Pixel dreams in vectors bright,
Compressed to DEFLATE's delight,
Gradients bend by corner's might,
React hooks dance through the night!
Colors parsed with Color 4 sight! 🎨✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 주요 변경 사항을 정확하게 요약합니다. 'comprehensive audit', 'core correctness', '30-100x smaller PNGs', 'react stability', 'packaging & CI fixes'는 모두 제공된 raw_summary와 pr_objectives에서 확인할 수 있는 실제 변경 내용을 반영합니다.
Docstring Coverage ✅ Passed Docstring coverage is 80.43% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/audit-improvements

… corepack after)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
packages/react/src/components/Pixel.tsx (1)

117-148: ⚡ Quick win

ResizeObserver가 artState 변경 시마다 재생성됩니다.

artState가 의존성 배열에 포함되어 있어, 크기가 변경될 때마다 observer가 disconnect/reconnect됩니다. 콜백 내부에서 artState를 ref로 접근하면 observer 재생성 없이 최신 값을 참조할 수 있습니다.

♻️ ref를 사용한 개선안
+  const artStateRef = useRef<ArtState | null>(null);
+  artStateRef.current = artState;
+
   useEffect(() => {
     if (!enabled) return;
     if (typeof ResizeObserver === 'undefined') return;
     const el = childRef.current;
     if (!el) return;

     let rafId: number | null = null;
     const observer = new ResizeObserver(() => {
       if (rafId !== null) return;
       rafId = requestAnimationFrame(() => {
         rafId = null;
         const node = childRef.current;
         if (!node) return;
         const w = node.offsetWidth;
         const h = node.offsetHeight;
-        if (artState) {
-          if (w !== artState.width || h !== artState.height) {
+        const currentArt = artStateRef.current;
+        if (currentArt) {
+          if (w !== currentArt.width || h !== currentArt.height) {
             setArtState(null);
             setMeasureToken((t) => t + 1);
           }
         } else if (w > 0 && h > 0) {
           setMeasureToken((t) => t + 1);
         }
       });
     });
     observer.observe(el);

     return () => {
       if (rafId !== null) cancelAnimationFrame(rafId);
       observer.disconnect();
     };
-  }, [enabled, artState]);
+  }, [enabled]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/react/src/components/Pixel.tsx` around lines 117 - 148, The effect
recreates the ResizeObserver on every artState change because artState is in the
dependency array; instead keep the observer stable by removing artState from the
deps and reading the latest value via a ref: create an artStateRef (synchronized
whenever artState changes) and use artStateRef.current inside the ResizeObserver
callback to compare widths/heights and decide whether to setArtState(null) or
setMeasureToken(...); keep other logic and cleanup (cancelAnimationFrame,
observer.disconnect) the same and ensure artStateRef is updated whenever
artState changes so the observer always sees the latest value without being
recreated.
packages/core/src/composer.ts (1)

79-79: 💤 Low value

JSON.stringify 키 생성 시 프로퍼티 순서에 따른 캐시 미스 가능성

JSON.stringify(config)는 객체 프로퍼티 생성 순서에 민감할 수 있습니다. 예를 들어 {borderColor: 'red', borderWidth: 1}{borderWidth: 1, borderColor: 'red'}는 동일한 설정이지만 다른 캐시 키를 생성할 수 있습니다.

현재 V8 엔진은 삽입 순서를 보존하므로 실무에서는 문제가 적지만, 캐시 적중률이 예상보다 낮다면 정규화된 키 생성(예: 정렬된 키 기반)을 고려할 수 있습니다. LRU 캐시이므로 오탐(false positive)이 아닌 누락(false negative)만 발생해 정확성에는 영향이 없습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/composer.ts` at line 79, The cache key uses
JSON.stringify(config) which is sensitive to object property order and can cause
cache misses; replace that with a deterministic serialization of config (e.g.,
implement or use a stable stringify that sorts object keys recursively) and use
it in the cacheKey expression (keep the rest:
`${width}|${height}|${want}|${...}`), ensuring the unique symbol names cacheKey,
width, height, want, and config are preserved while normalizing config before
concatenation.
packages/core/src/utils/png.ts (1)

222-259: 💤 Low value

LZ77 매칭 루프의 암묵적 경계 검사

Line 233의 data[j + l] === data[i + l] 비교에서 j + l >= n인 경우 data[j + l]undefined를 반환하며, 이는 data[i + l](유효한 숫자)와 비교 시 false가 되어 루프가 종료됩니다.

이는 JavaScript의 Uint8Array 범위 외 접근 동작에 의존하는 암묵적 안전 장치입니다. 테스트가 통과하고(대형 이미지 포함) 동작은 정확하지만, 명시적인 경계 검사(예: j + l < n)를 추가하면 의도가 더 명확해집니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/utils/png.ts` around lines 222 - 259, The inner
match-length loop in the LZ77 matcher (inside the while over i) relies on
out-of-bounds reads returning undefined; update the loop condition used when
computing l (currently inside the block starting with const h = hashAt(i) and
the inner while with data[j + l] === data[i + l]) to explicitly check bounds by
adding a j + l < n check (so it becomes while (l < maxLen && j + l < n && data[j
+ l] === data[i + l]) ...). This keeps the existing maxLen logic (Math.min(258,
n - i)) for i-side safety and only makes the j-side boundary explicit; no other
logic changes are required (keep prev[i] = head[h], head[h] = i and subsequent
match handling).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/MIGRATION.md`:
- Line 3: The migration note currently hard-codes the release date string
"v2.0.0 (2026-04-03)" in MIGRATION.md; update this to either the final release
date when known or replace the literal date with a placeholder token (e.g.,
"v2.0.0 (RELEASE_DATE)") so the document won't become stale while the PR is
open, and ensure any CI/docs generation that consumes this file can substitute
the real date later.

In `@packages/core/src/computed-style-parser.ts`:
- Around line 19-39: In resolveRadius, the current logic returns the raw
percentage number when size is missing which yields a unitless value; change the
percentage fallback so that when component.endsWith('%') and size is undefined
you return 0 (not n), ensuring percentage radii without size resolve to 0px;
update the function comment/JSDoc to note that percentage values require size
and will fallback to 0 when absent (function: resolveRadius, local symbols:
components, resolved).

In `@packages/core/src/utils/math.ts`:
- Around line 8-10: sanitizePixelSize currently lets Infinity (and non-finite
values) through because pixelSize || 4 doesn't guard against Infinity; update
sanitizePixelSize to treat only finite numeric pixelSize as valid and otherwise
fall back to 4, then floor and clamp to a minimum of 1. Specifically, in
function sanitizePixelSize, check Number.isFinite(pixelSize) (and that it's > 0)
before using it; otherwise use 4, then apply Math.floor and Math.max as before
to return a safe integer.

---

Nitpick comments:
In `@packages/core/src/composer.ts`:
- Line 79: The cache key uses JSON.stringify(config) which is sensitive to
object property order and can cause cache misses; replace that with a
deterministic serialization of config (e.g., implement or use a stable stringify
that sorts object keys recursively) and use it in the cacheKey expression (keep
the rest: `${width}|${height}|${want}|${...}`), ensuring the unique symbol names
cacheKey, width, height, want, and config are preserved while normalizing config
before concatenation.

In `@packages/core/src/utils/png.ts`:
- Around line 222-259: The inner match-length loop in the LZ77 matcher (inside
the while over i) relies on out-of-bounds reads returning undefined; update the
loop condition used when computing l (currently inside the block starting with
const h = hashAt(i) and the inner while with data[j + l] === data[i + l]) to
explicitly check bounds by adding a j + l < n check (so it becomes while (l <
maxLen && j + l < n && data[j + l] === data[i + l]) ...). This keeps the
existing maxLen logic (Math.min(258, n - i)) for i-side safety and only makes
the j-side boundary explicit; no other logic changes are required (keep prev[i]
= head[h], head[h] = i and subsequent match handling).

In `@packages/react/src/components/Pixel.tsx`:
- Around line 117-148: The effect recreates the ResizeObserver on every artState
change because artState is in the dependency array; instead keep the observer
stable by removing artState from the deps and reading the latest value via a
ref: create an artStateRef (synchronized whenever artState changes) and use
artStateRef.current inside the ResizeObserver callback to compare widths/heights
and decide whether to setArtState(null) or setMeasureToken(...); keep other
logic and cleanup (cancelAnimationFrame, observer.disconnect) the same and
ensure artStateRef is updated whenever artState changes so the observer always
sees the latest value without being recreated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 80a70e7d-f6ea-42ef-ac44-d3dab7fceadc

📥 Commits

Reviewing files that changed from the base of the PR and between aa74e91 and 2da26d1.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (48)
  • .changeset/config.json
  • .changeset/core-audit-improvements.md
  • .changeset/react-audit-improvements.md
  • .github/workflows/ci.yml
  • .github/workflows/release.yml
  • CHANGELOG.md
  • README.md
  • debug-test.js
  • docs/MIGRATION.md
  • package.json
  • packages/core/package.json
  • packages/core/src/composer.test.ts
  • packages/core/src/composer.ts
  • packages/core/src/computed-style-parser.ts
  • packages/core/src/generators/composite-pixel-image.test.ts
  • packages/core/src/generators/composite-pixel-image.ts
  • packages/core/src/generators/pixel-gradient.ts
  • packages/core/src/generators/pixel-shadow.test.ts
  • packages/core/src/generators/pixel-shadow.ts
  • packages/core/src/generators/staircase-polygon.ts
  • packages/core/src/generators/stepped-gradient.ts
  • packages/core/src/index.ts
  • packages/core/src/types.ts
  • packages/core/src/utils/box-shadow-parser.ts
  • packages/core/src/utils/color.test.ts
  • packages/core/src/utils/color.ts
  • packages/core/src/utils/gradient-parser.test.ts
  • packages/core/src/utils/gradient-parser.ts
  • packages/core/src/utils/math.ts
  • packages/core/src/utils/png.test.ts
  • packages/core/src/utils/png.ts
  • packages/eslint-config/package.json
  • packages/react/package.json
  • packages/react/src/components/Pixel.test.tsx
  • packages/react/src/components/Pixel.tsx
  • packages/react/src/components/PixelBox.test.tsx
  • packages/react/src/components/PixelBox.tsx
  • packages/react/src/components/PixelButton.test.tsx
  • packages/react/src/components/PixelButton.tsx
  • packages/react/src/context/PixelConfigProvider.test.tsx
  • packages/react/src/context/PixelConfigProvider.tsx
  • packages/react/src/hooks/usePixelArt.ts
  • packages/react/src/hooks/usePixelRef.ts
  • packages/react/src/hooks/useResponsiveSize.ts
  • packages/react/tsup.config.ts
  • packages/typescript-config/package.json
  • scripts/update-configs.js
  • turbo.json
💤 Files with no reviewable changes (2)
  • debug-test.js
  • scripts/update-configs.js

Comment thread docs/MIGRATION.md
@@ -0,0 +1,71 @@
# v1 → v2 Migration Guide

v2.0.0 (2026-04-03) was a full rewrite. This guide covers what changed

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

릴리스 날짜를 확인하세요.

문서에 2026-04-03이 v2.0.0 릴리스 날짜로 명시되어 있습니다. 현재 6월이고 PR이 아직 open 상태이므로, 실제 릴리스 시 날짜를 업데이트해야 할 수 있습니다. 또는 플레이스홀더를 사용하는 것도 고려하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/MIGRATION.md` at line 3, The migration note currently hard-codes the
release date string "v2.0.0 (2026-04-03)" in MIGRATION.md; update this to either
the final release date when known or replace the literal date with a placeholder
token (e.g., "v2.0.0 (RELEASE_DATE)") so the document won't become stale while
the PR is open, and ensure any CI/docs generation that consumes this file can
substitute the real date later.

Comment on lines +19 to +39
function resolveRadius(value: string, size?: ElementSize): number {
if (!value) return 0;
const components = value.trim().split(/\s+/).slice(0, 2);

const resolved = components.map((component, i) => {
const n = parseFloat(component);
if (Number.isNaN(n)) return 0;
if (component.endsWith('%')) {
if (!size) return n; // no size available — legacy px fallback
const reference = i === 0 ? size.width : size.height;
// A single "50%" component applies to both axes — use the smaller
// reference so the radius fits the element.
const ref =
components.length === 1 ? Math.min(size.width, size.height) : reference;
return (n / 100) * ref;
}
return n;
});

return Math.max(0, Math.min(...resolved));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

백분율 반지름의 폴백 동작이 잘못되었습니다.

27번 줄에서 size가 없을 때 백분율 값을 그대로 반환합니다(예: "50%"50). 이는 픽셀 값이 아니라 단위 없는 숫자이므로 호출자가 픽셀로 해석할 때 부정확한 반지름 계산을 야기합니다.

권장 수정 방안:

  • 옵션 1 (가장 안전): size 없이 백분율을 만나면 0을 반환
  • 옵션 2: parseComputedStylessize 매개변수를 필수로 변경
  • 옵션 3: 현재 동작을 JSDoc에 명확히 문서화하되, 이 경우에도 n 대신 0을 반환하는 것이 안전합니다.
🔧 제안 수정안 (옵션 1: 해석 불가 시 0 반환)
     if (component.endsWith('%')) {
-      if (!size) return n; // no size available — legacy px fallback
+      if (!size) return 0; // no size available — can't resolve percentage
       const reference = i === 0 ? size.width : size.height;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/computed-style-parser.ts` around lines 19 - 39, In
resolveRadius, the current logic returns the raw percentage number when size is
missing which yields a unitless value; change the percentage fallback so that
when component.endsWith('%') and size is undefined you return 0 (not n),
ensuring percentage radii without size resolve to 0px; update the function
comment/JSDoc to note that percentage values require size and will fallback to 0
when absent (function: resolveRadius, local symbols: components, resolved).

Comment on lines +8 to +10
export function sanitizePixelSize(pixelSize: number): number {
return Math.max(1, Math.floor(pixelSize || 4));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

sanitizePixelSizeInfinity를 안전값으로 정규화하지 못합니다.

Line 9에서 pixelSize || 4Infinity를 그대로 통과시키므로, 하위 연산(예: 그림자 스냅)에서 NaN 결과가 발생할 수 있습니다.

수정 예시
 export function sanitizePixelSize(pixelSize: number): number {
-  return Math.max(1, Math.floor(pixelSize || 4));
+  const fallback = 4;
+  const normalized = Number.isFinite(pixelSize) ? pixelSize : fallback;
+  return Math.max(1, Math.floor(normalized || fallback));
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function sanitizePixelSize(pixelSize: number): number {
return Math.max(1, Math.floor(pixelSize || 4));
}
export function sanitizePixelSize(pixelSize: number): number {
const fallback = 4;
const normalized = Number.isFinite(pixelSize) ? pixelSize : fallback;
return Math.max(1, Math.floor(normalized || fallback));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/utils/math.ts` around lines 8 - 10, sanitizePixelSize
currently lets Infinity (and non-finite values) through because pixelSize || 4
doesn't guard against Infinity; update sanitizePixelSize to treat only finite
numeric pixelSize as valid and otherwise fall back to 4, then floor and clamp to
a minimum of 1. Specifically, in function sanitizePixelSize, check
Number.isFinite(pixelSize) (and that it's > 0) before using it; otherwise use 4,
then apply Math.floor and Math.max as before to return a safe integer.

@Todari Todari merged commit 911fbb0 into main Jun 12, 2026
6 checks passed
@github-actions github-actions Bot mentioned this pull request Jun 12, 2026
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