From cbceae9f3fce376cc8aea18e4dc3b79d20e392ec Mon Sep 17 00:00:00 2001 From: Enis Budancamanak Date: Fri, 3 Apr 2026 15:43:54 +0200 Subject: [PATCH 1/3] feat: add custom bezier curves and optimize plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add matchUtilities for arbitrary bezier values (bg-ease-to-r-[0.22,1,0.36,1]) - Cache gradient stops per easing (32 → 4 computations) - Remove redundant class name ternary - Derive constants from source types - Batch matchUtilities into single call - Add getCoordinatesFromControlPoints and parseBezierValues exports - Accept readonly tuples in getCoordinatesFromControlPoints - Replace toBeCloseTo with exact toBe in tests - Import DIRECTIONS in tests instead of duplicating - Add interactive bezier curve editor to playground - Document custom bezier in utilities, examples, getting started - Update landing page hero and install button with PM tabs - Replace pseudo-element patterns with absolute divs - Add shared gradient-utils.ts and extend constants.ts - Add GitHub API error logging for star count debugging --- .changeset/custom-bezier-and-optimizations.md | 36 ++ .claude/skills/tw-easing-gradients/SKILL.md | 9 + apps/docs/.velite/docs.json | 30 +- apps/docs/.velite/index.js | 2 +- apps/docs/src/content/examples.md | 27 +- apps/docs/src/content/getting-started.md | 8 + apps/docs/src/content/index.md | 1 + apps/docs/src/content/utilities.md | 11 + .../src/lib/components/InstallButton.svelte | 80 ++-- .../docs/examples/PseudoOverlay.svelte | 16 +- .../lib/components/landing/HeroSection.svelte | 2 +- .../landing/PseudoElementSection.svelte | 8 +- apps/docs/src/lib/constants.ts | 23 ++ apps/docs/src/lib/gradient-utils.ts | 40 ++ .../src/routes/(docs)/playground/+page.svelte | 364 ++++++------------ .../docs/src/routes/(landing)/+page.server.ts | 8 +- .../src/routes/api/search.json/search.json | 2 +- packages/tw-easing-gradients/README.md | 13 +- packages/tw-easing-gradients/dist/index.cjs | 85 ++-- packages/tw-easing-gradients/dist/index.d.cts | 4 +- packages/tw-easing-gradients/dist/index.d.ts | 4 +- packages/tw-easing-gradients/dist/index.js | 81 ++-- .../tw-easing-gradients/src/easing.test.ts | 80 +++- packages/tw-easing-gradients/src/easing.ts | 38 +- .../tw-easing-gradients/src/index.test.ts | 221 ++++------- packages/tw-easing-gradients/src/index.ts | 76 ++-- .../src/integration.test.ts | 97 ++--- 27 files changed, 720 insertions(+), 646 deletions(-) create mode 100644 .changeset/custom-bezier-and-optimizations.md create mode 100644 apps/docs/src/lib/gradient-utils.ts diff --git a/.changeset/custom-bezier-and-optimizations.md b/.changeset/custom-bezier-and-optimizations.md new file mode 100644 index 0000000..aeb5a2e --- /dev/null +++ b/.changeset/custom-bezier-and-optimizations.md @@ -0,0 +1,36 @@ +--- +"tw-easing-gradients": minor +--- + +feat: add custom bezier curves and optimize plugin + +### Custom Bezier Curves + +Use arbitrary easing values with bracket notation: + +```html +
+``` + +The four values are cubic bezier control points (x1, y1, x2, y2). + +### New Exports + +- `getCoordinatesFromControlPoints(controlPoints, stops)` — compute easing coordinates from raw control points +- `parseBezierValues(input)` — parse comma-separated bezier strings + +### Optimizations + +- Gradient stops now cached per easing (4 computations instead of 32) +- Removed redundant class name ternary +- Constants derived from source types instead of manually duplicated +- `matchUtilities` batched into single call +- `getCoordinatesFromControlPoints` accepts `readonly` tuples (no unnecessary type casts) + +### Docs + +- Interactive custom bezier curve editor in playground +- Custom bezier documented in utilities, examples, and getting started +- Landing page updated with custom curves mention +- Pseudo-element examples replaced with absolute div pattern +- Package manager tabs on install button (pnpm/npm/yarn/bun) diff --git a/.claude/skills/tw-easing-gradients/SKILL.md b/.claude/skills/tw-easing-gradients/SKILL.md index 9a46aa0..8c630ef 100644 --- a/.claude/skills/tw-easing-gradients/SKILL.md +++ b/.claude/skills/tw-easing-gradients/SKILL.md @@ -107,6 +107,15 @@ Four easing functions available: | `to-bl` | to bottom left | | `to-br` | to bottom right | +## Custom Bezier + +Arbitrary easing curves via bracket notation: + +```html +
+
+``` + ## Configuration Configure gradient stops in CSS (default: 15): diff --git a/apps/docs/.velite/docs.json b/apps/docs/.velite/docs.json index e0cad39..76b1c7e 100644 --- a/apps/docs/.velite/docs.json +++ b/apps/docs/.velite/docs.json @@ -73,8 +73,8 @@ "title": "Examples", "description": "Practical use cases for easing gradients", "path": "examples", - "content": "\n

Hero Image Overlay

\n

Create smooth fade overlays on hero images:

\n\n
<div class=\"relative h-96\">\n\t<img src=\"/hero.jpg\" class=\"absolute inset-0 h-full w-full object-cover\" />\n\t<div class=\"absolute inset-0 bg-ease-to-t from-zinc-950\"></div>\n\t<div class=\"absolute bottom-8 left-8\">\n\t\t<h1 class=\"text-4xl font-bold text-white\">Hero Title</h1>\n\t</div>\n</div>\n
\n

Pseudo-Element Overlays

\n

Apply gradients to ::before and ::after pseudo-elements for dual overlays:

\n\n
<div\n\tclass=\"relative bg-white before:absolute before:inset-x-0 before:top-0 before:z-10 before:h-1/3\n    before:bg-ease-to-b before:from-black before:to-transparent before:content-['']\n    after:absolute after:inset-x-0 after:bottom-0 after:z-10 after:h-1/3\n    after:bg-ease-to-t after:from-black after:to-transparent after:content-['']\"\n>\n\t<span class=\"relative z-20\">Content</span>\n</div>\n
\n

Card Gradient Background

\n

Smooth color transitions for card backgrounds:

\n\n
<div class=\"rounded-xl bg-ease-in-out-to-br from-violet-600 to-pink-500 p-6\">\n\t<h2 class=\"text-xl font-semibold text-white\">Card Title</h2>\n\t<p class=\"text-white/80\">Card content goes here.</p>\n</div>\n
\n

Text Gradient

\n

Apply eased gradients to text:

\n\n
<h1\n\tclass=\"bg-ease-in-out-to-r from-indigo-500 to-cyan-500 bg-clip-text text-5xl font-bold text-transparent\"\n>\n\tGradient Text\n</h1>\n
\n

Fade Out Effect

\n

Smooth content fade at the bottom of a scrollable container:

\n\n
<div class=\"relative h-64 overflow-hidden\">\n\t<div class=\"h-full overflow-y-auto\">\n\t\t<!-- Long content here -->\n\t</div>\n\t<div\n\t\tclass=\"pointer-events-none absolute right-0 bottom-0 left-0 h-24 bg-ease-in-to-t from-background\"\n\t></div>\n</div>\n
\n

Button Hover Effect

\n

Gradient buttons with smooth transitions:

\n\n
<button\n\tclass=\"rounded-lg bg-ease-to-r from-blue-600 to-purple-600 px-6 py-3 font-medium text-white\"\n>\n\tClick Me\n</button>\n
\n

Loading Skeleton

\n

Animated loading placeholder with smooth gradients:

\n\n
<div\n\tclass=\"h-4 animate-pulse rounded bg-ease-in-out-to-r from-zinc-200 to-zinc-300\"\n></div>\n
\n\nThe `via-*` utility is not supported with easing gradients. Use only `from-*` and `to-*` for color stops.\n\n

Next Steps

\n\n\t\n\t\tConfiguration options and technical details.\n\t\n\t\n\t\tExperiment with easing gradients interactively.\n\t\n
", - "raw": "\n\n## Hero Image Overlay\n\nCreate smooth fade overlays on hero images:\n\n\n\n```html\n
\n\t\n\t
\n\t
\n\t\t

Hero Title

\n\t
\n
\n```\n\n## Pseudo-Element Overlays\n\nApply gradients to `::before` and `::after` pseudo-elements for dual overlays:\n\n\n\n```html\n\n\tContent\n\n```\n\n## Card Gradient Background\n\nSmooth color transitions for card backgrounds:\n\n\n\n```html\n
\n\t

Card Title

\n\t

Card content goes here.

\n
\n```\n\n## Text Gradient\n\nApply eased gradients to text:\n\n\n\n```html\n\n\tGradient Text\n\n```\n\n## Fade Out Effect\n\nSmooth content fade at the bottom of a scrollable container:\n\n\n\n```html\n
\n\t
\n\t\t\n\t
\n\t
\n\n```\n\n## Button Hover Effect\n\nGradient buttons with smooth transitions:\n\n\n\n```html\n\n\tClick Me\n\n```\n\n## Loading Skeleton\n\nAnimated loading placeholder with smooth gradients:\n\n\n\n```html\n\n```\n\n\nThe `via-*` utility is not supported with easing gradients. Use only `from-*` and `to-*` for color stops.\n\n\n## Next Steps\n\n\n\t\n\t\tConfiguration options and technical details.\n\t\n\t\n\t\tExperiment with easing gradients interactively.\n\t\n", + "content": "\n

Hero Image Overlay

\n

Create smooth fade overlays on hero images:

\n\n
<div class=\"relative h-96\">\n\t<img src=\"/hero.jpg\" class=\"absolute inset-0 h-full w-full object-cover\" />\n\t<div class=\"absolute inset-0 bg-ease-to-t from-zinc-950\"></div>\n\t<div class=\"absolute bottom-8 left-8\">\n\t\t<h1 class=\"text-4xl font-bold text-white\">Hero Title</h1>\n\t</div>\n</div>\n
\n

Dual Overlay

\n

Top and bottom fade overlays using absolute positioned divs:

\n\n
<div class=\"relative bg-white\">\n\t<div class=\"absolute inset-x-0 top-0 z-10 h-1/3 bg-ease-to-b from-black to-transparent\"></div>\n\t<div class=\"absolute inset-x-0 bottom-0 z-10 h-1/3 bg-ease-to-t from-black to-transparent\"></div>\n\t<span class=\"relative z-20\">Content</span>\n</div>\n
\n

Card Gradient Background

\n

Smooth color transitions for card backgrounds:

\n\n
<div class=\"rounded-xl bg-ease-in-out-to-br from-violet-600 to-pink-500 p-6\">\n\t<h2 class=\"text-xl font-semibold text-white\">Card Title</h2>\n\t<p class=\"text-white/80\">Card content goes here.</p>\n</div>\n
\n

Text Gradient

\n

Apply eased gradients to text:

\n\n
<h1\n\tclass=\"bg-ease-in-out-to-r from-indigo-500 to-cyan-500 bg-clip-text text-5xl font-bold text-transparent\"\n>\n\tGradient Text\n</h1>\n
\n

Fade Out Effect

\n

Smooth content fade at the bottom of a scrollable container:

\n\n
<div class=\"relative h-64 overflow-hidden\">\n\t<div class=\"h-full overflow-y-auto\">\n\t\t<!-- Long content here -->\n\t</div>\n\t<div\n\t\tclass=\"pointer-events-none absolute right-0 bottom-0 left-0 h-24 bg-ease-in-to-t from-background\"\n\t></div>\n</div>\n
\n

Button Hover Effect

\n

Gradient buttons with smooth transitions:

\n\n
<button\n\tclass=\"rounded-lg bg-ease-to-r from-blue-600 to-purple-600 px-6 py-3 font-medium text-white\"\n>\n\tClick Me\n</button>\n
\n

Loading Skeleton

\n

Animated loading placeholder with smooth gradients:

\n\n
<div\n\tclass=\"h-4 animate-pulse rounded bg-ease-in-out-to-r from-zinc-200 to-zinc-300\"\n></div>\n
\n\nThe `via-*` utility is not supported with easing gradients. Use only `from-*` and `to-*` for color stops.\n\n

Custom Bezier

\n

Define your own easing curve using arbitrary values with four cubic bezier control points:

\n
<!-- Aggressive ease-out -->\n<div class=\"bg-ease-to-b-[0.22,1,0.36,1] from-black\">\n  <img src=\"hero.jpg\" class=\"w-full\" />\n</div>\n\n<!-- Bouncy overshoot -->\n<div class=\"bg-ease-to-r-[0.68,0,0.27,1.55] from-indigo-500 to-cyan-500\"></div>\n
\n

Next Steps

\n\n\t\n\t\tConfiguration options and technical details.\n\t\n\t\n\t\tExperiment with easing gradients interactively.\n\t\n
", + "raw": "\n\n## Hero Image Overlay\n\nCreate smooth fade overlays on hero images:\n\n\n\n```html\n
\n\t\n\t
\n\t
\n\t\t

Hero Title

\n\t
\n
\n```\n\n## Dual Overlay\n\nTop and bottom fade overlays using absolute positioned divs:\n\n\n\n```html\n
\n\t
\n\t
\n\tContent\n
\n```\n\n## Card Gradient Background\n\nSmooth color transitions for card backgrounds:\n\n\n\n```html\n
\n\t

Card Title

\n\t

Card content goes here.

