Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ const config: Config = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
clearMocks: true,
moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
transform: {
'^.+\\.css\\.ts$': '@vanilla-extract/jest-transform',
'^.+\\.(ts|tsx|js|jsx)$': 'ts-jest',
},
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
collectCoverage: true,
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/index.{ts,tsx}'],
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/index.{ts,tsx}', '!src/styles/**/*.css.ts'],
coverageThreshold: {
global: {
statements: 90,
Expand Down
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@
"@types/react": "19.1.5",
"@types/react-dom": "19.1.5",
"@typescript-eslint/parser": "^8.34.1",
"autoprefixer": "^10.4.21",
"@vanilla-extract/css": "^1.17.5",
"@vanilla-extract/jest-transform": "^1.1.19",
"@vanilla-extract/rollup-plugin": "^1.5.0",
Comment on lines +63 to +65
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 | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

@vanilla-extract/css latest stable version 2025 2026 npm

💡 Result:

Latest stable @vanilla-extract/css on npm: 1.17.4. Install: npm i @vanilla-extract/css. [1]

Sources
[1] npm package page for @vanilla-extract/css.


🌐 Web query:

@vanilla-extract/jest-transform latest stable version 2025 2026 npm

💡 Result:

Latest stable on the npm registry: @vanilla-extract/jest-transform v1.1.17 (published June 12, 2025). [1][2]

Sources:

  • npm package page (version shown: 1.1.17). [1]
  • Snyk version listing (1.1.17 — published 12 Jun 2025). [2]

🌐 Web query:

@vanilla-extract/rollup-plugin latest stable version 2025 2026 npm

💡 Result:

As of Jan 9, 2026: the upstream releases page shows @vanilla-extract/rollup-plugin v1.4.2 (released Nov 14, 2025), while the npm registry page currently lists v1.4.1 as the published package. [1][2]

Sources:

  • GitHub releases (vanilla-extract): @vanilla-extract/rollup-plugin@1.4.2 (Nov 14, 2025). [1]
  • npm package page: @vanilla-extract/rollup-plugin — version 1.4.1 (published ~a month ago). [2]

🌐 Web query:

vanilla-extract security vulnerabilities CVE 2025 2026

💡 Result:

Short answer: There are no CVEs specifically assigned to the vanilla-extract project (packages like @vanilla-extract/css, /vite-plugin, /webpack-plugin, /esbuild-plugin) in 2025–2026 that I can find. However, there was a related npm supply‑chain incident: the unrelated package docusaurus-plugin-vanilla-extract was reported as malicious following an npm account takeover (advisory dated November 24, 2025). Security scanners (Snyk / ReversingLabs) report no known security issues for the official vanilla-extract packages. [1][2][3]

Sources:

  • GitLab advisory — docusaurus-plugin-vanilla-extract (supply‑chain malware, Nov 24, 2025). [1]
  • Snyk package pages for vanilla-extract plugins (no direct vulnerabilities reported). [2]
  • ReversingLabs / package scans for @vanilla-extract/css (no vulnerabilities detected). [3]

If you want, I can (a) search NVD/CVE databases for any newly published CVEs right now, or (b) list recommended mitigation steps (dependency scanning, lockfile pinning, rotate credentials) for supply‑chain incidents.


Update Vanilla Extract package versions—specified versions do not exist in npm registry.

The package.json references versions that have not been released:

  • @vanilla-extract/css: ^1.17.5 (latest is 1.17.4)
  • @vanilla-extract/jest-transform: ^1.1.19 (latest is 1.1.17)
  • @vanilla-extract/rollup-plugin: ^1.5.0 (latest is 1.4.2)

These will cause npm install to fail. Update to available versions: 1.17.4, 1.1.17, and 1.4.2 respectively. No security vulnerabilities were found in the official vanilla-extract packages.

🤖 Prompt for AI Agents
In @package.json around lines 63 - 65, Update the three vanilla-extract
dependency entries in package.json to published versions to avoid npm install
failures: change "@vanilla-extract/css" version to "1.17.4",
"@vanilla-extract/jest-transform" to "1.1.17", and
"@vanilla-extract/rollup-plugin" to "1.4.2" (replace the current ^1.17.5,
^1.1.19, ^1.5.0 entries respectively); run a fresh npm install and lockfile
update (npm install or yarn install) to regenerate package-lock.json/yarn.lock
and verify the project builds.

"eslint": "^9.29.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-import": "^2.32.0",
Expand All @@ -75,14 +77,11 @@
"jest": "^30.0.3",
"jest-environment-jsdom": "^30.0.2",
"lint-staged": "^16.1.2",
"postcss": "^8.5.6",
"prettier": "^3.6.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"rollup": "^4.43.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-postcss": "^4.0.2",
"sass": "^1.89.2",
"ts-jest": "^29.4.0",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
Expand Down
4,943 changes: 2,112 additions & 2,831 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

17 changes: 3 additions & 14 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import resolve from '@rollup/plugin-node-resolve';
import terser from '@rollup/plugin-terser';
import typescript from '@rollup/plugin-typescript';
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import postcss from 'rollup-plugin-postcss';
import autoprefixer from 'autoprefixer';
import { vanillaExtractPlugin } from '@vanilla-extract/rollup-plugin';

const extensions = ['.js', '.jsx', '.ts', '.tsx'];

Expand Down Expand Up @@ -50,19 +49,9 @@ export default {
presets: ['@babel/preset-env', '@babel/preset-react'],
}),

postcss({
plugins: [autoprefixer()],
extensions: ['.css', '.scss'],
minimize: true,
inject: true,
use: {
sass: {
silenceDeprecations: ['legacy-js-api'],
},
},
}),

terser(),

vanillaExtractPlugin(),
],
external: ['react', 'react-dom'],
};
4 changes: 3 additions & 1 deletion src/components/AnimatedSpan/AnimatedSpan.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { type CSSProperties, type FC, useCallback, useRef } from 'react';

import * as styles from '../../styles/motion.css.ts';

type Props = {
text: string;
style: CSSProperties;
Expand Down Expand Up @@ -31,7 +33,7 @@ export const AnimatedSpan: FC<Props> = ({ text, style, onAnimationEnd }) => {
}

return (
<span style={style} aria-hidden="true" onAnimationEnd={handleAnimationEnd}>
<span className={styles.motion} style={style} aria-hidden="true" onAnimationEnd={handleAnimationEnd}>
{text}
</span>
);
Expand Down
8 changes: 3 additions & 5 deletions src/components/NodeMotion/NodeMotion.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import '../../styles/animations.scss';
import '../../styles/motion.scss';

import { type FC, memo, useEffect } from 'react';

import { useAnimatedChildren } from '../../hooks/useAnimatedChildren';
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
import { useResolvedMotion } from '../../hooks/useResolvedMotion';
import { useValidation } from '../../hooks/useValidation';
import * as styles from '../../styles/motion.css.ts';
import type { NodeMotionProps } from '../../types';
import { splitNodeAndExtractText } from '../../utils/splitNodeAndExtractText';

Expand Down Expand Up @@ -110,14 +108,14 @@ export const NodeMotion: FC<NodeMotionProps> = memo(props => {

if (shouldAnimate) {
return (
<Tag ref={targetRef} className="node-motion" aria-label={text}>
<Tag ref={targetRef} aria-label={text}>
{animatedChildren}
</Tag>
);
}

return (
<Tag ref={targetRef} className="node-motion-inanimate" aria-label={text}>
<Tag ref={targetRef} className={styles.inanimate} aria-label={text}>
{children}
</Tag>
);
Expand Down
8 changes: 3 additions & 5 deletions src/components/TextMotion/TextMotion.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import '../../styles/animations.scss';
import '../../styles/motion.scss';

import { type FC, memo, useEffect } from 'react';

import { useAnimatedText } from '../../hooks/useAnimatedText';
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
import { useResolvedMotion } from '../../hooks/useResolvedMotion';
import { useValidation } from '../../hooks/useValidation';
import * as styles from '../../styles/motion.css.ts';
import type { TextMotionProps } from '../../types';
import { splitText } from '../../utils/splitText';

Expand Down Expand Up @@ -107,14 +105,14 @@ export const TextMotion: FC<TextMotionProps> = memo(props => {

if (shouldAnimate) {
return (
<Tag ref={targetRef} className="text-motion" aria-label={text}>
<Tag ref={targetRef} aria-label={text}>
{animatedText}
</Tag>
);
}

return (
<Tag ref={targetRef} className="text-motion-inanimate" aria-label={text}>
<Tag ref={targetRef} className={styles.inanimate} aria-label={text}>
{text}
</Tag>
);
Expand Down
87 changes: 87 additions & 0 deletions src/styles/animations.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { keyframes } from '@vanilla-extract/css';

export const fadeIn = keyframes({
from: { opacity: 'var(--fade-from, 0)' },
to: { opacity: 'var(--fade-to, 1)' },
});

export const fadeOut = keyframes({
from: { opacity: 'var(--fade-from, 1)' },
to: { opacity: 'var(--fade-to, 0)' },
});

export const slideUp = keyframes({
from: { transform: 'translateY(var(--slide-distance, 0.4rem))' },
to: { transform: 'translateY(0)' },
});

export const slideDown = keyframes({
from: { transform: 'translateY(var(--slide-distance, -0.4rem))' },
to: { transform: 'translateY(0)' },
});

export const slideRight = keyframes({
from: { transform: 'translateX(var(--slide-distance, -0.4rem))' },
to: { transform: 'translateX(0)' },
});

export const slideLeft = keyframes({
from: { transform: 'translateX(var(--slide-distance, 0.4rem))' },
to: { transform: 'translateX(0)' },
});

export const scaleIn = keyframes({
from: { transform: 'scale(var(--scale-from, 0.9))' },
to: { transform: 'scale(var(--scale-to, 1))' },
});

export const scaleOut = keyframes({
from: { transform: 'scale(var(--scale-from, 1))' },
to: { transform: 'scale(var(--scale-to, 0.9))' },
});

export const rotateClockwise = keyframes({
from: { transform: 'var(--rotate-from, rotate(0deg))' },
to: { transform: 'var(--rotate-to, rotate(360deg))' },
});

export const rotateCounterclockwise = keyframes({
from: { transform: 'var(--rotate-from, rotate(360deg))' },
to: { transform: 'var(--rotate-to, rotate(0deg))' },
});

export const bounceIn = keyframes({
'0%': { transform: 'scale(var(--bounce-from, 0.5))' },
'50%': { transform: 'scale(var(--bounce-mid, 1.05))' },
'100%': { transform: 'scale(var(--bounce-to, 1))' },
});

export const bounceOut = keyframes({
'0%': { transform: 'scale(var(--bounce-from, 1))' },
'50%': { transform: 'scale(var(--bounce-mid, 1.05))' },
'100%': { transform: 'scale(var(--bounce-to, 0.5))' },
});

export const elasticIn = keyframes({
'0%': { transform: 'scaleX(var(--elastic-from, 0))' },
'60%': { transform: 'scaleX(var(--elastic-mid1, 1.2))' },
'80%': { transform: 'scaleX(var(--elastic-mid2, 0.9))' },
'100%': { transform: 'scaleX(var(--elastic-to, 1))' },
});

export const elasticOut = keyframes({
'0%': { transform: 'scaleX(var(--elastic-from, 1))' },
'20%': { transform: 'scaleX(var(--elastic-mid1, 0.9))' },
'40%': { transform: 'scaleX(var(--elastic-mid2, 1.2))' },
'100%': { transform: 'scaleX(var(--elastic-to, 0))' },
});

export const flipIn = keyframes({
from: { transform: 'perspective(40rem) rotateY(var(--flip-from, 90deg))' },
to: { transform: 'perspective(40rem) rotateY(var(--flip-to, 0deg))' },
});

export const flipOut = keyframes({
from: { transform: 'perspective(40rem) rotateY(var(--flip-from, 0deg))' },
to: { transform: 'perspective(40rem) rotateY(var(--flip-to, 90deg))' },
});
Comment on lines +79 to +87
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

Perspective value uses unusual unit and may be too large.

The flip animations use perspective(40rem) which has two concerns:

  1. Unusual unit: Perspective is typically specified in px units, not rem. Using rem makes the perspective scale with the root font size, which could lead to inconsistent 3D effects across different devices or user font preferences.

  2. Large value: 40rem is extremely large (640px at default 16px font size). Standard perspective values for UI animations are typically 400-600px. A larger perspective creates a more subtle 3D effect, which may be intentional but should be documented or made customizable.

  3. Not customizable: Unlike other animation parameters, the perspective value is hardcoded and cannot be adjusted via CSS variables.

🎨 Proposed fix to use standard units and add customization
 export const flipIn = keyframes({
-  from: { transform: 'perspective(40rem) rotateY(var(--flip-from, 90deg))' },
-  to: { transform: 'perspective(40rem) rotateY(var(--flip-to, 0deg))' },
+  from: { transform: 'perspective(var(--flip-perspective, 400px)) rotateY(var(--flip-from, 90deg))' },
+  to: { transform: 'perspective(var(--flip-perspective, 400px)) rotateY(var(--flip-to, 0deg))' },
 });

 export const flipOut = keyframes({
-  from: { transform: 'perspective(40rem) rotateY(var(--flip-from, 0deg))' },
-  to: { transform: 'perspective(40rem) rotateY(var(--flip-to, 90deg))' },
+  from: { transform: 'perspective(var(--flip-perspective, 400px)) rotateY(var(--flip-from, 0deg))' },
+  to: { transform: 'perspective(var(--flip-perspective, 400px)) rotateY(var(--flip-to, 90deg))' },
 });
📝 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 const flipIn = keyframes({
from: { transform: 'perspective(40rem) rotateY(var(--flip-from, 90deg))' },
to: { transform: 'perspective(40rem) rotateY(var(--flip-to, 0deg))' },
});
export const flipOut = keyframes({
from: { transform: 'perspective(40rem) rotateY(var(--flip-from, 0deg))' },
to: { transform: 'perspective(40rem) rotateY(var(--flip-to, 90deg))' },
});
export const flipIn = keyframes({
from: { transform: 'perspective(var(--flip-perspective, 400px)) rotateY(var(--flip-from, 90deg))' },
to: { transform: 'perspective(var(--flip-perspective, 400px)) rotateY(var(--flip-to, 0deg))' },
});
export const flipOut = keyframes({
from: { transform: 'perspective(var(--flip-perspective, 400px)) rotateY(var(--flip-from, 0deg))' },
to: { transform: 'perspective(var(--flip-perspective, 400px)) rotateY(var(--flip-to, 90deg))' },
});
🤖 Prompt for AI Agents
In @src/styles/animations.css.ts around lines 79 - 87, The flip animations
flipIn and flipOut hardcode perspective(40rem); change this to use a pixel-based
default and make it customizable via a CSS variable (e.g.,
var(--flip-perspective, 600px)) so the perspective uses a standard px unit and a
reasonable default value; update both flipIn and flipOut to reference that
variable instead of 40rem and ensure the variable name is documented/consistent
with other style vars.

Loading