\n
\n```\n\n## Text Gradient\n\nApply eased gradients to text:\n\n\n\n```html\n\n\tGradient Text\n\n```\n\n## Fade Out Effect\n\nSmooth content fade at the bottom of a scrollable container:\n\n\n\n```html\n
\n\t
\n\t\t\n\t
\n\t
\n\n```\n\n## Button Hover Effect\n\nGradient buttons with smooth transitions:\n\n\n\n```html\n\n\tClick Me\n\n```\n\n## Loading Skeleton\n\nAnimated loading placeholder with smooth gradients:\n\n\n\n```html\n\n```\n\n\nThe `via-*` utility is not supported with easing gradients. Use only `from-*` and `to-*` for color stops.\n\n\n## Custom Bezier\n\nDefine your own easing curve using arbitrary values with four cubic bezier control points:\n\n```html\n\n
\n \n
\n\n\n
\n```\n\n## Next Steps\n\n\n\t\n\t\tConfiguration options and technical details.\n\t\n\t\n\t\tExperiment with easing gradients interactively.\n\t\n", "toc": [ { "title": "Hero Image Overlay", @@ -82,8 +82,8 @@ "items": [] }, { - "title": "Pseudo-Element Overlays", - "url": "#pseudo-element-overlays", + "title": "Dual Overlay", + "url": "#dual-overlay", "items": [] }, { @@ -111,6 +111,11 @@ "url": "#loading-skeleton", "items": [] }, + { + "title": "Custom Bezier", + "url": "#custom-bezier", + "items": [] + }, { "title": "Next Steps", "url": "#next-steps", @@ -125,8 +130,8 @@ "title": "Getting Started", "description": "Install and configure tw-easing-gradients", "path": "getting-started", - "content": "\n\nInstall the package\n

<Tabs items={[\"pnpm\", \"npm\", \"yarn\"]}>\n

\n
pnpm add tw-easing-gradients\n
\n\n\n
npm install tw-easing-gradients\n
\n
\n\n
yarn add tw-easing-gradients\n
\n
\n\n

Add the plugin to your CSS

\n
/* app.css */\n@import 'tailwindcss';\n@plugin 'tw-easing-gradients';\n
\n

You can optionally configure the number of color stops:

\n
@plugin 'tw-easing-gradients' {\n\tstops: 20;\n}\n
\n

Use easing gradients

\n

Use the utilities together with Tailwind's from-* and to-* color utilities:

\n
<div class=\"bg-ease-to-b from-black to-transparent\">\n\t<!-- Smooth fade to transparent -->\n</div>\n
\n
\n\nThis plugin requires Tailwind CSS v4.\n\n\nThis plugin uses modern CSS features (oklch and color-mix). For older browsers, a simple linear gradient fallback is automatically included.\n\n

Next Steps

\n\n\t\n\t\tAll available easing gradient classes and directions.\n\t\n\t\n\t\tPractical use cases with live previews.\n\t\n\t\n\t\tConfiguration options and technical details.\n\t\n", - "raw": "\n\n\nInstall the package\n\n\n\n\n```bash\npnpm add tw-easing-gradients\n```\n\n\n\n\n```bash\nnpm install tw-easing-gradients\n```\n\n\n\n\n```bash\nyarn add tw-easing-gradients\n```\n\n\n\n\nAdd the plugin to your CSS\n\n```css\n/* app.css */\n@import 'tailwindcss';\n@plugin 'tw-easing-gradients';\n```\n\nYou can optionally configure the number of color stops:\n\n```css\n@plugin 'tw-easing-gradients' {\n\tstops: 20;\n}\n```\n\nUse easing gradients\n\nUse the utilities together with Tailwind's `from-*` and `to-*` color utilities:\n\n```html\n
\n\t\n
\n```\n\n
\n\n\nThis plugin requires Tailwind CSS v4.\n\n\n\nThis plugin uses modern CSS features (oklch and color-mix). For older browsers, a simple linear gradient fallback is automatically included.\n\n\n## Next Steps\n\n\n\t\n\t\tAll available easing gradient classes and directions.\n\t\n\t\n\t\tPractical use cases with live previews.\n\t\n\t\n\t\tConfiguration options and technical details.\n\t\n", + "content": "\n\nInstall the package\n

<Tabs items={[\"pnpm\", \"npm\", \"yarn\"]}>\n

\n
pnpm add tw-easing-gradients\n
\n\n\n
npm install tw-easing-gradients\n
\n
\n\n
yarn add tw-easing-gradients\n
\n
\n\n

Add the plugin to your CSS

\n
/* app.css */\n@import 'tailwindcss';\n@plugin 'tw-easing-gradients';\n
\n

You can optionally configure the number of color stops:

\n
@plugin 'tw-easing-gradients' {\n\tstops: 20;\n}\n
\n

Use easing gradients

\n

Use the utilities together with Tailwind's from-* and to-* color utilities:

\n
<div class=\"bg-ease-to-b from-black to-transparent\">\n\t<!-- Smooth fade to transparent -->\n</div>\n
\n

You can also use custom easing curves with arbitrary values:

\n
<div class=\"bg-ease-to-r-[0.22,1,0.36,1] from-black to-transparent\">\n\t<!-- Custom cubic bezier easing -->\n</div>\n
\n
\n\nThis plugin requires Tailwind CSS v4.\n\n\nThis plugin uses modern CSS features (oklch and color-mix). For older browsers, a simple linear gradient fallback is automatically included.\n\n

Next Steps

\n\n\t\n\t\tAll available easing gradient classes and directions.\n\t\n\t\n\t\tPractical use cases with live previews.\n\t\n\t\n\t\tConfiguration options and technical details.\n\t\n", + "raw": "\n\n\nInstall the package\n\n\n\n\n```bash\npnpm add tw-easing-gradients\n```\n\n\n\n\n```bash\nnpm install tw-easing-gradients\n```\n\n\n\n\n```bash\nyarn add tw-easing-gradients\n```\n\n\n\n\nAdd the plugin to your CSS\n\n```css\n/* app.css */\n@import 'tailwindcss';\n@plugin 'tw-easing-gradients';\n```\n\nYou can optionally configure the number of color stops:\n\n```css\n@plugin 'tw-easing-gradients' {\n\tstops: 20;\n}\n```\n\nUse easing gradients\n\nUse the utilities together with Tailwind's `from-*` and `to-*` color utilities:\n\n```html\n
\n\t\n
\n```\n\nYou can also use custom easing curves with arbitrary values:\n\n```html\n
\n\t\n
\n```\n\n
\n\n\nThis plugin requires Tailwind CSS v4.\n\n\n\nThis plugin uses modern CSS features (oklch and color-mix). For older browsers, a simple linear gradient fallback is automatically included.\n\n\n## Next Steps\n\n\n\t\n\t\tAll available easing gradient classes and directions.\n\t\n\t\n\t\tPractical use cases with live previews.\n\t\n\t\n\t\tConfiguration options and technical details.\n\t\n", "toc": [ { "title": "Next Steps", @@ -142,8 +147,8 @@ "title": "Introduction", "description": "Smooth easing gradients for Tailwind CSS v4", "path": "index", - "content": "\n\n

tw-easing-gradients creates smooth, perceptually uniform gradients for Tailwind CSS v4. Drag the slider above to see the difference.

\n\n

The Problem

\n

Standard CSS gradients use linear interpolation, which creates visible \"banding\" - harsh edges especially noticeable when fading to transparency or between certain color pairs.

\n

Card Overlays

\n

A common use case: text overlays on images. The linear gradient stays solid and doesn't blend, while the eased version fades naturally into the image:

\n\n

Features

\n
    \n
  • Smooth transitions - No more harsh color bands
  • \n
  • oklch color space - Colors blend naturally without getting gray or muddy in the middle
  • \n
  • Multiple easing functions - ease, ease-in, ease-out, ease-in-out
  • \n
  • All directions - 8 gradient directions supported
  • \n
  • Works with Tailwind colors - Uses existing from-* and to-* utilities
  • \n
  • Zero runtime JS - Pure CSS output
  • \n
\n

Quick Start

\n
<!-- Standard Tailwind -->\n<div class=\"bg-linear-to-b from-black to-transparent\"></div>\n\n<!-- With tw-easing-gradients -->\n<div class=\"bg-ease-to-b from-black to-transparent\"></div>\n
\n\n

Next Steps

\n\n\t\n\t\tInstall and configure tw-easing-gradients in your project.\n\t\n\t\n\t\tExperiment with easing gradients interactively.\n\t\n
", - "raw": "\n\n\n\n**tw-easing-gradients** creates smooth, perceptually uniform gradients for Tailwind CSS v4. Drag the slider above to see the difference.\n\n\n\n## The Problem\n\nStandard CSS gradients use linear interpolation, which creates visible \"banding\" - harsh edges especially noticeable when fading to transparency or between certain color pairs.\n\n## Card Overlays\n\nA common use case: text overlays on images. The linear gradient stays solid and doesn't blend, while the eased version fades naturally into the image:\n\n\n\n## Features\n\n- **Smooth transitions** - No more harsh color bands\n- **oklch color space** - Colors blend naturally without getting gray or muddy in the middle\n- **Multiple easing functions** - ease, ease-in, ease-out, ease-in-out\n- **All directions** - 8 gradient directions supported\n- **Works with Tailwind colors** - Uses existing `from-*` and `to-*` utilities\n- **Zero runtime JS** - Pure CSS output\n\n## Quick Start\n\n```html\n\n
\n\n\n
\n```\n\n\n\n## Next Steps\n\n\n\t\n\t\tInstall and configure tw-easing-gradients in your project.\n\t\n\t\n\t\tExperiment with easing gradients interactively.\n\t\n", + "content": "\n\n

tw-easing-gradients creates smooth, perceptually uniform gradients for Tailwind CSS v4. Drag the slider above to see the difference.

\n\n

The Problem

\n

Standard CSS gradients use linear interpolation, which creates visible \"banding\" - harsh edges especially noticeable when fading to transparency or between certain color pairs.

\n

Card Overlays

\n

A common use case: text overlays on images. The linear gradient stays solid and doesn't blend, while the eased version fades naturally into the image:

\n\n

Features

\n
    \n
  • Smooth transitions - No more harsh color bands
  • \n
  • oklch color space - Colors blend naturally without getting gray or muddy in the middle
  • \n
  • Multiple easing functions - ease, ease-in, ease-out, ease-in-out
  • \n
  • All directions - 8 gradient directions supported
  • \n
  • Custom bezier - Arbitrary easing curves via bg-ease-to-r-[0.22,1,0.36,1]
  • \n
  • Works with Tailwind colors - Uses existing from-* and to-* utilities
  • \n
  • Zero runtime JS - Pure CSS output
  • \n
\n

Quick Start

\n
<!-- Standard Tailwind -->\n<div class=\"bg-linear-to-b from-black to-transparent\"></div>\n\n<!-- With tw-easing-gradients -->\n<div class=\"bg-ease-to-b from-black to-transparent\"></div>\n
\n\n

Next Steps

\n\n\t\n\t\tInstall and configure tw-easing-gradients in your project.\n\t\n\t\n\t\tExperiment with easing gradients interactively.\n\t\n
", + "raw": "\n\n\n\n**tw-easing-gradients** creates smooth, perceptually uniform gradients for Tailwind CSS v4. Drag the slider above to see the difference.\n\n\n\n## The Problem\n\nStandard CSS gradients use linear interpolation, which creates visible \"banding\" - harsh edges especially noticeable when fading to transparency or between certain color pairs.\n\n## Card Overlays\n\nA common use case: text overlays on images. The linear gradient stays solid and doesn't blend, while the eased version fades naturally into the image:\n\n\n\n## Features\n\n- **Smooth transitions** - No more harsh color bands\n- **oklch color space** - Colors blend naturally without getting gray or muddy in the middle\n- **Multiple easing functions** - ease, ease-in, ease-out, ease-in-out\n- **All directions** - 8 gradient directions supported\n- **Custom bezier** - Arbitrary easing curves via `bg-ease-to-r-[0.22,1,0.36,1]`\n- **Works with Tailwind colors** - Uses existing `from-*` and `to-*` utilities\n- **Zero runtime JS** - Pure CSS output\n\n## Quick Start\n\n```html\n\n
\n\n\n
\n```\n\n\n\n## Next Steps\n\n\n\t\n\t\tInstall and configure tw-easing-gradients in your project.\n\t\n\t\n\t\tExperiment with easing gradients interactively.\n\t\n", "toc": [ { "title": "The Problem", @@ -179,8 +184,8 @@ "title": "Utilities", "description": "All available easing gradient utilities", "path": "utilities", - "content": "\n

Syntax

\n

Combine an easing function with a direction: bg-{easing}-to-{direction}

\n

For the default ease curve, the easing part is just ease: bg-ease-to-r. For all others, use the full name: bg-ease-in-to-r, bg-ease-out-to-b, bg-ease-in-out-to-br.

\n

Easing Functions

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
UtilityEasing FunctionDescription
bg-ease-to-{dir}easeDefault easing (slow-fast-slow)
bg-ease-in-to-{dir}ease-inSlow start, fast end
bg-ease-out-to-{dir}ease-outFast start, slow end
bg-ease-in-out-to-{dir}ease-in-outSlow start and end
\n

Directions

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
ShortDirectionCSS Value
tTopto top
rRightto right
bBottomto bottom
lLeftto left
tlTop Leftto top left
trTop Rightto top right
blBottom Leftto bottom left
brBottom Rightto bottom right
\n

Transparency Fades

\n

Skip to-* and the plugin fades to transparent automatically.

\n\n
<div class=\"bg-ease-to-t from-blue-500\"></div>\n
\n

Using with Tailwind Colors

\n\n
<div class=\"bg-ease-to-r from-indigo-500 to-cyan-500\"></div>\n<div class=\"bg-ease-out-to-b from-emerald-400 to-cyan-500\"></div>\n<div class=\"bg-ease-in-out-to-br from-amber-500 to-rose-600\"></div>\n
\n

Full Class Reference

\n

<Tabs items={[\"ease\", \"ease-in\", \"ease-out\", \"ease-in-out\"]}>\n

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
DirectionClass
Topbg-ease-to-t
Top Rightbg-ease-to-tr
Rightbg-ease-to-r
Bottom Rightbg-ease-to-br
Bottombg-ease-to-b
Bottom Leftbg-ease-to-bl
Leftbg-ease-to-l
Top Leftbg-ease-to-tl
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
DirectionClass
Topbg-ease-in-to-t
Top Rightbg-ease-in-to-tr
Rightbg-ease-in-to-r
Bottom Rightbg-ease-in-to-br
Bottombg-ease-in-to-b
Bottom Leftbg-ease-in-to-bl
Leftbg-ease-in-to-l
Top Leftbg-ease-in-to-tl
\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
DirectionClass
Topbg-ease-out-to-t
Top Rightbg-ease-out-to-tr
Rightbg-ease-out-to-r
Bottom Rightbg-ease-out-to-br
Bottombg-ease-out-to-b
Bottom Leftbg-ease-out-to-bl
Leftbg-ease-out-to-l
Top Leftbg-ease-out-to-tl
\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
DirectionClass
Topbg-ease-in-out-to-t
Top Rightbg-ease-in-out-to-tr
Rightbg-ease-in-out-to-r
Bottom Rightbg-ease-in-out-to-br
Bottombg-ease-in-out-to-b
Bottom Leftbg-ease-in-out-to-bl
Leftbg-ease-in-out-to-l
Top Leftbg-ease-in-out-to-tl
\n
\n\n

Next Steps

\n\n\t\n\t\tPractical use cases with live previews.\n\t\n\t\n\t\tConfiguration options and technical details.\n\t\n
", - "raw": "\n\n## Syntax\n\nCombine an easing function with a direction: `bg-{easing}-to-{direction}`\n\nFor the default `ease` curve, the easing part is just `ease`: `bg-ease-to-r`. For all others, use the full name: `bg-ease-in-to-r`, `bg-ease-out-to-b`, `bg-ease-in-out-to-br`.\n\n## Easing Functions\n\n\n\n| Utility | Easing Function | Description |\n| ------------------------- | --------------- | ------------------------------- |\n| `bg-ease-to-{dir}` | ease | Default easing (slow-fast-slow) |\n| `bg-ease-in-to-{dir}` | ease-in | Slow start, fast end |\n| `bg-ease-out-to-{dir}` | ease-out | Fast start, slow end |\n| `bg-ease-in-out-to-{dir}` | ease-in-out | Slow start and end |\n\n## Directions\n\n\n\n| Short | Direction | CSS Value |\n| ----- | ------------ | ----------------- |\n| `t` | Top | `to top` |\n| `r` | Right | `to right` |\n| `b` | Bottom | `to bottom` |\n| `l` | Left | `to left` |\n| `tl` | Top Left | `to top left` |\n| `tr` | Top Right | `to top right` |\n| `bl` | Bottom Left | `to bottom left` |\n| `br` | Bottom Right | `to bottom right` |\n\n## Transparency Fades\n\nSkip `to-*` and the plugin fades to transparent automatically.\n\n\n\n```html\n
\n```\n\n## Using with Tailwind Colors\n\n\n\n```html\n
\n
\n
\n```\n\n## Full Class Reference\n\n\n\n\n| Direction | Class |\n| ------------ | --------------- |\n| Top | `bg-ease-to-t` |\n| Top Right | `bg-ease-to-tr` |\n| Right | `bg-ease-to-r` |\n| Bottom Right | `bg-ease-to-br` |\n| Bottom | `bg-ease-to-b` |\n| Bottom Left | `bg-ease-to-bl` |\n| Left | `bg-ease-to-l` |\n| Top Left | `bg-ease-to-tl` |\n\n\n\n\n| Direction | Class |\n| ------------ | ------------------ |\n| Top | `bg-ease-in-to-t` |\n| Top Right | `bg-ease-in-to-tr` |\n| Right | `bg-ease-in-to-r` |\n| Bottom Right | `bg-ease-in-to-br` |\n| Bottom | `bg-ease-in-to-b` |\n| Bottom Left | `bg-ease-in-to-bl` |\n| Left | `bg-ease-in-to-l` |\n| Top Left | `bg-ease-in-to-tl` |\n\n\n\n\n| Direction | Class |\n| ------------ | ------------------- |\n| Top | `bg-ease-out-to-t` |\n| Top Right | `bg-ease-out-to-tr` |\n| Right | `bg-ease-out-to-r` |\n| Bottom Right | `bg-ease-out-to-br` |\n| Bottom | `bg-ease-out-to-b` |\n| Bottom Left | `bg-ease-out-to-bl` |\n| Left | `bg-ease-out-to-l` |\n| Top Left | `bg-ease-out-to-tl` |\n\n\n\n\n| Direction | Class |\n| ------------ | ---------------------- |\n| Top | `bg-ease-in-out-to-t` |\n| Top Right | `bg-ease-in-out-to-tr` |\n| Right | `bg-ease-in-out-to-r` |\n| Bottom Right | `bg-ease-in-out-to-br` |\n| Bottom | `bg-ease-in-out-to-b` |\n| Bottom Left | `bg-ease-in-out-to-bl` |\n| Left | `bg-ease-in-out-to-l` |\n| Top Left | `bg-ease-in-out-to-tl` |\n\n\n\n\n## Next Steps\n\n\n\t\n\t\tPractical use cases with live previews.\n\t\n\t\n\t\tConfiguration options and technical details.\n\t\n", + "content": "\n

Syntax

\n

Combine an easing function with a direction: bg-{easing}-to-{direction}

\n

For the default ease curve, the easing part is just ease: bg-ease-to-r. For all others, use the full name: bg-ease-in-to-r, bg-ease-out-to-b, bg-ease-in-out-to-br.

\n

Easing Functions

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
UtilityEasing FunctionDescription
bg-ease-to-{dir}easeDefault easing (slow-fast-slow)
bg-ease-in-to-{dir}ease-inSlow start, fast end
bg-ease-out-to-{dir}ease-outFast start, slow end
bg-ease-in-out-to-{dir}ease-in-outSlow start and end
\n

Directions

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
ShortDirectionCSS Value
tTopto top
rRightto right
bBottomto bottom
lLeftto left
tlTop Leftto top left
trTop Rightto top right
blBottom Leftto bottom left
brBottom Rightto bottom right
\n

Transparency Fades

\n

Skip to-* and the plugin fades to transparent automatically.

\n\n
<div class=\"bg-ease-to-t from-blue-500\"></div>\n
\n

Using with Tailwind Colors

\n\n
<div class=\"bg-ease-to-r from-indigo-500 to-cyan-500\"></div>\n<div class=\"bg-ease-out-to-b from-emerald-400 to-cyan-500\"></div>\n<div class=\"bg-ease-in-out-to-br from-amber-500 to-rose-600\"></div>\n
\n

Full Class Reference

\n

<Tabs items={[\"ease\", \"ease-in\", \"ease-out\", \"ease-in-out\"]}>\n

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
DirectionClass
Topbg-ease-to-t
Top Rightbg-ease-to-tr
Rightbg-ease-to-r
Bottom Rightbg-ease-to-br
Bottombg-ease-to-b
Bottom Leftbg-ease-to-bl
Leftbg-ease-to-l
Top Leftbg-ease-to-tl
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
DirectionClass
Topbg-ease-in-to-t
Top Rightbg-ease-in-to-tr
Rightbg-ease-in-to-r
Bottom Rightbg-ease-in-to-br
Bottombg-ease-in-to-b
Bottom Leftbg-ease-in-to-bl
Leftbg-ease-in-to-l
Top Leftbg-ease-in-to-tl
\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
DirectionClass
Topbg-ease-out-to-t
Top Rightbg-ease-out-to-tr
Rightbg-ease-out-to-r
Bottom Rightbg-ease-out-to-br
Bottombg-ease-out-to-b
Bottom Leftbg-ease-out-to-bl
Leftbg-ease-out-to-l
Top Leftbg-ease-out-to-tl
\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
DirectionClass
Topbg-ease-in-out-to-t
Top Rightbg-ease-in-out-to-tr
Rightbg-ease-in-out-to-r
Bottom Rightbg-ease-in-out-to-br
Bottombg-ease-in-out-to-b
Bottom Leftbg-ease-in-out-to-bl
Leftbg-ease-in-out-to-l
Top Leftbg-ease-in-out-to-tl
\n
\n\n

Custom Bezier

\n

Use arbitrary values to define your own easing curve with four control points:

\n
<div class=\"bg-ease-to-r-[0.22,1,0.36,1] from-black\"></div>\n<div class=\"bg-ease-to-b-[0.68,0,0.27,1.55] from-indigo-500 to-cyan-500\"></div>\n
\n

The four values correspond to the control points of a cubic bezier curve: x1, y1, x2, y2.

\n

Next Steps

\n\n\t\n\t\tPractical use cases with live previews.\n\t\n\t\n\t\tConfiguration options and technical details.\n\t\n
", + "raw": "\n\n## Syntax\n\nCombine an easing function with a direction: `bg-{easing}-to-{direction}`\n\nFor the default `ease` curve, the easing part is just `ease`: `bg-ease-to-r`. For all others, use the full name: `bg-ease-in-to-r`, `bg-ease-out-to-b`, `bg-ease-in-out-to-br`.\n\n## Easing Functions\n\n\n\n| Utility | Easing Function | Description |\n| ------------------------- | --------------- | ------------------------------- |\n| `bg-ease-to-{dir}` | ease | Default easing (slow-fast-slow) |\n| `bg-ease-in-to-{dir}` | ease-in | Slow start, fast end |\n| `bg-ease-out-to-{dir}` | ease-out | Fast start, slow end |\n| `bg-ease-in-out-to-{dir}` | ease-in-out | Slow start and end |\n\n## Directions\n\n\n\n| Short | Direction | CSS Value |\n| ----- | ------------ | ----------------- |\n| `t` | Top | `to top` |\n| `r` | Right | `to right` |\n| `b` | Bottom | `to bottom` |\n| `l` | Left | `to left` |\n| `tl` | Top Left | `to top left` |\n| `tr` | Top Right | `to top right` |\n| `bl` | Bottom Left | `to bottom left` |\n| `br` | Bottom Right | `to bottom right` |\n\n## Transparency Fades\n\nSkip `to-*` and the plugin fades to transparent automatically.\n\n\n\n```html\n
\n```\n\n## Using with Tailwind Colors\n\n\n\n```html\n
\n
\n
\n```\n\n## Full Class Reference\n\n\n\n\n| Direction | Class |\n| ------------ | --------------- |\n| Top | `bg-ease-to-t` |\n| Top Right | `bg-ease-to-tr` |\n| Right | `bg-ease-to-r` |\n| Bottom Right | `bg-ease-to-br` |\n| Bottom | `bg-ease-to-b` |\n| Bottom Left | `bg-ease-to-bl` |\n| Left | `bg-ease-to-l` |\n| Top Left | `bg-ease-to-tl` |\n\n\n\n\n| Direction | Class |\n| ------------ | ------------------ |\n| Top | `bg-ease-in-to-t` |\n| Top Right | `bg-ease-in-to-tr` |\n| Right | `bg-ease-in-to-r` |\n| Bottom Right | `bg-ease-in-to-br` |\n| Bottom | `bg-ease-in-to-b` |\n| Bottom Left | `bg-ease-in-to-bl` |\n| Left | `bg-ease-in-to-l` |\n| Top Left | `bg-ease-in-to-tl` |\n\n\n\n\n| Direction | Class |\n| ------------ | ------------------- |\n| Top | `bg-ease-out-to-t` |\n| Top Right | `bg-ease-out-to-tr` |\n| Right | `bg-ease-out-to-r` |\n| Bottom Right | `bg-ease-out-to-br` |\n| Bottom | `bg-ease-out-to-b` |\n| Bottom Left | `bg-ease-out-to-bl` |\n| Left | `bg-ease-out-to-l` |\n| Top Left | `bg-ease-out-to-tl` |\n\n\n\n\n| Direction | Class |\n| ------------ | ---------------------- |\n| Top | `bg-ease-in-out-to-t` |\n| Top Right | `bg-ease-in-out-to-tr` |\n| Right | `bg-ease-in-out-to-r` |\n| Bottom Right | `bg-ease-in-out-to-br` |\n| Bottom | `bg-ease-in-out-to-b` |\n| Bottom Left | `bg-ease-in-out-to-bl` |\n| Left | `bg-ease-in-out-to-l` |\n| Top Left | `bg-ease-in-out-to-tl` |\n\n\n\n\n## Custom Bezier\n\nUse arbitrary values to define your own easing curve with four control points:\n\n```html\n
\n
\n```\n\nThe four values correspond to the control points of a cubic bezier curve: `x1, y1, x2, y2`.\n\n## Next Steps\n\n\n\t\n\t\tPractical use cases with live previews.\n\t\n\t\n\t\tConfiguration options and technical details.\n\t\n", "toc": [ { "title": "Syntax", @@ -212,6 +217,11 @@ "url": "#full-class-reference", "items": [] }, + { + "title": "Custom Bezier", + "url": "#custom-bezier", + "items": [] + }, { "title": "Next Steps", "url": "#next-steps", diff --git a/apps/docs/.velite/index.js b/apps/docs/.velite/index.js index 63f52f3..1790c31 100644 --- a/apps/docs/.velite/index.js +++ b/apps/docs/.velite/index.js @@ -1,3 +1,3 @@ // This file is generated by Velite -export { default as docs } from './docs.json' \ No newline at end of file +export { default as docs } from './docs.json' with { type: 'json' } \ No newline at end of file diff --git a/apps/docs/src/content/examples.md b/apps/docs/src/content/examples.md index b98ff87..5e85290 100644 --- a/apps/docs/src/content/examples.md +++ b/apps/docs/src/content/examples.md @@ -31,19 +31,16 @@ Create smooth fade overlays on hero images: ``` -## Pseudo-Element Overlays +## Dual Overlay -Apply gradients to `::before` and `::after` pseudo-elements for dual overlays: +Top and bottom fade overlays using absolute positioned divs: ```html -
+
+
+
Content
``` @@ -122,6 +119,20 @@ Animated loading placeholder with smooth gradients: The `via-*` utility is not supported with easing gradients. Use only `from-*` and `to-*` for color stops. +## Custom Bezier + +Define your own easing curve using arbitrary values with four cubic bezier control points: + +```html + +
+ +
+ + +
+``` + ## Next Steps diff --git a/apps/docs/src/content/getting-started.md b/apps/docs/src/content/getting-started.md index 54f4973..fc57ea5 100644 --- a/apps/docs/src/content/getting-started.md +++ b/apps/docs/src/content/getting-started.md @@ -61,6 +61,14 @@ Use the utilities together with Tailwind's `from-*` and `to-*` color utilities:
``` +You can also use custom easing curves with arbitrary values: + +```html +
+ +
+``` + diff --git a/apps/docs/src/content/index.md b/apps/docs/src/content/index.md index 67e167e..6f7f6a3 100644 --- a/apps/docs/src/content/index.md +++ b/apps/docs/src/content/index.md @@ -34,6 +34,7 @@ A common use case: text overlays on images. The linear gradient stays solid and - **oklch color space** - Colors blend naturally without getting gray or muddy in the middle - **Multiple easing functions** - ease, ease-in, ease-out, ease-in-out - **All directions** - 8 gradient directions supported +- **Custom bezier** - Arbitrary easing curves via `bg-ease-to-r-[0.22,1,0.36,1]` - **Works with Tailwind colors** - Uses existing `from-*` and `to-*` utilities - **Zero runtime JS** - Pure CSS output diff --git a/apps/docs/src/content/utilities.md b/apps/docs/src/content/utilities.md index 7024399..dff7055 100644 --- a/apps/docs/src/content/utilities.md +++ b/apps/docs/src/content/utilities.md @@ -125,6 +125,17 @@ Skip `to-*` and the plugin fades to transparent automatically. +## Custom Bezier + +Use arbitrary values to define your own easing curve with four control points: + +```html +
+
+``` + +The four values correspond to the control points of a cubic bezier curve: `x1, y1, x2, y2`. + ## Next Steps diff --git a/apps/docs/src/lib/components/InstallButton.svelte b/apps/docs/src/lib/components/InstallButton.svelte index af56dcd..d39bfff 100644 --- a/apps/docs/src/lib/components/InstallButton.svelte +++ b/apps/docs/src/lib/components/InstallButton.svelte @@ -5,39 +5,59 @@ import X from 'phosphor-svelte/lib/X'; import { scale } from 'svelte/transition'; - interface Props { - command?: string; - } + const PKG = 'tw-easing-gradients'; - let { command = 'npm install tw-easing-gradients' }: Props = $props(); + const managers = [ + { label: 'pnpm', verb: 'add' }, + { label: 'npm', verb: 'install' }, + { label: 'yarn', verb: 'add' }, + { label: 'bun', verb: 'add' } + ]; + + let active = $state(0); + let pm = $derived(managers[active]); + let currentCommand = $derived(`${pm.label} ${pm.verb} ${PKG}`); const clipboard = new UseClipboard(); - + {/each} + + + + {#if clipboard.status === 'success'} +
+ +
+ {:else if clipboard.status === 'failure'} +
+ +
+ {:else} +
+ +
+ {/if} + + diff --git a/apps/docs/src/lib/components/docs/examples/PseudoOverlay.svelte b/apps/docs/src/lib/components/docs/examples/PseudoOverlay.svelte index 771d4a9..70f612e 100644 --- a/apps/docs/src/lib/components/docs/examples/PseudoOverlay.svelte +++ b/apps/docs/src/lib/components/docs/examples/PseudoOverlay.svelte @@ -12,14 +12,18 @@
{#if variant === 'brand'}
+
+
tailwind
{:else}
+
+
tailwind
{/if} @@ -29,16 +33,20 @@
{#if variant === 'brand'}
+
+
tw-easing-gradients
{:else}
+
+
tw-easing-gradients diff --git a/apps/docs/src/lib/components/landing/HeroSection.svelte b/apps/docs/src/lib/components/landing/HeroSection.svelte index f325453..c2f56c8 100644 --- a/apps/docs/src/lib/components/landing/HeroSection.svelte +++ b/apps/docs/src/lib/components/landing/HeroSection.svelte @@ -42,7 +42,7 @@ class="text-muted-foreground mt-6 max-w-xl text-lg leading-relaxed text-balance" > Linear gradients produce hard color transitions. Fix them with easing - functions, directly in your utility classes. + functions or custom bezier curves, directly in your utility classes.

diff --git a/apps/docs/src/lib/components/landing/PseudoElementSection.svelte b/apps/docs/src/lib/components/landing/PseudoElementSection.svelte index 7cc82a2..5ead328 100644 --- a/apps/docs/src/lib/components/landing/PseudoElementSection.svelte +++ b/apps/docs/src/lib/components/landing/PseudoElementSection.svelte @@ -3,8 +3,10 @@
+
+
tailwind @@ -14,8 +16,10 @@
+
+
tw-easing-gradients diff --git a/apps/docs/src/lib/constants.ts b/apps/docs/src/lib/constants.ts index 4a1ce3d..1ad7739 100644 --- a/apps/docs/src/lib/constants.ts +++ b/apps/docs/src/lib/constants.ts @@ -2,3 +2,26 @@ export const BRAND_COLORS = { from: '#6366F1', to: '#06B6D4' }; + +export type BezierCurve = [number, number, number, number]; + +export const EASING_CURVES: Record = { + ease: [0.25, 0.1, 0.25, 1], + 'ease-in': [0.42, 0, 1, 1], + 'ease-out': [0, 0, 0.58, 1], + 'ease-in-out': [0.42, 0, 0.58, 1] +}; + +export const DIRECTION_CSS: Record = { + t: 'to top', r: 'to right', b: 'to bottom', l: 'to left', + tl: 'to top left', tr: 'to top right', bl: 'to bottom left', br: 'to bottom right' +}; + +export const COLOR_PRESETS = [ + { fromHex: BRAND_COLORS.from, toHex: BRAND_COLORS.to, label: 'Indigo → Cyan' }, + { fromHex: '#8b5cf6', toHex: '#ec4899', label: 'Violet → Pink' }, + { fromHex: '#10b981', toHex: '#3b82f6', label: 'Emerald → Blue' }, + { fromHex: '#f59e0b', toHex: '#ef4444', label: 'Amber → Red' }, + { fromHex: '#000000', toHex: '#ffffff', label: 'Black → White' }, + { fromHex: '#f43f5e', toHex: '#fb923c', label: 'Rose → Orange' } +]; diff --git a/apps/docs/src/lib/gradient-utils.ts b/apps/docs/src/lib/gradient-utils.ts new file mode 100644 index 0000000..ef55650 --- /dev/null +++ b/apps/docs/src/lib/gradient-utils.ts @@ -0,0 +1,40 @@ +import { getCoordinatesFromControlPoints } from 'tw-easing-gradients'; +import type { BezierCurve } from './constants'; +import { DIRECTION_CSS } from './constants'; + +export function buildGradientStops(curve: BezierCurve): string { + const coords = getCoordinatesFromControlPoints(curve, 15); + return coords.map(({ x, y }) => { + const pos = Math.round(x * 1000) / 10; + const pct = Math.round(y * 1000) / 10; + if (pct === 0) return `var(--tw-gradient-from) ${pos}%`; + if (pct === 100) return `oklch(from var(--tw-gradient-to, oklch(from var(--tw-gradient-from) l c h / 0)) l c h / alpha) ${pos}%`; + return `oklch(from color-mix(in oklch, var(--tw-gradient-to, oklch(from var(--tw-gradient-from) l c h / 0)) ${pct}%, var(--tw-gradient-from)) l c h / alpha) ${pos}%`; + }).join(', '); +} + +export function buildGradientStyle(curve: BezierCurve, direction: string): string { + return `background-image: linear-gradient(${DIRECTION_CSS[direction]}, ${buildGradientStops(curve)});`; +} + +export function hexToArbitrary(hex: string): string { + if (hex.length === 9) { + const color = hex.slice(0, 7); + const alpha = Math.round((parseInt(hex.slice(7, 9), 16) / 255) * 100); + if (alpha === 100) return `[${color}]`; + return `[${color}/${alpha}]`; + } + return `[${hex}]`; +} + +export function curveToSvgPath(curve: BezierCurve, size: number): string { + const points: string[] = []; + for (let i = 0; i <= 44; i++) { + const t = i / 44; + const u = 1 - t; + const bx = 3 * u * u * t * curve[0] + 3 * u * t * t * curve[2] + t * t * t; + const by = 3 * u * u * t * curve[1] + 3 * u * t * t * curve[3] + t * t * t; + points.push(`${bx * size},${size - by * size}`); + } + return `M ${points.join(' L ')}`; +} diff --git a/apps/docs/src/routes/(docs)/playground/+page.svelte b/apps/docs/src/routes/(docs)/playground/+page.svelte index 6db7e24..d53a92c 100644 --- a/apps/docs/src/routes/(docs)/playground/+page.svelte +++ b/apps/docs/src/routes/(docs)/playground/+page.svelte @@ -2,7 +2,8 @@ import { Label, Separator, Select } from '@svecodocs/kit'; import ColorPicker, { ChromeVariant } from 'svelte-awesome-color-picker'; import CopyButton from '$lib/components/CopyButton.svelte'; - import { BRAND_COLORS } from '$lib/constants'; + import { BRAND_COLORS, EASING_CURVES, DIRECTION_CSS, COLOR_PRESETS, type BezierCurve } from '$lib/constants'; + import { buildGradientStyle, hexToArbitrary, curveToSvgPath } from '$lib/gradient-utils'; import ArrowUp from 'phosphor-svelte/lib/ArrowUp'; import ArrowDown from 'phosphor-svelte/lib/ArrowDown'; import ArrowLeft from 'phosphor-svelte/lib/ArrowLeft'; @@ -12,130 +13,84 @@ import ArrowDownLeft from 'phosphor-svelte/lib/ArrowDownLeft'; import ArrowDownRight from 'phosphor-svelte/lib/ArrowDownRight'; + const EASINGS = [ + { value: 'ease', label: 'Ease' }, + { value: 'ease-in', label: 'Ease In' }, + { value: 'ease-out', label: 'Ease Out' }, + { value: 'ease-in-out', label: 'Ease In Out' }, + { value: 'custom', label: 'Custom' } + ]; + + const DIRECTIONS = [ + { value: 't', label: 'Top', icon: ArrowUp }, + { value: 'r', label: 'Right', icon: ArrowRight }, + { value: 'b', label: 'Bottom', icon: ArrowDown }, + { value: 'l', label: 'Left', icon: ArrowLeft }, + { value: 'tl', label: 'Top Left', icon: ArrowUpLeft }, + { value: 'tr', label: 'Top Right', icon: ArrowUpRight }, + { value: 'bl', label: 'Bottom Left', icon: ArrowDownLeft }, + { value: 'br', label: 'Bottom Right', icon: ArrowDownRight } + ]; + let fromHex = $state(BRAND_COLORS.from); let toHex = $state(BRAND_COLORS.to); let easing = $state('ease-in-out'); let direction = $state('r'); + let customCurve = $state([0.35, 0, 0.2, 1]); + let activeHandle = $state<1 | 2 | null>(null); - function hexToArbitrary(hex: string): string { - if (hex.length === 9) { - const color = hex.slice(0, 7); - const alpha = Math.round((parseInt(hex.slice(7, 9), 16) / 255) * 100); - if (alpha === 100) return `[${color}]`; - return `[${color}/${alpha}]`; - } - return `[${hex}]`; - } - - let fromColor = $derived(hexToArbitrary(fromHex)); - let toColor = $derived(hexToArbitrary(toHex)); - - const easingCurves: Record = { - ease: [0.25, 0.1, 0.25, 1], - 'ease-in': [0.42, 0, 1, 1], - 'ease-out': [0, 0, 0.58, 1], - 'ease-in-out': [0.42, 0, 0.58, 1] - }; - - let currentCurve = $derived(easingCurves[easing]); - - let curvePath = $derived.by(() => { - const [x1, y1, x2, y2] = currentCurve; - const w = 120, - h = 70, - pad = 12; - const sx = pad, - sy = h - pad, - ex = w - pad, - ey = pad; - return `M ${sx} ${sy} C ${sx + x1 * (ex - sx)} ${sy - y1 * (sy - ey)}, ${sx + x2 * (ex - sx)} ${sy - y2 * (sy - ey)}, ${ex} ${ey}`; - }); + let isCustom = $derived(easing === 'custom'); + let currentCurve = $derived(isCustom ? customCurve : EASING_CURVES[easing]); let className = $derived( - easing === 'ease' - ? `bg-ease-to-${direction}` + isCustom + ? `bg-ease-to-${direction}-[${customCurve.join(',')}]` : `bg-${easing}-to-${direction}` ); - let fullClass = $derived(`${className} from-${fromColor} to-${toColor}`); + let fullClass = $derived( + `${className} from-${hexToArbitrary(fromHex)} to-${hexToArbitrary(toHex)}` + ); - const easings = [ - { value: 'ease', label: 'Ease' }, - { value: 'ease-in', label: 'Ease In' }, - { value: 'ease-out', label: 'Ease Out' }, - { value: 'ease-in-out', label: 'Ease In Out' } - ]; + let previewClass = $derived(isCustom ? '' : `bg-${easing}-to-${direction}`); + let customStyle = $derived(isCustom ? buildGradientStyle(customCurve, direction) : ''); - let easingLabel = $derived( - easings.find((e) => e.value === easing)?.label ?? 'Select easing' - ); + const PLOT_SIZE = 220; + let svgPath = $derived(curveToSvgPath(currentCurve, PLOT_SIZE)); - const directionIcons = { - t: ArrowUp, - r: ArrowRight, - b: ArrowDown, - l: ArrowLeft, - tl: ArrowUpLeft, - tr: ArrowUpRight, - bl: ArrowDownLeft, - br: ArrowDownRight - }; - - const directions = [ - { value: 't', label: 'Top' }, - { value: 'r', label: 'Right' }, - { value: 'b', label: 'Bottom' }, - { value: 'l', label: 'Left' }, - { value: 'tl', label: 'Top Left' }, - { value: 'tr', label: 'Top Right' }, - { value: 'bl', label: 'Bottom Left' }, - { value: 'br', label: 'Bottom Right' } - ]; + let handle1 = $derived({ + cx: currentCurve[0] * PLOT_SIZE, + cy: PLOT_SIZE - currentCurve[1] * PLOT_SIZE + }); + + let handle2 = $derived({ + cx: currentCurve[2] * PLOT_SIZE, + cy: PLOT_SIZE - currentCurve[3] * PLOT_SIZE + }); - const presets = [ - { - from: 'indigo-500', - to: 'cyan-500', - fromHex: BRAND_COLORS.from, - toHex: BRAND_COLORS.to, - label: 'Indigo → Cyan' - }, - { - from: 'violet-500', - to: 'pink-500', - fromHex: '#8b5cf6', - toHex: '#ec4899', - label: 'Violet → Pink' - }, - { - from: 'emerald-500', - to: 'blue-500', - fromHex: '#10b981', - toHex: '#3b82f6', - label: 'Emerald → Blue' - }, - { - from: 'amber-500', - to: 'red-500', - fromHex: '#f59e0b', - toHex: '#ef4444', - label: 'Amber → Red' - }, - { - from: 'black', - to: 'white', - fromHex: '#000000', - toHex: '#ffffff', - label: 'Black → White' - }, - { - from: 'rose-500', - to: 'orange-400', - fromHex: '#f43f5e', - toHex: '#fb923c', - label: 'Rose → Orange' + function switchToCustom() { + if (!isCustom) { + customCurve = [...currentCurve] as BezierCurve; + easing = 'custom'; } - ]; + } + + function clamp(v: number, min: number, max: number) { + return Math.min(max, Math.max(min, v)); + } + + function updateHandle(event: PointerEvent, handle: 1 | 2) { + const svg = event.currentTarget as SVGSVGElement; + const rect = svg.getBoundingClientRect(); + const nx = Number(clamp((event.clientX - rect.left) / rect.width, 0, 1).toFixed(2)); + const ny = Number(clamp(1 - (event.clientY - rect.top) / rect.height, 0, 1).toFixed(2)); + + if (handle === 1) { + customCurve = [nx, ny, customCurve[2], customCurve[3]]; + } else { + customCurve = [customCurve[0], customCurve[1], nx, ny]; + } + } @@ -143,12 +98,9 @@
-

Playground

-

+

Build and preview easing gradients interactively.

@@ -156,19 +108,16 @@
- + {className}
-
@@ -176,20 +125,11 @@
- + {fromHex}
- +
@@ -197,45 +137,29 @@
- + {toHex}
- +
- +
- {#each presets as preset (preset.label)} + {#each COLOR_PRESETS as preset (preset.label)} @@ -249,114 +173,71 @@

Options

- + - {easingLabel} + {EASINGS.find((e) => e.value === easing)?.label ?? 'Select'} - {#each easings as e (e.value)} - - {e.label} - + {#each EASINGS as e (e.value)} + {e.label} {/each} -
+ +
{ if (activeHandle) updateHandle(event, activeHandle); }} + onpointerup={() => (activeHandle = null)} + onpointercancel={() => (activeHandle = null)} + onpointerleave={() => (activeHandle = null)} > - - - - - - - - + + + + + + + + + + + + + {#each [[handle1, 1] as const, [handle2, 2] as const] as [handle, id] (id)} + { + switchToCustom(); + (event.currentTarget as SVGCircleElement).setPointerCapture(event.pointerId); + activeHandle = id; + }} + /> + + {/each} + + + +
- +
- {#each directions as dir (dir.value)} - {@const Icon = - directionIcons[dir.value as keyof typeof directionIcons]} + {#each DIRECTIONS as dir (dir.value)} {/each}
@@ -370,11 +251,8 @@

Code

-
-
{className} from-{fromColor} to-{toColor}
+
{className} from-{hexToArbitrary(fromHex)} to-{hexToArbitrary(toHex)}
diff --git a/apps/docs/src/routes/(landing)/+page.server.ts b/apps/docs/src/routes/(landing)/+page.server.ts index 98d91e2..ffd6e84 100644 --- a/apps/docs/src/routes/(landing)/+page.server.ts +++ b/apps/docs/src/routes/(landing)/+page.server.ts @@ -7,7 +7,6 @@ let cacheTimestamp = 0; export const load = async ({ fetch }) => { const now = Date.now(); - // Cache noch gültig? if (cachedStars !== null && now - cacheTimestamp < CACHE_DURATION) { return { stars: cachedStars }; } @@ -24,10 +23,11 @@ export const load = async ({ fetch }) => { const data = await res.json(); cachedStars = data.stargazers_count; cacheTimestamp = now; - return { stars: cachedStars }; + } else { + console.warn(`GitHub API ${res.status}: ${res.statusText}`); } - } catch { - // Silently fail + } catch (err) { + console.warn('GitHub API fetch failed:', err); } return { stars: cachedStars }; diff --git a/apps/docs/src/routes/api/search.json/search.json b/apps/docs/src/routes/api/search.json/search.json index 952d2fb..e8653c8 100644 --- a/apps/docs/src/routes/api/search.json/search.json +++ b/apps/docs/src/routes/api/search.json/search.json @@ -1 +1 @@ -[{"title":"API Reference","href":"/docs/api-reference","description":"Technical details and configuration options","content":"import { Callout, Collapsible } from '@svecodocs/kit' Plugin Options The plugin accepts an optional configuration object: import plugin from 'tw-easing-gradients'; // Default options plugin({ stops: 15 }); Options | Option | Type | Default | Description | | ------- | -------- | ------- | ------------------------------------- | | stops | number | 15 | Number of color stops in the gradient | More stops produce smoother gradients. 15 is the default and works well for most cases. How It Works 1. Easing Coordinates The plugin uses cubic bezier easing functions to calculate color stop positions: const EASING_FUNCTIONS = { ease: [0.25, 0.1, 0.25, 1], 'ease-in': [0.42, 0, 1, 1], 'ease-out': [0, 0, 0.58, 1], 'ease-in-out': [0.42, 0, 0.58, 1] }; 2. Color Interpolation Colors are interpolated using CSS relative color syntax with oklch() for perceptually uniform results. The color-mix() function blends colors, then oklch(from ... l c h / alpha) extracts the final value for maximum color fidelity. 3. Generated CSS For a utility like bg-ease-in-out-to-r, the plugin generates a linear-gradient with multiple color stops using relative color syntax. .bg-ease-in-out-to-r { background-image: linear-gradient( to right, var(--tw-gradient-from) 0%, oklch(from color-mix(in oklch, var(--tw-gradient-to, oklch(from var(--tw-gradient-from) l c h / 0)) 1.3%, var(--tw-gradient-from)) l c h / alpha) 8.1%, oklch(from color-mix(in oklch, var(--tw-gradient-to, oklch(from var(--tw-gradient-from) l c h / 0)) 4.9%, var(--tw-gradient-from)) l c h / alpha) 15.5%, oklch(from color-mix(in oklch, var(--tw-gradient-to, oklch(from var(--tw-gradient-from) l c h / 0)) 10.4%, var(--tw-gradient-from)) l c h / alpha) 22.5%, /* ... more stops interpolated along the easing curve ... */ oklch(from color-mix(in oklch, var(--tw-gradient-to, oklch(from var(--tw-gradient-from) l c h / 0)) 98.7%, var(--tw-gradient-from)) l c h / alpha) 91.9%, oklch(from var(--tw-gradient-to, oklch(from var(--tw-gradient-from) l c h / 0)) l c h / alpha) 100% ); } Browser Support Requires browsers with $2 support — available in all major browsers since September 2024. Exported Functions The plugin also exports utility functions for programmatic use: getCoordinates(easing, stops) Get easing coordinates for a given easing function: import { getCoordinates } from 'tw-easing-gradients'; const coords = getCoordinates('ease-in-out', 15); // Returns: [{ x: 0, y: 0 }, { x: 0.081, y: 0.021 }, ...] TypeScript The plugin is written in TypeScript and includes full type definitions: import type { EasingFunction, Direction, Coordinate } from 'tw-easing-gradients'; const easing: EasingFunction = 'ease-in-out'; const dir: Direction = 'br'; `"},{"title":"Examples","href":"/docs/examples","description":"Practical use cases for easing gradients","content":"import HeroOverlay from '$lib/components/docs/examples/HeroOverlay.svelte' import CardGradient from '$lib/components/docs/examples/CardGradient.svelte' import TextGradient from '$lib/components/docs/examples/TextGradient.svelte' import FadeOut from '$lib/components/docs/examples/FadeOut.svelte' import ButtonGradient from '$lib/components/docs/examples/ButtonGradient.svelte' import LoadingSkeleton from '$lib/components/docs/examples/LoadingSkeleton.svelte' import { Callout } from '@svecodocs/kit' Hero Image Overlay Create smooth fade overlays on hero images: Hero Title Card Gradient Background Smooth color transitions for card backgrounds: Card Title Card content goes here. Text Gradient Apply eased gradients to text: Gradient Text Fade Out Effect Smooth content fade at the bottom of a scrollable container: Button Hover Effect Gradient buttons with smooth transitions: Click Me Loading Skeleton Animated loading placeholder with smooth gradients: The via- utility is not supported with easing gradients. Use only from- and to-* for color stops."},{"title":"Getting Started","href":"/docs/getting-started","description":"Install and configure tw-easing-gradients","content":"import { Tabs, TabItem, Steps, Step, Callout, Card, CardGrid } from '@svecodocs/kit' Install the package pnpm add tw-easing-gradients npm install tw-easing-gradients yarn add tw-easing-gradients Add the plugin to your CSS /* app.css */ @import 'tailwindcss'; @plugin 'tw-easing-gradients'; You can optionally configure the number of color stops: @plugin 'tw-easing-gradients' { stops: 20; } Use easing gradients Use the utilities together with Tailwind's from- and to- color utilities: This plugin requires Tailwind CSS v4. This plugin uses modern CSS features (oklch and color-mix). For older browsers, a simple linear gradient fallback is automatically included. Next Steps All available easing gradient classes and directions. Practical use cases with live previews. Configuration options and technical details."},{"title":"Introduction","href":"/docs/index","description":"Smooth easing gradients for Tailwind CSS v4","content":"import ImageOverlayDemo from '$lib/components/docs/ImageOverlayDemo.svelte' import CardOverlay from '$lib/components/docs/CardOverlay.svelte' import GradientComparison from '$lib/components/docs/GradientComparison.svelte' tw-easing-gradients creates smooth, perceptually uniform gradients for Tailwind CSS v4. Drag the slider above to see the difference. The Problem Standard CSS gradients use linear interpolation, which creates visible \"banding\" - harsh edges especially noticeable when fading to transparency or between certain color pairs. Card Overlays A common use case: text overlays on images. The linear gradient stays solid and doesn't blend, while the eased version fades naturally into the image: Features Smooth transitions** - No more harsh color bands oklch color space** - Colors blend naturally without getting gray or muddy in the middle Multiple easing functions** - ease, ease-in, ease-out, ease-in-out All directions** - 8 gradient directions supported Works with Tailwind colors* - Uses existing from- and to-* utilities Zero runtime JS** - Pure CSS output Quick Start"},{"title":"Utilities","href":"/docs/utilities","description":"All available easing gradient utilities","content":"import EasingComparison from '$lib/components/docs/utilities/EasingComparison.svelte' import DirectionsShowcase from '$lib/components/docs/utilities/DirectionsShowcase.svelte' import ColorExamples from '$lib/components/docs/utilities/ColorExamples.svelte' import TransparencyExamples from '$lib/components/docs/utilities/TransparencyExamples.svelte' import { Tabs, TabItem } from '@svecodocs/kit' Syntax Combine an easing function with a direction: bg-{easing}-to-{direction} For the default ease curve, the easing part is just ease: bg-ease-to-r. For all others, use the full name: bg-ease-in-to-r, bg-ease-out-to-b, bg-ease-in-out-to-br. Easing Functions | Utility | Easing Function | Description | | ------------------------- | --------------- | ------------------------------- | | bg-ease-to-{dir} | ease | Default easing (slow-fast-slow) | | bg-ease-in-to-{dir} | ease-in | Slow start, fast end | | bg-ease-out-to-{dir} | ease-out | Fast start, slow end | | bg-ease-in-out-to-{dir} | ease-in-out | Slow start and end | Directions | Short | Direction | CSS Value | | ----- | ------------ | ----------------- | | t | Top | to top | | r | Right | to right | | b | Bottom | to bottom | | l | Left | to left | | tl | Top Left | to top left | | tr | Top Right | to top right | | bl | Bottom Left | to bottom left | | br | Bottom Right | to bottom right | Transparency Fades Skip to-* and the plugin fades to transparent automatically. Using with Tailwind Colors Full Class Reference | Direction | Class | | ------------ | --------------- | | Top | bg-ease-to-t | | Top Right | bg-ease-to-tr | | Right | bg-ease-to-r | | Bottom Right | bg-ease-to-br | | Bottom | bg-ease-to-b | | Bottom Left | bg-ease-to-bl | | Left | bg-ease-to-l | | Top Left | bg-ease-to-tl | | Direction | Class | | ------------ | ------------------ | | Top | bg-ease-in-to-t | | Top Right | bg-ease-in-to-tr | | Right | bg-ease-in-to-r | | Bottom Right | bg-ease-in-to-br | | Bottom | bg-ease-in-to-b | | Bottom Left | bg-ease-in-to-bl | | Left | bg-ease-in-to-l | | Top Left | bg-ease-in-to-tl | | Direction | Class | | ------------ | ------------------- | | Top | bg-ease-out-to-t | | Top Right | bg-ease-out-to-tr | | Right | bg-ease-out-to-r | | Bottom Right | bg-ease-out-to-br | | Bottom | bg-ease-out-to-b | | Bottom Left | bg-ease-out-to-bl | | Left | bg-ease-out-to-l | | Top Left | bg-ease-out-to-tl | | Direction | Class | | ------------ | ---------------------- | | Top | bg-ease-in-out-to-t | | Top Right | bg-ease-in-out-to-tr | | Right | bg-ease-in-out-to-r | | Bottom Right | bg-ease-in-out-to-br | | Bottom | bg-ease-in-out-to-b | | Bottom Left | bg-ease-in-out-to-bl | | Left | bg-ease-in-out-to-l | | Top Left | bg-ease-in-out-to-tl |"}] \ No newline at end of file +[{"title":"API Reference","href":"/docs/api-reference","description":"Technical details and configuration options","content":"import { Callout, Collapsible, Card, CardGrid } from '@svecodocs/kit' Plugin Options The plugin accepts an optional configuration object: import plugin from 'tw-easing-gradients'; // Default options plugin({ stops: 15 }); Options | Option | Type | Default | Description | | ------- | -------- | ------- | ------------------------------------- | | stops | number | 15 | Number of color stops in the gradient | More stops produce smoother gradients. 15 is the default and works well for most cases. How It Works 1. Easing Coordinates The plugin uses cubic bezier easing functions to calculate color stop positions: const EASING_FUNCTIONS = { ease: [0.25, 0.1, 0.25, 1], 'ease-in': [0.42, 0, 1, 1], 'ease-out': [0, 0, 0.58, 1], 'ease-in-out': [0.42, 0, 0.58, 1] }; 2. Color Interpolation Colors are interpolated using CSS relative color syntax with oklch() for perceptually uniform results. The color-mix() function blends colors, then oklch(from ... l c h / alpha) extracts the final value for maximum color fidelity. 3. Generated CSS For a utility like bg-ease-in-out-to-r, the plugin generates a linear-gradient with multiple color stops using relative color syntax. .bg-ease-in-out-to-r { background-image: linear-gradient( to right, var(--tw-gradient-from) 0%, oklch(from color-mix(in oklch, var(--tw-gradient-to, oklch(from var(--tw-gradient-from) l c h / 0)) 1.3%, var(--tw-gradient-from)) l c h / alpha) 8.1%, oklch(from color-mix(in oklch, var(--tw-gradient-to, oklch(from var(--tw-gradient-from) l c h / 0)) 4.9%, var(--tw-gradient-from)) l c h / alpha) 15.5%, oklch(from color-mix(in oklch, var(--tw-gradient-to, oklch(from var(--tw-gradient-from) l c h / 0)) 10.4%, var(--tw-gradient-from)) l c h / alpha) 22.5%, /* ... more stops interpolated along the easing curve ... */ oklch(from color-mix(in oklch, var(--tw-gradient-to, oklch(from var(--tw-gradient-from) l c h / 0)) 98.7%, var(--tw-gradient-from)) l c h / alpha) 91.9%, oklch(from var(--tw-gradient-to, oklch(from var(--tw-gradient-from) l c h / 0)) l c h / alpha) 100% ); } Browser Support Requires browsers with $2 support — available in all major browsers since September 2024. Exported Functions The plugin also exports utility functions for programmatic use: getCoordinates(easing, stops) Get easing coordinates for a given easing function: import { getCoordinates } from 'tw-easing-gradients'; const coords = getCoordinates('ease-in-out', 15); // Returns: [{ x: 0, y: 0 }, { x: 0.081, y: 0.021 }, ...] TypeScript The plugin is written in TypeScript and includes full type definitions: import type { EasingFunction, Direction, Coordinate } from 'tw-easing-gradients'; const easing: EasingFunction = 'ease-in-out'; const dir: Direction = 'br'; Next Steps Practical use cases with live previews. Experiment with easing gradients interactively."},{"title":"Examples","href":"/docs/examples","description":"Practical use cases for easing gradients","content":"import HeroOverlay from '$lib/components/docs/examples/HeroOverlay.svelte' import CardGradient from '$lib/components/docs/examples/CardGradient.svelte' import TextGradient from '$lib/components/docs/examples/TextGradient.svelte' import FadeOut from '$lib/components/docs/examples/FadeOut.svelte' import ButtonGradient from '$lib/components/docs/examples/ButtonGradient.svelte' import LoadingSkeleton from '$lib/components/docs/examples/LoadingSkeleton.svelte' import PseudoOverlay from '$lib/components/docs/examples/PseudoOverlay.svelte' import { Callout, Card, CardGrid } from '@svecodocs/kit' Hero Image Overlay Create smooth fade overlays on hero images: Hero Title Dual Overlay Top and bottom fade overlays using absolute positioned divs: Content Card Gradient Background Smooth color transitions for card backgrounds: Card Title Card content goes here. Text Gradient Apply eased gradients to text: Gradient Text Fade Out Effect Smooth content fade at the bottom of a scrollable container: Button Hover Effect Gradient buttons with smooth transitions: Click Me Loading Skeleton Animated loading placeholder with smooth gradients: The via- utility is not supported with easing gradients. Use only from- and to-* for color stops. Custom Bezier Define your own easing curve using arbitrary values with four cubic bezier control points: Next Steps Configuration options and technical details. Experiment with easing gradients interactively."},{"title":"Getting Started","href":"/docs/getting-started","description":"Install and configure tw-easing-gradients","content":"import { Tabs, TabItem, Steps, Step, Callout, Card, CardGrid } from '@svecodocs/kit' Install the package pnpm add tw-easing-gradients npm install tw-easing-gradients yarn add tw-easing-gradients Add the plugin to your CSS /* app.css */ @import 'tailwindcss'; @plugin 'tw-easing-gradients'; You can optionally configure the number of color stops: @plugin 'tw-easing-gradients' { stops: 20; } Use easing gradients Use the utilities together with Tailwind's from- and to- color utilities: You can also use custom easing curves with arbitrary values: This plugin requires Tailwind CSS v4. This plugin uses modern CSS features (oklch and color-mix). For older browsers, a simple linear gradient fallback is automatically included. Next Steps All available easing gradient classes and directions. Practical use cases with live previews. Configuration options and technical details."},{"title":"Introduction","href":"/docs/index","description":"Smooth easing gradients for Tailwind CSS v4","content":"import ImageOverlayDemo from '$lib/components/docs/ImageOverlayDemo.svelte' import CardOverlay from '$lib/components/docs/CardOverlay.svelte' import PseudoOverlay from '$lib/components/docs/examples/PseudoOverlay.svelte' import GradientComparison from '$lib/components/docs/GradientComparison.svelte' import { Card, CardGrid } from '@svecodocs/kit' tw-easing-gradients creates smooth, perceptually uniform gradients for Tailwind CSS v4. Drag the slider above to see the difference. The Problem Standard CSS gradients use linear interpolation, which creates visible \"banding\" - harsh edges especially noticeable when fading to transparency or between certain color pairs. Card Overlays A common use case: text overlays on images. The linear gradient stays solid and doesn't blend, while the eased version fades naturally into the image: Features Smooth transitions** - No more harsh color bands oklch color space** - Colors blend naturally without getting gray or muddy in the middle Multiple easing functions** - ease, ease-in, ease-out, ease-in-out All directions** - 8 gradient directions supported Custom bezier** - Arbitrary easing curves via bg-ease-to-r-[0.22,1,0.36,1] Works with Tailwind colors* - Uses existing from- and to-* utilities Zero runtime JS** - Pure CSS output Quick Start Next Steps Install and configure tw-easing-gradients in your project. Experiment with easing gradients interactively."},{"title":"Utilities","href":"/docs/utilities","description":"All available easing gradient utilities","content":"import EasingComparison from '$lib/components/docs/utilities/EasingComparison.svelte' import DirectionsShowcase from '$lib/components/docs/utilities/DirectionsShowcase.svelte' import ColorExamples from '$lib/components/docs/utilities/ColorExamples.svelte' import TransparencyExamples from '$lib/components/docs/utilities/TransparencyExamples.svelte' import { Tabs, TabItem, Card, CardGrid } from '@svecodocs/kit' Syntax Combine an easing function with a direction: bg-{easing}-to-{direction} For the default ease curve, the easing part is just ease: bg-ease-to-r. For all others, use the full name: bg-ease-in-to-r, bg-ease-out-to-b, bg-ease-in-out-to-br. Easing Functions | Utility | Easing Function | Description | | ------------------------- | --------------- | ------------------------------- | | bg-ease-to-{dir} | ease | Default easing (slow-fast-slow) | | bg-ease-in-to-{dir} | ease-in | Slow start, fast end | | bg-ease-out-to-{dir} | ease-out | Fast start, slow end | | bg-ease-in-out-to-{dir} | ease-in-out | Slow start and end | Directions | Short | Direction | CSS Value | | ----- | ------------ | ----------------- | | t | Top | to top | | r | Right | to right | | b | Bottom | to bottom | | l | Left | to left | | tl | Top Left | to top left | | tr | Top Right | to top right | | bl | Bottom Left | to bottom left | | br | Bottom Right | to bottom right | Transparency Fades Skip to-* and the plugin fades to transparent automatically. Using with Tailwind Colors Full Class Reference | Direction | Class | | ------------ | --------------- | | Top | bg-ease-to-t | | Top Right | bg-ease-to-tr | | Right | bg-ease-to-r | | Bottom Right | bg-ease-to-br | | Bottom | bg-ease-to-b | | Bottom Left | bg-ease-to-bl | | Left | bg-ease-to-l | | Top Left | bg-ease-to-tl | | Direction | Class | | ------------ | ------------------ | | Top | bg-ease-in-to-t | | Top Right | bg-ease-in-to-tr | | Right | bg-ease-in-to-r | | Bottom Right | bg-ease-in-to-br | | Bottom | bg-ease-in-to-b | | Bottom Left | bg-ease-in-to-bl | | Left | bg-ease-in-to-l | | Top Left | bg-ease-in-to-tl | | Direction | Class | | ------------ | ------------------- | | Top | bg-ease-out-to-t | | Top Right | bg-ease-out-to-tr | | Right | bg-ease-out-to-r | | Bottom Right | bg-ease-out-to-br | | Bottom | bg-ease-out-to-b | | Bottom Left | bg-ease-out-to-bl | | Left | bg-ease-out-to-l | | Top Left | bg-ease-out-to-tl | | Direction | Class | | ------------ | ---------------------- | | Top | bg-ease-in-out-to-t | | Top Right | bg-ease-in-out-to-tr | | Right | bg-ease-in-out-to-r | | Bottom Right | bg-ease-in-out-to-br | | Bottom | bg-ease-in-out-to-b | | Bottom Left | bg-ease-in-out-to-bl | | Left | bg-ease-in-out-to-l | | Top Left | bg-ease-in-out-to-tl | Custom Bezier Use arbitrary values to define your own easing curve with four control points: The four values correspond to the control points of a cubic bezier curve: x1, y1, x2, y2. Next Steps Practical use cases with live previews. Configuration options and technical details."}] \ No newline at end of file diff --git a/packages/tw-easing-gradients/README.md b/packages/tw-easing-gradients/README.md index 285e6af..f67d9a8 100644 --- a/packages/tw-easing-gradients/README.md +++ b/packages/tw-easing-gradients/README.md @@ -15,7 +15,7 @@
Smooth, naturally blending gradients for Tailwind CSS
- Cubic bezier easing • oklch color interpolation • Drop-in replacement + Cubic bezier easing • Custom curves • oklch color interpolation • Drop-in replacement
--- @@ -68,6 +68,15 @@ npm install tw-easing-gradients `to-t` · `to-r` · `to-b` · `to-l` · `to-tl` · `to-tr` · `to-bl` · `to-br` +## Custom Bezier + +Use arbitrary values for custom easing curves: + +```html +
+
+``` + ## Documentation [tw-easing-gradients.enisdev.com/docs](https://tw-easing-gradients.enisdev.com/docs) @@ -86,7 +95,7 @@ A [SKILL.md](https://github.com/enisbudancamanak/tw-easing-gradients/tree/main/. ## Roadmap -- [ ] Custom bezier functions +- [x] Custom bezier functions - [ ] Radial gradients - [ ] Conic gradients diff --git a/packages/tw-easing-gradients/dist/index.cjs b/packages/tw-easing-gradients/dist/index.cjs index 208ae0f..9ce268e 100644 --- a/packages/tw-easing-gradients/dist/index.cjs +++ b/packages/tw-easing-gradients/dist/index.cjs @@ -31,7 +31,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru var index_exports = {}; __export(index_exports, { default: () => index_default, - getCoordinates: () => getCoordinates + getCoordinates: () => getCoordinates, + getCoordinatesFromControlPoints: () => getCoordinatesFromControlPoints, + parseBezierValues: () => parseBezierValues }); module.exports = __toCommonJS(index_exports); var import_plugin = __toESM(require("tailwindcss/plugin"), 1); @@ -59,16 +61,11 @@ function cubicBezier(t, p1, p2) { const mt = 1 - t; return 3 * mt * mt * t * p1 + 3 * mt * t * t * p2 + t * t * t; } -function getCoordinates(easing, stops = 15) { - if (!(easing in EASING_FUNCTIONS)) { - throw new Error( - `Invalid easing: "${easing}". Valid options: ease, ease-in, ease-out, ease-in-out` - ); - } +function getCoordinatesFromControlPoints(controlPoints, stops = 15) { if (stops < 2 || stops > 100) { throw new Error(`stops must be between 2 and 100, got: ${stops}`); } - const [x1, y1, x2, y2] = EASING_FUNCTIONS[easing]; + const [x1, y1, x2, y2] = controlPoints; const coords = []; for (let i = 0; i <= stops; i++) { const t = i / stops; @@ -76,24 +73,24 @@ function getCoordinates(easing, stops = 15) { } return coords; } +function getCoordinates(easing, stops = 15) { + if (!(easing in EASING_FUNCTIONS)) { + throw new Error( + `Invalid easing: "${easing}". Valid options: ease, ease-in, ease-out, ease-in-out` + ); + } + return getCoordinatesFromControlPoints(EASING_FUNCTIONS[easing], stops); +} +function parseBezierValues(input) { + const cleaned = input.replace(/^cubic-bezier\(/i, "").replace(/\)$/, "").trim(); + const parts = cleaned.split(",").map((s) => Number(s.trim())); + if (parts.length !== 4 || parts.some(Number.isNaN)) return null; + return parts; +} // src/index.ts -var EASINGS = [ - "ease", - "ease-in", - "ease-out", - "ease-in-out" -]; -var DIRECTIONS_KEYS = [ - "t", - "r", - "b", - "l", - "tl", - "tr", - "bl", - "br" -]; +var EASINGS = Object.keys(EASING_FUNCTIONS); +var DIRECTION_KEYS = Object.keys(DIRECTIONS); function generateGradientStops(coordinates) { return coordinates.map(({ x, y }) => { const position = Math.round(x * 1e3) / 10; @@ -107,29 +104,43 @@ function generateGradientStops(coordinates) { return `oklch(from color-mix(in oklch, var(--tw-gradient-to, oklch(from var(--tw-gradient-from) l c h / 0)) ${percentage}%, var(--tw-gradient-from)) l c h / alpha) ${position}%`; }).join(", "); } +function makeGradientUtility(cssDirection, gradientStops) { + return { + "background-image": `linear-gradient(${cssDirection}, var(--tw-gradient-from), var(--tw-gradient-to, transparent))`, + "@supports (color: oklch(from red l c h))": { + "background-image": `linear-gradient(${cssDirection}, ${gradientStops})` + } + }; +} var easingGradients = import_plugin.default.withOptions( - (options = {}) => ({ addUtilities }) => { + (options = {}) => ({ addUtilities, matchUtilities }) => { const stops = options.stops ?? 15; const utilities = {}; for (const easing of EASINGS) { - for (const dir of DIRECTIONS_KEYS) { - const className = easing === "ease" ? `.bg-ease-to-${dir}` : `.bg-${easing}-to-${dir}`; - const gradientStops = generateGradientStops( - getCoordinates(easing, stops) - ); - utilities[className] = { - "background-image": `linear-gradient(${DIRECTIONS[dir]}, var(--tw-gradient-from), var(--tw-gradient-to, transparent))`, - "@supports (color: oklch(from red l c h))": { - "background-image": `linear-gradient(${DIRECTIONS[dir]}, ${gradientStops})` - } - }; + const gradientStops = generateGradientStops( + getCoordinates(easing, stops) + ); + for (const dir of DIRECTION_KEYS) { + utilities[`.bg-${easing}-to-${dir}`] = makeGradientUtility(DIRECTIONS[dir], gradientStops); } } addUtilities(utilities); + const matchers = {}; + for (const dir of DIRECTION_KEYS) { + matchers[`bg-ease-to-${dir}`] = (value) => { + const points = parseBezierValues(value); + if (!points) return {}; + const coords = getCoordinatesFromControlPoints(points, stops); + return makeGradientUtility(DIRECTIONS[dir], generateGradientStops(coords)); + }; + } + matchUtilities(matchers, { values: {}, type: "any" }); } ); var index_default = easingGradients; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { - getCoordinates + getCoordinates, + getCoordinatesFromControlPoints, + parseBezierValues }); diff --git a/packages/tw-easing-gradients/dist/index.d.cts b/packages/tw-easing-gradients/dist/index.d.cts index 26577c9..019348d 100644 --- a/packages/tw-easing-gradients/dist/index.d.cts +++ b/packages/tw-easing-gradients/dist/index.d.cts @@ -10,9 +10,11 @@ interface PluginOptions { stops?: number; } +declare function getCoordinatesFromControlPoints(controlPoints: readonly [number, number, number, number], stops?: number): Coordinate[]; declare function getCoordinates(easing: EasingFunction, stops?: number): Coordinate[]; +declare function parseBezierValues(input: string): [number, number, number, number] | null; type TailwindPlugin = ReturnType>; declare const easingGradients: TailwindPlugin; -export { type Coordinate, type Direction, type EasingFunction, type PluginOptions, easingGradients as default, getCoordinates }; +export { type Coordinate, type Direction, type EasingFunction, type PluginOptions, easingGradients as default, getCoordinates, getCoordinatesFromControlPoints, parseBezierValues }; diff --git a/packages/tw-easing-gradients/dist/index.d.ts b/packages/tw-easing-gradients/dist/index.d.ts index 26577c9..019348d 100644 --- a/packages/tw-easing-gradients/dist/index.d.ts +++ b/packages/tw-easing-gradients/dist/index.d.ts @@ -10,9 +10,11 @@ interface PluginOptions { stops?: number; } +declare function getCoordinatesFromControlPoints(controlPoints: readonly [number, number, number, number], stops?: number): Coordinate[]; declare function getCoordinates(easing: EasingFunction, stops?: number): Coordinate[]; +declare function parseBezierValues(input: string): [number, number, number, number] | null; type TailwindPlugin = ReturnType>; declare const easingGradients: TailwindPlugin; -export { type Coordinate, type Direction, type EasingFunction, type PluginOptions, easingGradients as default, getCoordinates }; +export { type Coordinate, type Direction, type EasingFunction, type PluginOptions, easingGradients as default, getCoordinates, getCoordinatesFromControlPoints, parseBezierValues }; diff --git a/packages/tw-easing-gradients/dist/index.js b/packages/tw-easing-gradients/dist/index.js index d37abd1..2a0b791 100644 --- a/packages/tw-easing-gradients/dist/index.js +++ b/packages/tw-easing-gradients/dist/index.js @@ -24,16 +24,11 @@ function cubicBezier(t, p1, p2) { const mt = 1 - t; return 3 * mt * mt * t * p1 + 3 * mt * t * t * p2 + t * t * t; } -function getCoordinates(easing, stops = 15) { - if (!(easing in EASING_FUNCTIONS)) { - throw new Error( - `Invalid easing: "${easing}". Valid options: ease, ease-in, ease-out, ease-in-out` - ); - } +function getCoordinatesFromControlPoints(controlPoints, stops = 15) { if (stops < 2 || stops > 100) { throw new Error(`stops must be between 2 and 100, got: ${stops}`); } - const [x1, y1, x2, y2] = EASING_FUNCTIONS[easing]; + const [x1, y1, x2, y2] = controlPoints; const coords = []; for (let i = 0; i <= stops; i++) { const t = i / stops; @@ -41,24 +36,24 @@ function getCoordinates(easing, stops = 15) { } return coords; } +function getCoordinates(easing, stops = 15) { + if (!(easing in EASING_FUNCTIONS)) { + throw new Error( + `Invalid easing: "${easing}". Valid options: ease, ease-in, ease-out, ease-in-out` + ); + } + return getCoordinatesFromControlPoints(EASING_FUNCTIONS[easing], stops); +} +function parseBezierValues(input) { + const cleaned = input.replace(/^cubic-bezier\(/i, "").replace(/\)$/, "").trim(); + const parts = cleaned.split(",").map((s) => Number(s.trim())); + if (parts.length !== 4 || parts.some(Number.isNaN)) return null; + return parts; +} // src/index.ts -var EASINGS = [ - "ease", - "ease-in", - "ease-out", - "ease-in-out" -]; -var DIRECTIONS_KEYS = [ - "t", - "r", - "b", - "l", - "tl", - "tr", - "bl", - "br" -]; +var EASINGS = Object.keys(EASING_FUNCTIONS); +var DIRECTION_KEYS = Object.keys(DIRECTIONS); function generateGradientStops(coordinates) { return coordinates.map(({ x, y }) => { const position = Math.round(x * 1e3) / 10; @@ -72,29 +67,43 @@ function generateGradientStops(coordinates) { return `oklch(from color-mix(in oklch, var(--tw-gradient-to, oklch(from var(--tw-gradient-from) l c h / 0)) ${percentage}%, var(--tw-gradient-from)) l c h / alpha) ${position}%`; }).join(", "); } +function makeGradientUtility(cssDirection, gradientStops) { + return { + "background-image": `linear-gradient(${cssDirection}, var(--tw-gradient-from), var(--tw-gradient-to, transparent))`, + "@supports (color: oklch(from red l c h))": { + "background-image": `linear-gradient(${cssDirection}, ${gradientStops})` + } + }; +} var easingGradients = plugin.withOptions( - (options = {}) => ({ addUtilities }) => { + (options = {}) => ({ addUtilities, matchUtilities }) => { const stops = options.stops ?? 15; const utilities = {}; for (const easing of EASINGS) { - for (const dir of DIRECTIONS_KEYS) { - const className = easing === "ease" ? `.bg-ease-to-${dir}` : `.bg-${easing}-to-${dir}`; - const gradientStops = generateGradientStops( - getCoordinates(easing, stops) - ); - utilities[className] = { - "background-image": `linear-gradient(${DIRECTIONS[dir]}, var(--tw-gradient-from), var(--tw-gradient-to, transparent))`, - "@supports (color: oklch(from red l c h))": { - "background-image": `linear-gradient(${DIRECTIONS[dir]}, ${gradientStops})` - } - }; + const gradientStops = generateGradientStops( + getCoordinates(easing, stops) + ); + for (const dir of DIRECTION_KEYS) { + utilities[`.bg-${easing}-to-${dir}`] = makeGradientUtility(DIRECTIONS[dir], gradientStops); } } addUtilities(utilities); + const matchers = {}; + for (const dir of DIRECTION_KEYS) { + matchers[`bg-ease-to-${dir}`] = (value) => { + const points = parseBezierValues(value); + if (!points) return {}; + const coords = getCoordinatesFromControlPoints(points, stops); + return makeGradientUtility(DIRECTIONS[dir], generateGradientStops(coords)); + }; + } + matchUtilities(matchers, { values: {}, type: "any" }); } ); var index_default = easingGradients; export { index_default as default, - getCoordinates + getCoordinates, + getCoordinatesFromControlPoints, + parseBezierValues }; diff --git a/packages/tw-easing-gradients/src/easing.test.ts b/packages/tw-easing-gradients/src/easing.test.ts index d11b929..72205ad 100644 --- a/packages/tw-easing-gradients/src/easing.test.ts +++ b/packages/tw-easing-gradients/src/easing.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { getCoordinates } from './easing.js'; +import { + getCoordinates, + getCoordinatesFromControlPoints, + parseBezierValues, +} from './easing.js'; describe('getCoordinates', () => { describe('valid inputs', () => { @@ -15,15 +19,15 @@ describe('getCoordinates', () => { it('first point starts at (0, 0)', () => { const coords = getCoordinates('ease', 15); - expect(coords[0].x).toBeCloseTo(0, 5); - expect(coords[0].y).toBeCloseTo(0, 5); + expect(coords[0].x).toBe(0); + expect(coords[0].y).toBe(0); }); it('last point ends at (1, 1)', () => { const coords = getCoordinates('ease', 15); const last = coords[coords.length - 1]; - expect(last.x).toBeCloseTo(1, 5); - expect(last.y).toBeCloseTo(1, 5); + expect(last.x).toBe(1); + expect(last.y).toBe(1); }); it('all coordinates are between 0 and 1', () => { @@ -100,3 +104,69 @@ describe('getCoordinates', () => { }); }); }); + +describe('getCoordinatesFromControlPoints', () => { + it('returns correct number of points', () => { + const coords = getCoordinatesFromControlPoints([0.25, 0.1, 0.25, 1], 10); + expect(coords).toHaveLength(11); + }); + + it('first point is (0, 0) and last is (1, 1)', () => { + const coords = getCoordinatesFromControlPoints([0.42, 0, 0.58, 1], 15); + expect(coords[0].x).toBe(0); + expect(coords[0].y).toBe(0); + expect(coords[coords.length - 1].x).toBe(1); + expect(coords[coords.length - 1].y).toBe(1); + }); + + it('produces same result as getCoordinates for known easings', () => { + const fromNamed = getCoordinates('ease-in', 15); + const fromPoints = getCoordinatesFromControlPoints([0.42, 0, 1, 1], 15); + expect(fromPoints).toEqual(fromNamed); + }); + + it('works with custom control points', () => { + const coords = getCoordinatesFromControlPoints([0.22, 1, 0.36, 1], 10); + expect(coords).toHaveLength(11); + for (const { x, y } of coords) { + expect(x).toBeGreaterThanOrEqual(0); + expect(x).toBeLessThanOrEqual(1); + } + }); + + it('throws for stops out of range', () => { + expect(() => getCoordinatesFromControlPoints([0, 0, 1, 1], 1)).toThrow( + 'stops must be between 2 and 100', + ); + }); +}); + +describe('parseBezierValues', () => { + it('parses comma-separated values', () => { + expect(parseBezierValues('0.22,1,0.36,1')).toEqual([0.22, 1, 0.36, 1]); + }); + + it('parses with spaces', () => { + expect(parseBezierValues('0.22, 1, 0.36, 1')).toEqual([0.22, 1, 0.36, 1]); + }); + + it('parses cubic-bezier() syntax', () => { + expect(parseBezierValues('cubic-bezier(0.42,0,0.58,1)')).toEqual([0.42, 0, 0.58, 1]); + }); + + it('returns null for too few values', () => { + expect(parseBezierValues('0.22,1,0.36')).toBeNull(); + }); + + it('returns null for too many values', () => { + expect(parseBezierValues('0.22,1,0.36,1,0.5')).toBeNull(); + }); + + it('returns null for non-numeric values', () => { + expect(parseBezierValues('a,b,c,d')).toBeNull(); + }); + + it('returns null for empty string', () => { + expect(parseBezierValues('')).toBeNull(); + }); +}); diff --git a/packages/tw-easing-gradients/src/easing.ts b/packages/tw-easing-gradients/src/easing.ts index b4f37d8..3a54c85 100644 --- a/packages/tw-easing-gradients/src/easing.ts +++ b/packages/tw-easing-gradients/src/easing.ts @@ -6,20 +6,15 @@ function cubicBezier(t: number, p1: number, p2: number): number { return 3 * mt * mt * t * p1 + 3 * mt * t * t * p2 + t * t * t; } -export function getCoordinates( - easing: EasingFunction, +export function getCoordinatesFromControlPoints( + controlPoints: readonly [number, number, number, number], stops = 15, ): Coordinate[] { - if (!(easing in EASING_FUNCTIONS)) { - throw new Error( - `Invalid easing: "${easing}". Valid options: ease, ease-in, ease-out, ease-in-out`, - ); - } if (stops < 2 || stops > 100) { throw new Error(`stops must be between 2 and 100, got: ${stops}`); } - const [x1, y1, x2, y2] = EASING_FUNCTIONS[easing]; + const [x1, y1, x2, y2] = controlPoints; const coords: Coordinate[] = []; for (let i = 0; i <= stops; i++) { @@ -29,3 +24,30 @@ export function getCoordinates( return coords; } + +export function getCoordinates( + easing: EasingFunction, + stops = 15, +): Coordinate[] { + if (!(easing in EASING_FUNCTIONS)) { + throw new Error( + `Invalid easing: "${easing}". Valid options: ease, ease-in, ease-out, ease-in-out`, + ); + } + + return getCoordinatesFromControlPoints(EASING_FUNCTIONS[easing], stops); +} + +export function parseBezierValues( + input: string, +): [number, number, number, number] | null { + const cleaned = input + .replace(/^cubic-bezier\(/i, '') + .replace(/\)$/, '') + .trim(); + const parts = cleaned.split(',').map((s) => Number(s.trim())); + + if (parts.length !== 4 || parts.some(Number.isNaN)) return null; + + return parts as [number, number, number, number]; +} diff --git a/packages/tw-easing-gradients/src/index.test.ts b/packages/tw-easing-gradients/src/index.test.ts index e8f6cc3..74018c8 100644 --- a/packages/tw-easing-gradients/src/index.test.ts +++ b/packages/tw-easing-gradients/src/index.test.ts @@ -1,5 +1,34 @@ import { describe, it, expect, vi } from 'vitest'; -import easingGradients, { getCoordinates } from './index.js'; +import easingGradients, { + getCoordinates, + getCoordinatesFromControlPoints, + parseBezierValues, +} from './index.js'; +import { DIRECTIONS } from './types.js'; + +function runPlugin(options?: { stops?: number }) { + const addUtilities = vi.fn(); + const matchUtilities = vi.fn(); + const p = options ? easingGradients(options) : easingGradients(); + + p.handler({ + addUtilities, + matchUtilities, + addComponents: vi.fn(), + matchComponents: vi.fn(), + addBase: vi.fn(), + addVariant: vi.fn(), + matchVariant: vi.fn(), + theme: vi.fn(), + config: vi.fn(), + } as any); + + return { + utilities: addUtilities.mock.calls[0]?.[0] ?? {}, + addUtilities, + matchUtilities, + }; +} describe('easingGradients plugin', () => { it('exports getCoordinates function', () => { @@ -11,25 +40,10 @@ describe('easingGradients plugin', () => { }); describe('plugin generates utilities', () => { - it('generates all easing-direction combinations', () => { - const addUtilities = vi.fn(); - const plugin = easingGradients(); - - plugin.handler({ - addUtilities, - matchUtilities: vi.fn(), - addComponents: vi.fn(), - matchComponents: vi.fn(), - addBase: vi.fn(), - addVariant: vi.fn(), - matchVariant: vi.fn(), - theme: vi.fn(), - config: vi.fn(), - } as any); + it('generates all 32 easing-direction combinations', () => { + const { utilities, addUtilities } = runPlugin(); expect(addUtilities).toHaveBeenCalledTimes(1); - - const utilities = addUtilities.mock.calls[0][0]; const classNames = Object.keys(utilities); expect(classNames).toHaveLength(32); @@ -40,25 +54,9 @@ describe('easingGradients plugin', () => { }); it('generates fallback CSS as base property', () => { - const addUtilities = vi.fn(); - const plugin = easingGradients(); - - plugin.handler({ - addUtilities, - matchUtilities: vi.fn(), - addComponents: vi.fn(), - matchComponents: vi.fn(), - addBase: vi.fn(), - addVariant: vi.fn(), - matchVariant: vi.fn(), - theme: vi.fn(), - config: vi.fn(), - } as any); - - const utilities = addUtilities.mock.calls[0][0]; + const { utilities } = runPlugin(); const fallbackValue = utilities['.bg-ease-to-r']['background-image']; - // Fallback sollte einfacher 2-stop linear-gradient sein expect(fallbackValue).toContain('linear-gradient'); expect(fallbackValue).toContain('to right'); expect(fallbackValue).toContain('var(--tw-gradient-from)'); @@ -68,23 +66,7 @@ describe('easingGradients plugin', () => { }); it('generates CSS with color-mix in nested @supports block', () => { - const addUtilities = vi.fn(); - const plugin = easingGradients(); - - plugin.handler({ - addUtilities, - matchUtilities: vi.fn(), - addComponents: vi.fn(), - matchComponents: vi.fn(), - addBase: vi.fn(), - addVariant: vi.fn(), - matchVariant: vi.fn(), - theme: vi.fn(), - config: vi.fn(), - } as any); - - expect(addUtilities).toHaveBeenCalledTimes(1); - const utilities = addUtilities.mock.calls[0][0]; + const { utilities } = runPlugin(); const supportsBlock = utilities['.bg-ease-to-r'][ '@supports (color: oklch(from red l c h))' @@ -99,22 +81,7 @@ describe('easingGradients plugin', () => { }); it('respects custom stops option', () => { - const addUtilities = vi.fn(); - const plugin = easingGradients({ stops: 5 }); - - plugin.handler({ - addUtilities, - matchUtilities: vi.fn(), - addComponents: vi.fn(), - matchComponents: vi.fn(), - addBase: vi.fn(), - addVariant: vi.fn(), - matchVariant: vi.fn(), - theme: vi.fn(), - config: vi.fn(), - } as any); - - const utilities = addUtilities.mock.calls[0][0]; + const { utilities } = runPlugin({ stops: 5 }); const supportsBlock = utilities['.bg-ease-to-r'][ '@supports (color: oklch(from red l c h))' @@ -126,22 +93,7 @@ describe('easingGradients plugin', () => { }); it('uses default 15 stops when no option provided', () => { - const addUtilities = vi.fn(); - const plugin = easingGradients(); - - plugin.handler({ - addUtilities, - matchUtilities: vi.fn(), - addComponents: vi.fn(), - matchComponents: vi.fn(), - addBase: vi.fn(), - addVariant: vi.fn(), - matchVariant: vi.fn(), - theme: vi.fn(), - config: vi.fn(), - } as any); - - const utilities = addUtilities.mock.calls[0][0]; + const { utilities } = runPlugin(); const supportsBlock = utilities['.bg-ease-to-r'][ '@supports (color: oklch(from red l c h))' @@ -154,37 +106,10 @@ describe('easingGradients plugin', () => { }); describe('gradient directions', () => { - const directionMap = { - t: 'to top', - r: 'to right', - b: 'to bottom', - l: 'to left', - tl: 'to top left', - tr: 'to top right', - bl: 'to bottom left', - br: 'to bottom right', - }; - - it.each(Object.entries(directionMap))( + it.each(Object.entries(DIRECTIONS))( 'generates correct direction for %s → %s', (dir, expected) => { - const addUtilities = vi.fn(); - const plugin = easingGradients(); - - plugin.handler({ - addUtilities, - matchUtilities: vi.fn(), - addComponents: vi.fn(), - matchComponents: vi.fn(), - addBase: vi.fn(), - addVariant: vi.fn(), - matchVariant: vi.fn(), - theme: vi.fn(), - config: vi.fn(), - } as any); - - const utilities = addUtilities.mock.calls[0][0]; - // Prüfe Fallback-Richtung + const { utilities } = runPlugin(); const fallbackValue = utilities[`.bg-ease-to-${dir}`]['background-image']; expect(fallbackValue).toContain(`linear-gradient(${expected},`); @@ -194,54 +119,58 @@ describe('easingGradients plugin', () => { describe('class naming', () => { it('uses "bg-ease-to-" for ease (no redundant "ease")', () => { - const addUtilities = vi.fn(); - const plugin = easingGradients(); - - plugin.handler({ - addUtilities, - matchUtilities: vi.fn(), - addComponents: vi.fn(), - matchComponents: vi.fn(), - addBase: vi.fn(), - addVariant: vi.fn(), - matchVariant: vi.fn(), - theme: vi.fn(), - config: vi.fn(), - } as any); - - const classNames = Object.keys(addUtilities.mock.calls[0][0]); + const classNames = Object.keys(runPlugin().utilities); expect(classNames).toContain('.bg-ease-to-r'); expect(classNames).not.toContain('.bg-ease-ease-to-r'); }); it('uses "bg-ease-in-to-" for ease-in', () => { - const addUtilities = vi.fn(); - const plugin = easingGradients(); - - plugin.handler({ - addUtilities, - matchUtilities: vi.fn(), - addComponents: vi.fn(), - matchComponents: vi.fn(), - addBase: vi.fn(), - addVariant: vi.fn(), - matchVariant: vi.fn(), - theme: vi.fn(), - config: vi.fn(), - } as any); - - const classNames = Object.keys(addUtilities.mock.calls[0][0]); + const classNames = Object.keys(runPlugin().utilities); expect(classNames).toContain('.bg-ease-in-to-r'); }); }); + + describe('matchUtilities for custom bezier', () => { + it('registers matchUtilities once with all direction handlers', () => { + const { matchUtilities } = runPlugin(); + + expect(matchUtilities).toHaveBeenCalledTimes(1); + + const matchers = matchUtilities.mock.calls[0][0]; + expect(Object.keys(matchers)).toHaveLength(8); + expect(matchers['bg-ease-to-r']).toBeDefined(); + expect(matchers['bg-ease-to-b']).toBeDefined(); + expect(matchers['bg-ease-to-tl']).toBeDefined(); + }); + + it('handler returns gradient CSS for valid bezier', () => { + const { matchUtilities } = runPlugin(); + const matchers = matchUtilities.mock.calls[0][0]; + + const result = matchers['bg-ease-to-r']('0.42,0,0.58,1'); + + expect(result['background-image']).toContain('linear-gradient'); + expect(result['background-image']).toContain('to right'); + expect(result['@supports (color: oklch(from red l c h))']).toBeDefined(); + }); + + it('handler returns empty for invalid bezier', () => { + const { matchUtilities } = runPlugin(); + const matchers = matchUtilities.mock.calls[0][0]; + + expect(matchers['bg-ease-to-r']('invalid')).toEqual({}); + }); + }); }); describe('exports', () => { - it('exports all types', async () => { + it('exports all functions and types', async () => { const exports = await import('./index.js'); expect(exports.getCoordinates).toBeDefined(); + expect(exports.getCoordinatesFromControlPoints).toBeDefined(); + expect(exports.parseBezierValues).toBeDefined(); expect(exports.default).toBeDefined(); }); }); diff --git a/packages/tw-easing-gradients/src/index.ts b/packages/tw-easing-gradients/src/index.ts index 094ee03..228b88b 100644 --- a/packages/tw-easing-gradients/src/index.ts +++ b/packages/tw-easing-gradients/src/index.ts @@ -1,9 +1,13 @@ import plugin from 'tailwindcss/plugin'; -import { getCoordinates } from './easing.js'; -import type { Direction, EasingFunction, PluginOptions } from './types.js'; +import { + getCoordinates, + getCoordinatesFromControlPoints, + parseBezierValues, +} from './easing.js'; +import type { EasingFunction, PluginOptions } from './types.js'; import { DIRECTIONS, EASING_FUNCTIONS } from './types.js'; -export { getCoordinates } from './easing.js'; +export { getCoordinates, getCoordinatesFromControlPoints, parseBezierValues } from './easing.js'; export type { Coordinate, Direction, @@ -13,22 +17,8 @@ export type { type TailwindPlugin = ReturnType>; -const EASINGS: readonly EasingFunction[] = [ - 'ease', - 'ease-in', - 'ease-out', - 'ease-in-out', -]; -const DIRECTIONS_KEYS: readonly Direction[] = [ - 't', - 'r', - 'b', - 'l', - 'tl', - 'tr', - 'bl', - 'br', -]; +const EASINGS = Object.keys(EASING_FUNCTIONS) as EasingFunction[]; +const DIRECTION_KEYS = Object.keys(DIRECTIONS) as (keyof typeof DIRECTIONS)[]; function generateGradientStops( coordinates: Array<{ x: number; y: number }>, @@ -49,9 +39,21 @@ function generateGradientStops( .join(', '); } +function makeGradientUtility( + cssDirection: string, + gradientStops: string, +): Record> { + return { + 'background-image': `linear-gradient(${cssDirection}, var(--tw-gradient-from), var(--tw-gradient-to, transparent))`, + '@supports (color: oklch(from red l c h))': { + 'background-image': `linear-gradient(${cssDirection}, ${gradientStops})`, + }, + }; +} + const easingGradients: TailwindPlugin = plugin.withOptions( (options = {}) => - ({ addUtilities }) => { + ({ addUtilities, matchUtilities }) => { const stops = options.stops ?? 15; const utilities: Record< string, @@ -59,25 +61,31 @@ const easingGradients: TailwindPlugin = plugin.withOptions( > = {}; for (const easing of EASINGS) { - for (const dir of DIRECTIONS_KEYS) { - const className = - easing === 'ease' - ? `.bg-ease-to-${dir}` - : `.bg-${easing}-to-${dir}`; - const gradientStops = generateGradientStops( - getCoordinates(easing, stops), - ); + const gradientStops = generateGradientStops( + getCoordinates(easing, stops), + ); - utilities[className] = { - 'background-image': `linear-gradient(${DIRECTIONS[dir]}, var(--tw-gradient-from), var(--tw-gradient-to, transparent))`, - '@supports (color: oklch(from red l c h))': { - 'background-image': `linear-gradient(${DIRECTIONS[dir]}, ${gradientStops})`, - }, - }; + for (const dir of DIRECTION_KEYS) { + utilities[`.bg-${easing}-to-${dir}`] = + makeGradientUtility(DIRECTIONS[dir], gradientStops); } } addUtilities(utilities); + + // Custom bezier via arbitrary values: bg-ease-to-r-[0.22,1,0.36,1] + const matchers: Record Record> | {}> = {}; + + for (const dir of DIRECTION_KEYS) { + matchers[`bg-ease-to-${dir}`] = (value: string) => { + const points = parseBezierValues(value); + if (!points) return {}; + const coords = getCoordinatesFromControlPoints(points, stops); + return makeGradientUtility(DIRECTIONS[dir], generateGradientStops(coords)); + }; + } + + matchUtilities(matchers, { values: {}, type: 'any' }); }, ); diff --git a/packages/tw-easing-gradients/src/integration.test.ts b/packages/tw-easing-gradients/src/integration.test.ts index 0f6d7d5..34f2fa8 100644 --- a/packages/tw-easing-gradients/src/integration.test.ts +++ b/packages/tw-easing-gradients/src/integration.test.ts @@ -3,6 +3,7 @@ import { compile } from 'tailwindcss'; import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, resolve, join } from 'path'; +import { DIRECTIONS } from './types.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -49,7 +50,7 @@ async function compileWithPlugin( } describe('Tailwind CSS Integration', () => { - it('generates CSS when plugin is used in real Tailwind config', async () => { + it('generates CSS for linear gradient utilities', async () => { const css = await compileWithPlugin(['bg-ease-to-r', 'bg-ease-in-to-b']); expect(css).toContain('.bg-ease-to-r'); @@ -57,7 +58,6 @@ describe('Tailwind CSS Integration', () => { expect(css).toContain('linear-gradient'); expect(css).toContain('to right'); expect(css).toContain('to bottom'); - // @supports Block für erweiterte Version expect(css).toContain('@supports (color: oklch(from red l c h))'); expect(css).toContain('color-mix'); expect(css).toContain('oklch'); @@ -65,42 +65,14 @@ describe('Tailwind CSS Integration', () => { it('generates all 32 utility classes', async () => { const allClasses = [ - // ease - 'bg-ease-to-t', - 'bg-ease-to-r', - 'bg-ease-to-b', - 'bg-ease-to-l', - 'bg-ease-to-tl', - 'bg-ease-to-tr', - 'bg-ease-to-bl', - 'bg-ease-to-br', - // ease-in - 'bg-ease-in-to-t', - 'bg-ease-in-to-r', - 'bg-ease-in-to-b', - 'bg-ease-in-to-l', - 'bg-ease-in-to-tl', - 'bg-ease-in-to-tr', - 'bg-ease-in-to-bl', - 'bg-ease-in-to-br', - // ease-out - 'bg-ease-out-to-t', - 'bg-ease-out-to-r', - 'bg-ease-out-to-b', - 'bg-ease-out-to-l', - 'bg-ease-out-to-tl', - 'bg-ease-out-to-tr', - 'bg-ease-out-to-bl', - 'bg-ease-out-to-br', - // ease-in-out - 'bg-ease-in-out-to-t', - 'bg-ease-in-out-to-r', - 'bg-ease-in-out-to-b', - 'bg-ease-in-out-to-l', - 'bg-ease-in-out-to-tl', - 'bg-ease-in-out-to-tr', - 'bg-ease-in-out-to-bl', - 'bg-ease-in-out-to-br', + 'bg-ease-to-t', 'bg-ease-to-r', 'bg-ease-to-b', 'bg-ease-to-l', + 'bg-ease-to-tl', 'bg-ease-to-tr', 'bg-ease-to-bl', 'bg-ease-to-br', + 'bg-ease-in-to-t', 'bg-ease-in-to-r', 'bg-ease-in-to-b', 'bg-ease-in-to-l', + 'bg-ease-in-to-tl', 'bg-ease-in-to-tr', 'bg-ease-in-to-bl', 'bg-ease-in-to-br', + 'bg-ease-out-to-t', 'bg-ease-out-to-r', 'bg-ease-out-to-b', 'bg-ease-out-to-l', + 'bg-ease-out-to-tl', 'bg-ease-out-to-tr', 'bg-ease-out-to-bl', 'bg-ease-out-to-br', + 'bg-ease-in-out-to-t', 'bg-ease-in-out-to-r', 'bg-ease-in-out-to-b', 'bg-ease-in-out-to-l', + 'bg-ease-in-out-to-tl', 'bg-ease-in-out-to-tr', 'bg-ease-in-out-to-bl', 'bg-ease-in-out-to-br', ]; const css = await compileWithPlugin(allClasses); @@ -113,33 +85,17 @@ describe('Tailwind CSS Integration', () => { it('uses default 15 stops producing exact gradient positions', async () => { const css = await compileWithPlugin(['bg-ease-to-r']); - // Die erweiterte Version ist im @supports Block expect(css).toContain('@supports (color: oklch(from red l c h))'); const expectedPositions = [ - '0%', - '4.7%', - '8.9%', - '12.8%', - '16.6%', - '20.4%', - '24.4%', - '28.8%', - '33.8%', - '39.6%', - '46.3%', - '54.1%', - '63.2%', - '73.8%', - '86%', - '100%', + '0%', '4.7%', '8.9%', '12.8%', '16.6%', '20.4%', '24.4%', '28.8%', + '33.8%', '39.6%', '46.3%', '54.1%', '63.2%', '73.8%', '86%', '100%', ]; for (const position of expectedPositions) { expect(css).toContain(position); } - // color-mix ist im nested @supports Block const colorMixCount = (css.match(/color-mix/g) || []).length; expect(colorMixCount).toBeGreaterThanOrEqual(15); }); @@ -147,8 +103,6 @@ describe('Tailwind CSS Integration', () => { it('generates simple 2-stop fallback as base property', async () => { const css = await compileWithPlugin(['bg-ease-to-r']); - // Der Fallback ist die erste background-image Property in der Regel - // Format: .bg-ease-to-r { background-image: linear-gradient(...simple...); @supports...} const ruleMatch = css.match( /\.bg-ease-to-r\s*\{\s*background-image:\s*([^;]+);/, ); @@ -157,7 +111,6 @@ describe('Tailwind CSS Integration', () => { expect(fallbackValue).toContain('linear-gradient'); expect(fallbackValue).toContain('var(--tw-gradient-from)'); expect(fallbackValue).toContain('var(--tw-gradient-to'); - // Fallback sollte KEIN oklch oder color-mix enthalten expect(fallbackValue).not.toContain('oklch'); expect(fallbackValue).not.toContain('color-mix'); }); @@ -173,18 +126,7 @@ describe('Tailwind CSS Integration', () => { }); it('generates correct gradient directions', async () => { - const directionMap = { - t: 'to top', - r: 'to right', - b: 'to bottom', - l: 'to left', - tl: 'to top left', - tr: 'to top right', - bl: 'to bottom left', - br: 'to bottom right', - }; - - for (const [dir, expected] of Object.entries(directionMap)) { + for (const [dir, expected] of Object.entries(DIRECTIONS)) { const css = await compileWithPlugin([`bg-ease-to-${dir}`]); expect(css).toContain(`linear-gradient(${expected},`); } @@ -209,7 +151,6 @@ describe('Tailwind CSS Integration', () => { expect(css).toContain('linear-gradient'); expect(css).toContain('@supports (color: oklch(from red l c h))'); - // Mit 5 Stops sollten weniger color-mix Aufrufe sein als mit 15 const colorMixCount = (css.match(/color-mix/g) || []).length; expect(colorMixCount).toBeGreaterThanOrEqual(4); expect(colorMixCount).toBeLessThan(14); @@ -222,4 +163,16 @@ describe('Tailwind CSS Integration', () => { expect(css1).toBe(css2); }); + describe('custom bezier arbitrary values', () => { + it('generates CSS for custom bezier gradient', async () => { + const css = await compileWithPlugin([ + 'bg-ease-to-r-[0.22,1,0.36,1]', + ]); + + expect(css).toContain('linear-gradient'); + expect(css).toContain('to right'); + expect(css).toContain('@supports (color: oklch(from red l c h))'); + expect(css).toContain('color-mix'); + }); + }); }); From bf56844faa44d6a29e6f345bd1fae45d97c53ac9 Mon Sep 17 00:00:00 2001 From: Enis Budancamanak Date: Fri, 3 Apr 2026 15:48:58 +0200 Subject: [PATCH 2/3] ci: update actions to latest versions and fix type check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - actions/checkout v4 → v6 - actions/setup-node v4 → v6 - pnpm/action-setup v4 → v5 - Fix tsc --noEmit with explicit tsconfig.json path --- .github/workflows/ci.yml | 12 ++++++------ .github/workflows/release.yml | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4057e4..33eb464 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,13 +15,13 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v5 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: '24' cache: 'pnpm' @@ -35,8 +35,8 @@ jobs: - name: Build package run: pnpm --filter tw-easing-gradients build + - name: Type check + run: pnpm --filter tw-easing-gradients tsc --noEmit -p packages/tw-easing-gradients/tsconfig.json + - name: Run tests run: pnpm --filter tw-easing-gradients test - - - name: Type check - run: pnpm --filter tw-easing-gradients tsc --noEmit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 34099e1..607607c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,13 +19,13 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v5 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: '24' cache: 'pnpm' From 3d9bfd7a9353ee40d80fc4983829c15683e630e1 Mon Sep 17 00:00:00 2001 From: Enis Budancamanak Date: Fri, 3 Apr 2026 15:50:38 +0200 Subject: [PATCH 3/3] ci: add typecheck script and fix CI type check step --- .github/workflows/ci.yml | 2 +- packages/tw-easing-gradients/package.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33eb464..0a47e82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: run: pnpm --filter tw-easing-gradients build - name: Type check - run: pnpm --filter tw-easing-gradients tsc --noEmit -p packages/tw-easing-gradients/tsconfig.json + run: pnpm --filter tw-easing-gradients typecheck - name: Run tests run: pnpm --filter tw-easing-gradients test diff --git a/packages/tw-easing-gradients/package.json b/packages/tw-easing-gradients/package.json index 1b443a6..3d4efdc 100644 --- a/packages/tw-easing-gradients/package.json +++ b/packages/tw-easing-gradients/package.json @@ -20,7 +20,8 @@ "dev": "tsup src/index.ts --format cjs,esm --dts --watch", "test": "vitest run", "test:watch": "vitest", - "test:coverage": "vitest run --coverage" + "test:coverage": "vitest run --coverage", + "typecheck": "tsc --noEmit" }, "peerDependencies": { "tailwindcss": ">=4.0.0"